semaphoreui/semaphore · warning

This external account is already linked to another user.

Error message

This external account is already linked to another user.

What it means

linkExternalIdentity rejected the link because the external identity (provider pid + claims.sub) is already attached to a different Semaphore user (errIdentityLinkedToAnother). The handler maps this sentinel to HTTP 409 with this message. This prevents one external account from being linked to multiple local users.

Solutions

  1. Unlink the external account from the other Semaphore user first (or have an admin remove that identity row), then retry linking
  2. Sign in with the original account that already owns this external identity instead of linking it again
  3. Check the identities table for the (provider, external_uid) pair to see which user owns it
  4. If the other account is stale, delete it or its identity so the link can proceed

Example fix

-- find conflicting identity
SELECT u.username FROM identities i JOIN users u ON u.id = i.user_id
WHERE i.provider='keycloak' AND i.external_uid='<sub>';
-- after: unlink/delete from the other user, then retry link
Defensive patterns

Strategy: validation

Validate before calling

existing, err := store.GetIdentityByExternalUID(db.IdentityTypeOidc, pid, claims.sub)
if err == nil && existing.UserID != sessionUser.ID {
    return fmt.Errorf("identity already linked to user %s", existing.UserID)
}

Try / catch

if lErr := linkExternalIdentity(store, user, db.IdentityTypeOidc, pid, sub); lErr != nil {
    if errors.Is(lErr, errIdentityLinkedToAnother) {
        http.Error(w, "This external account is already linked to another user.", http.StatusConflict)
        return
    }
}

Prevention

When it happens

Trigger: Link flow where the same IdP account (same sub) was previously linked to another Semaphore user; two local accounts (e.g. created via different providers or manually) both attempting to claim the same external identity; shared/testing IdP account used by multiple users.

Common situations: User created a second Semaphore account and tries to link their existing corporate SSO identity already used by the first account; shared service account at the IdP; leftover identity row from a previous user with the same sub.

Related errors


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

Appendix: source

Thrown at api/login.go:978

		}

		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)
			}
			return
		}

		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,

View on GitHub (pinned to 1774ccb71a)