immich-app/immich · error · UnauthorizedException

Incorrect email or password

Error message

Incorrect email or password

What it means

After fetching the user by email (with password hash) and comparing bcrypt (against a dummy hash when the user is unknown, for constant-time behavior), login throws this UnauthorizedException (401) when the user does not exist, has no password set, or the password does not match. Failed attempts are logged server-side with the client IP.

Solutions

  1. Verify the email exists and the password is correct; use the web UI to confirm the credentials work.
  2. For OAuth-provisioned users, use the OAuth login flow or set a password via the admin/user password reset flow.
  3. Trim whitespace and check for case issues in the email before retrying.
  4. If credentials are definitely correct, check server logs for 'Failed login attempt' to confirm which email/ip was attempted, and confirm you are hitting the intended instance.
  5. Repeated failures: reset the password through an admin or the offline password-reset CLI.

Example fix

// before
const { accessToken } = await api.login({ email: ' Admin@example.com ', password });
// after
const email = 'admin@example.com'.trim();
if (!(await api.validatePassword(email, password))) {
  throw new Error('Check credentials or reset password via admin');
}
Defensive patterns

Strategy: try-catch

Validate before calling

const email = emailInput.trim().toLowerCase();
if (!email || !password) throw new Error('Email and password are required');

Try / catch

try { return await api.login({ email, password }); } catch (e) { if (e.status === 401) { showCredentialError(); logLocalAttempt(); } else throw e; }

Prevention

When it happens

Trigger: POST /api/auth/login where getByEmail returns null (unknown email), user.password is null (OAuth-provisioned user), or compareBcrypt(dto.password, hash) is false (wrong password).

Common situations: Typos in email/password; users created via OAuth who never set a local password (or used the 'Change Password' reset flow incorrectly); caps-lock/whitespace in credentials; clients pointing at the wrong Immich instance; after admin reset of a user's password.

Related errors


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

Appendix: source

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

  };
};

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