Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@
- `oidc.map_legacy_users` is now `false` by default
[#2350](https://github.com/juanfont/headscale/pull/2350)

## 0.24.1 (2025-01-xx)

### Changes

- Relax username validation to allow emails
[#2364](https://github.com/juanfont/headscale/pull/2364)

## 0.24.0 (2025-01-17)

### Security fix: OIDC changes in Headscale 0.24.0
Expand Down
148 changes: 148 additions & 0 deletions hscontrol/types/users_test.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
package types

import (
"database/sql"
"encoding/json"
"testing"

"github.com/google/go-cmp/cmp"
"github.com/juanfont/headscale/hscontrol/util"
)

func TestUnmarshallOIDCClaims(t *testing.T) {
Expand Down Expand Up @@ -73,3 +75,149 @@ func TestUnmarshallOIDCClaims(t *testing.T) {
})
}
}

func TestOIDCClaimsJSONToUser(t *testing.T) {
tests := []struct {
name string
jsonstr string
want User
}{
{
name: "normal-bool",
jsonstr: `
{
"sub": "test",
"email": "[email protected]",
"email_verified": true
}
`,
want: User{
Provider: util.RegisterMethodOIDC,
Email: "[email protected]",
ProviderIdentifier: sql.NullString{
String: "/test",
Valid: true,
},
},
},
{
name: "string-bool-true",
jsonstr: `
{
"sub": "test2",
"email": "[email protected]",
"email_verified": "true"
}
`,
want: User{
Provider: util.RegisterMethodOIDC,
Email: "[email protected]",
ProviderIdentifier: sql.NullString{
String: "/test2",
Valid: true,
},
},
},
{
name: "string-bool-false",
jsonstr: `
{
"sub": "test3",
"email": "[email protected]",
"email_verified": "false"
}
`,
want: User{
Provider: util.RegisterMethodOIDC,
ProviderIdentifier: sql.NullString{
String: "/test3",
Valid: true,
},
},
},
{
// From https://github.com/juanfont/headscale/issues/2333
name: "okta-oidc-claim-20250121",
jsonstr: `
{
"sub": "00u7dr4qp7XXXXXXXXXX",
"name": "Tim Horton",
"email": "[email protected]",
"ver": 1,
"iss": "https://sso.company.com/oauth2/default",
"aud": "0oa8neto4tXXXXXXXXXX",
"iat": 1737455152,
"exp": 1737458752,
"jti": "ID.zzJz93koTunMKv5Bq-XXXXXXXXXXXXXXXXXXXXXXXXX",
"amr": [
"pwd"
],
"idp": "00o42r3s2cXXXXXXXX",
"nonce": "nonce",
"preferred_username": "[email protected]",
"auth_time": 1000,
"at_hash": "preview_at_hash"
}
`,
want: User{
Provider: util.RegisterMethodOIDC,
DisplayName: "Tim Horton",
Name: "[email protected]",
ProviderIdentifier: sql.NullString{
String: "https://sso.company.com/oauth2/default/00u7dr4qp7XXXXXXXXXX",
Valid: true,
},
},
},
{
// From https://github.com/juanfont/headscale/issues/2333
name: "okta-oidc-claim-20250121",
jsonstr: `
{
"aud": "79xxxxxx-xxxx-xxxx-xxxx-892146xxxxxx",
"iss": "https://login.microsoftonline.com//v2.0",
"iat": 1737346441,
"nbf": 1737346441,
"exp": 1737350341,
"aio": "AWQAm/8ZAAAABKne9EWr6ygVO2DbcRmoPIpRM819qqlP/mmK41AAWv/C2tVkld4+znbG8DaXFdLQa9jRUzokvsT7rt9nAT6Fg7QC+/ecDWsF5U+QX11f9Ox7ZkK4UAIWFcIXpuZZvRS7",
"email": "[email protected]",
"name": "XXXXXX XXXX",
"oid": "54c2323d-5052-4130-9588-ad751909003f",
"preferred_username": "[email protected]",
"rh": "1.AXUAXdg0Rfc11UifLDJv67ChfSluoXmD9z1EmK-JIUYuSK9cAQl1AA.",
"sid": "5250a0a2-0b4e-4e68-8652-b4e97866411d",
"sub": "I-70OQnj3TogrNSfkZQqB3f7dGwyBWSm1dolHNKrMzQ",
"tid": "<redacted>",
"uti": "zAuXeEtMM0GwcTAcOsBZAA",
"ver": "2.0"
}
`,
want: User{
Provider: util.RegisterMethodOIDC,
DisplayName: "XXXXXX XXXX",
Name: "[email protected]",
ProviderIdentifier: sql.NullString{
String: "https://login.microsoftonline.com//v2.0/I-70OQnj3TogrNSfkZQqB3f7dGwyBWSm1dolHNKrMzQ",
Valid: true,
},
},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var got OIDCClaims
if err := json.Unmarshal([]byte(tt.jsonstr), &got); err != nil {
t.Errorf("TestOIDCClaimsJSONToUser() error = %v", err)
return
}

var user User

user.FromClaim(&got)
if diff := cmp.Diff(user, tt.want); diff != "" {
t.Errorf("TestOIDCClaimsJSONToUser() mismatch (-want +got):\n%s", diff)
}
})
}
}
11 changes: 10 additions & 1 deletion hscontrol/util/dns.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ var invalidCharsInUserRegex = regexp.MustCompile("[^a-z0-9-.]+")

var ErrInvalidUserName = errors.New("invalid user name")

// ValidateUsername checks if a username is valid.
// It must be at least 2 characters long, start with a letter, and contain
// only letters, numbers, hyphens, dots, and underscores.
// It cannot contain more than one '@'.
// It cannot contain invalid characters.
func ValidateUsername(username string) error {
// Ensure the username meets the minimum length requirement
if len(username) < 2 {
Expand All @@ -40,7 +45,11 @@ func ValidateUsername(username string) error {
atCount := 0
for _, char := range username {
switch {
case unicode.IsLetter(char), unicode.IsDigit(char), char == '-':
case unicode.IsLetter(char),
unicode.IsDigit(char),
char == '-',
char == '.',
char == '_':
// Valid characters
case char == '@':
atCount++
Expand Down
Loading