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
- Re-authenticate to obtain a fresh session for a valid user, then retry PIN setup.
- Confirm the account still exists before calling setupPinCode.
- 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
- Re-authenticate when a 401 is returned from PIN setup rather than retrying blindly.
- Confirm the account still exists before PIN operations.
- Do not cache PIN-setup ability for deleted users.
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
- Authentication and authorization failures — expired tokens, bad credentials, and missing scopes.
Related errors
- Invalid user token
- This endpoint can only be used with a session token
- Password login has been disabled
- Incorrect email or password
- Invalid logout token: it must contain either a sub or a sid
AI-assisted analysis of immich-app/immich@199723261c (2026-08-12).
Data as JSON: /api/errors/3768b5a2ad8a04ee.
Report an issue: GitHub.