Providers

Prev Next

OAuth Providers

The provider layer encapsulates all IdP-specific logic: authorization URL construction, token exchange, and response parsing. A factory maps the provider enum to a concrete class.

Factory

// src/lib/external/oauth/factory.ts
class SocialAuthFactory {
  static get(provider: SocialProvider): SocialAuthProvider {
    switch (provider) {
      case 'google':    return new GoogleProvider();
      case 'microsoft': return new MicrosoftProvider();
      default:          throw AppError.create('auth.failed', { ... });
    }
  }
}

The factory is server-only. It creates a fresh instance per request (no singleton caching). The switch acts as an exhaustiveness check at the type level since SocialProvider is a union of 'google' | 'microsoft'.

SocialAuthProvider Interface

Every provider implements two methods:

Method Signature Purpose
getAuthorizationUrl (state?: string) => string Build the IdP authorization URL with all required params
exchangeCodeForIdToken (code: string) => Promise<string> POST to the IdP token endpoint, return the id_token

Both methods throw AppError on any failure — missing config, network errors, invalid responses, or missing tokens.

Google Provider

src/lib/external/oauth/providers/google.ts

Authorization

Constructs a URL against OAuthConfig.Google.authUri (defaults to https://accounts.google.com/o/oauth2/v2/auth).

Parameter Value
client_id OAuthConfig.Google.clientId
redirect_uri OAuthConfig.Google.redirectUris
response_type code
scope openid email profile
state Passed through from caller

Google authorization does not force prompt or access_type, so Google can apply default account/session behavior without an extra consent interstitial on every login.

Token Exchange

POSTs to OAuthConfig.Google.tokenUri (defaults to https://oauth2.googleapis.com/token).

Request body (application/x-www-form-urlencoded):

  • client_id, client_secret, code, redirect_uri, grant_type: authorization_code

Expected response shape:

type GoogleTokenResponse = {
  access_token?: string;
  expires_in?: number;
  refresh_token?: string;
  scope?: string;
  token_type?: string;
  id_token?: string;
  error?: string;
  error_description?: string;
};

Returns id_token. Throws if the response contains error or is missing id_token.

Error Surface

Condition Error
Missing config config.missing — "Google OAuth configuration missing"
Empty code auth.oauth_failed — "Google authorization code is missing"
Network failure auth.oauth_failed — "Unable to connect to Google OAuth service"
Non-200 response auth.oauth_failed — "Google sign-in could not be completed"
Parse failure auth.oauth_failed — "Invalid response from Google OAuth service"
Response has error auth.oauth_failed — "Google sign-in could not be completed"
Missing id_token auth.oauth_failed — "Google sign-in response is incomplete"

Microsoft Provider

src/lib/external/oauth/providers/microsoft.ts

Authorization

Uses the Microsoft common endpoint: https://login.microsoftonline.com/common/oauth2/v2.0/authorize.

Parameter Value
client_id OAuthConfig.Microsoft.clientId
response_type code
redirect_uri OAuthConfig.Microsoft.redirectUris
response_mode query
scope openid profile email
prompt select_account
state Passed through from caller

The Microsoft provider does not use access_type: offline and continues to request select_account.

Token Exchange

POSTs to https://login.microsoftonline.com/common/oauth2/v2.0/token.

Request body (application/x-www-form-urlencoded):

  • client_id, client_secret, code, redirect_uri, grant_type: authorization_code

Expected response shape:

type MicrosoftTokenResponse = {
  token_type?: string;
  scope?: string;
  expires_in?: number;
  access_token?: string;
  id_token?: string;
  error?: string;
  error_description?: string;
};

Returns id_token. Throws if the response contains error or is missing either access_token or id_token.

common vs Tenant-Specific

The provider hard-codes common as the tenant, meaning any Microsoft account (personal or organizational) can attempt sign-in. Tenant-level restrictions are applied by the backend during the GraphQL mutation, not at the OAuth layer.

Error Surface

Condition Error
Missing config config.missing — "Microsoft OAuth configuration missing"
Empty code auth.oauth_failed — "Microsoft authorization code is missing"
Network failure auth.oauth_failed — "Unable to connect to Microsoft OAuth service"
Non-200 response auth.oauth_failed — "Microsoft sign-in could not be completed"
Parse failure auth.oauth_failed — "Invalid response from Microsoft OAuth service"
Response has error auth.oauth_failed — "Microsoft sign-in could not be completed"
Missing token auth.oauth_failed — "Microsoft sign-in response is incomplete"

Comparison

Aspect Google Microsoft
Auth endpoint Configurable via env Hard-coded (login.microsoftonline.com/common)
Token endpoint Configurable via env Hard-coded
Scope openid email profile openid profile email
Prompt Not set (Google default) select_account
access_type Not set Not set
response_mode Not set (default) query
Validation check id_token present Both access_token and id_token present

Adding a New Provider

To add a third provider (e.g., Apple, Okta):

1. Extend the Enum

// src/types/identity/index.ts
enum OAuthProviderEnum {
  Google = 'google',
  Microsoft = 'microsoft',
  Apple = 'apple', // add
}

2. Update SocialProvider

// src/lib/external/oauth/types.ts
type SocialProvider = OAuthProviderEnum.Google | OAuthProviderEnum.Microsoft | OAuthProviderEnum.Apple; // add

3. Create the Provider

// src/lib/external/oauth/providers/apple.ts
import 'server-only';

import { SocialAuthProvider } from '../types';

export class AppleProvider implements SocialAuthProvider {
  getAuthorizationUrl(state?: string): string {
    // Build Apple authorization URL
  }

  async exchangeCodeForIdToken(code: string): Promise<string> {
    // Exchange code for id_token via Apple's token endpoint
  }
}

4. Register in the Factory

// src/lib/external/oauth/factory.ts
case 'apple':
  return new AppleProvider();

5. Add Configuration

// src/config/identity/oauth.config.ts
Apple: {
  clientId: process.env.APPLE_CLIENT_ID,
  // ...
}

6. Add a Feature Flag

Add a raw flag (e.g., enable-apple-signin) and map it in the UI mapper:

showAppleSignin: flags['enable-apple-signin'] === true,

7. Add the GraphQL Mutation

Create the mutation and add a case to IdentityService.handleOAuthCallback.

8. Add the Button

Create a button component under modules/authentication/signin/social/apple/ and render it in SocialLoginModule behind the feature flag.

The server-side callback route (/api/.../callback/route.ts) also needs its provider validation updated.