{"record":{"id":"1c6a4a3819c6b662","repo":"immich-app/immich","slug":"oauth-link-account-failed-sub-is-already-linked-to-another","errorCode":null,"errorMessage":"OAuth link account failed: sub is already linked to another user (${duplicate.email}).","messagePattern":"OAuth link account failed: sub is already linked to another user \\((.+?)\\)\\.","errorType":"console","errorClass":null,"httpStatus":null,"severity":"error","filePath":"server/src/services/auth.service.ts","lineNumber":424,"sourceCode":"    const expectedState = dto.state ?? this.getCookieOauthState(headers);\n    if (!expectedState?.length) {\n      throw new BadRequestException('OAuth state is missing');\n    }\n\n    const codeVerifier = dto.codeVerifier ?? this.getCookieCodeVerifier(headers);\n    if (!codeVerifier?.length) {\n      throw new BadRequestException('OAuth code verifier is missing');\n    }\n\n    const { oauth } = await this.getConfig({ withCache: false });\n    const {\n      profile: { sub: oauthId },\n      sid,\n      idToken,\n    } = await this.oauthRepository.getProfileAndOAuthSid(oauth, dto.url, expectedState, codeVerifier);\n    const duplicate = await this.userRepository.getByOAuthId(oauthId);\n    if (duplicate && duplicate.id !== auth.user.id) {\n      this.logger.warn(`OAuth link account failed: sub is already linked to another user (${duplicate.email}).`);\n      throw new BadRequestException('This OAuth account has already been linked to another user.');\n    }\n\n    if (auth.session && (sid || idToken)) {\n      await this.sessionRepository.update(auth.session.id, {\n        oauthSid: sid,\n        oauthBearerToken: idToken,\n      });\n    }\n\n    const user = await this.userRepository.update(auth.user.id, { oauthId });\n    return mapUserAdmin(user);\n  }\n\n  async unlink(auth: AuthDto): Promise<UserAdminResponseDto> {\n    if (auth.session) {\n      await this.sessionRepository.update(auth.session.id, { oauthSid: null, oauthBearerToken: null });\n    }","sourceCodeStart":406,"sourceCodeEnd":442,"githubUrl":"https://github.com/immich-app/immich/blob/f48d4b332127ad365ba256108799ca8f571d2dd5/server/src/services/auth.service.ts#L406-L442","documentation":"When linking an OAuth identity to an already-logged-in account (POST /oauth/link), the server checks whether the OAuth subject (sub) is already attached to a different user. If so, it logs this warning naming the duplicate user's email and throws BadRequestException('This OAuth account has already been linked to another user.').","triggerScenarios":"oauth/link called with dto.url for an OAuth identity whose sub resolves (getByOAuthId) to a user with a different id than auth.user.id.","commonSituations":"Two server accounts both trying to bind the same Google/OIDC identity; reusing a test OAuth account; IdP reusing a sub across accounts; family members sharing one provider login.","solutions":["Unlink the OAuth identity from the other account first (that account's OAuth settings page).","Use a distinct identity (different IdP account/email) for each server user.","If the other account is stale, delete it or remove its OAuth binding via admin tools.","Verify the IdP isn't returning the same sub for different end users."],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":"// client-side pre-check before linking\nconst linked = await api.getMyOAuthAccount();\nif (linked) throw new Error('This identity is already linked to another account');","typeGuard":null,"tryCatchPattern":"try {\n  await api.oauthLink(url);\n} catch (e) {\n  if (e instanceof BadRequestException && e.message.includes('already been linked')) {\n    showToast('This OAuth account is bound to a different user; unlink it there first');\n  } else throw e;\n}","preventionTips":["Use a unique provider identity per server account.","Unlink OAuth from the old account before linking elsewhere.","Avoid sharing one IdP login among multiple people.","Audit existing OAuth bindings before reconfiguring the provider."],"tags":["oauth","account-linking","conflict"],"backgroundTag":"conflicting-config-options","analyzedSha":"f48d4b332127ad365ba256108799ca8f571d2dd5","analyzedAt":"2026-09-15T07:20:19.675Z","contentChangedAt":"2026-09-15T07:20:19.675Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}