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
- Fix the issuer URL in the OIDC provider configuration (must be an absolute URL with scheme)
- Ensure the server trusts only the intended provider/issuer
- 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
- Configure the IdP with an absolute issuer URL including scheme
- Pin expected issuer values server-side and validate tokens against them
- Log the raw iss claim when rejecting tokens
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
- issuer url %q may not contain a fragment
- user does not exist and auto-registration is disabled
- token exchange failed: %w
- username claim %q is missing
- a local user with the username %s already exists and linking
AI-assisted analysis of gotify/server@14bfc25627 (2026-09-05).
Data as JSON: /api/errors/e6cc721901c18640.
Report an issue: GitHub.