gotify/server · error

username claim was empty

Error message

username claim was empty

What it means

resolveUser throws 500 'username claim was empty' when the configured username claim (a.UsernameClaim, default typically 'username' or preferred_username) is present but its value is empty, while resolving an OIDC user (during username-based linking).

Source

Thrown at api/oidc.go:471

	if user != nil {
		if len(a.GroupsAdmin) > 0 && user.Admin != hasAdminGroup {
			user.Admin = hasAdminGroup
			if err := a.DB.UpdateUser(user); err != nil {
				return nil, http.StatusInternalServerError, fmt.Errorf("database error: %w", err)
			}
			log.Warn().Str("oidc_id", oidcID).Str("username", user.Name).Bool("admin", user.Admin).Msg("OIDC change permission")
		}
		return user, 0, nil
	}

	usernameRaw, ok := lookupClaim(a.UsernameClaim, idToken.Claims, info.Claims)
	if !ok {
		return nil, http.StatusInternalServerError, fmt.Errorf("username claim %q is missing", a.UsernameClaim)
	}
	username := fmt.Sprint(usernameRaw)
	if username == "" || usernameRaw == nil {
		return nil, http.StatusInternalServerError, errors.New("username claim was empty")
	}

	byUsername, err := a.DB.GetUserByName(username)
	if err != nil {
		return nil, http.StatusInternalServerError, fmt.Errorf("database error: %w", err)
	}
	if byUsername != nil {
		return a.linkExistingUser(byUsername, oidcID, hasAdminGroup)
	}
	return a.registerUser(username, oidcID, hasAdminGroup)
}

func (a *OIDCAPI) linkExistingUser(user *model.User, oidcID string, hasAdminGroup bool) (*model.User, int, error) {
	if !a.LinkByUsername {
		log.Warn().Str("oidc_id", oidcID).Str("username", user.Name).Msgf("OIDC login rejected: a local user with the username already exists and %s is disabled", config.EnvOIDCLinkByUsername)
		return nil, http.StatusForbidden, fmt.Errorf("a local user with the username %s already exists and linking by username is disabled", user.Name)
	}
	if user.OIDCID != nil {

View on GitHub (pinned to 14bfc25627)

Solutions

  1. Set GOTIFY_OIDC_USERNAME_CLAIM to a claim your provider actually populates (e.g. preferred_username, email).
  2. Fix the user's profile at the IdP so the claim has a value.
  3. Decode the ID token and confirm the claim key and value.
  4. Check for case-sensitivity/typos in the claim name.

Example fix

// before
GOTIFY_OIDC_USERNAME_CLAIM=login // provider has no 'login' claim value
// after
GOTIFY_OIDC_USERNAME_CLAIM=preferred_username
Defensive patterns

Strategy: validation

Validate before calling

const claims = decodeJwt(idToken);
const raw = claims[process.env.GOTIFY_OIDC_USERNAME_CLAIM ?? 'preferred_username'];
if (raw == null || String(raw) === '') throw new Error('username claim empty; fix GOTIFY_OIDC_USERNAME_CLAIM or IdP mapping');

Type guard

function hasNonEmptyClaim(claims, name) { const v = claims[name]; return v != null && String(v) !== ''; }

Prevention

When it happens

Trigger: resolveUser falls back to linking by username (user has no existing OIDC binding and GOTIFY_OIDC_LINK_BY_USERNAME is enabled), and the claim named by GOTIFY_OIDC_USERNAME_CLAIM exists in the token but resolves to "" or null.

Common situations: Renamed GOTIFY_OIDC_USERNAME_CLAIM to a claim the provider never populates; provider returns preferred_username as null for federated users; misconfigured claim transformation at the IdP.

Related errors


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