{"record":{"id":"d8f013e80cd1576c","repo":"immich-app/immich","slug":"oauth-authentication-failed","errorCode":null,"errorMessage":"OAuth authentication failed","messagePattern":"OAuth authentication failed","errorType":"http","errorClass":"BadRequestException","httpStatus":400,"severity":"error","filePath":"server/src/services/auth.service.ts","lineNumber":318,"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":300,"sourceCodeEnd":336,"githubUrl":"https://github.com/immich-app/immich/blob/199723261c6ffa897fec8ccdaea6359e39c37cc3/server/src/services/auth.service.ts#L300-L336","documentation":"BadRequestException (HTTP 400) thrown inside the email-linking branch of AuthService.callback. When no user matches the OAuth sub but a user with the same normalized email exists AND already has a non-empty oauthId, Immich refuses to overwrite the link. The generic message avoids revealing which account owns the email; the debug log records the conflict.","triggerScenarios":"POST /oauth/callback for an IdP identity whose email matches an Immich user that is already linked to a different IdP account (different sub). The user must log in with the original linked identity instead.","commonSituations":"User changed IdP accounts (e.g. new Google account with same Gmail); IdP rotated subject IDs; admin pre-registered users by email and the user tries OAuth before being linked; multiple IdPs sharing email space.","solutions":["Log in with the original OAuth identity that is already linked to that email.","Have an admin clear the existing oauthId on the user row so the new identity can link.","Disable auto-link-by-email in OAuth settings if you do not want silent linking.","Check the server debug log 'OAuth login conflict: email already linked' to confirm the diagnosis."],"exampleFix":"// before\nPOST /oauth/callback { url }  // from a new IdP account sharing the existing email\n// -> 400 OAuth authentication failed\n\n// after\n// admin runs (or user logs in with the original IdP):\nUPDATE users SET \"oauthId\" = '' WHERE email = 'user@example.com';\n// user retries OAuth login","handlingStrategy":"try-catch","validationCode":"// No safe client-side pre-check (the conflict is server-side).\n// Mitigate by ensuring each user has at most one linked IdP identity.","typeGuard":"function isOauthLinkConflict(message: string): boolean {\n  return message === 'OAuth authentication failed';\n}","tryCatchPattern":"try {\n  await axios.post('/oauth/callback', { url });\n} catch (e) {\n  if (e.response?.data?.message === 'OAuth authentication failed') {\n    showHelp('Sign in with the originally linked OAuth account, or ask an admin to clear the link.');\n  } else throw e;\n}","preventionTips":["Document one-identity-per-user policy for users.","Admin tooling to list/clear oauthId per user.","Avoid mixing IdPs that share email spaces."],"tags":["auth","oauth","account-linking","nestjs","immich"],"backgroundTag":null,"analyzedSha":"199723261c6ffa897fec8ccdaea6359e39c37cc3","analyzedAt":"2026-08-12T04:54:27.085Z","schemaVersion":2},"datasetVersion":"2026-08-12T13:17:24.610Z"}