OAuth Integration
Social sign-in via external identity providers. The system uses a factory-based architecture so new providers can be added without changing the auth pipeline. Currently ships Google and Microsoft, both gated by feature flags.
Architecture

Source Layout
src/
lib/external/oauth/
types.ts # SocialAuthProvider interface, SocialProvider type, VerifiedIdentity
factory.ts # SocialAuthFactory — maps provider key to class instance
providers/
google.ts # GoogleProvider — Authorization Code flow with Google
microsoft.ts # MicrosoftProvider — Authorization Code flow with Microsoft
GoogleOAuth.ts # Legacy (unused) — superseded by provider pattern
app/api/identity/oauth/
[provider]/
init/route.ts # GET — flag check, factory, build auth URL, set state cookie
callback/route.ts # GET — validate state, exchange code, redirect with token hash
services/
IdentityService.ts # initOAuthLogin(), handleOAuthCallback() — client-side orchestration
hooks/auth/
useAuth.ts # oauthInit(), handleOAuthCallback() — React wrapper
modules/authentication/signin/social/
index.tsx # SocialLoginModule — reads hash, dispatches callback
google/index.tsx # GoogleSignin button
microsoft/index.tsx # MicrosoftSignin button
graphql/mutations/identity/
googleLogin.mutation.ts # googleLogin(token, deviceId)
microsoftLogin.mutation.ts # microsoftLogin(token, deviceId)
config/identity/
oauth.config.ts # OAuthConfig — env-var-backed config (server-only)
types/identity/
index.ts # OAuthProviderEnum, related auth types
Configuration
All OAuth config is server-only (guarded by import 'server-only').
| Key | Env Var | Default |
|---|---|---|
clientId |
GOOGLE_OAUTH_CLIENT_ID |
— |
clientSecret |
CLIENT_SECRET |
— |
redirectUris |
GOOGLE_OAUTH_REDIRECT_URIS |
— |
authUri |
GOOGLE_OAUTH_AUTH_URI |
https://accounts.google.com/o/oauth2/v2/auth |
tokenUri |
GOOGLE_OAUTH_TOKEN_URI |
https://oauth2.googleapis.com/token |
profileUri |
GOOGLE_OAUTH_PROFILE_URI |
https://www.googleapis.com/oauth2/v1/userinfo |
Microsoft
| Key | Env Var | Default |
|---|---|---|
clientId |
MS_CLIENT_ID |
— |
clientSecret |
MS_CLIENT_SECRET |
— |
redirectUris |
MS_OAUTH_REDIRECT_URIS |
— |
tenantId |
MS_TENANT_ID |
— |
Feature Flags
Both providers are independently gated:
| Flag (raw) | UI key | Controls |
|---|---|---|
enable-google-signin |
showGoogleSignin |
Google button visibility + init route |
enable-microsoft-signin |
showMicrosoftSignin |
Microsoft button visibility + init route |
Flags are checked in two places:
- Client —
SocialLoginModulereads flags fromFeatureFlagContextto conditionally render buttons. - Server — the
/initroute callsFeatureFlagService.list()and returns 404 if the provider is disabled.
Types
OAuthProviderEnum
enum OAuthProviderEnum {
Google = 'google',
Microsoft = 'microsoft',
}
SocialProvider
type SocialProvider = OAuthProviderEnum.Google | OAuthProviderEnum.Microsoft;
SocialAuthProvider
The interface every provider must implement:
interface SocialAuthProvider {
getAuthorizationUrl(state?: string): string;
exchangeCodeForIdToken(code: string): Promise<string>;
}
VerifiedIdentity
Data shape returned after successful token verification (reserved for future use with direct profile parsing):
interface VerifiedIdentity {
provider: SocialProvider;
providerUserId: string;
email: string;
emailVerified: boolean;
firstName?: string;
lastName?: string;
picture?: string;
}
Error Handling
All OAuth errors surface through the AppError system using the auth.oauth_failed code.
| Error Code | Status | Default Message |
|---|---|---|
auth.oauth_failed |
401 | OAuth authentication failed |
config.missing |
— | Provider configuration missing |
config.graphql_query_missing |
— | GraphQL mutation not found |
system.not_implemented |
— | Unsupported provider |
Provider classes throw granular messages (e.g., "Unable to connect to Google OAuth service", "Invalid response from Microsoft OAuth service") which bubble up through AppError.create().
Design Decisions
Why Authorization Code flow, not Implicit? Authorization Code flow keeps the client_secret on the server. The callback route exchanges the code server-side, so secrets never reach the browser.
Why redirect with hash fragment? The callback route redirects to ${frontendUrl}#provider=...&token=.... Hash fragments are never sent to the server by the browser, so the id_token stays client-side during the redirect.
Why factory instead of a provider registry? The switch-based factory in a server-only file keeps the mapping static and tree-shakeable. A dynamic registry would add complexity without clear benefit at two providers.
Why feature-flag the init route? Double-gating (client + server) ensures a disabled provider cannot be invoked even if the UI flag check is bypassed.
Why GoogleOAuth.ts is still in the tree? Legacy file from the pre-factory implementation. It is not imported anywhere and can be removed.
What's in This Section
| Doc | Covers |
|---|---|
| Flow | End-to-end request lifecycle from button click to tenant selection |
| Providers | Factory, provider implementations, adding a new provider |