immich-app/immich · error · BadRequestException

This OAuth account has already been linked to another user.

Error message

This OAuth account has already been linked to another user.

What it means

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.

Source

Thrown at server/src/services/auth.service.ts:421

    if (!expectedState?.length) {
      throw new BadRequestException('OAuth state is missing');
    }

    const codeVerifier = dto.codeVerifier ?? this.getCookieCodeVerifier(headers);
    if (!codeVerifier?.length) {
      throw new BadRequestException('OAuth code verifier is missing');
    }

    const { oauth } = await this.getConfig({ withCache: false });
    const {
      profile: { sub: oauthId },
      sid,
      idToken,
    } = await this.oauthRepository.getProfileAndOAuthSid(oauth, dto.url, expectedState, codeVerifier);
    const duplicate = await this.userRepository.getByOAuthId(oauthId);
    if (duplicate && duplicate.id !== auth.user.id) {
      this.logger.warn(`OAuth link account failed: sub is already linked to another user (${duplicate.email}).`);
      throw new BadRequestException('This OAuth account has already been linked to another user.');
    }

    if (auth.session && (sid || idToken)) {
      await this.sessionRepository.update(auth.session.id, {
        oauthSid: sid,
        oauthBearerToken: idToken,
      });
    }

    const user = await this.userRepository.update(auth.user.id, { oauthId });
    return mapUserAdmin(user);
  }

  async unlink(auth: AuthDto): Promise<UserAdminResponseDto> {
    if (auth.session) {
      await this.sessionRepository.update(auth.session.id, { oauthSid: null, oauthBearerToken: null });
    }

View on GitHub (pinned to 199723261c)

Solutions

  1. On the originally-linked account (user B), call POST /oauth/unlink to release the identity, then retry the link on user A.
  2. Have an admin clear user B's oauthId out of band if user B is inaccessible.
  3. Log in to user A using the OAuth identity directly instead of linking it.
  4. Check server logs for 'sub is already linked to another user' to identify the conflicting email.

Example fix

// before
// as user A
await axios.post('/oauth/link', { url }, { headers: { Authorization: `Bearer ${tokenA}` } });
// -> 400 This OAuth account has already been linked to another user.

// after
// on user B (the existing owner):
await axios.post('/oauth/unlink', {}, { headers: { Authorization: `Bearer ${tokenB}` } });
// back on user A:
await axios.post('/oauth/link', { url }, { headers: { Authorization: `Bearer ${tokenA}` } });
Defensive patterns

Strategy: try-catch

Validate before calling

// Caller cannot resolve the conflict directly; pre-check the other account is unlinked is not possible client-side.
// Surface as an admin/user workflow.

Type guard

function isAlreadyLinked(message: string): boolean {
  return message === 'This OAuth account has already been linked to another user.';
}

Try / catch

try {
  await axios.post('/oauth/link', { url }, { headers: auth() });
} catch (e) {
  if (isAlreadyLinked(e.response?.data?.message || '')) {
    showHelp('Unlink the OAuth identity from the other account first, or sign in with it directly.');
  } else throw e;
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of immich-app/immich@199723261c (2026-08-12). Data as JSON: /api/errors/69c5e224cba5e5fe. Report an issue: GitHub.