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

  1. Reconfigure the IdP to advertise a fragment-free issuer URL
  2. Restrict accepted issuers to known-good values
  3. 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

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


AI-assisted analysis of gotify/server@14bfc25627 (2026-09-05). Data as JSON: /api/errors/abf56805ab5d060f. Report an issue: GitHub.