diff --git a/model/oauth.go b/model/oauth.go index 62943844..0c700455 100644 --- a/model/oauth.go +++ b/model/oauth.go @@ -1,6 +1,7 @@ package model import ( + "encoding/json" "errors" "strconv" "strings" @@ -112,7 +113,10 @@ type OauthUserBase struct { type OidcUser struct { OauthUserBase Sub string `json:"sub"` - VerifiedEmail bool `json:"email_verified"` + // VerifiedEmail is declared as json.RawMessage because some OIDC providers + // (e.g. Amazon Cognito) return email_verified as a JSON string ("true"/"false") + // instead of a JSON boolean, which violates the OIDC Core spec (section 5.1). + VerifiedEmail json.RawMessage `json:"email_verified"` PreferredUsername string `json:"preferred_username"` Picture string `json:"picture"` } @@ -126,12 +130,16 @@ func (ou *OidcUser) ToOauthUser() *OauthUser { username = strings.ToLower(ou.Email) } + // email_verified may be a JSON boolean or a JSON string depending on the provider. + // When the field is absent or null, ou.VerifiedEmail is nil and defaults to false. + verifiedEmail := strings.EqualFold(strings.Trim(string(ou.VerifiedEmail), `"`), "true") + return &OauthUser{ OpenId: ou.Sub, Name: ou.Name, Username: username, Email: ou.Email, - VerifiedEmail: ou.VerifiedEmail, + VerifiedEmail: verifiedEmail, Picture: ou.Picture, } } diff --git a/model/oauth_test.go b/model/oauth_test.go new file mode 100644 index 00000000..bad0bfc9 --- /dev/null +++ b/model/oauth_test.go @@ -0,0 +1,57 @@ +package model + +import ( + "encoding/json" + "testing" +) + +func TestOidcUser_ToOauthUser_EmailVerifiedBoolTrue(t *testing.T) { + u := &OidcUser{VerifiedEmail: json.RawMessage(`true`)} + if !u.ToOauthUser().VerifiedEmail { + t.Fatal("expected true for JSON boolean true") + } +} + +func TestOidcUser_ToOauthUser_EmailVerifiedBoolFalse(t *testing.T) { + u := &OidcUser{VerifiedEmail: json.RawMessage(`false`)} + if u.ToOauthUser().VerifiedEmail { + t.Fatal("expected false for JSON boolean false") + } +} + +func TestOidcUser_ToOauthUser_EmailVerifiedStringTrue(t *testing.T) { + u := &OidcUser{VerifiedEmail: json.RawMessage(`"true"`)} + if !u.ToOauthUser().VerifiedEmail { + t.Fatal("expected true for JSON string \"true\"") + } +} + +func TestOidcUser_ToOauthUser_EmailVerifiedStringFalse(t *testing.T) { + u := &OidcUser{VerifiedEmail: json.RawMessage(`"false"`)} + if u.ToOauthUser().VerifiedEmail { + t.Fatal("expected false for JSON string \"false\"") + } +} + +func TestOidcUser_ToOauthUser_EmailVerifiedStringCaseInsensitive(t *testing.T) { + for _, s := range []string{`"True"`, `"TRUE"`} { + u := &OidcUser{VerifiedEmail: json.RawMessage(s)} + if !u.ToOauthUser().VerifiedEmail { + t.Fatalf("expected true for %s", s) + } + } +} + +func TestOidcUser_ToOauthUser_EmailVerifiedNull(t *testing.T) { + u := &OidcUser{VerifiedEmail: json.RawMessage(`null`)} + if u.ToOauthUser().VerifiedEmail { + t.Fatal("expected false for JSON null") + } +} + +func TestOidcUser_ToOauthUser_EmailVerifiedAbsent(t *testing.T) { + u := &OidcUser{} + if u.ToOauthUser().VerifiedEmail { + t.Fatal("expected false when email_verified field is absent") + } +}