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

  1. Attach a valid credential: Authorization: Bearer <accessToken>, x-api-key, session cookie, or share key.
  2. For browser clients, ensure cookies are sent (credentials: 'include') and same-site settings allow them.
  3. If the token expired, call POST /auth/login again to obtain a fresh accessToken.
  4. 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

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

Related errors


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