immich-app/immich · error
OAuth link account failed: sub is already linked to another…
Error message
OAuth link account failed: sub is already linked to another user (${duplicate.email}). What it means
When linking an OAuth identity to an already-logged-in account (POST /oauth/link), the server checks whether the OAuth subject (sub) is already attached to a different user. If so, it logs this warning naming the duplicate user's email and throws BadRequestException('This OAuth account has already been linked to another user.').
Solutions
- Unlink the OAuth identity from the other account first (that account's OAuth settings page).
- Use a distinct identity (different IdP account/email) for each server user.
- If the other account is stale, delete it or remove its OAuth binding via admin tools.
- Verify the IdP isn't returning the same sub for different end users.
Defensive patterns
Strategy: try-catch
Validate before calling
// client-side pre-check before linking
const linked = await api.getMyOAuthAccount();
if (linked) throw new Error('This identity is already linked to another account'); Try / catch
try {
await api.oauthLink(url);
} catch (e) {
if (e instanceof BadRequestException && e.message.includes('already been linked')) {
showToast('This OAuth account is bound to a different user; unlink it there first');
} else throw e;
} Prevention
- Use a unique provider identity per server account.
- Unlink OAuth from the old account before linking elsewhere.
- Avoid sharing one IdP login among multiple people.
- Audit existing OAuth bindings before reconfiguring the provider.
When it happens
Trigger: oauth/link called with dto.url for an OAuth identity whose sub resolves (getByOAuthId) to a user with a different id than auth.user.id.
Common situations: Two server accounts both trying to bind the same Google/OIDC identity; reusing a test OAuth account; IdP reusing a sub across accounts; family members sharing one provider login.
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
- OAuth authentication failed
- This OAuth account has already been linked to another user.
- Error backchannel logout: token validation failed
- Error in OAuth discovery
- Failed to fetch picture
AI-assisted analysis of immich-app/immich@f48d4b3321 (2026-09-15).
Data as JSON: /api/errors/1c6a4a3819c6b662.
Report an issue: GitHub.
Appendix: source
Thrown at server/src/services/auth.service.ts:424
const expectedState = dto.state ?? this.getCookieOauthState(headers);
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)