{"record":{"id":"d8f013e80cd1576c","repo":"immich-app/immich","slug":"oauth-authentication-failed","errorCode":null,"errorMessage":"OAuth authentication failed","messagePattern":"OAuth authentication failed","errorType":"exception","errorClass":"BadRequestException","httpStatus":400,"severity":"error","filePath":"server/src/services/auth.service.ts","lineNumber":322,"sourceCode":"\n    const url = this.resolveRedirectUri(oauth, dto.url);\n    const {\n      profile,\n      sid: oauthSid,\n      idToken: oauthBearerToken,\n    } = await this.oauthRepository.getProfileAndOAuthSid(oauth, url, expectedState, codeVerifier);\n    const normalizedEmail = profile.email ? profile.email.trim().toLowerCase() : undefined;\n    const { autoRegister, defaultStorageQuota, storageLabelClaim, storageQuotaClaim, roleClaim } = oauth;\n    this.logger.debug(`Logging in with OAuth: ${JSON.stringify(profile)}`);\n    let user: UserAdmin | undefined = await this.userRepository.getByOAuthId(profile.sub);\n\n    // link by email\n    if (!user && normalizedEmail) {\n      const emailUser = await this.userRepository.getByEmail(normalizedEmail);\n      if (emailUser) {\n        if (emailUser.oauthId) {\n          this.logger.debug('OAuth login conflict: email already linked to different account');\n          throw new BadRequestException('OAuth authentication failed');\n        }\n        user = await this.userRepository.update(emailUser.id, { oauthId: profile.sub });\n      }\n    }\n\n    const role = this.getRoleClaim(profile, roleClaim);\n    const isAdmin = role === 'admin';\n\n    if (user && role && isAdmin !== user.isAdmin) {\n      user = await this.userRepository.update(user.id, { isAdmin });\n    }\n\n    // register new user\n    if (!user) {\n      if (!autoRegister) {\n        this.logger.warn(\n          `Unable to register ${profile.sub}/${normalizedEmail || '(no email)'}. User does not exist and auto registering is disabled. To enable set OAuth Auto Register to true in admin settings.`,\n        );","sourceCodeStart":304,"sourceCodeEnd":340,"githubUrl":"https://github.com/immich-app/immich/blob/f48d4b332127ad365ba256108799ca8f571d2dd5/server/src/services/auth.service.ts#L304-L340","documentation":"Thrown by callback() during account linking: a user exists with the profile's email but that user is already linked to a different oauthId, so the server refuses to link and fails authentication, preventing account takeover via email collision.","triggerScenarios":"OAuth profile's email matches an Immich user whose oauthId differs from profile.sub; user previously linked to another provider/subject.","commonSituations":"Switching OAuth providers (same emails, new subject IDs); duplicate accounts sharing an email; provider re-issuing sub values.","solutions":["Unlink the existing OAuth account (admin user settings) or clear its oauthId, then log in again to relink","Delete or merge the stale duplicate account","Ensure the provider returns a stable sub claim","Verify the intended provider/issuer is configured"],"exampleFix":"// before\n// alice@x.com has oauthId 'old-sub', logs in via 'new-sub' -> 400\n// after\n// admin: unlink OAuth from alice's account, then retry login to relink with 'new-sub'","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"try { await api.oauthCallback(dto, headers) } catch (e) { if (e.status === 400 && /OAuth authentication failed/.test(e.message)) { /* check for account already linked to another oauthId */ } throw e; }","preventionTips":["Use one OAuth provider consistently per server","Keep provider subject IDs stable across migrations","Avoid duplicate emails across users"],"tags":["oauth","account-linking","conflict","email"],"backgroundTag":"oauth-token-exchange-failed","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"}