mirror of
https://github.com/project-zot/zot.git
synced 2026-06-17 12:58:02 +08:00
feat: allow claim mapping for user name with oidc (#3540)
* feat: allow claim mapping for user name with oidc * feat: bats test for claim mapping * test: fix dex config in openid mapping test Signed-off-by: Ramkumar Chinchani <rchincha.dev@gmail.com> * test: add panva idp Signed-off-by: Ramkumar Chinchani <rchincha.dev@gmail.com> * fix: address copilot comments Signed-off-by: Ramkumar Chinchani <rchincha.dev@gmail.com> --------- Signed-off-by: Ramkumar Chinchani <rchincha.dev@gmail.com> Co-authored-by: Sky Moore <i@msky.me>
This commit is contained in:
committed by
GitHub
parent
7fa53f5b0f
commit
64829f9502
+1
-1
@@ -567,7 +567,7 @@ func (rh *RouteHandler) AuthURLHandler() http.HandlerFunc {
|
||||
callback ui where we will redirect after openid/oauth2 logic is completed*/
|
||||
session, _ := rh.c.CookieStore.Get(r, "statecookie")
|
||||
|
||||
session.Options.Secure = true
|
||||
session.Options.Secure = rh.c.Config.UseSecureSession()
|
||||
session.Options.HttpOnly = true
|
||||
session.Options.SameSite = http.SameSiteDefaultMode
|
||||
session.Options.Path = constants.CallbackBasePath
|
||||
|
||||
@@ -170,6 +170,16 @@ type OpenIDProviderConfig struct {
|
||||
AuthURL string
|
||||
TokenURL string
|
||||
Scopes []string
|
||||
ClaimMapping *ClaimMapping `mapstructure:",omitempty"`
|
||||
}
|
||||
|
||||
// ClaimMapping specifies how OpenID claims are mapped to application fields.
|
||||
// It allows customization of which claim is used as the username when authenticating users.
|
||||
type ClaimMapping struct {
|
||||
// Username specifies which OpenID claim to use as the username for the authenticated user.
|
||||
// Acceptable values include "preferred_username", "email", "sub", "name", or any custom claim name.
|
||||
// If not configured, the default is "email".
|
||||
Username string `mapstructure:"username,omitempty"`
|
||||
}
|
||||
|
||||
type MethodRatelimitConfig struct {
|
||||
@@ -611,6 +621,7 @@ func (c *Config) Sanitize() *Config {
|
||||
AuthURL: config.AuthURL,
|
||||
TokenURL: config.TokenURL,
|
||||
Scopes: config.Scopes,
|
||||
ClaimMapping: config.ClaimMapping,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+64
-8
@@ -90,7 +90,7 @@ func (rh *RouteHandler) SetupRoutes() {
|
||||
rp.CodeExchangeHandler(rh.GithubCodeExchangeCallback(), relyingParty))
|
||||
} else if config.IsOpenIDSupported(provider) {
|
||||
rh.c.Router.HandleFunc(constants.CallbackBasePath+"/"+provider,
|
||||
rp.CodeExchangeHandler(rp.UserinfoCallback(rh.OpenIDCodeExchangeCallback()), relyingParty))
|
||||
rp.CodeExchangeHandler(rp.UserinfoCallback(rh.OpenIDCodeExchangeCallbackWithProvider(provider)), relyingParty))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1998,17 +1998,73 @@ func (rh *RouteHandler) GithubCodeExchangeCallback() rp.CodeExchangeCallback[*oi
|
||||
}
|
||||
}
|
||||
|
||||
// Openid CodeExchange callback.
|
||||
// Openid CodeExchange callback (legacy, kept for compatibility).
|
||||
func (rh *RouteHandler) OpenIDCodeExchangeCallback() rp.CodeExchangeUserinfoCallback[
|
||||
*oidc.IDTokenClaims,
|
||||
*oidc.UserInfo,
|
||||
] {
|
||||
return rh.OpenIDCodeExchangeCallbackWithProvider("")
|
||||
}
|
||||
|
||||
// OpenIDCodeExchangeCallbackWithProvider is the OIDC CodeExchange callback that supports configurable claim mapping.
|
||||
// The providerName parameter is used to lookup provider-specific claim mapping configuration.
|
||||
// This differs from the legacy version by allowing per-provider claim mapping based on the providerName.
|
||||
func (rh *RouteHandler) OpenIDCodeExchangeCallbackWithProvider(providerName string) rp.CodeExchangeUserinfoCallback[
|
||||
*oidc.IDTokenClaims,
|
||||
*oidc.UserInfo,
|
||||
] {
|
||||
return func(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens[*oidc.IDTokenClaims], state string,
|
||||
relyingParty rp.RelyingParty, info *oidc.UserInfo,
|
||||
) {
|
||||
email := info.UserInfoEmail.Email
|
||||
if email == "" {
|
||||
rh.c.Log.Error().Msg("failed to set user record for empty email value")
|
||||
// Extract username based on claim mapping configuration
|
||||
var username string
|
||||
authConfig := rh.c.Config.CopyAuthConfig()
|
||||
|
||||
if authConfig != nil && authConfig.OpenID != nil && providerName != "" {
|
||||
if providerConfig, ok := authConfig.OpenID.Providers[providerName]; ok {
|
||||
// Check if claim mapping is configured
|
||||
if providerConfig.ClaimMapping != nil && providerConfig.ClaimMapping.Username != "" {
|
||||
claimName := providerConfig.ClaimMapping.Username
|
||||
|
||||
// Use the configured claim
|
||||
switch claimName {
|
||||
case "preferred_username":
|
||||
username = info.PreferredUsername
|
||||
case "email":
|
||||
username = info.UserInfoEmail.Email
|
||||
case "sub":
|
||||
username = info.Subject
|
||||
case "name":
|
||||
username = info.Name
|
||||
default:
|
||||
// Try to get from custom claims in UserInfo
|
||||
if val, ok := info.Claims[claimName].(string); ok {
|
||||
username = val
|
||||
}
|
||||
}
|
||||
|
||||
if username != "" {
|
||||
rh.c.Log.Debug().
|
||||
Str("provider", providerName).
|
||||
Str("claim", claimName).
|
||||
Str("username", username).
|
||||
Msg("extracted username from configured claim")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to email if no username was extracted
|
||||
if username == "" {
|
||||
username = info.UserInfoEmail.Email
|
||||
rh.c.Log.Debug().
|
||||
Str("provider", providerName).
|
||||
Str("username", username).
|
||||
Msg("using email as username (fallback)")
|
||||
}
|
||||
|
||||
if username == "" {
|
||||
rh.c.Log.Error().Msg("failed to set user record for empty username value")
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
|
||||
return
|
||||
@@ -2018,7 +2074,7 @@ func (rh *RouteHandler) OpenIDCodeExchangeCallback() rp.CodeExchangeUserinfoCall
|
||||
|
||||
val, ok := info.Claims["groups"].([]interface{})
|
||||
if !ok {
|
||||
rh.c.Log.Info().Msgf("failed to find any 'groups' claim for user %s in UserInfo", email)
|
||||
rh.c.Log.Info().Msgf("failed to find any 'groups' claim for user %s in UserInfo", username)
|
||||
}
|
||||
|
||||
for _, group := range val {
|
||||
@@ -2027,7 +2083,7 @@ func (rh *RouteHandler) OpenIDCodeExchangeCallback() rp.CodeExchangeUserinfoCall
|
||||
|
||||
val, ok = tokens.IDTokenClaims.Claims["groups"].([]interface{})
|
||||
if !ok {
|
||||
rh.c.Log.Info().Msgf("failed to find any 'groups' claim for user %s in IDTokenClaimsToken", email)
|
||||
rh.c.Log.Info().Msgf("failed to find any 'groups' claim for user %s in IDTokenClaimsToken", username)
|
||||
}
|
||||
|
||||
for _, group := range val {
|
||||
@@ -2037,7 +2093,7 @@ func (rh *RouteHandler) OpenIDCodeExchangeCallback() rp.CodeExchangeUserinfoCall
|
||||
slices.Sort(groups)
|
||||
groups = slices.Compact(groups)
|
||||
|
||||
callbackUI, err := OAuth2Callback(rh.c, w, r, state, email, groups)
|
||||
callbackUI, err := OAuth2Callback(rh.c, w, r, state, username, groups)
|
||||
if err != nil {
|
||||
if errors.Is(err, zerr.ErrInvalidStateCookie) {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
|
||||
Reference in New Issue
Block a user