{"record":{"id":"dd65d5cccd5d795d","repo":"gotify/server","slug":"issuer-claim-was-empty","errorCode":null,"errorMessage":"issuer claim was empty","messagePattern":"issuer claim was empty","errorType":"http","errorClass":null,"httpStatus":500,"severity":"error","filePath":"api/oidc.go","lineNumber":429,"sourceCode":"func (a *OIDCAPI) generateState() (string, error) {\n\tnonce := make([]byte, 20)\n\tif _, err := rand.Read(nonce); err != nil {\n\t\treturn \"\", err\n\t}\n\treturn hex.EncodeToString(nonce), nil\n}\n\n// resolveUser looks up, links, or creates the user bound to an OIDC identity.\n//\n//  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","sourceCodeStart":411,"sourceCodeEnd":447,"githubUrl":"https://github.com/gotify/server/blob/14bfc256276775c425f988d621dccfe705de18ac/api/oidc.go#L411-L447","documentation":"resolveUser throws 500 'issuer claim was empty' when the OIDC ID token claims have no issuer (iss) value. Gotify builds the OIDC identity as issuer#subject, so a missing issuer makes the identity unusable.","triggerScenarios":"Calling ExternalTokenHandler (and thus resolveUser) with an ID token whose claims lack the iss claim, e.g. a malformed or hand-crafted token or a provider that omits iss.","commonSituations":"Misconfigured custom OIDC provider; testing with hand-built JWTs that omit 'iss'; an upstream proxy or token transformation stripping claims; using a token from a different endpoint than the intended provider.","solutions":["Check the ID token (jwt.io) and ensure the 'iss' claim is present and matches the configured provider URL.","Fix the provider's token issuance configuration.","Obtain the ID token via the standard authorization-code flow instead of hand-crafting it.","Verify GOTIFY_OIDC_ISSUER points to the correct provider."],"exampleFix":"// before\n{\"sub\":\"123\",\"aud\":\"app\"} // no iss\n// after\n{\"iss\":\"https://idp.example.com\",\"sub\":\"123\",\"aud\":\"app\"}","handlingStrategy":"type-guard","validationCode":"const claims = decodeJwt(idToken);\nif (typeof claims.iss !== 'string' || claims.iss.length === 0) throw new Error('token missing iss claim');","typeGuard":"function hasIssuer(claims) { return typeof claims.iss === 'string' && claims.iss.trim() !== ''; }","tryCatchPattern":null,"preventionTips":["Verify ID tokens at jwt.io before integrating a new provider.","Ensure tokens come from the standard authorization-code flow.","Keep GOTIFY_OIDC_ISSUER aligned with the provider's token issuer.","Watch for proxies that rewrite/strip token claims."],"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"}