{"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":"exception","errorClass":"BadRequestException","httpStatus":400,"severity":"error","filePath":"server/src/services/auth.service.ts","lineNumber":425,"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":407,"sourceCodeEnd":443,"githubUrl":"https://github.com/immich-app/immich/blob/f48d4b332127ad365ba256108799ca8f571d2dd5/server/src/services/auth.service.ts#L407-L443","documentation":"When linking an OAuth identity to the current user, the service looks up whether the OAuth `sub` is already attached to a different user account. If so, linking would create a duplicate identity binding, so it refuses with a BadRequestException.","triggerScenarios":"Calling the link endpoint with an OAuth account whose `sub` is already stored on another user (duplicate.id !== auth.user.id).","commonSituations":"User tries to link a Google/Microsoft/OIDC account already used to sign into another local account; shared service accounts; testing with a second account reusing the same OAuth identity.","solutions":["Unlink the OAuth account from the other user first, then retry the link","Sign in with the OAuth account and link to the desired local account from that session","Use a different OAuth account that is not already bound to another user"],"exampleFix":"// before\nPOST /oauth/link  // google account already bound to user B\n// after\nDELETE /oauth/unlink (as user B)  // then POST /oauth/link as user A","handlingStrategy":"try-catch","validationCode":"const dup = await userRepository.getByOAuthId(sub); if (dup && dup.id !== currentUserId) alert('already linked elsewhere');","typeGuard":null,"tryCatchPattern":"catch (e) { if (e.status === 400 && /already been linked/.test(e.message)) { /* show unlink-first guidance */ } }","preventionTips":["Check oauth bindings before attempting link","Provide an unlink UI","Avoid shared OAuth accounts"],"tags":["oauth","conflict","account-linking"],"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"}