Skip to content
Live in production, in shadow mode: FingerTrackID recommends, your system decides.Status →

Login with Google, OAuth and other redirects

A password login stays on your page until its submit, so the collection recipe covers it. A login through Google sign-in or another OAuth/OIDC provider usually does not: the browser leaves your page for the provider and comes back to a callback on your server. This page shows how to keep browser evidence in that flow and how to verify it.

The SDK keeps its session, observations, event handles and the token only in page memory. It never writes them to storage, and it must not: the token is a single-use credential for one business event. A full-page navigation discards all of it. If the page starts the SDK and then navigates to the provider:

  • the SDK’s first request may be cut off by the navigation, so no collection reaches FingerTrackID;
  • even a completed collection is useless once its handle is gone, because only that page could have asked for the event context;
  • your callback runs on the server, where there is no SDK, so the login is assessed with event_context_token: null.

Nothing fails visibly: the login works and the assessment is stored. What is missing is the browser evidence.

Your backend still sends the login assessment (tag login; LOGIN in v1) with event_context_token: null, which the request schema accepts as “no collection supplied”. The stored assessment then has:

Field Without browser evidence
assessment_status INCONCLUSIVE
trust_score null (no score; not 0, not 100)
device_intelligence_snapshot_id, device_intelligence_snapshot_version, sdk_version, device_id null
signals All 13 indicators NOT_EVALUATED
limitation_codes For example COLLECTOR_NOT_LOADED
evidence_coverage 0

device_intelligence_snapshot_id: null is the reliable way to tell, from the response alone, that no browser evidence was attached.

Use the recipes on the collection page, for a classic form POST or a single-page application. The context goes with the request your backend assesses: the password form, or the MFA code form when your login asks for one. The backend assesses after it has verified the password and any MFA, using the token only while its expiresAt leaves enough time (fail-open wrapper).

Both patterns produce one context for the login and carry it to your backend without ever putting it in a URL, the OAuth state or browser storage. Pick by how long users spend at the provider compared with your application’s evidence window:

Pattern Collects Best when Cost
A. Before the redirect On the first intent toward “Continue with Google”, taken on its click The round trip to the provider fits comfortably inside the window Up to your ceiling before the redirect
B. After the callback On an intermediate page of yours, before the session is finished Users may spend long at the provider (account choice, password, provider 2FA), or the window is 60 seconds One extra page, which waits up to your ceiling

With a 60-second window, time spent at the provider counts against the token, so pattern B is the more reliable choice. With 300 seconds, pattern A covers most provider round trips. Popup, One Tap and FedCM sign-ins do not leave the page, and the user may never touch a button of yours: call prepareNextAction() on the first focusin or pointerdown of the login form or of the container that holds the sign-in, on the sign-in button’s pointerenter, focus or pointerdown, or when your code displays the One Tap or FedCM prompt after a user action. Never at page load: do not prepare when One Tap or FedCM displays itself automatically as the page loads, because a prompt displayed on every load would open a collection that counts as usage (the usage unit) on every load, also for visitors who never sign in. Take the context with takeActionContext() when the credential arrives and send it with the request that posts the credential, as in a password login. Without a prepare beforehand, the credential callback waits up to your ceiling while the collection starts, and often reaches your backend with null.

Pattern A: take the context on the click, before redirecting

Section titled “Pattern A: take the context on the click, before redirecting”
  1. Call prepareNextAction() from the collection recipe on the first intent toward the Google button (pointerenter, focus, pointerdown), never at page load, so the context can be ready by the click. On the click, take it with takeActionContext(), which waits at most your ceiling. On touch devices pointerdown comes just before the click, so the click may wait up to SUBMIT_WAIT_MS.
  2. Send the context (or null with its reason) to your backend in a same-origin, CSRF-protected request. The backend keeps it server-side, in the pre-login session that already holds your OAuth state and PKCE verifier (or the encrypted, HttpOnly, short-lived cookie your framework uses for them), bound to that state.
  3. Start the provider redirect as you do today. Do not call close() first: closing can invalidate the unconsumed context.
  4. In the callback, after verifying the provider’s response and any MFA of your own, take the stored context and delete it (single use). Send the token if its expiresAt still leaves your margin; otherwise send null and log EXPIRED_BEFORE_CALLBACK.

If your authentication library starts the provider flow from the browser (a signIn('google') style call), keep that call as the last step and send the context in its own request just before it, as above. The page is discarded at the redirect without close(); its SDK session keeps one of your concurrent-session slots until its 30 minutes run out.

// prepareNextAction and takeActionContext come from the collection recipe; csrfToken and
// startGoogleSignIn stand for your existing CSRF token and provider redirect.
// The first intent prepares the context; a page load alone opens no session and bills nothing.
for (const intent of ['pointerenter', 'focus', 'pointerdown'])
googleButton.addEventListener(intent, () => prepareNextAction())
googleButton.addEventListener('click', async (event) => {
event.preventDefault()
googleButton.disabled = true // Show progress; the wait is bounded by SUBMIT_WAIT_MS.
const { context, failure } = await takeActionContext()
try {
// Your endpoint: stores { context, failure } server-side with the OAuth state.
await fetch('/auth/login-context', {
method: 'POST',
credentials: 'same-origin',
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrfToken },
body: JSON.stringify({ eventContext: context, contextFailure: failure }),
})
} catch {
// The login must not depend on this request; the callback then assesses with null.
}
startGoogleSignIn() // Your existing redirect to the provider, unchanged.
})

Pattern B: collect on a page after the callback

Section titled “Pattern B: collect on a page after the callback”
  1. The callback verifies the provider’s response but does not finish the session yet. It stores a short-lived pending login server-side (bound to the browser, for example with the same pre-login session) and redirects to an intermediate page of yours, such as /login/continue, on one of the application’s allowed origins.
  2. That page collects and obtains the context with takeActionContext(), which waits at most your ceiling, then posts it (or null with its reason) to your backend.
  3. The backend verifies the pending login, runs any MFA of your own, assesses the login, then finishes the session as it does today and answers with where to go next.
  4. The page calls close() after that answer and navigates on.

The intermediate page must always finish the login: keep the bounded wait, and give it a <noscript> form (or a plain “Continue” button) that posts without a context. If the user never reaches /login/finish, the pending login expires like any abandoned sign-in. When your application asks its own MFA after the provider, that MFA page already is the page after the callback: use the password-login recipe on its form and skip the extra page.

// On /login/continue. takeActionContext and closeSdk come from the collection recipe.
async function finishLogin() {
const { context, failure } = await takeActionContext()
const response = await fetch('/login/finish', {
method: 'POST',
credentials: 'same-origin',
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrfToken },
body: JSON.stringify({ eventContext: context, contextFailure: failure }),
})
const { next } = await response.json() // Your own response shape.
await closeSdk() // The backend has answered: free the session slot (at most 1500 ms).
window.location.assign(next)
}
finishLogin()
  • Send exactly one login assessment (tag login; LOGIN in v1) for every completed login, password or provider, with the same subject_ref and account_ref pseudonyms for the same person and account. Never send the provider’s subject identifier or the email. An account stays bound to the subject_ref it was first sent with: a provider login that maps the same account to another person returns 409 binding-conflict (person and account references).
  • Accept the context only from the request your own page sent for this login; delete a stored context after its first use, and never use it for another login.
  • Send the token only while expiresAt leaves your margin; otherwise send null. On 400 event-context-invalid, assess once more as a new operation with null (fail-open wrapper).
  • Log, for every assessment, whether it carried a token and, when not, the browser’s contextFailure or your own reason (NO_CONTEXT_RECEIVED, EXPIRED_BEFORE_CALLBACK). Never log the token.
  • The login itself never waits on FingerTrackID beyond your own budget, and never fails because of it.

Run these checks on each allowed origin you use, for a password login and for each provider, before turning the integration on for everyone:

  1. Browser network panel. Requests to your collection domain complete with a response before the page navigates away. A request shown as cancelled right before a redirect means the collection ran too late or the redirect did not wait. Each OPTIONS request to the collection domain is answered with 204; a 400 there means page instrumentation added a request header (tracing headers).
  2. Your backend logs. Most logins carry a token. Logins without one have a reason, and the reasons are expected ones (for example WAIT_EXCEEDED on slow networks), not one fixed code on every provider login.
  3. The assessment response. With browser evidence, device_intelligence_snapshot_id and sdk_version are not null, BOT_AUTOMATION is DETECTED or NOT_DETECTED, evidence_coverage is above 0 and limitation_codes does not contain COLLECTOR_NOT_LOADED. data_freshness.maximum_required_age_ms shows your application’s evidence window.
  4. Console, Integração e saúde. Última coleta shows a recent time instead of nenhuma coleta na janela; in Da coleta à avaliação, Coletas aceitas grows with your logins and Correlacionadas (assessments that found their browser collection) stays close to Avaliações; Correlação navegador → backend shows that share. These numbers cover the last 24 recorded hours and are not live.
  5. Console, Acessos. Open a provider login: Cobertura de evidência is above zero, the Automação (robô) indicator shows Detectado or Não detectado instead of Não avaliado, and the limitations do not include Coletor não carregado.

If every provider login fails these checks while password logins pass, the redirect is discarding the context: apply pattern A or B. See also the integration checklist.