semaphoreui/semaphore · error

OIDC sign-in failed: the provider returned no user ID (sub…

Error message

OIDC sign-in failed: the provider returned no user ID (sub claim). Contact your administrator.

What it means

oidcRedirect requires a stable user identifier (sub claim) to resolve or create the local account. If claims.sub is empty after reading token/userinfo claims, it logs 'oidc provider %s returned no sub claim' and returns HTTP 502. OIDC mandates sub; its absence means a non-conformant provider or a claim mapping that dropped it.

Solutions

  1. Check the provider config's claim mapping and ensure the subject claim is mapped from 'sub'
  2. Decode the returned ID token (jwt.io or logs) to confirm whether sub is actually present
  3. If the IdP omits sub, switch to a conformant IdP config/realm or file an issue with the vendor
  4. Update provider firmware/config for non-conformant identity products

Example fix

// before
claims_mapping: {sub: "user_id"}   // IdP never emits user_id
// after
claims_mapping: {sub: "sub"}
Defensive patterns

Strategy: validation

Validate before calling

var payload map[string]any
json.Unmarshal(idTokenBytes, &payload)
if sub, _ := payload["sub"].(string); sub == "" {
    return errors.New("provider token has no sub claim")
}

Type guard

func hasSub(claims map[string]any) bool {
    s, ok := claims["sub"].(string)
    return ok && s != ""
}

Prevention

When it happens

Trigger: IdP returns an ID token/userinfo without a sub claim, claimOidcToken/claimOidcUserInfo mapping is configured to a wrong claim name so sub never gets populated, or a token from a non-conformant OAuth2 (not OIDC) endpoint.

Common situations: Custom/legacy IdPs that omit sub; misconfigured claim mapping in Semaphore's provider config pointing sub at a custom attribute; providers that only return email without subject.

Understand the failure class

Background: "must not be empty", "cannot be empty" — required-field validation errors across open-source libraries — this error's family across 41 libraries.

Related errors


AI-assisted analysis of semaphoreui/semaphore@1774ccb71a (2026-09-07). Data as JSON: /api/errors/4588692858f8cdbc. Report an issue: GitHub.

Appendix: source

Thrown at api/login.go:951

				claims.emailVerified = oidcEmailVerified(userInfo, provider)
			}
		}

		claims.username = getRandomUsername()
		if userInfo.Profile == "" {
			claims.name = getRandomProfileName()
		}
	}

	if err != nil {
		log.Error(err.Error())
		http.Error(w, "OIDC sign-in failed: could not read user info from the provider. Contact your administrator.", http.StatusBadGateway)
		return
	}

	if claims.sub == "" {
		log.Error(fmt.Errorf("oidc provider %s returned no sub claim", pid))
		http.Error(w, "OIDC sign-in failed: the provider returned no user ID (sub claim). Contact your administrator.", http.StatusBadGateway)
		return
	}

	if stateData.Link {
		session, ok := getSession(r)
		if !ok || !session.IsVerified() {
			http.Error(w, "You must be signed in to link an external account.", http.StatusUnauthorized)
			return
		}

		sessionUser, uErr := helpers.Store(r).GetUser(session.UserID)
		if uErr != nil {
			log.Error(uErr.Error())
			http.Error(w, "Failed to link external account.", http.StatusInternalServerError)
			return
		}

		if lErr := linkExternalIdentity(helpers.Store(r), sessionUser, db.IdentityTypeOidc, pid, claims.sub); lErr != nil {

View on GitHub (pinned to 1774ccb71a)