{"record":{"id":"d7c08e5f6e6c0356","repo":"gotify/server","slug":"subject-claim-was-empty","errorCode":null,"errorMessage":"subject claim was empty","messagePattern":"subject claim was empty","errorType":"http","errorClass":null,"httpStatus":500,"severity":"error","filePath":"api/oidc.go","lineNumber":439,"sourceCode":"//  1. Look up the user by OIDC id (<iss>#<sub>). If found, use it.\n//  2. Otherwise look up a user by the username claim. If one exists, link it to\n//     this OIDC identity, which requires GOTIFY_OIDC_LINK_BY_USERNAME and\n//     that the user is not already bound to a different identity.\n//  3. Otherwise auto-register a new user, which requires GOTIFY_OIDC_AUTOREGISTER.\nfunc (a *OIDCAPI) resolveUser(idToken *oidc.IDTokenClaims, info *oidc.UserInfo) (*model.User, int, error) {\n\tissuer := idToken.GetIssuer()\n\tif issuer == \"\" {\n\t\treturn nil, http.StatusInternalServerError, errors.New(\"issuer claim was empty\")\n\t}\n\tif _, err := url.Parse(issuer); err != nil {\n\t\treturn nil, http.StatusInternalServerError, fmt.Errorf(\"issuer url %q is not a valid url: %w\", issuer, err)\n\t}\n\tif strings.Contains(issuer, \"#\") {\n\t\treturn nil, http.StatusInternalServerError, fmt.Errorf(\"issuer url %q may not contain a fragment\", issuer)\n\t}\n\tsubject := info.GetSubject()\n\tif subject == \"\" {\n\t\treturn nil, http.StatusInternalServerError, errors.New(\"subject claim was empty\")\n\t}\n\toidcID := issuer + \"#\" + subject\n\n\tuser, err := a.DB.GetUserByOIDC(oidcID)\n\tif err != nil {\n\t\treturn nil, http.StatusInternalServerError, fmt.Errorf(\"database error: %w\", err)\n\t}\n\n\thasAdminGroup, status, err := a.resolvePermission(idToken.Claims, info.Claims)\n\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 {","sourceCodeStart":421,"sourceCodeEnd":457,"githubUrl":"https://github.com/gotify/server/blob/14bfc256276775c425f988d621dccfe705de18ac/api/oidc.go#L421-L457","documentation":"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).","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nscope=profile // sub may be missing in userinfo\n// after\nscope=openid profile email // ensures sub is returned","handlingStrategy":"validation","validationCode":"const userinfo = await fetch(providerUserinfoEndpoint, {headers:{Authorization:`Bearer ${access}`}}).then(r=>r.json());\nif (!userinfo.sub) throw new Error('provider userinfo missing sub; request openid scope');","typeGuard":"function hasSubject(info) { return info != null && typeof info.sub === 'string' && info.sub !== ''; }","tryCatchPattern":null,"preventionTips":["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."],"tags":["oidc","jwt","http-500","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"}