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

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.

Solutions

  1. Unlink the OAuth account from the other user first, then retry the link
  2. Sign in with the OAuth account and link to the desired local account from that session
  3. Use a different OAuth account that is not already bound to another user

Example fix

// before
POST /oauth/link  // google account already bound to user B
// after
DELETE /oauth/unlink (as user B)  // then POST /oauth/link as user A
Defensive patterns

Strategy: try-catch

Validate before calling

const dup = await userRepository.getByOAuthId(sub); if (dup && dup.id !== currentUserId) alert('already linked elsewhere');

Try / catch

catch (e) { if (e.status === 400 && /already been linked/.test(e.message)) { /* show unlink-first guidance */ } }

Prevention

When it happens

Trigger: Calling the link endpoint with an OAuth account whose `sub` is already stored on another user (duplicate.id !== auth.user.id).

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

Understand the failure class

Background: Conflicting config options: "cannot be used together" — configuration validation errors across open-source libraries — this error's family across 162 libraries.

Related errors


AI-assisted analysis of immich-app/immich@f48d4b3321 (2026-09-15). Data as JSON: /api/errors/69c5e224cba5e5fe. Report an issue: GitHub.

Appendix: source

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

    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 f48d4b3321)