oidc: make email verification configurable

Co-authored-by: Kristoffer Dalby <kristoffer@tailscale.com>
This commit is contained in:
Justin Angel
2025-12-18 06:42:32 -05:00
committed by GitHub
parent e8753619de
commit 7be20912f5
7 changed files with 292 additions and 46 deletions

View File

@@ -185,6 +185,7 @@ type OIDCConfig struct {
AllowedDomains []string
AllowedUsers []string
AllowedGroups []string
EmailVerifiedRequired bool
Expiry time.Duration
UseExpiryFromToken bool
PKCE PKCEConfig
@@ -384,6 +385,7 @@ func LoadConfig(path string, isFile bool) error {
viper.SetDefault("oidc.use_expiry_from_token", false)
viper.SetDefault("oidc.pkce.enabled", false)
viper.SetDefault("oidc.pkce.method", "S256")
viper.SetDefault("oidc.email_verified_required", true)
viper.SetDefault("logtail.enabled", false)
viper.SetDefault("randomize_client_port", false)
@@ -1022,14 +1024,15 @@ func LoadServerConfig() (*Config, error) {
OnlyStartIfOIDCIsAvailable: viper.GetBool(
"oidc.only_start_if_oidc_is_available",
),
Issuer: viper.GetString("oidc.issuer"),
ClientID: viper.GetString("oidc.client_id"),
ClientSecret: oidcClientSecret,
Scope: viper.GetStringSlice("oidc.scope"),
ExtraParams: viper.GetStringMapString("oidc.extra_params"),
AllowedDomains: viper.GetStringSlice("oidc.allowed_domains"),
AllowedUsers: viper.GetStringSlice("oidc.allowed_users"),
AllowedGroups: viper.GetStringSlice("oidc.allowed_groups"),
Issuer: viper.GetString("oidc.issuer"),
ClientID: viper.GetString("oidc.client_id"),
ClientSecret: oidcClientSecret,
Scope: viper.GetStringSlice("oidc.scope"),
ExtraParams: viper.GetStringMapString("oidc.extra_params"),
AllowedDomains: viper.GetStringSlice("oidc.allowed_domains"),
AllowedUsers: viper.GetStringSlice("oidc.allowed_users"),
AllowedGroups: viper.GetStringSlice("oidc.allowed_groups"),
EmailVerifiedRequired: viper.GetBool("oidc.email_verified_required"),
Expiry: func() time.Duration {
// if set to 0, we assume no expiry
if value := viper.GetString("oidc.expiry"); value == "0" {

View File

@@ -353,7 +353,7 @@ type OIDCUserInfo struct {
// FromClaim overrides a User from OIDC claims.
// 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)
if err == nil {
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)
}
if claims.EmailVerified {
if claims.EmailVerified || !FlexibleBoolean(emailVerifiedRequired) {
_, err = mail.ParseAddress(claims.Email)
if err == nil {
u.Email = claims.Email

View File

@@ -291,12 +291,14 @@ func TestCleanIdentifier(t *testing.T) {
func TestOIDCClaimsJSONToUser(t *testing.T) {
tests := []struct {
name string
jsonstr string
want User
name string
jsonstr string
emailVerifiedRequired bool
want User
}{
{
name: "normal-bool",
name: "normal-bool",
emailVerifiedRequired: true,
jsonstr: `
{
"sub": "test",
@@ -314,7 +316,8 @@ func TestOIDCClaimsJSONToUser(t *testing.T) {
},
},
{
name: "string-bool-true",
name: "string-bool-true",
emailVerifiedRequired: true,
jsonstr: `
{
"sub": "test2",
@@ -332,7 +335,8 @@ func TestOIDCClaimsJSONToUser(t *testing.T) {
},
},
{
name: "string-bool-false",
name: "string-bool-false",
emailVerifiedRequired: true,
jsonstr: `
{
"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
name: "okta-oidc-claim-20250121",
name: "okta-oidc-claim-20250121",
emailVerifiedRequired: true,
jsonstr: `
{
"sub": "00u7dr4qp7XXXXXXXXXX",
@@ -375,6 +399,7 @@ func TestOIDCClaimsJSONToUser(t *testing.T) {
want: User{
Provider: util.RegisterMethodOIDC,
DisplayName: "Tim Horton",
Email: "",
Name: "tim.horton@company.com",
ProviderIdentifier: sql.NullString{
String: "https://sso.company.com/oauth2/default/00u7dr4qp7XXXXXXXXXX",
@@ -384,7 +409,8 @@ func TestOIDCClaimsJSONToUser(t *testing.T) {
},
{
// From https://github.com/juanfont/headscale/issues/2333
name: "okta-oidc-claim-20250121",
name: "okta-oidc-claim-20250121",
emailVerifiedRequired: true,
jsonstr: `
{
"aud": "79xxxxxx-xxxx-xxxx-xxxx-892146xxxxxx",
@@ -409,6 +435,7 @@ func TestOIDCClaimsJSONToUser(t *testing.T) {
Provider: util.RegisterMethodOIDC,
DisplayName: "XXXXXX XXXX",
Name: "user@domain.com",
Email: "",
ProviderIdentifier: sql.NullString{
String: "https://login.microsoftonline.com/v2.0/I-70OQnj3TogrNSfkZQqB3f7dGwyBWSm1dolHNKrMzQ",
Valid: true,
@@ -417,7 +444,8 @@ func TestOIDCClaimsJSONToUser(t *testing.T) {
},
{
// From https://github.com/juanfont/headscale/issues/2333
name: "casby-oidc-claim-20250513",
name: "casby-oidc-claim-20250513",
emailVerifiedRequired: true,
jsonstr: `
{
"sub": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
@@ -458,7 +486,7 @@ func TestOIDCClaimsJSONToUser(t *testing.T) {
var user User
user.FromClaim(&got)
user.FromClaim(&got, tt.emailVerifiedRequired)
if diff := cmp.Diff(user, tt.want); diff != "" {
t.Errorf("TestOIDCClaimsJSONToUser() mismatch (-want +got):\n%s", diff)
}