OAuth Flow
End-to-end lifecycle of a social sign-in request, from the user clicking "Sign in with Google/Microsoft" to arriving at the tenant selection screen.
Sequence

Phase 1: Initialization
Trigger: User clicks the Google or Microsoft sign-in button.
The button component calls oauthInit(OAuthProviderEnum.Google) (or .Microsoft) from AuthContext.
Client Side (useAuth.oauthInit)
- Sets loading state.
- Calls
IdentityService.getDeviceId()to obtain a device identifier. - Stores
deviceIdandoauthProviderin cookies (used later in callback). - Calls
IdentityService.initOAuthLogin(provider), which fetchesGET /api/identity/oauth/:provider/init. - Returns the authorization URL. The button component sets
window.location.hrefto it.
Server Side (/api/.../init/route.ts)
- Feature flag check — calls
FeatureFlagService.list(), returns 404 if the provider is disabled. - Factory lookup —
SocialAuthFactory.get(provider)returns aGoogleProviderorMicrosoftProviderinstance. - State generation —
crypto.randomUUID()creates a CSRF token. - URL construction —
provider.getAuthorizationUrl(state)builds the full authorization URL with query params. - Response — returns
{ url }as JSON, sets aoauth_statecookie (httpOnly, secure, sameSite: lax, 10-minute TTL).
Authorization URL Parameters
| Parameter | Microsoft | |
|---|---|---|
client_id |
from config | from config |
redirect_uri |
GOOGLE_OAUTH_REDIRECT_URIS |
MS_OAUTH_REDIRECT_URIS |
response_type |
code |
code |
scope |
openid email profile |
openid profile email |
access_type |
— | — |
prompt |
— | select_account |
response_mode |
— | query |
state |
CSRF UUID | CSRF UUID |
Phase 2: Identity Provider
The browser redirects to Google or Microsoft. The user authenticates and (if needed) consents to the requested scopes. On success, the IdP redirects to the configured callback URL with code and state as query parameters.
Phase 3: Callback
Server Side (/api/.../callback/route.ts)
- Extract params — reads
codeandstatefrom the URL query string. - Validate provider — rejects anything other than
googleormicrosoft. - CSRF check — compares
stateto theoauth_statecookie. Rejects on mismatch. - Clear state cookie — sets
oauth_stateto empty withmaxAge: 0. - Token exchange —
SocialAuthFactory.get(provider)creates the provider, callsexchangeCodeForIdToken(code).- The provider POSTs to the IdP's token endpoint with
client_id,client_secret,code,redirect_uri, andgrant_type: authorization_code. - Returns the
id_tokenfrom the response.
- The provider POSTs to the IdP's token endpoint with
- Redirect —
NextResponse.redirect(frontendUrl#provider=...&token=...).
:::tip Why a Hash Fragment?
The redirect uses a URL hash (#provider=...&token=...) rather than query parameters. Hash fragments are never sent to the server by the browser, so:
- The id_token is only accessible client-side.
- Server-side logging never captures the token.
- Next.js routing does not need to handle the token as a search param.
:::
Error Path
If any step fails, the callback redirects to frontendUrl#error=<message>. SocialLoginModule detects the error key in the hash and displays it via the message popup.
Phase 4: Client Callback Processing
SocialLoginModule (modules/authentication/signin/social/index.tsx)
On mount, the component:
- Reads
window.location.hash. - Checks for an
errorkey — if present, shows the error and clears the hash. - Checks for
providerandtokenkeys — if present, callshandleOAuthCallback(provider, token). - Clears the hash to prevent reprocessing.
useAuth.handleOAuthCallback
- Reads the
deviceIdcookie (set during initialization). - Validates that
provider,code(the id_token), anddeviceIdare all present. - Calls
IdentityService.handleOAuthCallback(provider, code, deviceId).
IdentityService.handleOAuthCallback
- Dynamically imports the correct GraphQL mutation based on provider:
- Google:
GOOGLE_LOGIN_MUTATION(googleLogin) - Microsoft:
MICROSOFT_LOGIN_MUTATION(microsoftLogin)
- Google:
- Executes the mutation via
GraphQLClient.mutatethroughAsyncHandler.run. - Returns
{ status, mfaToken, selectionToken, tenants }.
GraphQL Mutations
Both mutations have the same shape:
mutation googleLogin($token: String!, $deviceId: String!) {
googleLogin(token: $token, deviceId: $deviceId) {
status
mfaToken
selectionToken
tenants {
name
logo
internalName
}
}
}
The backend validates the id_token, resolves the user, and returns the tenant list. The mutation name is the only difference between providers (googleLogin vs microsoftLogin).
Phase 5: Tenant Selection
After receiving the response:
useAuth.handleOAuthCallbackchecks thatstatus === LoginStatusEnum.SUCCESSandmfaTokenis falsy.- Calls
handleTenants(selectionToken, tenants)— the shared tenant-selection flow used by all auth methods.
From this point forward, the OAuth flow merges into the same path as password-based login.
Security Model
| Concern | Mitigation |
|---|---|
| CSRF | oauth_state cookie + state param comparison |
| Secret exposure | All provider config is server-only; client_secret never reaches the client |
| Token in URL | Hash fragment, not query param; never sent to server |
| Replay | State cookie cleared after use; 10-minute TTL |
| Disabled provider bypass | Server-side feature flag check at init route |
| SSR safety | SocialLoginModule guards with typeof window === 'undefined' |