gotify/server · error

issuer url %q is not a valid url: %w

Error message

issuer url %q is not a valid url: %w

What it means

In resolveUser, the `iss` claim from the ID token is validated before deriving the user's oidcID. If `url.Parse` on the issuer fails, the handler returns 500 'issuer url %q is not a valid url: %w'. (Note: Go's url.Parse rarely errors, so this mostly guards truly malformed issuer strings.)

Source

Thrown at api/oidc.go:432

		return "", err
	}
	return hex.EncodeToString(nonce), nil
}

// 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")

View on GitHub (pinned to 14bfc25627)

Solutions

  1. Fix the issuer URL in the OIDC provider configuration (must be an absolute URL with scheme)
  2. Ensure the server trusts only the intended provider/issuer
  3. Inspect the wrapped parse error to see which character broke parsing

Example fix

// before (IdP config)
issuer: "localhost:8080"
// after
issuer: "https://localhost:8080"
Defensive patterns

Strategy: validation

Validate before calling

function isValidIssuer(iss) {
  try { const u = new URL(iss); return u.protocol === 'https:' || u.protocol === 'http:'; }
  catch { return false; }
}
if (!isValidIssuer(idToken.iss)) rejectToken(idToken);

Type guard

function isParseableURL(s) {
  try { new URL(s); return true; } catch { return false; }
}

Try / catch

try {
  new URL(issuer);
} catch (err) {
  return res.status(500).json({error: `issuer url ${issuer} is not a valid url`});
}

Prevention

When it happens

Trigger: An ID token whose issuer claim is empty-adjacent garbage or otherwise unparseable as a URL — typically a misconfigured IdP or a forged/malformed token.

Common situations: IdP configured with a wrong issuer string (e.g. missing scheme); tokens minted by a test/staging IdP with a malformed issuer; token from a different, badly configured provider accepted due to lax issuer checks.

Related errors


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