immich-app/immich · warning · BadRequestException

Wrong PIN code

Error message

Wrong PIN code

What it means

BadRequestException (HTTP 400) raised inside validatePinCode when dto.pinCode was supplied (and dto.password was not) but it does not bcrypt-match the stored user.pinCode hash. Guards the PIN-protected reset/change/unlock endpoints. Failure does not lock the session; the caller may retry.

Source

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

    const hashed = await this.cryptoRepository.hashBcrypt(dto.newPinCode, SALT_ROUNDS);
    await this.userRepository.update(auth.user.id, { pinCode: hashed });
  }

  private validatePinCode(
    user: { pinCode: string | null; password: string | null },
    dto: { pinCode?: string; password?: string },
  ) {
    if (!user.pinCode) {
      throw new BadRequestException('User does not have a PIN code');
    }

    if (dto.password) {
      if (!this.validateSecret(dto.password, user.password)) {
        throw new BadRequestException('Wrong password');
      }
    } else if (dto.pinCode) {
      if (!this.validateSecret(dto.pinCode, user.pinCode)) {
        throw new BadRequestException('Wrong PIN code');
      }
    } else {
      throw new BadRequestException('Either password or pinCode is required');
    }
  }

  async adminSignUp(dto: SignUpDto): Promise<UserAdminResponseDto> {
    const admin = await this.createUser({
      isAdmin: true,
      email: dto.email,
      name: dto.name,
      password: dto.password,
      storageLabel: 'admin',
    });

    return mapUserAdmin(admin);
  }

View on GitHub (pinned to 199723261c)

Solutions

  1. Re-prompt the user for the current PIN (not the new one in a change flow).
  2. If the PIN is forgotten, fall back to the password branch: send {password} instead of {pinCode}.
  3. If both PIN and password are lost, an admin must clear the pinCode column out of band so the user can re-setup.
  4. Confirm the PIN matches /^\d{6}$/ before sending (validation runs client-side too).

Example fix

// before
await api.authApi.unlockSession({ pinCode: userInput });

// after
if (!/^\d{6}$/.test(userInput)) {
  showPinFormatError();
  return;
}
try {
  await api.authApi.unlockSession({ pinCode: userInput });
} catch (e) {
  // fall back to password auth for unlock
  await api.authApi.unlockSession({ password: await askPassword() });
}
Defensive patterns

Strategy: try-catch

Validate before calling

function isValidPinFormat(pin: string): boolean {
  return /^\d{6}$/.test(pin);
}
if (!isValidPinFormat(pin)) throw new Error('PIN must be 6 digits');

Type guard

function isSixDigitPin(value: unknown): value is string {
  return typeof value === 'string' && /^\d{6}$/.test(value);
}

Try / catch

try {
  await api.authApi.unlockSession({ pinCode });
} catch (e) {
  if (e.response?.data?.message === 'Wrong PIN code') {
    retryOrFailToFallback();
  } else throw e;
}

Prevention

When it happens

Trigger: PUT /auth/pin-code, DELETE /auth/pin-code, or POST /auth/session/unlock with body {pinCode: '<wrong>'} and no password field. Reached only after the !user.pinCode check passes.

Common situations: User mistyped the 6-digit PIN; PIN was changed on another device; PIN hash corrupted or migrated incorrectly; client sent the new PIN instead of the current one.

Related errors


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