immich-app/immich · critical · UnauthorizedException
Authentication required
Error message
Authentication required
What it means
UnauthorizedException (HTTP 401) thrown by the private validate method when none of the supported credentials are present: no share key, share slug, session token (x-immich-user-token / session-token / ?sessionKey / Bearer / access_token cookie), and no API key. It is the fallback after every credential source has been checked.
Source
Thrown at server/src/services/auth.service.ts:263
const apiKey = (headers[ImmichHeader.ApiKey] || queryParams[ImmichQuery.ApiKey]) as string;
if (shareKey) {
return this.validateSharedLinkKey(shareKey);
}
if (shareSlug) {
return this.validateSharedLinkSlug(shareSlug);
}
if (session) {
return this.validateSession(session, headers);
}
if (apiKey) {
return this.validateApiKey(apiKey);
}
throw new UnauthorizedException('Authentication required');
}
getMobileRedirect(url: string) {
return `${MOBILE_REDIRECT}?${url.split('?', 2)[1] || ''}`;
}
async authorize(dto: OAuthConfigDto) {
const { oauth } = await this.getConfig({ withCache: false });
if (!oauth.enabled) {
throw new BadRequestException('OAuth is not enabled');
}
return await this.oauthRepository.authorize(
oauth,
this.resolveRedirectUri(oauth, dto.redirectUri),
dto.state,
dto.codeChallenge,View on GitHub (pinned to 199723261c)
Solutions
- Attach a valid credential: Authorization: Bearer <accessToken>, x-api-key, session cookie, or share key.
- For browser clients, ensure cookies are sent (credentials: 'include') and same-site settings allow them.
- If the token expired, call POST /auth/login again to obtain a fresh accessToken.
- Check the reverse proxy config preserves the Authorization header and Immich-prefixed headers.
Example fix
// before
await axios.get('/albums');
// after
const { data } = await axios.post('/auth/login', { email, password });
axios.defaults.headers.Authorization = `Bearer ${data.accessToken}`;
await axios.get('/albums'); Defensive patterns
Strategy: validation
Validate before calling
function hasCredentials(headers: Record<string, string>): boolean {
return Boolean(
headers.Authorization ||
headers['x-immich-user-token'] ||
headers['x-immich-session-token'] ||
headers['x-api-key'] ||
headers['x-immich-share-key'] ||
headers.cookie,
);
} Type guard
function hasAuthHeader(headers: Record<string, unknown>): headers is Record<string, string> & { Authorization: string } {
return typeof headers.Authorization === 'string' && headers.Authorization.length > 0;
} Try / catch
try {
await axios.get('/albums', { headers: auth() });
} catch (e) {
if (e.response?.status === 401) {
const fresh = (await axios.post('/auth/login', creds)).data.accessToken;
axios.defaults.headers.Authorization = `Bearer ${fresh}`;
await axios.get('/albums');
} else throw e;
} Prevention
- Always set Authorization (or a cookie) on authenticated requests.
- Use an axios/fetch interceptor to refresh on 401.
- Verify the reverse proxy forwards the Authorization header.
When it happens
Trigger: Any authenticated route called with no Authorization header, no session cookie, no x-api-key, and no share key/slug. Common with fresh API clients that forgot to set credentials, or after the access-token cookie expired and was cleared.
Common situations: Frontend forgot to attach the bearer token; cookie blocked by third-party cookie restrictions; token expired and the refresh path is broken; curl/script missing -H headers; reverse proxy stripping the Authorization header.
Understand the failure class
- Authentication and authorization failures — expired tokens, bad credentials, and missing scopes.
Related errors
- Invalid share key
- User already has a PIN code
- User does not have a PIN code
- Wrong PIN code
- Either password or pinCode is required
AI-assisted analysis of immich-app/immich@199723261c (2026-08-12).
Data as JSON: /api/errors/a1952075b5123410.
Report an issue: GitHub.