{"record":{"id":"abf56805ab5d060f","repo":"gotify/server","slug":"issuer-url-q-may-not-contain-a-fragment","errorCode":null,"errorMessage":"issuer url %q may not contain a fragment","messagePattern":"issuer url %q may not contain a fragment","errorType":"http","errorClass":null,"httpStatus":500,"severity":"error","filePath":"api/oidc.go","lineNumber":435,"sourceCode":"}\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\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","sourceCodeStart":417,"sourceCodeEnd":453,"githubUrl":"https://github.com/gotify/server/blob/14bfc256276775c425f988d621dccfe705de18ac/api/oidc.go#L417-L453","documentation":"resolveUser composes the user's oidcID as `issuer + \"#\" + subject`. An issuer containing a fragment would corrupt that composite key, so any issuer containing '#' is rejected with 500 'issuer url %q may not contain a fragment'.","triggerScenarios":"An ID token whose iss claim contains a '#' fragment (e.g. 'https://idp.example.com/app#fragment') reaches resolveUser during the external token flow.","commonSituations":"IdP (e.g. some multi-tenant or portalled providers) that appends fragments to its issuer; misconfigured issuer including an anchor; accepting tokens from a nonstandard provider.","solutions":["Reconfigure the IdP to advertise a fragment-free issuer URL","Restrict accepted issuers to known-good values","If the provider cannot change, proxy it with a normalized issuer"],"exampleFix":"// before\nissuer: \"https://idp.example.com/oidc#tenant-a\"\n// after\nissuer: \"https://idp.example.com/oidc/tenant-a\"","handlingStrategy":"validation","validationCode":"if (typeof idToken.iss === 'string' && idToken.iss.includes('#')) {\n  throw new Error(`issuer url ${idToken.iss} may not contain a fragment`);\n}","typeGuard":"function isFragmentFreeIssuer(iss) {\n  return typeof iss === 'string' && iss.length > 0 && !iss.includes('#');\n}","tryCatchPattern":null,"preventionTips":["Restrict accepted issuers to an allowlist of fragment-free URLs","Reconfigure IdPs that include fragments in their issuer","Validate the issuer against discovery metadata before token acceptance"],"tags":["oidc","url-validation","authentication","issuer"],"backgroundTag":"invalid-issuer-url","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"}