gotify/server · error

username claim %q is missing

Error message

username claim %q is missing

What it means

After OIDC authentication succeeds but no user is matched, resolveUser extracts the username from the configured claim (a.UsernameClaim) in the ID token or userinfo. If lookupClaim cannot find that claim at all, it returns HTTP 500 with 'username claim %q is missing'. The library requires a non-empty username to map the OIDC identity to a local account.

Source

Thrown at api/oidc.go:467

	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 {
				return nil, http.StatusInternalServerError, fmt.Errorf("database error: %w", err)
			}
			log.Warn().Str("oidc_id", oidcID).Str("username", user.Name).Bool("admin", user.Admin).Msg("OIDC change permission")
		}
		return user, 0, nil
	}

	usernameRaw, ok := lookupClaim(a.UsernameClaim, idToken.Claims, info.Claims)
	if !ok {
		return nil, http.StatusInternalServerError, fmt.Errorf("username claim %q is missing", a.UsernameClaim)
	}
	username := fmt.Sprint(usernameRaw)
	if username == "" || usernameRaw == nil {
		return nil, http.StatusInternalServerError, errors.New("username claim was empty")
	}

	byUsername, err := a.DB.GetUserByName(username)
	if err != nil {
		return nil, http.StatusInternalServerError, fmt.Errorf("database error: %w", err)
	}
	if byUsername != nil {
		return a.linkExistingUser(byUsername, oidcID, hasAdminGroup)
	}
	return a.registerUser(username, oidcID, hasAdminGroup)
}

func (a *OIDCAPI) linkExistingUser(user *model.User, oidcID string, hasAdminGroup bool) (*model.User, int, error) {
	if !a.LinkByUsername {

View on GitHub (pinned to 14bfc25627)

Solutions

  1. Set the username claim env/config to a claim the IdP actually emits (e.g. preferred_username, email, upn)
  2. Request the required scopes (openid profile email) on the OIDC client so the claim is populated
  3. Decode a real ID token (jwt.io) to confirm which claims are present
  4. Enable the userinfo endpoint if relying on info.Claims

Example fix

// before
OIDC_USERNAME_CLAIM=upn
// after
OIDC_USERNAME_CLAIM=preferred_username
Defensive patterns

Strategy: validation

Validate before calling

// before configuring, decode a token and check the claim
claims := map[string]any{}
json.Unmarshal(tokenPayload, &claims)
if _, ok := claims["preferred_username"]; !ok {
    log.Fatal("configured username claim missing from tokens")
}

Type guard

func hasUsernameClaim(claims map[string]any, name string) (string, bool) {
    v, ok := claims[name]
    if !ok { return "", false }
    s, ok := v.(string)
    return s, ok && s != ""
}

Try / catch

user, status, err := resolveUser(...)
if err != nil && strings.Contains(err.Error(), "username claim") {
    // fix OIDC username-claim config or IdP scopes; 500 is server-side misconfig
    http.Error(w, "identity mapping misconfigured", http.StatusInternalServerError)
    return
}

Prevention

When it happens

Trigger: The configured UsernameClaim name does not exist in idToken.Claims or the userinfo claims — e.g. OIDC_USERNAME_CLAIM set to 'preferred_username' while the IdP issues 'email', or 'upn' for Azure AD tokens that lack it.

Common situations: Switching IdPs (Keycloak -> Azure AD/Entra) changes claim names; client scopes (profile/email) not requested so preferred_username is absent; typo in the claim config key; userinfo endpoint disabled while the token lacks the claim.

Related errors


AI-assisted analysis of gotify/server@14bfc25627 (2026-09-05). Data as JSON: /api/errors/61d2db3b3ef1c55d. Report an issue: GitHub.