Flow

Prev Next

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

Image

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)

  1. Sets loading state.
  2. Calls IdentityService.getDeviceId() to obtain a device identifier.
  3. Stores deviceId and oauthProvider in cookies (used later in callback).
  4. Calls IdentityService.initOAuthLogin(provider), which fetches GET /api/identity/oauth/:provider/init.
  5. Returns the authorization URL. The button component sets window.location.href to it.

Server Side (/api/.../init/route.ts)

  1. Feature flag check — calls FeatureFlagService.list(), returns 404 if the provider is disabled.
  2. Factory lookupSocialAuthFactory.get(provider) returns a GoogleProvider or MicrosoftProvider instance.
  3. State generationcrypto.randomUUID() creates a CSRF token.
  4. URL constructionprovider.getAuthorizationUrl(state) builds the full authorization URL with query params.
  5. Response — returns { url } as JSON, sets a oauth_state cookie (httpOnly, secure, sameSite: lax, 10-minute TTL).

Authorization URL Parameters

Parameter Google 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)

  1. Extract params — reads code and state from the URL query string.
  2. Validate provider — rejects anything other than google or microsoft.
  3. CSRF check — compares state to the oauth_state cookie. Rejects on mismatch.
  4. Clear state cookie — sets oauth_state to empty with maxAge: 0.
  5. Token exchangeSocialAuthFactory.get(provider) creates the provider, calls exchangeCodeForIdToken(code).
    • The provider POSTs to the IdP's token endpoint with client_id, client_secret, code, redirect_uri, and grant_type: authorization_code.
    • Returns the id_token from the response.
  6. RedirectNextResponse.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:

  1. Reads window.location.hash.
  2. Checks for an error key — if present, shows the error and clears the hash.
  3. Checks for provider and token keys — if present, calls handleOAuthCallback(provider, token).
  4. Clears the hash to prevent reprocessing.

useAuth.handleOAuthCallback

  1. Reads the deviceId cookie (set during initialization).
  2. Validates that provider, code (the id_token), and deviceId are all present.
  3. Calls IdentityService.handleOAuthCallback(provider, code, deviceId).

IdentityService.handleOAuthCallback

  1. Dynamically imports the correct GraphQL mutation based on provider:
    • Google: GOOGLE_LOGIN_MUTATION (googleLogin)
    • Microsoft: MICROSOFT_LOGIN_MUTATION (microsoftLogin)
  2. Executes the mutation via GraphQLClient.mutate through AsyncHandler.run.
  3. 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:

  1. useAuth.handleOAuthCallback checks that status === LoginStatusEnum.SUCCESS and mfaToken is falsy.
  2. 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'