semaphoreui/semaphore · error

OIDC sign-in failed: could not find or create the user…

Error message

OIDC sign-in failed: could not find or create the user account. Contact your administrator.

What it means

oidcRedirect returns this 500 when resolveExternalUser (api/login_identity.go:43) fails to map the OIDC identity to a Semaphore user: it looks up the identity by (provider, external_uid), optionally matches by verified email under external_auth_email_matching mode (never adopting local accounts), and otherwise creates a new external user. Any DB error in those steps, or an empty sub/email claim, surfaces as this message. The real cause is only in the server log line printed just before.

Solutions

  1. Check the server log for the exact error line logged immediately before this response — it names the failing store call.
  2. Verify the IdP sends non-empty sub and, if relying on matching, a verified email claim.
  3. Check config external_auth_email_matching (never/auto/...) matches intent; set it so existing external users are matched by verified email.
  4. Ensure the database is reachable, migrations are current, and no duplicate username/identity rows exist (delete orphaned users with external=true if needed).
  5. Test a full fresh sign-in for a brand-new user to confirm user creation + identity linking works.

Example fix

// before (IdP scope missing email)
scope: "openid"
// after
scope: "openid email profile"  // ensures claims.email/claims.emailVerified are populated
Defensive patterns

Strategy: validation

Validate before calling

// client pre-check before starting OIDC flow
if (!idTokenClaims.sub || (relyOnEmailMatch && (!idTokenClaims.email || !idTokenClaims.email_verified))) {
  throw new Error("IdP must return sub and (for matching) a verified email claim");
}

Type guard

function hasOidcIdentityClaims(c) {
  return typeof c === "object" && c !== null &&
    typeof c.sub === "string" && c.sub.length > 0 &&
    (!relyOnEmailMatch || (typeof c.email === "string" && c.email_verified === true));
}

Try / catch

try {
  await signInWithOidc(providerId);
} catch (e) {
  if (e.status === 500 && /could not find or create the user account/.test(e.body)) {
    // surface admin-facing message; check server logs for the store error
  }
}

Prevention

When it happens

Trigger: POST/GET to /api/auth/oidc/<provider>/redirect with a valid state cookie and token exchange, but resolveExternalUser errors: empty claims.sub, database failure in GetExternalIdentity/GetUserByLoginOrEmail/CreateUserWithoutPassword/CreateExternalIdentity, duplicate username on user creation, or email matching rejected (local account / already-pinned identity in 'auto' mode).

Common situations: IdP omits the email claim or email_verified is false while external_auth_email_matching is set; IdP changes the sub value; DB out of space or migration missing; username claim collides with an existing orphaned user from a previous rolled-back flow; matching mode changed between deployments.

Related errors


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

Appendix: source

Thrown at api/login.go:1005

		redirectURL, _ := url.JoinPath(util.Config.WebHost, "/")
		http.Redirect(w, r, redirectURL, http.StatusTemporaryRedirect)
		return
	}

	user, err := resolveExternalUser(helpers.Store(r), externalUserProfile{
		Type:          db.IdentityTypeOidc,
		Provider:      pid,
		ExternalUID:   claims.sub,
		Username:      claims.username,
		Name:          claims.name,
		Email:         claims.email,
		EmailVerified: claims.emailVerified,
		// MatchByUsername stays false: OIDC matches by email only
		// (username matching "creates a lot of problems" - see old comment).
	})
	if err != nil {
		log.Error(err.Error())
		http.Error(w, "OIDC sign-in failed: could not find or create the user account. Contact your administrator.", http.StatusInternalServerError)
		return
	}

	createSession(w, r, user, true)

	config, ok := util.Config.OidcProviders[pid]
	if !ok {
		log.Error(fmt.Errorf("no such provider: %s", pid))
		http.Error(w, "Unknown OIDC provider.", http.StatusNotFound)
		return
	}

	redirectPath := ""
	if config.ReturnViaState {
		redirectPath = stateData.Return
	} else {
		redirectPath = mux.Vars(r)["redirect_path"]
	}

View on GitHub (pinned to 1774ccb71a)