mirror of
https://github.com/juanfont/headscale.git
synced 2025-12-23 08:46:12 +00:00
oidc: make email verification configurable
Co-authored-by: Kristoffer Dalby <kristoffer@tailscale.com>
This commit is contained in:
@@ -57,6 +57,9 @@ sequentially through each stable release, selecting the latest patch version ava
|
|||||||
|
|
||||||
- Smarter change notifications send partial map updates and node removals instead of full maps [#2961](https://github.com/juanfont/headscale/pull/2961)
|
- Smarter change notifications send partial map updates and node removals instead of full maps [#2961](https://github.com/juanfont/headscale/pull/2961)
|
||||||
- Send lightweight endpoint and DERP region updates instead of full maps [#2856](https://github.com/juanfont/headscale/pull/2856)
|
- Send lightweight endpoint and DERP region updates instead of full maps [#2856](https://github.com/juanfont/headscale/pull/2856)
|
||||||
|
- Add `oidc.email_verified_required` config option to control email verification requirement [#2860](https://github.com/juanfont/headscale/pull/2860)
|
||||||
|
- When `true` (default), only verified emails can authenticate via OIDC with `allowed_domains` or `allowed_users`
|
||||||
|
- When `false`, unverified emails are allowed for OIDC authentication
|
||||||
- Add NixOS module in repository for faster iteration [#2857](https://github.com/juanfont/headscale/pull/2857)
|
- Add NixOS module in repository for faster iteration [#2857](https://github.com/juanfont/headscale/pull/2857)
|
||||||
- Add favicon to webpages [#2858](https://github.com/juanfont/headscale/pull/2858)
|
- Add favicon to webpages [#2858](https://github.com/juanfont/headscale/pull/2858)
|
||||||
- Redesign OIDC callback and registration web templates [#2832](https://github.com/juanfont/headscale/pull/2832)
|
- Redesign OIDC callback and registration web templates [#2832](https://github.com/juanfont/headscale/pull/2832)
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ type LockFreeBatcher struct {
|
|||||||
workCh chan work
|
workCh chan work
|
||||||
workChOnce sync.Once // Ensures workCh is only closed once
|
workChOnce sync.Once // Ensures workCh is only closed once
|
||||||
done chan struct{}
|
done chan struct{}
|
||||||
|
doneOnce sync.Once // Ensures done is only closed once
|
||||||
|
|
||||||
// Batching state
|
// Batching state
|
||||||
pendingChanges *xsync.Map[types.NodeID, []change.Change]
|
pendingChanges *xsync.Map[types.NodeID, []change.Change]
|
||||||
@@ -151,10 +152,12 @@ func (b *LockFreeBatcher) Start() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (b *LockFreeBatcher) Close() {
|
func (b *LockFreeBatcher) Close() {
|
||||||
// Signal shutdown to all goroutines
|
// Signal shutdown to all goroutines, only once
|
||||||
|
b.doneOnce.Do(func() {
|
||||||
if b.done != nil {
|
if b.done != nil {
|
||||||
close(b.done)
|
close(b.done)
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// Only close workCh once using sync.Once to prevent races
|
// Only close workCh once using sync.Once to prevent races
|
||||||
b.workChOnce.Do(func() {
|
b.workChOnce.Do(func() {
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ var (
|
|||||||
errOIDCAllowedUsers = errors.New(
|
errOIDCAllowedUsers = errors.New(
|
||||||
"authenticated principal does not match any allowed user",
|
"authenticated principal does not match any allowed user",
|
||||||
)
|
)
|
||||||
|
errOIDCUnverifiedEmail = errors.New("authenticated principal has an unverified email")
|
||||||
)
|
)
|
||||||
|
|
||||||
// RegistrationInfo contains both machine key and verifier information for OIDC validation.
|
// RegistrationInfo contains both machine key and verifier information for OIDC validation.
|
||||||
@@ -264,17 +265,8 @@ func (a *AuthProviderOIDC) OIDCCallbackHandler(
|
|||||||
|
|
||||||
// The user claims are now updated from the userinfo endpoint so we can verify the user
|
// The user claims are now updated from the userinfo endpoint so we can verify the user
|
||||||
// against allowed emails, email domains, and groups.
|
// against allowed emails, email domains, and groups.
|
||||||
if err := validateOIDCAllowedDomains(a.cfg.AllowedDomains, &claims); err != nil {
|
err = doOIDCAuthorization(a.cfg, &claims)
|
||||||
httpError(writer, err)
|
if err != nil {
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := validateOIDCAllowedGroups(a.cfg.AllowedGroups, &claims); err != nil {
|
|
||||||
httpError(writer, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := validateOIDCAllowedUsers(a.cfg.AllowedUsers, &claims); err != nil {
|
|
||||||
httpError(writer, err)
|
httpError(writer, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -434,7 +426,6 @@ func validateOIDCAllowedGroups(
|
|||||||
allowedGroups []string,
|
allowedGroups []string,
|
||||||
claims *types.OIDCClaims,
|
claims *types.OIDCClaims,
|
||||||
) error {
|
) error {
|
||||||
if len(allowedGroups) > 0 {
|
|
||||||
for _, group := range allowedGroups {
|
for _, group := range allowedGroups {
|
||||||
if slices.Contains(claims.Groups, group) {
|
if slices.Contains(claims.Groups, group) {
|
||||||
return nil
|
return nil
|
||||||
@@ -442,9 +433,6 @@ func validateOIDCAllowedGroups(
|
|||||||
}
|
}
|
||||||
|
|
||||||
return NewHTTPError(http.StatusUnauthorized, "unauthorised group", errOIDCAllowedGroups)
|
return NewHTTPError(http.StatusUnauthorized, "unauthorised group", errOIDCAllowedGroups)
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// validateOIDCAllowedUsers checks that if AllowedUsers is provided,
|
// validateOIDCAllowedUsers checks that if AllowedUsers is provided,
|
||||||
@@ -453,14 +441,62 @@ func validateOIDCAllowedUsers(
|
|||||||
allowedUsers []string,
|
allowedUsers []string,
|
||||||
claims *types.OIDCClaims,
|
claims *types.OIDCClaims,
|
||||||
) error {
|
) error {
|
||||||
if len(allowedUsers) > 0 &&
|
if !slices.Contains(allowedUsers, claims.Email) {
|
||||||
!slices.Contains(allowedUsers, claims.Email) {
|
|
||||||
return NewHTTPError(http.StatusUnauthorized, "unauthorised user", errOIDCAllowedUsers)
|
return NewHTTPError(http.StatusUnauthorized, "unauthorised user", errOIDCAllowedUsers)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// doOIDCAuthorization applies authorization tests to claims.
|
||||||
|
//
|
||||||
|
// The following tests are always applied:
|
||||||
|
//
|
||||||
|
// - validateOIDCAllowedGroups
|
||||||
|
//
|
||||||
|
// The following tests are applied if cfg.EmailVerifiedRequired=false
|
||||||
|
// or claims.email_verified=true:
|
||||||
|
//
|
||||||
|
// - validateOIDCAllowedDomains
|
||||||
|
// - validateOIDCAllowedUsers
|
||||||
|
//
|
||||||
|
// NOTE that, contrary to the function name, validateOIDCAllowedUsers
|
||||||
|
// only checks the email address -- not the username.
|
||||||
|
func doOIDCAuthorization(
|
||||||
|
cfg *types.OIDCConfig,
|
||||||
|
claims *types.OIDCClaims,
|
||||||
|
) error {
|
||||||
|
if len(cfg.AllowedGroups) > 0 {
|
||||||
|
err := validateOIDCAllowedGroups(cfg.AllowedGroups, claims)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
trustEmail := !cfg.EmailVerifiedRequired || bool(claims.EmailVerified)
|
||||||
|
|
||||||
|
hasEmailTests := len(cfg.AllowedDomains) > 0 || len(cfg.AllowedUsers) > 0
|
||||||
|
if !trustEmail && hasEmailTests {
|
||||||
|
return NewHTTPError(http.StatusUnauthorized, "unverified email", errOIDCUnverifiedEmail)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(cfg.AllowedDomains) > 0 {
|
||||||
|
err := validateOIDCAllowedDomains(cfg.AllowedDomains, claims)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(cfg.AllowedUsers) > 0 {
|
||||||
|
err := validateOIDCAllowedUsers(cfg.AllowedUsers, claims)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// getRegistrationIDFromState retrieves the registration ID from the state.
|
// getRegistrationIDFromState retrieves the registration ID from the state.
|
||||||
func (a *AuthProviderOIDC) getRegistrationIDFromState(state string) *types.RegistrationID {
|
func (a *AuthProviderOIDC) getRegistrationIDFromState(state string) *types.RegistrationID {
|
||||||
regInfo, ok := a.registrationCache.Get(state)
|
regInfo, ok := a.registrationCache.Get(state)
|
||||||
@@ -493,7 +529,7 @@ func (a *AuthProviderOIDC) createOrUpdateUserFromClaim(
|
|||||||
user = &types.User{}
|
user = &types.User{}
|
||||||
}
|
}
|
||||||
|
|
||||||
user.FromClaim(claims)
|
user.FromClaim(claims, a.cfg.EmailVerifiedRequired)
|
||||||
|
|
||||||
if newUser {
|
if newUser {
|
||||||
user, c, err = a.h.state.CreateUser(*user)
|
user, c, err = a.h.state.CreateUser(*user)
|
||||||
|
|||||||
173
hscontrol/oidc_test.go
Normal file
173
hscontrol/oidc_test.go
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
package hscontrol
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/juanfont/headscale/hscontrol/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDoOIDCAuthorization(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
cfg *types.OIDCConfig
|
||||||
|
claims *types.OIDCClaims
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "verified email domain",
|
||||||
|
wantErr: false,
|
||||||
|
cfg: &types.OIDCConfig{
|
||||||
|
EmailVerifiedRequired: true,
|
||||||
|
AllowedDomains: []string{"test.com"},
|
||||||
|
AllowedUsers: []string{},
|
||||||
|
AllowedGroups: []string{},
|
||||||
|
},
|
||||||
|
claims: &types.OIDCClaims{
|
||||||
|
Email: "user@test.com",
|
||||||
|
EmailVerified: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "verified email user",
|
||||||
|
wantErr: false,
|
||||||
|
cfg: &types.OIDCConfig{
|
||||||
|
EmailVerifiedRequired: true,
|
||||||
|
AllowedDomains: []string{},
|
||||||
|
AllowedUsers: []string{"user@test.com"},
|
||||||
|
AllowedGroups: []string{},
|
||||||
|
},
|
||||||
|
claims: &types.OIDCClaims{
|
||||||
|
Email: "user@test.com",
|
||||||
|
EmailVerified: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "unverified email domain",
|
||||||
|
wantErr: true,
|
||||||
|
cfg: &types.OIDCConfig{
|
||||||
|
EmailVerifiedRequired: true,
|
||||||
|
AllowedDomains: []string{"test.com"},
|
||||||
|
AllowedUsers: []string{},
|
||||||
|
AllowedGroups: []string{},
|
||||||
|
},
|
||||||
|
claims: &types.OIDCClaims{
|
||||||
|
Email: "user@test.com",
|
||||||
|
EmailVerified: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "group member",
|
||||||
|
wantErr: false,
|
||||||
|
cfg: &types.OIDCConfig{
|
||||||
|
EmailVerifiedRequired: true,
|
||||||
|
AllowedDomains: []string{},
|
||||||
|
AllowedUsers: []string{},
|
||||||
|
AllowedGroups: []string{"test"},
|
||||||
|
},
|
||||||
|
claims: &types.OIDCClaims{Groups: []string{"test"}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "non group member",
|
||||||
|
wantErr: true,
|
||||||
|
cfg: &types.OIDCConfig{
|
||||||
|
EmailVerifiedRequired: true,
|
||||||
|
AllowedDomains: []string{},
|
||||||
|
AllowedUsers: []string{},
|
||||||
|
AllowedGroups: []string{"nope"},
|
||||||
|
},
|
||||||
|
claims: &types.OIDCClaims{Groups: []string{"testo"}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "group member but bad domain",
|
||||||
|
wantErr: true,
|
||||||
|
cfg: &types.OIDCConfig{
|
||||||
|
EmailVerifiedRequired: true,
|
||||||
|
AllowedDomains: []string{"user@good.com"},
|
||||||
|
AllowedUsers: []string{},
|
||||||
|
AllowedGroups: []string{"test group"},
|
||||||
|
},
|
||||||
|
claims: &types.OIDCClaims{Groups: []string{"test group"}, Email: "bad@bad.com", EmailVerified: true},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "all checks pass",
|
||||||
|
wantErr: false,
|
||||||
|
cfg: &types.OIDCConfig{
|
||||||
|
EmailVerifiedRequired: true,
|
||||||
|
AllowedDomains: []string{"test.com"},
|
||||||
|
AllowedUsers: []string{"user@test.com"},
|
||||||
|
AllowedGroups: []string{"test group"},
|
||||||
|
},
|
||||||
|
claims: &types.OIDCClaims{Groups: []string{"test group"}, Email: "user@test.com", EmailVerified: true},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "all checks pass with unverified email",
|
||||||
|
wantErr: false,
|
||||||
|
cfg: &types.OIDCConfig{
|
||||||
|
EmailVerifiedRequired: false,
|
||||||
|
AllowedDomains: []string{"test.com"},
|
||||||
|
AllowedUsers: []string{"user@test.com"},
|
||||||
|
AllowedGroups: []string{"test group"},
|
||||||
|
},
|
||||||
|
claims: &types.OIDCClaims{Groups: []string{"test group"}, Email: "user@test.com", EmailVerified: false},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "fail on unverified email",
|
||||||
|
wantErr: true,
|
||||||
|
cfg: &types.OIDCConfig{
|
||||||
|
EmailVerifiedRequired: true,
|
||||||
|
AllowedDomains: []string{"test.com"},
|
||||||
|
AllowedUsers: []string{"user@test.com"},
|
||||||
|
AllowedGroups: []string{"test group"},
|
||||||
|
},
|
||||||
|
claims: &types.OIDCClaims{Groups: []string{"test group"}, Email: "user@test.com", EmailVerified: false},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "unverified email user only",
|
||||||
|
wantErr: true,
|
||||||
|
cfg: &types.OIDCConfig{
|
||||||
|
EmailVerifiedRequired: true,
|
||||||
|
AllowedDomains: []string{},
|
||||||
|
AllowedUsers: []string{"user@test.com"},
|
||||||
|
AllowedGroups: []string{},
|
||||||
|
},
|
||||||
|
claims: &types.OIDCClaims{
|
||||||
|
Email: "user@test.com",
|
||||||
|
EmailVerified: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no filters configured",
|
||||||
|
wantErr: false,
|
||||||
|
cfg: &types.OIDCConfig{
|
||||||
|
EmailVerifiedRequired: true,
|
||||||
|
AllowedDomains: []string{},
|
||||||
|
AllowedUsers: []string{},
|
||||||
|
AllowedGroups: []string{},
|
||||||
|
},
|
||||||
|
claims: &types.OIDCClaims{
|
||||||
|
Email: "anyone@anywhere.com",
|
||||||
|
EmailVerified: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "multiple allowed groups second matches",
|
||||||
|
wantErr: false,
|
||||||
|
cfg: &types.OIDCConfig{
|
||||||
|
EmailVerifiedRequired: true,
|
||||||
|
AllowedDomains: []string{},
|
||||||
|
AllowedUsers: []string{},
|
||||||
|
AllowedGroups: []string{"group1", "group2", "group3"},
|
||||||
|
},
|
||||||
|
claims: &types.OIDCClaims{Groups: []string{"group2"}},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tC := range testCases {
|
||||||
|
t.Run(tC.name, func(t *testing.T) {
|
||||||
|
err := doOIDCAuthorization(tC.cfg, tC.claims)
|
||||||
|
if ((err != nil) && !tC.wantErr) || ((err == nil) && tC.wantErr) {
|
||||||
|
t.Errorf("bad authorization: %s > want=%v | got=%v", tC.name, tC.wantErr, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -185,6 +185,7 @@ type OIDCConfig struct {
|
|||||||
AllowedDomains []string
|
AllowedDomains []string
|
||||||
AllowedUsers []string
|
AllowedUsers []string
|
||||||
AllowedGroups []string
|
AllowedGroups []string
|
||||||
|
EmailVerifiedRequired bool
|
||||||
Expiry time.Duration
|
Expiry time.Duration
|
||||||
UseExpiryFromToken bool
|
UseExpiryFromToken bool
|
||||||
PKCE PKCEConfig
|
PKCE PKCEConfig
|
||||||
@@ -384,6 +385,7 @@ func LoadConfig(path string, isFile bool) error {
|
|||||||
viper.SetDefault("oidc.use_expiry_from_token", false)
|
viper.SetDefault("oidc.use_expiry_from_token", false)
|
||||||
viper.SetDefault("oidc.pkce.enabled", false)
|
viper.SetDefault("oidc.pkce.enabled", false)
|
||||||
viper.SetDefault("oidc.pkce.method", "S256")
|
viper.SetDefault("oidc.pkce.method", "S256")
|
||||||
|
viper.SetDefault("oidc.email_verified_required", true)
|
||||||
|
|
||||||
viper.SetDefault("logtail.enabled", false)
|
viper.SetDefault("logtail.enabled", false)
|
||||||
viper.SetDefault("randomize_client_port", false)
|
viper.SetDefault("randomize_client_port", false)
|
||||||
@@ -1030,6 +1032,7 @@ func LoadServerConfig() (*Config, error) {
|
|||||||
AllowedDomains: viper.GetStringSlice("oidc.allowed_domains"),
|
AllowedDomains: viper.GetStringSlice("oidc.allowed_domains"),
|
||||||
AllowedUsers: viper.GetStringSlice("oidc.allowed_users"),
|
AllowedUsers: viper.GetStringSlice("oidc.allowed_users"),
|
||||||
AllowedGroups: viper.GetStringSlice("oidc.allowed_groups"),
|
AllowedGroups: viper.GetStringSlice("oidc.allowed_groups"),
|
||||||
|
EmailVerifiedRequired: viper.GetBool("oidc.email_verified_required"),
|
||||||
Expiry: func() time.Duration {
|
Expiry: func() time.Duration {
|
||||||
// if set to 0, we assume no expiry
|
// if set to 0, we assume no expiry
|
||||||
if value := viper.GetString("oidc.expiry"); value == "0" {
|
if value := viper.GetString("oidc.expiry"); value == "0" {
|
||||||
|
|||||||
@@ -353,7 +353,7 @@ type OIDCUserInfo struct {
|
|||||||
|
|
||||||
// FromClaim overrides a User from OIDC claims.
|
// FromClaim overrides a User from OIDC claims.
|
||||||
// All fields will be updated, except for the ID.
|
// All fields will be updated, except for the ID.
|
||||||
func (u *User) FromClaim(claims *OIDCClaims) {
|
func (u *User) FromClaim(claims *OIDCClaims, emailVerifiedRequired bool) {
|
||||||
err := util.ValidateUsername(claims.Username)
|
err := util.ValidateUsername(claims.Username)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
u.Name = claims.Username
|
u.Name = claims.Username
|
||||||
@@ -361,7 +361,7 @@ func (u *User) FromClaim(claims *OIDCClaims) {
|
|||||||
log.Debug().Caller().Err(err).Msgf("Username %s is not valid", claims.Username)
|
log.Debug().Caller().Err(err).Msgf("Username %s is not valid", claims.Username)
|
||||||
}
|
}
|
||||||
|
|
||||||
if claims.EmailVerified {
|
if claims.EmailVerified || !FlexibleBoolean(emailVerifiedRequired) {
|
||||||
_, err = mail.ParseAddress(claims.Email)
|
_, err = mail.ParseAddress(claims.Email)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
u.Email = claims.Email
|
u.Email = claims.Email
|
||||||
|
|||||||
@@ -293,10 +293,12 @@ func TestOIDCClaimsJSONToUser(t *testing.T) {
|
|||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
jsonstr string
|
jsonstr string
|
||||||
|
emailVerifiedRequired bool
|
||||||
want User
|
want User
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "normal-bool",
|
name: "normal-bool",
|
||||||
|
emailVerifiedRequired: true,
|
||||||
jsonstr: `
|
jsonstr: `
|
||||||
{
|
{
|
||||||
"sub": "test",
|
"sub": "test",
|
||||||
@@ -315,6 +317,7 @@ func TestOIDCClaimsJSONToUser(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "string-bool-true",
|
name: "string-bool-true",
|
||||||
|
emailVerifiedRequired: true,
|
||||||
jsonstr: `
|
jsonstr: `
|
||||||
{
|
{
|
||||||
"sub": "test2",
|
"sub": "test2",
|
||||||
@@ -333,6 +336,7 @@ func TestOIDCClaimsJSONToUser(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "string-bool-false",
|
name: "string-bool-false",
|
||||||
|
emailVerifiedRequired: true,
|
||||||
jsonstr: `
|
jsonstr: `
|
||||||
{
|
{
|
||||||
"sub": "test3",
|
"sub": "test3",
|
||||||
@@ -348,9 +352,29 @@ func TestOIDCClaimsJSONToUser(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "allow-unverified-email",
|
||||||
|
emailVerifiedRequired: false,
|
||||||
|
jsonstr: `
|
||||||
|
{
|
||||||
|
"sub": "test4",
|
||||||
|
"email": "test4@test.no",
|
||||||
|
"email_verified": "false"
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
want: User{
|
||||||
|
Provider: util.RegisterMethodOIDC,
|
||||||
|
Email: "test4@test.no",
|
||||||
|
ProviderIdentifier: sql.NullString{
|
||||||
|
String: "/test4",
|
||||||
|
Valid: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
// From https://github.com/juanfont/headscale/issues/2333
|
// From https://github.com/juanfont/headscale/issues/2333
|
||||||
name: "okta-oidc-claim-20250121",
|
name: "okta-oidc-claim-20250121",
|
||||||
|
emailVerifiedRequired: true,
|
||||||
jsonstr: `
|
jsonstr: `
|
||||||
{
|
{
|
||||||
"sub": "00u7dr4qp7XXXXXXXXXX",
|
"sub": "00u7dr4qp7XXXXXXXXXX",
|
||||||
@@ -375,6 +399,7 @@ func TestOIDCClaimsJSONToUser(t *testing.T) {
|
|||||||
want: User{
|
want: User{
|
||||||
Provider: util.RegisterMethodOIDC,
|
Provider: util.RegisterMethodOIDC,
|
||||||
DisplayName: "Tim Horton",
|
DisplayName: "Tim Horton",
|
||||||
|
Email: "",
|
||||||
Name: "tim.horton@company.com",
|
Name: "tim.horton@company.com",
|
||||||
ProviderIdentifier: sql.NullString{
|
ProviderIdentifier: sql.NullString{
|
||||||
String: "https://sso.company.com/oauth2/default/00u7dr4qp7XXXXXXXXXX",
|
String: "https://sso.company.com/oauth2/default/00u7dr4qp7XXXXXXXXXX",
|
||||||
@@ -385,6 +410,7 @@ func TestOIDCClaimsJSONToUser(t *testing.T) {
|
|||||||
{
|
{
|
||||||
// From https://github.com/juanfont/headscale/issues/2333
|
// From https://github.com/juanfont/headscale/issues/2333
|
||||||
name: "okta-oidc-claim-20250121",
|
name: "okta-oidc-claim-20250121",
|
||||||
|
emailVerifiedRequired: true,
|
||||||
jsonstr: `
|
jsonstr: `
|
||||||
{
|
{
|
||||||
"aud": "79xxxxxx-xxxx-xxxx-xxxx-892146xxxxxx",
|
"aud": "79xxxxxx-xxxx-xxxx-xxxx-892146xxxxxx",
|
||||||
@@ -409,6 +435,7 @@ func TestOIDCClaimsJSONToUser(t *testing.T) {
|
|||||||
Provider: util.RegisterMethodOIDC,
|
Provider: util.RegisterMethodOIDC,
|
||||||
DisplayName: "XXXXXX XXXX",
|
DisplayName: "XXXXXX XXXX",
|
||||||
Name: "user@domain.com",
|
Name: "user@domain.com",
|
||||||
|
Email: "",
|
||||||
ProviderIdentifier: sql.NullString{
|
ProviderIdentifier: sql.NullString{
|
||||||
String: "https://login.microsoftonline.com/v2.0/I-70OQnj3TogrNSfkZQqB3f7dGwyBWSm1dolHNKrMzQ",
|
String: "https://login.microsoftonline.com/v2.0/I-70OQnj3TogrNSfkZQqB3f7dGwyBWSm1dolHNKrMzQ",
|
||||||
Valid: true,
|
Valid: true,
|
||||||
@@ -418,6 +445,7 @@ func TestOIDCClaimsJSONToUser(t *testing.T) {
|
|||||||
{
|
{
|
||||||
// From https://github.com/juanfont/headscale/issues/2333
|
// From https://github.com/juanfont/headscale/issues/2333
|
||||||
name: "casby-oidc-claim-20250513",
|
name: "casby-oidc-claim-20250513",
|
||||||
|
emailVerifiedRequired: true,
|
||||||
jsonstr: `
|
jsonstr: `
|
||||||
{
|
{
|
||||||
"sub": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
|
"sub": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
|
||||||
@@ -458,7 +486,7 @@ func TestOIDCClaimsJSONToUser(t *testing.T) {
|
|||||||
|
|
||||||
var user User
|
var user User
|
||||||
|
|
||||||
user.FromClaim(&got)
|
user.FromClaim(&got, tt.emailVerifiedRequired)
|
||||||
if diff := cmp.Diff(user, tt.want); diff != "" {
|
if diff := cmp.Diff(user, tt.want); diff != "" {
|
||||||
t.Errorf("TestOIDCClaimsJSONToUser() mismatch (-want +got):\n%s", diff)
|
t.Errorf("TestOIDCClaimsJSONToUser() mismatch (-want +got):\n%s", diff)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user