semaphoreui/semaphore · error

OIDC sign-in failed: could not exchange authorization code…

Error message

OIDC sign-in failed: could not exchange authorization code. Contact your administrator.

What it means

oidcRedirect exchanges the ?code query parameter for OAuth2 tokens via oauth.Exchange(ctx, code). If the exchange fails, the handler returns HTTP 401 with this message. The real cause (expired/used code, bad client credentials, redirect_uri mismatch) is only in the server log.

Solutions

  1. Read the underlying error in server logs from oauth.Exchange
  2. Have the user restart sign-in (codes are single-use and short-lived)
  3. Verify client_id, client_secret, and redirect_uri exactly match the IdP application settings
  4. Check server clock sync (NTP) and IdP token endpoint reachability
  5. Ensure no proxy rewrites the callback URL path/query

Example fix

// before (IdP app)
redirect_uri: https://semaphore.example.com/api/auth/oidc/keycloak/cb
// after - must match callback route exactly
redirect_uri: https://semaphore.example.com/api/auth/oidc/keycloak/callback
Defensive patterns

Strategy: try-catch

Validate before calling

// pre-check alignment of registered redirect URI with the route
if registeredRedirectURI != callbackURL(r) {
    return errors.New("redirect_uri mismatch with IdP app config")
}

Try / catch

oauth2Token, err := oauth.Exchange(ctx, code)
if err != nil {
    log.Errorf("code exchange failed: %v", err)
    // redirect user to restart login instead of a dead-end 401
    http.Redirect(w, r, loginURL, http.StatusTemporaryRedirect)
    return
}

Prevention

When it happens

Trigger: Authorization code already redeemed (browser refresh/retry), code expired, client_id/client_secret mismatch with the IdP, redirect_uri not exactly matching the registered callback, IdP token endpoint unreachable.

Common situations: Users double-clicking or refreshing the callback; wrong callback URL registered in the IdP app config; rotated client secret not updated in Semaphore config; clock skew invalidating tokens.

Related errors


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

Appendix: source

Thrown at api/login.go:905

		http.Error(w, "Failed to initialize OIDC provider. Contact your administrator.", http.StatusInternalServerError)
		return
	}

	provider, 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
	}

	verifier := _oidc.Verifier(&oidc.Config{ClientID: oauth.ClientID})

	code := r.URL.Query().Get("code")

	oauth2Token, err := oauth.Exchange(ctx, code)
	if err != nil {
		log.Error(err.Error())
		http.Error(w, "OIDC sign-in failed: could not exchange authorization code. Contact your administrator.", http.StatusUnauthorized)
		return
	}

	var claims claimResult

	// Extract the ID Token from OAuth2 token.
	rawIDToken, ok := oauth2Token.Extra("id_token").(string)

	if ok && rawIDToken != "" {
		var idToken *oidc.IDToken
		// Parse and verify ID Token payload.
		idToken, err = verifier.Verify(ctx, rawIDToken)

		if err == nil {
			claims, err = claimOidcToken(idToken, provider)
		}
	} else {
		var userInfo *oidc.UserInfo

View on GitHub (pinned to 1774ccb71a)