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
- Re-prompt the user for the current PIN (not the new one in a change flow).
- If the PIN is forgotten, fall back to the password branch: send {password} instead of {pinCode}.
- If both PIN and password are lost, an admin must clear the pinCode column out of band so the user can re-setup.
- 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
- Validate /^\d{6}$/ client-side before sending.
- Distinguish current vs. new PIN in change flows.
- Provide a password-based fallback for users who forgot the PIN.
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
- User already has a PIN code
- User does not have a PIN code
- Either password or pinCode is required
- Forbidden
- Missing required permission: ${requestedPermission}
AI-assisted analysis of immich-app/immich@199723261c (2026-08-12).
Data as JSON: /api/errors/f29dd38f2e6e5855.
Report an issue: GitHub.