immich-app/immich · error · UnauthorizedException

Incorrect email or password

Error message

Incorrect email or password

What it means

Thrown by AuthService.login when authentication fails: the user is not found, has no password set, or the bcrypt comparison fails. The check runs a dummy bcrypt hash against a constant (LOGIN_DUMMY_HASH) when the user is missing so response time stays constant and prevents user enumeration. The email is logged at warn level with the client IP, and a 401 Unauthorized is returned.

Source

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

  };
};

@Injectable()
export class AuthService extends BaseService {
  async login(dto: LoginCredentialDto, details: LoginDetails) {
    const config = await this.getConfig({ withCache: false });
    if (!config.passwordLogin.enabled) {
      throw new UnauthorizedException('Password login has been disabled');
    }

    const user = await this.userRepository.getByEmail(dto.email, { withPassword: true });
    // Always run bcrypt so response time is constant regardless of whether the email
    // is registered, preventing timing-based user enumeration.
    const isAuthenticated = this.cryptoRepository.compareBcrypt(dto.password, user?.password ?? LOGIN_DUMMY_HASH);

    if (!user || !user.password || !isAuthenticated) {
      this.logger.warn(`Failed login attempt for user ${dto.email} from ip address ${details.clientIp}`);
      throw new UnauthorizedException('Incorrect email or password');
    }

    return this.createLoginResponse(user, details);
  }

  async logout(auth: AuthDto, authType: AuthType): Promise<LogoutResponseDto> {
    let oauthBearerToken: string | undefined;
    if (auth.session) {
      const session = await this.sessionRepository.get(auth.session.id);
      oauthBearerToken = session?.oauthBearerToken ?? undefined;
      await this.sessionRepository.delete(auth.session.id);
      await this.eventRepository.emit('SessionDelete', { sessionId: auth.session.id });
    }

    return {
      successful: true,
      redirectUri: await this.getLogoutEndpoint(authType, oauthBearerToken),
    };

View on GitHub (pinned to 199723261c)

Solutions

  1. Verify the email is registered and the password is correct.
  2. If the account is OAuth-only, use the OAuth login flow instead.
  3. Use the password reset flow if the password is forgotten.
  4. Check server logs for the warn line to confirm whether the email itself is unknown.
Defensive patterns

Strategy: try-catch

Validate before calling

// Light client-side validation; full validation happens server-side.
if (!/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email) || !password) {
  throw new Error('Enter a valid email and password.');
}
await api.post('/auth/login', { email, password });

Try / catch

try {
  await api.post('/auth/login', { email, password });
} catch (e) {
  if (e.response?.status === 401) {
    showCredentialError(); // do NOT reveal whether email vs password was wrong
  } else throw e;
}

Prevention

When it happens

Trigger: POST /auth/login with an unregistered email; correct email but wrong password; user exists but has no password (OAuth-only account) yet tries password login.

Common situations: Typos in email or password; user created via OAuth trying password login; migrated users without passwords; caps-lock / wrong keyboard layout.

Related errors


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