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
- Check the server log for the exact error line logged immediately before this response — it names the failing store call.
- Verify the IdP sends non-empty sub and, if relying on matching, a verified email claim.
- Check config external_auth_email_matching (never/auto/...) matches intent; set it so existing external users are matched by verified email.
- Ensure the database is reachable, migrations are current, and no duplicate username/identity rows exist (delete orphaned users with external=true if needed).
- 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
- Ensure the IdP includes openid email profile scopes so sub and verified email are present
- Keep external_auth_email_matching configured deliberately and document it
- Monitor server logs for resolveExternalUser errors after IdP changes
- Pre-provision or pre-link users when migrating providers/sub formats
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
- You must be signed in to link an external account.
- OIDC sign-in failed: invalid redirect URL.
- Unauthorized
- Account linking must be initiated with a POST request.
- OIDC sign-in failed: state cookie is missing. Try signing…
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)