gotify/server · error
subject claim was empty
Error message
subject claim was empty
What it means
resolveUser throws 500 'subject claim was empty' when the OIDC UserInfo has an empty subject (sub). The sub uniquely identifies the end user at the provider and is required to construct the stored OIDC identity (issuer#subject).
Source
Thrown at api/oidc.go:439
// 1. Look up the user by OIDC id (<iss>#<sub>). If found, use it.
// 2. Otherwise look up a user by the username claim. If one exists, link it to
// this OIDC identity, which requires GOTIFY_OIDC_LINK_BY_USERNAME and
// that the user is not already bound to a different identity.
// 3. Otherwise auto-register a new user, which requires GOTIFY_OIDC_AUTOREGISTER.
func (a *OIDCAPI) resolveUser(idToken *oidc.IDTokenClaims, info *oidc.UserInfo) (*model.User, int, error) {
issuer := idToken.GetIssuer()
if issuer == "" {
return nil, http.StatusInternalServerError, errors.New("issuer claim was empty")
}
if _, err := url.Parse(issuer); err != nil {
return nil, http.StatusInternalServerError, fmt.Errorf("issuer url %q is not a valid url: %w", issuer, err)
}
if strings.Contains(issuer, "#") {
return nil, http.StatusInternalServerError, fmt.Errorf("issuer url %q may not contain a fragment", issuer)
}
subject := info.GetSubject()
if subject == "" {
return nil, http.StatusInternalServerError, errors.New("subject claim was empty")
}
oidcID := issuer + "#" + subject
user, err := a.DB.GetUserByOIDC(oidcID)
if err != nil {
return nil, http.StatusInternalServerError, fmt.Errorf("database error: %w", err)
}
hasAdminGroup, status, err := a.resolvePermission(idToken.Claims, info.Claims)
if err != nil {
log.Err(err).Str("oidc_id", oidcID).Interface("idTokenClaims", idToken.Claims).Interface("userinfoClaims", info.Claims).Msg("OIDC: resolve permission")
return nil, status, err
}
if user != nil {
if len(a.GroupsAdmin) > 0 && user.Admin != hasAdminGroup {
user.Admin = hasAdminGroup
if err := a.DB.UpdateUser(user); err != nil {View on GitHub (pinned to 14bfc25627)
Solutions
- Request the required scopes (at least openid) so sub is returned.
- Inspect the UserInfo response from the provider to confirm sub is present and non-empty.
- Fix the provider's claim mapping for the client.
- Update the provider configuration so UserInfo always includes the subject.
Example fix
// before scope=profile // sub may be missing in userinfo // after scope=openid profile email // ensures sub is returned
Defensive patterns
Strategy: validation
Validate before calling
const userinfo = await fetch(providerUserinfoEndpoint, {headers:{Authorization:`Bearer ${access}`}}).then(r=>r.json());
if (!userinfo.sub) throw new Error('provider userinfo missing sub; request openid scope'); Type guard
function hasSubject(info) { return info != null && typeof info.sub === 'string' && info.sub !== ''; } Prevention
- Always request the openid scope.
- Test the provider's /userinfo response during setup.
- Ensure IdP claim mapping populates sub for all user types.
- Re-test after any provider scope changes.
When it happens
Trigger: ExternalTokenHandler resolving a user where idToken/UserInfo carry an empty or missing sub claim — providers that don't return UserInfo sub, or misconfigured claim mapping/scopes.
Common situations: Provider that returns an empty UserInfo due to missing 'openid'/'profile' scopes; buggy custom identity provider; scopes changed on the provider side after initial setup.
Related errors
- issuer claim was empty
- username claim was empty
- username claim %q is missing
- failed to bind user to OIDC identity: %w
- failed to create user: %w
AI-assisted analysis of gotify/server@14bfc25627 (2026-09-05).
Data as JSON: /api/errors/d7c08e5f6e6c0356.
Report an issue: GitHub.