immich-app/immich · warning · BadRequestException

Either password or pinCode is required

Error message

Either password or pinCode is required

What it means

BadRequestException (HTTP 400) thrown at the end of validatePinCode when neither dto.password nor dto.pinCode is truthy. The DTO (PinCodeResetSchema/PinCodeChangeSchema) marks both optional, so the runtime check is the only line of defense. Reached only when the user has a PIN set.

Source

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

  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);
  }

  async authenticate({ headers, queryParams, metadata }: ValidateRequest): Promise<AuthDto> {
    const authDto = await this.validate({ headers, queryParams });
    const { adminRoute, sharedLinkRoute, uri } = metadata;

View on GitHub (pinned to 199723261c)

Solutions

  1. Ensure the request body contains exactly one of `password` or `pinCode` (non-empty).
  2. Upgrade the client to send `pinCode` for unlock flows and `password` for reset flows per the API docs.
  3. Add a client-side check that one credential field is non-empty before POSTing.
  4. If using a generated SDK, regenerate it from the current open-api spec to pick up schema changes.

Example fix

// before
await api.authApi.resetPinCode({});

// after
if (!body.password && !body.pinCode) {
  throw new Error('password or pinCode required');
}
await api.authApi.resetPinCode(body);
Defensive patterns

Strategy: validation

Validate before calling

function validatePinResetBody(body: { password?: string; pinCode?: string }) {
  if (!body.password && !body.pinCode) {
    throw new Error('password or pinCode required');
  }
}

Type guard

function hasPinCredential(body: { password?: string; pinCode?: string }): body is { password: string } | { pinCode: string } {
  return Boolean(body.password) || Boolean(body.pinCode);
}

Try / catch

try {
  await api.authApi.resetPinCode(body);
} catch (e) {
  if (e.response?.data?.message === 'Either password or pinCode is required') {
    showCredentialRequiredError();
  } else throw e;
}

Prevention

When it happens

Trigger: PUT /auth/pin-code, DELETE /auth/pin-code, or POST /auth/session/unlock with body that omits both password and pinCode, or sends empty strings. Because PinCodeResetSchema has both as optional, the request passes DTO validation and fails here.

Common situations: Client bug submitting an empty form; serialization issue that drops falsy fields; client assumes the session is already elevated and sends no credentials; version mismatch where the client predates the password-or-pinCode requirement.

Related errors


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