goharbor/harbor · error
State mismatch
Error message
State mismatch
What it means
OIDC Callback answers HTTP 400 'State mismatch' when the state query parameter returned by the OIDC provider differs from the state stored in the user's Harbor session. This is the CSRF check of the authorization-code flow: the callback must land in the same browser session that initiated login.
Source
Thrown at src/core/controllers/oidc.go:113
return
}
if err := oc.SetSession(stateKey, state); err != nil {
log.Errorf("failed to set session for key: %s, error: %v", stateKey, err)
oc.SendInternalServerError(err)
return
}
log.Debugf("State dumped to session: %s", state)
// Force to use the func 'Redirect' of beego.Controller
oc.Controller.Redirect(url, http.StatusFound)
}
// Callback handles redirection from OIDC provider. It will exchange the token and
// kick off onboard if needed.
func (oc *OIDCController) Callback() {
if oc.Ctx.Request.URL.Query().Get("state") != oc.GetSession(stateKey) {
log.Errorf("State mismatch, in session: %s, in url: %s", oc.GetSession(stateKey),
oc.Ctx.Request.URL.Query().Get("state"))
oc.SendBadRequestError(errors.New("State mismatch"))
return
}
errorCode := oc.Ctx.Request.URL.Query().Get("error")
if errorCode != "" {
errorDescription := oc.Ctx.Request.URL.Query().Get("error_description")
log.Errorf("OIDC callback returned error: %s - %s", errorCode, errorDescription)
oc.SendBadRequestError(errors.Errorf("OIDC callback returned error: %s - %s", errorCode, errorDescription))
return
}
var redirectURLStr string
redirectURL := oc.GetSession(redirectURLKey)
if redirectURL != nil {
redirectURLStr = redirectURL.(string)
if err := oc.DelSession(redirectURLKey); err != nil {
log.Errorf("failed to delete session for key:%s, error: %v", redirectURLKey, err)
oc.SendInternalServerError(err)
return
}View on GitHub (pinned to 7b2fd08cc5)
Solutions
- Restart the login from the Harbor sign-on page and finish it in the same browser
- Enable session affinity (or a shared session store) between core replicas
- Check the browser accepts Harbor's session cookie (domain, Secure, SameSite)
- Verify the OIDC redirect URI registered at the provider exactly matches Harbor's callback URL
Defensive patterns
Strategy: retry
Validate before calling
// Client-side: verify the browser still holds the Harbor session before redirecting to the IdP
if !harborSessionCookiePresent(document) { /* clear login state and start fresh */ } Type guard
func isStateMismatch(resp *http.Response) bool {
return resp.StatusCode == http.StatusBadRequest && strings.Contains(readBody(resp), "State mismatch")
} Try / catch
// OIDC client middleware: on state mismatch, restart the flow once
if isStateMismatch(resp) {
resp = restartLoginFlow() // new state, new nonce, same session cookie
} Prevention
- Complete OIDC login in one browser session; never re-use callback URLs
- Enable sticky sessions or shared session storage for core replicas
- Keep session cookie attributes (Secure, SameSite) compatible with the SSO redirect chain
When it happens
Trigger: GET /c/oidc/callback when the Harbor session cookie was not sent (blocked, incognito, new browser), the flow was started in a different browser, an old callback URL is replayed, or the session lives in another core replica.
Common situations: Core replicas behind a load balancer without sticky sessions (session affinity); SameSite/Secure cookie settings blocking the session cookie; clock- or expiry-related session loss; user copying the callback URL into another tab.
Related errors
- File {} not exist
- invalid {}
- key file {} permission is not 600
- File {} should readable by owner
- cert file {} should include SAN
AI-assisted analysis of goharbor/harbor@7b2fd08cc5 (2026-08-16).
Data as JSON: /api/errors/d28ca863c9b4b4d7.
Report an issue: GitHub.