{"record":{"id":"2ff13842aadac086","repo":"semaphoreui/semaphore","slug":"this-external-account-is-already-linked-to-another","errorCode":null,"errorMessage":"This external account is already linked to another user.","messagePattern":"This external account is already linked to another user\\.","errorType":"http","errorClass":null,"httpStatus":409,"severity":"warning","filePath":"api/login.go","lineNumber":978,"sourceCode":"\t\t}\n\n\t\tsessionUser, uErr := helpers.Store(r).GetUser(session.UserID)\n\t\tif uErr != nil {\n\t\t\tlog.Error(uErr.Error())\n\t\t\thttp.Error(w, \"Failed to link external account.\", http.StatusInternalServerError)\n\t\t\treturn\n\t\t}\n\n\t\tif lErr := linkExternalIdentity(helpers.Store(r), sessionUser, db.IdentityTypeOidc, pid, claims.sub); lErr != nil {\n\t\t\tlog.WithError(lErr).WithFields(log.Fields{\n\t\t\t\t\"user_id\":  sessionUser.ID,\n\t\t\t\t\"provider\": pid,\n\t\t\t\t\"context\":  \"oidc_link\",\n\t\t\t}).Error(\"Failed to link external identity\")\n\n\t\t\tswitch {\n\t\t\tcase errors.Is(lErr, errIdentityLinkedToAnother):\n\t\t\t\thttp.Error(w, \"This external account is already linked to another user.\", http.StatusConflict)\n\t\t\tcase errors.Is(lErr, errProviderAlreadyLinked):\n\t\t\t\thttp.Error(w, \"Your account already has a linked identity for this provider. Unlink it first.\", http.StatusConflict)\n\t\t\tdefault:\n\t\t\t\thttp.Error(w, \"Failed to link external account.\", http.StatusInternalServerError)\n\t\t\t}\n\t\t\treturn\n\t\t}\n\n\t\tredirectURL, _ := url.JoinPath(util.Config.WebHost, \"/\")\n\t\thttp.Redirect(w, r, redirectURL, http.StatusTemporaryRedirect)\n\t\treturn\n\t}\n\n\tuser, err := resolveExternalUser(helpers.Store(r), externalUserProfile{\n\t\tType:          db.IdentityTypeOidc,\n\t\tProvider:      pid,\n\t\tExternalUID:   claims.sub,\n\t\tUsername:      claims.username,","sourceCodeStart":960,"sourceCodeEnd":996,"githubUrl":"https://github.com/semaphoreui/semaphore/blob/1774ccb71a0a8b82eb74ea24c23ac9ab713de2fa/api/login.go#L960-L996","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Unlink the external account from the other Semaphore user first (or have an admin remove that identity row), then retry linking","Sign in with the original account that already owns this external identity instead of linking it again","Check the identities table for the (provider, external_uid) pair to see which user owns it","If the other account is stale, delete it or its identity so the link can proceed"],"exampleFix":"-- find conflicting identity\nSELECT u.username FROM identities i JOIN users u ON u.id = i.user_id\nWHERE i.provider='keycloak' AND i.external_uid='<sub>';\n-- after: unlink/delete from the other user, then retry link","handlingStrategy":"validation","validationCode":"existing, err := store.GetIdentityByExternalUID(db.IdentityTypeOidc, pid, claims.sub)\nif err == nil && existing.UserID != sessionUser.ID {\n    return fmt.Errorf(\"identity already linked to user %s\", existing.UserID)\n}","typeGuard":null,"tryCatchPattern":"if lErr := linkExternalIdentity(store, user, db.IdentityTypeOidc, pid, sub); lErr != nil {\n    if errors.Is(lErr, errIdentityLinkedToAnother) {\n        http.Error(w, \"This external account is already linked to another user.\", http.StatusConflict)\n        return\n    }\n}","preventionTips":["Show currently linked identities in the UI before offering link","Do not share one IdP account across multiple Semaphore users","Provide admin tooling to inspect/unlink identities"],"tags":["oidc","conflict","http-409"],"backgroundTag":"resource-already-exists","analyzedSha":"1774ccb71a0a8b82eb74ea24c23ac9ab713de2fa","analyzedAt":"2026-09-07T11:00:33.293Z","contentChangedAt":"2026-09-07T11:00:33.293Z","schemaVersion":2},"datasetVersion":"2026-09-16T09:17:16.951Z"}