From 1517041363a8075b7c63899e90a693a2b9bf28fc Mon Sep 17 00:00:00 2001 From: Aram Akhavan Date: Thu, 28 May 2026 11:48:37 -0700 Subject: [PATCH] fix: fetch user info from OIDC userinfo endpoint (#698) The OIDC client was only extracting claims from the ID token, but many OIDC providers (like Authelia) don't include all user information in the ID token. Fields like 'preferred_username' are typically only available via the userinfo endpoint. This fix fetches additional user information from the provider's userinfo endpoint and merges it with the ID token claims, ensuring that all required user fields are available for user registration and login. Fixes #697 Signed-off-by: Aram Akhavan <1147328+kaysond@users.noreply.github.com> --- internal/app/auth/auth_oidc.go | 37 +++++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/internal/app/auth/auth_oidc.go b/internal/app/auth/auth_oidc.go index 5bcdbc7..bfb0bcb 100644 --- a/internal/app/auth/auth_oidc.go +++ b/internal/app/auth/auth_oidc.go @@ -144,7 +144,7 @@ func (o OidcAuthenticator) Exchange(ctx context.Context, code string, opts ...oa return o.cfg.Exchange(ctx, code, opts...) } -// GetUserInfo retrieves the user info from the token. +// GetUserInfo retrieves the user info from the token and the userinfo endpoint. func (o OidcAuthenticator) GetUserInfo(ctx context.Context, token *oauth2.Token, nonce string) ( map[string]any, error, @@ -182,6 +182,41 @@ func (o OidcAuthenticator) GetUserInfo(ctx context.Context, token *oauth2.Token, return nil, fmt.Errorf("failed to parse extra claims: %w", err) } + // Fetch additional user information from the userinfo endpoint + userInfo, err := o.provider.UserInfo(ctx, oauth2.StaticTokenSource(token)) + if err != nil { + if o.sensitiveInfoLogging { + slog.Debug("OIDC: failed to fetch user info from endpoint", "provider", o.name, "error", err) + } + // Don't fail the entire flow if userinfo endpoint is unavailable; + // ID token claims may be sufficient + slog.Debug("OIDC: proceeding with ID token claims only", "provider", o.name) + } else { + // Parse claims from userinfo endpoint response + var userInfoFields map[string]any + if err = userInfo.Claims(&userInfoFields); err != nil { + if o.sensitiveInfoLogging { + slog.Debug("OIDC: failed to parse userinfo claims", "provider", o.name, "error", err) + } + // Don't fail if we can't parse userinfo; continue with ID token claims + slog.Debug("OIDC: proceeding with ID token claims only", "provider", o.name) + } else { + // Merge userinfo fields into tokenFields, preferring ID token claims + for key, value := range userInfoFields { + if _, exists := tokenFields[key]; !exists { + tokenFields[key] = value + } + } + + if o.userInfoLogging { + contents, _ := json.Marshal(userInfoFields) + slog.Debug("OIDC: user info from endpoint", + "source", o.name, + "info", string(contents)) + } + } + } + if o.userInfoLogging { contents, _ := json.Marshal(tokenFields) slog.Debug("OIDC: user info debug",