{"record":{"id":"61d2db3b3ef1c55d","repo":"gotify/server","slug":"username-claim-q-is-missing","errorCode":null,"errorMessage":"username claim %q is missing","messagePattern":"username claim %q is missing","errorType":"http","errorClass":null,"httpStatus":500,"severity":"error","filePath":"api/oidc.go","lineNumber":467,"sourceCode":"\tif err != nil {\n\t\tlog.Err(err).Str(\"oidc_id\", oidcID).Interface(\"idTokenClaims\", idToken.Claims).Interface(\"userinfoClaims\", info.Claims).Msg(\"OIDC: resolve permission\")\n\t\treturn nil, status, err\n\t}\n\n\tif user != nil {\n\t\tif len(a.GroupsAdmin) > 0 && user.Admin != hasAdminGroup {\n\t\t\tuser.Admin = hasAdminGroup\n\t\t\tif err := a.DB.UpdateUser(user); err != nil {\n\t\t\t\treturn nil, http.StatusInternalServerError, fmt.Errorf(\"database error: %w\", err)\n\t\t\t}\n\t\t\tlog.Warn().Str(\"oidc_id\", oidcID).Str(\"username\", user.Name).Bool(\"admin\", user.Admin).Msg(\"OIDC change permission\")\n\t\t}\n\t\treturn user, 0, nil\n\t}\n\n\tusernameRaw, ok := lookupClaim(a.UsernameClaim, idToken.Claims, info.Claims)\n\tif !ok {\n\t\treturn nil, http.StatusInternalServerError, fmt.Errorf(\"username claim %q is missing\", a.UsernameClaim)\n\t}\n\tusername := fmt.Sprint(usernameRaw)\n\tif username == \"\" || usernameRaw == nil {\n\t\treturn nil, http.StatusInternalServerError, errors.New(\"username claim was empty\")\n\t}\n\n\tbyUsername, err := a.DB.GetUserByName(username)\n\tif err != nil {\n\t\treturn nil, http.StatusInternalServerError, fmt.Errorf(\"database error: %w\", err)\n\t}\n\tif byUsername != nil {\n\t\treturn a.linkExistingUser(byUsername, oidcID, hasAdminGroup)\n\t}\n\treturn a.registerUser(username, oidcID, hasAdminGroup)\n}\n\nfunc (a *OIDCAPI) linkExistingUser(user *model.User, oidcID string, hasAdminGroup bool) (*model.User, int, error) {\n\tif !a.LinkByUsername {","sourceCodeStart":449,"sourceCodeEnd":485,"githubUrl":"https://github.com/gotify/server/blob/14bfc256276775c425f988d621dccfe705de18ac/api/oidc.go#L449-L485","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Set the username claim env/config to a claim the IdP actually emits (e.g. preferred_username, email, upn)","Request the required scopes (openid profile email) on the OIDC client so the claim is populated","Decode a real ID token (jwt.io) to confirm which claims are present","Enable the userinfo endpoint if relying on info.Claims"],"exampleFix":"// before\nOIDC_USERNAME_CLAIM=upn\n// after\nOIDC_USERNAME_CLAIM=preferred_username","handlingStrategy":"validation","validationCode":"// before configuring, decode a token and check the claim\nclaims := map[string]any{}\njson.Unmarshal(tokenPayload, &claims)\nif _, ok := claims[\"preferred_username\"]; !ok {\n    log.Fatal(\"configured username claim missing from tokens\")\n}","typeGuard":"func hasUsernameClaim(claims map[string]any, name string) (string, bool) {\n    v, ok := claims[name]\n    if !ok { return \"\", false }\n    s, ok := v.(string)\n    return s, ok && s != \"\"\n}","tryCatchPattern":"user, status, err := resolveUser(...)\nif err != nil && strings.Contains(err.Error(), \"username claim\") {\n    // fix OIDC username-claim config or IdP scopes; 500 is server-side misconfig\n    http.Error(w, \"identity mapping misconfigured\", http.StatusInternalServerError)\n    return\n}","preventionTips":["Request openid profile email scopes on the OIDC client","Verify claim names against a decoded token after any IdP change","Document the required claim per IdP (Keycloak/Azure/ADFS differ)","Add a startup self-test that validates a sample token's claims"],"tags":["oidc","jwt","configuration","claims"],"backgroundTag":"jwt-missing-claim","analyzedSha":"14bfc256276775c425f988d621dccfe705de18ac","analyzedAt":"2026-09-05T12:52:36.781Z","contentChangedAt":"2026-09-05T12:52:36.781Z","schemaVersion":2},"datasetVersion":"2026-09-12T17:17:11.597Z"}