{"record":{"id":"69c5e224cba5e5fe","repo":"immich-app/immich","slug":"this-oauth-account-has-already-been-linked-to-anot","errorCode":null,"errorMessage":"This OAuth account has already been linked to another user.","messagePattern":"This OAuth account has already been linked to another user\\.","errorType":"http","errorClass":"BadRequestException","httpStatus":400,"severity":"error","filePath":"server/src/services/auth.service.ts","lineNumber":421,"sourceCode":"    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    }\n","sourceCodeStart":403,"sourceCodeEnd":439,"githubUrl":"https://github.com/immich-app/immich/blob/199723261c6ffa897fec8ccdaea6359e39c37cc3/server/src/services/auth.service.ts#L403-L439","documentation":"BadRequestException (HTTP 400) thrown by AuthService.link when the OAuth sub returned by the IdP already belongs to a different Immich user. Immich will not let one OAuth identity link to two accounts; the warn log exposes the conflicting email for admin debugging. Reachable after the duplicate lookup by oauthId.","triggerScenarios":"POST /oauth/link (authenticated as user A) with an OAuth identity whose sub is already set on user B's row. The check `duplicate.id !== auth.user.id` fails and the link is rejected.","commonSituations":"User previously linked the OAuth identity on another Immich account; admin merged accounts and left the old oauthId in place; IdP reused a sub after deletion; multi-tenant deployment where the same SSO backs multiple instances.","solutions":["On the originally-linked account (user B), call POST /oauth/unlink to release the identity, then retry the link on user A.","Have an admin clear user B's oauthId out of band if user B is inaccessible.","Log in to user A using the OAuth identity directly instead of linking it.","Check server logs for 'sub is already linked to another user' to identify the conflicting email."],"exampleFix":"// before\n// as user A\nawait axios.post('/oauth/link', { url }, { headers: { Authorization: `Bearer ${tokenA}` } });\n// -> 400 This OAuth account has already been linked to another user.\n\n// after\n// on user B (the existing owner):\nawait axios.post('/oauth/unlink', {}, { headers: { Authorization: `Bearer ${tokenB}` } });\n// back on user A:\nawait axios.post('/oauth/link', { url }, { headers: { Authorization: `Bearer ${tokenA}` } });","handlingStrategy":"try-catch","validationCode":"// Caller cannot resolve the conflict directly; pre-check the other account is unlinked is not possible client-side.\n// Surface as an admin/user workflow.","typeGuard":"function isAlreadyLinked(message: string): boolean {\n  return message === 'This OAuth account has already been linked to another user.';\n}","tryCatchPattern":"try {\n  await axios.post('/oauth/link', { url }, { headers: auth() });\n} catch (e) {\n  if (isAlreadyLinked(e.response?.data?.message || '')) {\n    showHelp('Unlink the OAuth identity from the other account first, or sign in with it directly.');\n  } else throw e;\n}","preventionTips":["Maintain a one-to-one mapping between OAuth identities and Immich users.","Provide an unlink endpoint flow in the user profile UI.","On account merges, clear stale oauthId values."],"tags":["auth","oauth","account-linking","nestjs","immich"],"backgroundTag":null,"analyzedSha":"199723261c6ffa897fec8ccdaea6359e39c37cc3","analyzedAt":"2026-08-12T04:54:27.085Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}