OAuth 2.0 Authentication in auth.service.ts: Implementation Details in Immich

The AuthService class in Immich implements a standards-compliant OAuth 2.0 Authorization Code flow with PKCE, handling authorization URL generation, token exchange, user provisioning, and session management through methods like authorize(), callback(), and createLoginResponse().

Immich leverages OpenID Connect (OIDC) to enable single sign-on (SSO) via external identity providers. The server-side implementation resides primarily in server/src/services/auth.service.ts, which orchestrates the authentication lifecycle alongside server/src/repositories/oauth.repository.ts for low-level OIDC client operations. This article examines the complete OAuth 2.0 implementation, from initial authorization requests through session token generation.

Authorization Request Flow

The OAuth flow begins when a client requests an authorization URL. The authorize() method validates configuration and delegates URL construction to the OAuthRepository.

The authorize() Method

Located at lines 48‑61 in auth.service.ts, the authorize() method performs an initial configuration check before generating the provider URL:

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

Key implementation details:

  • Config validation – Immediately aborts if OAuth is disabled in the server configuration
  • Redirect URI resolution – Applies mobile-specific overrides via resolveRedirectUri() before passing to the repository
  • PKCE support – Accepts an optional codeChallenge from clients that generate their own PKCE parameters

PKCE and State Generation

The OAuthRepository.authorize() method (lines 26‑53 in oauth.repository.ts) handles cryptographic parameter generation:

async authorize(config: OAuthConfig, redirectUrl: string, state?: string, codeChallenge?: string) {
  const { buildAuthorizationUrl, randomState, randomPKCECodeVerifier, calculatePKCECodeChallenge } =
    await import('openid-client');
  const client = await this.getClient(config);
  state ??= randomState();

  let codeVerifier: string | null;
  if (codeChallenge) {
    codeVerifier = null;
  } else {
    codeVerifier = randomPKCECodeVerifier();
    codeChallenge = await calculatePKCECodeChallenge(codeVerifier);
  }

  const params: Record<string, string> = {
    redirect_uri: redirectUrl,
    scope: config.scope,
    state,
  };

  if (client.serverMetadata().supportsPKCE()) {
    params.code_challenge = codeChallenge;
    params.code_challenge_method = 'S256';
  }

  const url = buildAuthorizationUrl(client, params).toString();
  return { url, state, codeVerifier };
}

This implementation generates a cryptographically random state for CSRF protection and a PKCE code verifier using the openid-client library. When the provider supports PKCE, it includes the code_challenge with method S256 (SHA-256).

Mobile Redirect Handling

Immich supports custom mobile app schemes through the private resolveRedirectUri() method (lines 64‑71):

private resolveRedirectUri(
  { mobileRedirectUri, mobileOverrideEnabled }: { mobileRedirectUri: string; mobileOverrideEnabled: boolean },
  url: string,
) {
  if (mobileOverrideEnabled && mobileRedirectUri) {
    return url.replace(/app\.immich:\/+oauth-callback/, mobileRedirectUri);
  }
  return url;
}

When mobile override is enabled, this method rewrites the standard app.immich://oauth-callback URL to a custom scheme configured by the administrator.

Callback and Token Exchange

After the user authenticates with the provider, the callback() method (lines 63‑138) handles authorization code validation, token exchange, and user provisioning.

State and Verifier Validation

The callback implementation first validates security parameters extracted from either the request body or HTTP cookies:

async callback(dto: OAuthCallbackDto, headers: IncomingHttpHeaders, loginDetails: LoginDetails) {
  const expectedState = dto.state ?? this.getCookieOauthState(headers);
  if (!expectedState?.length) {
    throw new BadRequestException('OAuth state is missing');
  }

  const codeVerifier = dto.codeVerifier ?? this.getCookieCodeVerifier(headers);
  if (!codeVerifier?.length) {
    throw new BadRequestException('OAuth code verifier is missing');
  }

  const { oauth } = await this.getConfig({ withCache: false });
  const url = this.resolveRedirectUri(oauth, dto.url);
  const profile = await this.oauthRepository.getProfile(oauth, url, expectedState, codeVerifier);
  // ... user provisioning logic
}

Security checks performed:

  • CSRF protection – Validates the state parameter matches the value issued during authorization
  • PKCE verification – Ensures the codeVerifier is present for token exchange
  • URL resolution – Applies the same mobile redirect logic to ensure consistency

The actual token exchange occurs in OAuthRepository.getProfile() (lines 61‑78), which executes the Authorization Code grant:

async getProfile(
  config: OAuthConfig,
  url: string,
  expectedState: string,
  codeVerifier: string,
): Promise<OAuthProfile> {
  const { authorizationCodeGrant, fetchUserInfo, ...oidc } = await import('openid-client');
  const client = await this.getClient(config);
  const pkceCodeVerifier = client.serverMetadata().supportsPKCE() ? codeVerifier : undefined;

  const tokens = await authorizationCodeGrant(
    client,
    new URL(url),
    { expectedState, pkceCodeVerifier },
  );
  const profile = await fetchUserInfo(client, tokens.access_token, oidc.skipSubjectCheck);
  if (!profile.sub) {
    throw new Error('Unexpected profile response, no `sub`');
  }
  return profile;
}

User Provisioning and Auto-Registration

Upon successful profile retrieval, Immich implements a cascading user lookup strategy:

  1. OAuth ID lookup – Searches for existing users via userRepository.getByOAuthId(profile.sub)
  2. Email linking – Falls back to email matching if no OAuth ID exists
  3. Auto-registration – Creates a new user when oauth.autoRegister is enabled and no existing account is found

The implementation extracts claims from the OIDC profile to configure the new user:

  • storageLabelClaim – Maps to the user's storage label
  • storageQuotaClaim – Sets the storage quota limit
  • roleClaim – Assigns administrative or user roles

Profile Picture Synchronization

If the OAuth profile contains a picture URL and the Immich user lacks a profile image, the service downloads and stores the image:

if (!user.profileImagePath && profile.picture) {
  await this.syncProfilePicture(user, profile.picture);
}

This occurs before session creation, ensuring the user entity is fully synchronized with the identity provider.

Session Management

After authentication succeeds, createLoginResponse() (lines 44‑56) generates a cryptographically secure session token:

private async createLoginResponse(user: UserAdmin, loginDetails: LoginDetails) {
  const token = this.cryptoRepository.randomBytesAsText(32);
  const hashed = this.cryptoRepository.hashSha256(token);

  await this.sessionRepository.create({
    token: hashed,
    deviceOS: loginDetails.deviceOS,
    deviceType: loginDetails.deviceType,
    appVersion: loginDetails.appVersion,
    userId: user.id,
  });

  return mapLoginResponse(user, token);
}

Session security features:

  • 32-byte random tokens – Generated using cryptoRepository.randomBytesAsText(32)
  • Hash storage – Only the SHA-256 hash is persisted; the clear token is returned to the client
  • Device tracking – Records user agent details for session management

Account Linking and Logout

Immich supports linking existing local accounts to OAuth providers and provides provider-initiated logout functionality.

Linking Existing Accounts

Users with existing Immich credentials can link their accounts to an OAuth provider. The linking flow validates state and verifier parameters identically to the login flow, then stores the OAuth sub claim in the user's oauthId field. Duplicate link attempts are rejected to prevent account takeovers.

Logout Endpoint Resolution

When sessions terminate, getLogoutEndpoint() (lines 92‑102) determines whether to redirect to the provider's end-session endpoint:

private async getLogoutEndpoint(authType: AuthType): Promise<string> {
  if (authType !== AuthType.OAuth) {
    return LOGIN_URL;
  }

  const config = await this.getConfig({ withCache: false });
  if (!config.oauth.enabled) {
    return LOGIN_URL;
  }

  return (await this.oauthRepository.getLogoutEndpoint(config.oauth)) || LOGIN_URL;
}

If the OAuth provider exposes a end_session_endpoint in its discovery metadata, Immich redirects the user there to ensure single logout (SLO) compliance.

Summary

  • PKCE by default – Immich generates S256 code challenges for all authorization requests, with fallback support for clients providing their own challenges
  • Dual validation strategy – The callback() method validates both CSRF state and PKCE verifiers, accepting them from either request parameters or secure cookies
  • Automatic user provisioning – First-time OAuth users are automatically registered when autoRegister is enabled, with claims mapping storage quotas and roles
  • Secure session tokens – Sessions use 32-byte cryptographically random tokens stored as SHA-256 hashes in the database
  • Mobile compatibility – The resolveRedirectUri() method enables deep-link callbacks for mobile applications via configurable URL rewriting

Frequently Asked Questions

How does Immich handle PKCE in the OAuth flow?

Immich implements PKCE (Proof Key for Code Exchange) automatically in OAuthRepository.authorize(). When the OIDC provider supports PKCE (detected via client.serverMetadata().supportsPKCE()), the repository generates a random code verifier, calculates the S256 hash, and includes code_challenge and code_challenge_method parameters in the authorization URL. During the callback, OAuthRepository.getProfile() passes the original verifier to authorizationCodeGrant() for token exchange validation.

What happens when a user logs in via OAuth for the first time?

If oauth.autoRegister is enabled, Immich creates a new user account automatically. The service extracts claims from the OIDC profile using configurable claim names (storageLabelClaim, storageQuotaClaim, roleClaim) to set initial user properties. If auto-registration is disabled and no existing user matches the OAuth ID or email, the authentication fails with an error.

How does Immich secure OAuth callbacks against CSRF attacks?

The implementation uses state parameter validation in AuthService.callback(). The method retrieves the expected state from either the request DTO or HTTP cookies using getCookieOauthState(), then passes it to authorizationCodeGrant() which validates it against the state returned by the provider. Requests with missing or mismatched state values are rejected with BadRequestException.

Yes. Immich provides a linking flow that validates the OAuth state and verifier, retrieves the provider's sub claim, checks for conflicts with existing linked accounts, and then updates the current user's oauthId field. Users can also unlink their accounts, which clears the oauthId field and reverts authentication to password-based login.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →