gotify/server · error
issuer url %q may not contain a fragment
Error message
issuer url %q may not contain a fragment
What it means
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'.
Source
Thrown at api/oidc.go:435
}
// resolveUser looks up, links, or creates the user bound to an OIDC identity.
//
// 1. Look up the user by OIDC id (<iss>#<sub>). If found, use it.
// 2. Otherwise look up a user by the username claim. If one exists, link it to
// this OIDC identity, which requires GOTIFY_OIDC_LINK_BY_USERNAME and
// that the user is not already bound to a different identity.
// 3. Otherwise auto-register a new user, which requires GOTIFY_OIDC_AUTOREGISTER.
func (a *OIDCAPI) resolveUser(idToken *oidc.IDTokenClaims, info *oidc.UserInfo) (*model.User, int, error) {
issuer := idToken.GetIssuer()
if issuer == "" {
return nil, http.StatusInternalServerError, errors.New("issuer claim was empty")
}
if _, err := url.Parse(issuer); err != nil {
return nil, http.StatusInternalServerError, fmt.Errorf("issuer url %q is not a valid url: %w", issuer, err)
}
if strings.Contains(issuer, "#") {
return nil, http.StatusInternalServerError, fmt.Errorf("issuer url %q may not contain a fragment", issuer)
}
subject := info.GetSubject()
if subject == "" {
return nil, http.StatusInternalServerError, errors.New("subject claim was empty")
}
oidcID := issuer + "#" + subject
user, err := a.DB.GetUserByOIDC(oidcID)
if err != nil {
return nil, http.StatusInternalServerError, fmt.Errorf("database error: %w", err)
}
hasAdminGroup, status, err := a.resolvePermission(idToken.Claims, info.Claims)
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
}
View on GitHub (pinned to 14bfc25627)
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
Example fix
// before issuer: "https://idp.example.com/oidc#tenant-a" // after issuer: "https://idp.example.com/oidc/tenant-a"
Defensive patterns
Strategy: validation
Validate before calling
if (typeof idToken.iss === 'string' && idToken.iss.includes('#')) {
throw new Error(`issuer url ${idToken.iss} may not contain a fragment`);
} Type guard
function isFragmentFreeIssuer(iss) {
return typeof iss === 'string' && iss.length > 0 && !iss.includes('#');
} Prevention
- 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
When it happens
Trigger: An ID token whose iss claim contains a '#' fragment (e.g. 'https://idp.example.com/app#fragment') reaches resolveUser during the external token flow.
Common situations: 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.
Related errors
- issuer url %q is not a valid url: %w
- token exchange failed: %w
- err.Error()
- unknown or expired state
- issuer claim was empty
AI-assisted analysis of gotify/server@14bfc25627 (2026-09-05).
Data as JSON: /api/errors/abf56805ab5d060f.
Report an issue: GitHub.