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

> Explore OAuth 2.0 authentication details in Immichs auth.service.ts. Learn about authorization code flow, PKCE, token exchange, and user provisioning in this technical deep dive.

- Repository: [Immich/immich](https://github.com/immich-app/immich)
- Tags: internals
- Published: 2026-02-27

---

**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`](https://github.com/immich-app/immich/blob/main/server/src/services/auth.service.ts), which orchestrates the authentication lifecycle alongside [`server/src/repositories/oauth.repository.ts`](https://github.com/immich-app/immich/blob/main/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`](https://github.com/immich-app/immich/blob/main/auth.service.ts), the `authorize()` method performs an initial configuration check before generating the provider URL:

```typescript
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`](https://github.com/immich-app/immich/blob/main/oauth.repository.ts)) handles cryptographic parameter generation:

```typescript
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):

```typescript
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:

```typescript
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:

```typescript
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:

```typescript
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:

```typescript
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:

```typescript
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`.

### Can existing Immich users link their accounts to OAuth providers?

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.