immich-app/immich · error · UnauthorizedException

Unauthorized

Error message

Unauthorized

What it means

Thrown by AuthService.setupPinCode (POST /auth/pin-code) when userRepository.getForPinCode(auth.user.id) returns null. Because the caller is already authenticated (the route requires PinCodeCreate permission), a null user is unexpected and is treated as an authorization failure, raising a bare UnauthorizedException (401). It signals the authenticated subject does not map to a user row in the state required for PIN setup.

Source

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

    }

    const hashedPassword = await this.cryptoRepository.hashBcrypt(newPassword, SALT_ROUNDS);

    const updatedUser = await this.userRepository.update(user.id, { password: hashedPassword });

    await this.eventRepository.emit('AuthChangePassword', {
      userId: user.id,
      currentSessionId: auth.session?.id,
      invalidateSessions: dto.invalidateSessions,
    });

    return mapUserAdmin(updatedUser);
  }

  async setupPinCode(auth: AuthDto, { pinCode }: PinCodeSetupDto) {
    const user = await this.userRepository.getForPinCode(auth.user.id);
    if (!user) {
      throw new UnauthorizedException();
    }

    if (user.pinCode) {
      throw new BadRequestException('User already has a PIN code');
    }

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

  async resetPinCode(auth: AuthDto, dto: PinCodeResetDto) {
    const user = await this.userRepository.getForPinCode(auth.user.id);
    this.validatePinCode(user, dto);

    await this.userRepository.update(auth.user.id, { pinCode: null });
    await this.sessionRepository.lockAll(auth.user.id);
  }

View on GitHub (pinned to 199723261c)

Solutions

  1. Re-authenticate to obtain a fresh session for a valid user, then retry PIN setup.
  2. Confirm the account still exists before calling setupPinCode.
  3. If the account was deleted, no PIN setup is possible; surface a re-login prompt to the user.

Example fix

// before
await api.post('/auth/pin-code', { pinCode });

// after
try {
  await api.post('/auth/pin-code', { pinCode });
} catch (e) {
  if (e.response?.status === 401) {
    await reAuthenticate(); // session/user no longer valid
    await api.post('/auth/pin-code', { pinCode });
  } else throw e;
}
Defensive patterns

Strategy: try-catch

Validate before calling

// PIN setup requires a live user; confirm the session is still valid first.
const { data } = await api.get('/auth/status').catch(() => ({ data: null }));
if (!data?.user) {
  throw new Error('Session no longer valid; re-authenticate before PIN setup.');
}
await api.post('/auth/pin-code', { pinCode });

Try / catch

try {
  await api.post('/auth/pin-code', { pinCode });
} catch (e) {
  if (e.response?.status === 401) {
    await reAuthenticate();
    await api.post('/auth/pin-code', { pinCode });
  } else throw e;
}

Prevention

When it happens

Trigger: POST /auth/pin-code by an authenticated session whose user cannot be loaded via getForPinCode (user deleted mid-session, or getForPinCode's query excludes the user); session token valid but user row gone.

Common situations: User account deleted while a session was still active; the authenticated identity is a non-user service account that has no PIN row; race between account deletion and a PIN-setup request.

Understand the failure class

Related errors


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