OIDC: Add "address" to the default userinfo request scopes #782

see https://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims

Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
Michael Mayer
2024-07-09 06:55:06 +02:00
parent 5546a56183
commit e87f32fa5c
10 changed files with 54 additions and 43 deletions

View File

@@ -136,8 +136,13 @@ func OIDCRedirect(router *gin.RouterGroup) {
userName = user.Username() userName = user.Username()
event.AuditInfo([]string{clientIp, "create session", "oidc", "found user", userName}) event.AuditInfo([]string{clientIp, "create session", "oidc", "found user", userName})
// Check if OIDC subject identifier matches. // Check if the account is enabled and the OIDC Subject ID matches.
if authn.ProviderOIDC.NotEqual(user.AuthProvider) { if !user.CanLogIn() {
event.AuditErr([]string{clientIp, "create session", "oidc", userName, authn.ErrAccountDisabled.Error()})
event.LoginError(clientIp, "oidc", userName, userAgent, authn.ErrAccountDisabled.Error())
c.HTML(http.StatusUnauthorized, "auth.gohtml", CreateSessionError(http.StatusUnauthorized, i18n.Error(i18n.ErrInvalidCredentials)))
return
} else if authn.ProviderOIDC.NotEqual(user.AuthProvider) {
event.AuditErr([]string{clientIp, "create session", "oidc", userName, authn.ErrAuthProviderIsNotOIDC.Error()}) event.AuditErr([]string{clientIp, "create session", "oidc", userName, authn.ErrAuthProviderIsNotOIDC.Error()})
event.LoginError(clientIp, "oidc", userName, userAgent, authn.ErrAuthProviderIsNotOIDC.Error()) event.LoginError(clientIp, "oidc", userName, userAgent, authn.ErrAuthProviderIsNotOIDC.Error())
c.HTML(http.StatusUnauthorized, "auth.gohtml", CreateSessionError(http.StatusUnauthorized, i18n.Error(i18n.ErrInvalidCredentials))) c.HTML(http.StatusUnauthorized, "auth.gohtml", CreateSessionError(http.StatusUnauthorized, i18n.Error(i18n.ErrInvalidCredentials)))
@@ -297,7 +302,7 @@ func OIDCRedirect(router *gin.RouterGroup) {
return return
} }
// Login allowed? // Check if login is allowed.
if !user.CanLogIn() { if !user.CanLogIn() {
event.AuditErr([]string{clientIp, "create session", "oidc", userName, authn.ErrAccountDisabled.Error()}) event.AuditErr([]string{clientIp, "create session", "oidc", userName, authn.ErrAccountDisabled.Error()})
event.LoginError(clientIp, "oidc", userName, userAgent, authn.ErrAccountDisabled.Error()) event.LoginError(clientIp, "oidc", userName, userAgent, authn.ErrAccountDisabled.Error())
@@ -305,7 +310,7 @@ func OIDCRedirect(router *gin.RouterGroup) {
return return
} }
// Update subject identifier (auth id). // Update Subject ID (auth_id).
user.SetAuthID(userInfo.Subject) user.SetAuthID(userInfo.Subject)
// Step 2: Create user session. // Step 2: Create user session.

View File

@@ -16,6 +16,7 @@ import (
"github.com/zitadel/oidc/v3/pkg/oidc" "github.com/zitadel/oidc/v3/pkg/oidc"
"github.com/photoprism/photoprism/internal/event" "github.com/photoprism/photoprism/internal/event"
"github.com/photoprism/photoprism/pkg/authn"
"github.com/photoprism/photoprism/pkg/clean" "github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/rnd" "github.com/photoprism/photoprism/pkg/rnd"
) )
@@ -96,7 +97,7 @@ func NewClient(issuerUri *url.URL, oidcClient, oidcSecret, oidcScopes, siteUrl s
// Set default scopes if no scopes were specified. // Set default scopes if no scopes were specified.
if oidcScopes == "" { if oidcScopes == "" {
oidcScopes = "openid email profile" oidcScopes = authn.OidcRequiredScopes
} }
event.AuditDebug([]string{"oidc", "provider", "scopes", oidcScopes}) event.AuditDebug([]string{"oidc", "provider", "scopes", oidcScopes})

View File

@@ -4,6 +4,8 @@ import (
"net/url" "net/url"
"testing" "testing"
"github.com/photoprism/photoprism/pkg/authn"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
@@ -17,7 +19,7 @@ func TestNewClient(t *testing.T) {
uri, uri,
"csg6yqvykh0780f9", "csg6yqvykh0780f9",
"nd09wkee0ElsMvzLGkgWS9wJAttHwF2h", "nd09wkee0ElsMvzLGkgWS9wJAttHwF2h",
"openid email profile", authn.OidcDefaultScopes,
"https://app.localssl.dev/", "https://app.localssl.dev/",
false, false,
) )
@@ -34,7 +36,7 @@ func TestNewClient(t *testing.T) {
uri, uri,
"csg6yqvykh0780f9", "csg6yqvykh0780f9",
"nd09wkee0ElsMvzLGkgWS9wJAttHwF2h", "nd09wkee0ElsMvzLGkgWS9wJAttHwF2h",
"openid email profile", authn.OidcDefaultScopes,
"https://app.localssl.dev/", "https://app.localssl.dev/",
true, true,
) )

View File

@@ -10,7 +10,7 @@ import (
// Username returns the preferred username based on the userinfo and the preferred username OIDC claim. // Username returns the preferred username based on the userinfo and the preferred username OIDC claim.
func Username(userInfo *oidc.UserInfo, preferredClaim string) (userName string) { func Username(userInfo *oidc.UserInfo, preferredClaim string) (userName string) {
switch preferredClaim { switch preferredClaim {
case authn.ClaimName: case authn.OidcClaimName:
if name := clean.Handle(userInfo.Name); len(name) > 0 { if name := clean.Handle(userInfo.Name); len(name) > 0 {
userName = name userName = name
} else if name = clean.Handle(userInfo.PreferredUsername); len(name) > 0 { } else if name = clean.Handle(userInfo.PreferredUsername); len(name) > 0 {
@@ -20,7 +20,7 @@ func Username(userInfo *oidc.UserInfo, preferredClaim string) (userName string)
} else if name = clean.Email(userInfo.Email); userInfo.EmailVerified && len(name) > 4 { } else if name = clean.Email(userInfo.Email); userInfo.EmailVerified && len(name) > 4 {
userName = name userName = name
} }
case authn.ClaimNickname: case authn.OidcClaimNickname:
if name := clean.Handle(userInfo.Nickname); len(name) > 0 { if name := clean.Handle(userInfo.Nickname); len(name) > 0 {
userName = name userName = name
} else if name = clean.Handle(userInfo.PreferredUsername); len(name) > 0 { } else if name = clean.Handle(userInfo.PreferredUsername); len(name) > 0 {
@@ -30,7 +30,7 @@ func Username(userInfo *oidc.UserInfo, preferredClaim string) (userName string)
} else if name = clean.Email(userInfo.Email); userInfo.EmailVerified && len(name) > 4 { } else if name = clean.Email(userInfo.Email); userInfo.EmailVerified && len(name) > 4 {
userName = name userName = name
} }
case authn.ClaimEmail: case authn.OidcClaimEmail:
if name := clean.Email(userInfo.Email); userInfo.EmailVerified && len(name) > 4 { if name := clean.Email(userInfo.Email); userInfo.EmailVerified && len(name) > 4 {
userName = name userName = name
} else if name = clean.Handle(userInfo.PreferredUsername); len(name) > 0 { } else if name = clean.Handle(userInfo.PreferredUsername); len(name) > 0 {

View File

@@ -10,7 +10,7 @@ import (
) )
func TestUsername(t *testing.T) { func TestUsername(t *testing.T) {
t.Run("ClaimPreferredUsername", func(t *testing.T) { t.Run("PreferredUsername", func(t *testing.T) {
info := &oidc.UserInfo{} info := &oidc.UserInfo{}
info.Name = "Jane Doe" info.Name = "Jane Doe"
info.GivenName = "Jane" info.GivenName = "Jane"
@@ -19,19 +19,19 @@ func TestUsername(t *testing.T) {
info.EmailVerified = true info.EmailVerified = true
info.Subject = "e3a9f4a6-9d60-47cb-9bf5-02bd15b0c68d" info.Subject = "e3a9f4a6-9d60-47cb-9bf5-02bd15b0c68d"
info.PreferredUsername = "Jane Doe" info.PreferredUsername = "Jane Doe"
result := Username(info, authn.ClaimPreferredUsername) result := Username(info, authn.OidcClaimPreferredUsername)
assert.Equal(t, "jane.doe", result) assert.Equal(t, "jane.doe", result)
}) })
t.Run("ClaimPreferredUsernameMissing", func(t *testing.T) { t.Run("PreferredUsernameMissing", func(t *testing.T) {
info := &oidc.UserInfo{} info := &oidc.UserInfo{}
info.Name = "Jane Doe" info.Name = "Jane Doe"
info.FamilyName = "Doe" info.FamilyName = "Doe"
info.Email = "jane@doe.com" info.Email = "jane@doe.com"
info.EmailVerified = true info.EmailVerified = true
result := Username(info, authn.ClaimPreferredUsername) result := Username(info, authn.OidcClaimPreferredUsername)
assert.Equal(t, "jane.doe", result) assert.Equal(t, "jane.doe", result)
}) })
t.Run("ClaimName", func(t *testing.T) { t.Run("Name", func(t *testing.T) {
info := &oidc.UserInfo{} info := &oidc.UserInfo{}
info.Name = "Jane Doe" info.Name = "Jane Doe"
info.GivenName = "Jane" info.GivenName = "Jane"
@@ -40,10 +40,10 @@ func TestUsername(t *testing.T) {
info.Email = "jane@doe.com" info.Email = "jane@doe.com"
info.EmailVerified = true info.EmailVerified = true
info.Subject = "abcd123" info.Subject = "abcd123"
result := Username(info, authn.ClaimName) result := Username(info, authn.OidcClaimName)
assert.Equal(t, "jane.doe", result) assert.Equal(t, "jane.doe", result)
}) })
t.Run("ClaimNickname", func(t *testing.T) { t.Run("Nickname", func(t *testing.T) {
info := &oidc.UserInfo{} info := &oidc.UserInfo{}
info.Name = "Jane Doe" info.Name = "Jane Doe"
info.GivenName = "Jane" info.GivenName = "Jane"
@@ -52,10 +52,10 @@ func TestUsername(t *testing.T) {
info.Email = "jane@doe.com" info.Email = "jane@doe.com"
info.EmailVerified = true info.EmailVerified = true
info.Subject = "abcd123" info.Subject = "abcd123"
result := Username(info, authn.ClaimNickname) result := Username(info, authn.OidcClaimNickname)
assert.Equal(t, "jens.mander", result) assert.Equal(t, "jens.mander", result)
}) })
t.Run("ClaimEmail", func(t *testing.T) { t.Run("Email", func(t *testing.T) {
info := &oidc.UserInfo{} info := &oidc.UserInfo{}
info.Name = "Jane Doe" info.Name = "Jane Doe"
info.GivenName = "Jane" info.GivenName = "Jane"
@@ -63,7 +63,7 @@ func TestUsername(t *testing.T) {
info.Email = "jane@doe.com" info.Email = "jane@doe.com"
info.EmailVerified = true info.EmailVerified = true
info.Subject = "abcd123" info.Subject = "abcd123"
result := Username(info, authn.ClaimEmail) result := Username(info, authn.OidcClaimEmail)
assert.Equal(t, "jane@doe.com", result) assert.Equal(t, "jane@doe.com", result)
}) })
} }

View File

@@ -61,7 +61,7 @@ func (c *Config) OIDCSecret() string {
// OIDCScopes returns the user information scopes for single sign-on via OIDC. // OIDCScopes returns the user information scopes for single sign-on via OIDC.
func (c *Config) OIDCScopes() string { func (c *Config) OIDCScopes() string {
if c.options.OIDCScopes == "" { if c.options.OIDCScopes == "" {
return authn.OidcScopes return authn.OidcDefaultScopes
} }
return c.options.OIDCScopes return c.options.OIDCScopes
@@ -98,14 +98,14 @@ func (c *Config) OIDCRegister() bool {
// OIDCUsername returns the preferred username claim for new users signing up via OIDC. // OIDCUsername returns the preferred username claim for new users signing up via OIDC.
func (c *Config) OIDCUsername() string { func (c *Config) OIDCUsername() string {
switch c.options.OIDCUsername { switch c.options.OIDCUsername {
case authn.ClaimName: case authn.OidcClaimName:
return authn.ClaimName return authn.OidcClaimName
case authn.ClaimNickname: case authn.OidcClaimNickname:
return authn.ClaimNickname return authn.OidcClaimNickname
case authn.ClaimEmail: case authn.OidcClaimEmail:
return authn.ClaimEmail return authn.OidcClaimEmail
default: default:
return authn.ClaimPreferredUsername return authn.OidcClaimPreferredUsername
} }
} }

View File

@@ -77,11 +77,11 @@ func TestConfig_OIDCSecret(t *testing.T) {
func TestConfig_OIDCScopes(t *testing.T) { func TestConfig_OIDCScopes(t *testing.T) {
c := NewConfig(CliTestContext()) c := NewConfig(CliTestContext())
assert.Equal(t, authn.OidcScopes, c.OIDCScopes()) assert.Equal(t, authn.OidcDefaultScopes, c.OIDCScopes())
c.options.OIDCScopes = "" c.options.OIDCScopes = ""
assert.Equal(t, authn.OidcScopes, c.OIDCScopes()) assert.Equal(t, authn.OidcDefaultScopes, c.OIDCScopes())
} }
func TestConfig_OIDCProvider(t *testing.T) { func TestConfig_OIDCProvider(t *testing.T) {
@@ -113,23 +113,23 @@ func TestConfig_OIDCRedirect(t *testing.T) {
func TestConfig_OIDCUsername(t *testing.T) { func TestConfig_OIDCUsername(t *testing.T) {
c := NewConfig(CliTestContext()) c := NewConfig(CliTestContext())
assert.Equal(t, authn.ClaimPreferredUsername, c.OIDCUsername()) assert.Equal(t, authn.OidcClaimPreferredUsername, c.OIDCUsername())
c.options.OIDCUsername = "name" c.options.OIDCUsername = "name"
assert.Equal(t, authn.ClaimName, c.OIDCUsername()) assert.Equal(t, authn.OidcClaimName, c.OIDCUsername())
c.options.OIDCUsername = "nickname" c.options.OIDCUsername = "nickname"
assert.Equal(t, authn.ClaimNickname, c.OIDCUsername()) assert.Equal(t, authn.OidcClaimNickname, c.OIDCUsername())
c.options.OIDCUsername = "email" c.options.OIDCUsername = "email"
assert.Equal(t, authn.ClaimEmail, c.OIDCUsername()) assert.Equal(t, authn.OidcClaimEmail, c.OIDCUsername())
c.options.OIDCUsername = "" c.options.OIDCUsername = ""
assert.Equal(t, authn.ClaimPreferredUsername, c.OIDCUsername()) assert.Equal(t, authn.OidcClaimPreferredUsername, c.OIDCUsername())
} }
func TestConfig_OIDCDomain(t *testing.T) { func TestConfig_OIDCDomain(t *testing.T) {

View File

@@ -66,7 +66,7 @@ var Flags = CliFlags{
Name: "oidc-scopes", Name: "oidc-scopes",
Hidden: true, Hidden: true,
Usage: "user information `SCOPES` for single sign-on via OpenID Connect", Usage: "user information `SCOPES` for single sign-on via OpenID Connect",
Value: authn.OidcScopes, Value: authn.OidcDefaultScopes,
EnvVar: EnvVar("OIDC_SCOPES"), EnvVar: EnvVar("OIDC_SCOPES"),
}}, { }}, {
Flag: cli.StringFlag{ Flag: cli.StringFlag{
@@ -94,7 +94,7 @@ var Flags = CliFlags{
Flag: cli.StringFlag{ Flag: cli.StringFlag{
Name: "oidc-username", Name: "oidc-username",
Usage: "preferred username `CLAIM` for new OpenID Connect users (preferred_username, name, nickname, email)", Usage: "preferred username `CLAIM` for new OpenID Connect users (preferred_username, name, nickname, email)",
Value: authn.ClaimPreferredUsername, Value: authn.OidcClaimPreferredUsername,
EnvVar: EnvVar("OIDC_USERNAME"), EnvVar: EnvVar("OIDC_USERNAME"),
}}, { }}, {
Flag: cli.BoolFlag{ Flag: cli.BoolFlag{

View File

@@ -267,7 +267,7 @@ func CliTestContext() *cli.Context {
LogErr(c.Set("oidc-uri", config.OIDCUri)) LogErr(c.Set("oidc-uri", config.OIDCUri))
LogErr(c.Set("oidc-client", config.OIDCClient)) LogErr(c.Set("oidc-client", config.OIDCClient))
LogErr(c.Set("oidc-secret", config.OIDCSecret)) LogErr(c.Set("oidc-secret", config.OIDCSecret))
LogErr(c.Set("oidc-scopes", authn.OidcScopes)) LogErr(c.Set("oidc-scopes", authn.OidcDefaultScopes))
LogErr(c.Set("storage-path", config.StoragePath)) LogErr(c.Set("storage-path", config.StoragePath))
LogErr(c.Set("sidecar-path", config.SidecarPath)) LogErr(c.Set("sidecar-path", config.SidecarPath))
LogErr(c.Set("sidecar-yaml", fmt.Sprintf("%t", config.SidecarYaml))) LogErr(c.Set("sidecar-yaml", fmt.Sprintf("%t", config.SidecarYaml)))

View File

@@ -1,9 +1,12 @@
package authn package authn
// OpenID Connect (OIDC) scope and claim identifiers:
// https://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims
const ( const (
ClaimPreferredUsername = "preferred_username" OidcClaimPreferredUsername = "preferred_username"
ClaimEmail = "email" OidcClaimEmail = "email"
ClaimName = "name" OidcClaimName = "name"
ClaimNickname = "nickname" OidcClaimNickname = "nickname"
OidcScopes = "openid email profile" OidcRequiredScopes = "openid email profile"
OidcDefaultScopes = "openid email profile address"
) )