semaphoreui/semaphore · error

Failed to link external account.

Error message

Failed to link external account.

What it means

During the link flow, oidcRedirect loads the logged-in user via helpers.Store(r).GetUser(session.UserID). If the store returns an error (user row missing, DB failure), it returns HTTP 500 'Failed to link external account.' The detailed cause goes to the log only.

Solutions

  1. Check the server log for the underlying GetUser error
  2. Verify the user in session still exists in the database (users table)
  3. Clear the stale session (log out/in) so a deleted-user session is not reused
  4. Check database connectivity and recent migrations for integrity issues

Example fix

-- before: session points at deleted user
DELETE FROM sessions WHERE user_id NOT IN (SELECT id FROM users);
-- after: sessions cleaned, user re-logs in and retries linking
Defensive patterns

Strategy: validation

Validate before calling

if _, err := helpers.Store(r).GetUser(session.UserID); err != nil {
    // invalidate stale session before proceeding
    return errors.New("session user no longer exists")
}

Try / catch

sessionUser, uErr := helpers.Store(r).GetUser(session.UserID)
if uErr != nil {
    log.Errorf("GetUser(%s) failed: %v", session.UserID, uErr)
    // clear session and force re-login
    clearSession(w, r)
    return
}

Prevention

When it happens

Trigger: Session references a user that was deleted from the database while the session was still valid; database connectivity/query error in GetUser; replica/read inconsistency where the user record isn't visible.

Common situations: Admin deleted a user account that still had an active session; transient DB outage during linking; data migration left orphaned sessions pointing at removed user IDs.

Understand the failure class

Background: Record Not Found Errors: "not found", RecordNotFound, and "was not found" — what they mean and how to fix them — this error's family across 28 libraries.

Related errors


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

Appendix: source

Thrown at api/login.go:965

	}

	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 {
			log.WithError(lErr).WithFields(log.Fields{
				"user_id":  sessionUser.ID,
				"provider": pid,
				"context":  "oidc_link",
			}).Error("Failed to link external identity")

			switch {
			case errors.Is(lErr, errIdentityLinkedToAnother):
				http.Error(w, "This external account is already linked to another user.", http.StatusConflict)
			case errors.Is(lErr, errProviderAlreadyLinked):
				http.Error(w, "Your account already has a linked identity for this provider. Unlink it first.", http.StatusConflict)
			default:
				http.Error(w, "Failed to link external account.", http.StatusInternalServerError)
			}

View on GitHub (pinned to 1774ccb71a)