Google OAuth & Account Security

Google Cross-Account Protection (RISC): The Receiver Endpoint Rules Behind token-revoked

If users sign in to your product with Google, Google will tell you when one of those Google Accounts is hijacked, disabled, or has a token revoked. The signal is free. Actually receiving it is a gated, cryptographically strict integration with a service account role, an authorized-domain requirement, and a token-indexing decision you cannot retrofit cheaply.

What Cross-Account Protection actually sends you

Cross-Account Protection is Google’s implementation of the RISC standard from the OpenID Foundation. When something security-relevant happens to a Google Account that is linked to your service, Google pushes a security event token to an HTTPS endpoint you registered — a signed JWT describing one event.

The design is deliberately minimal. A security event token exposes the event type, when it occurred, and an identifier for the affected user. It is not a data feed and it is not an audit log. Google is explicit that these signals may only be used for security, anti-fraud, and session-management purposes, under a separate set of RISC Terms of Service that sit on top of the standard Google APIs Terms — and that failure to comply may result in project or account suspension.

What you get

A signed JWT per event, containing the event type, a jti event identifier, and a subject identifying the user by their Google Account sub — the same sub your ID tokens already carry.

What you do not get

No profile data, no reason narrative beyond a small fixed set of values, no events for Google Workspace users, and no delivery promise if your receiver is down for an extended period. Retries are limited.

The prerequisite that disqualifies projects before they start

You only receive security event tokens for Google users who granted your service permission to access their profile information or email address — meaning your app must be requesting the profile or email scope. Sign In With Google requests these by default, but an integration that hits Google’s OpenID Connect endpoint directly with a custom scope list may not, and in that case the stream is silently empty rather than broken.

The second prerequisite is organisational rather than technical. Registering a receiver requires a service account holding the roles/riscconfigs.admin role in the same API Console project you already use for Google Sign-In, with the RISC API enabled and the RISC Terms accepted. If the project belongs to an organisation, someone with authority to bind that organisation to those terms has to click Enable. That is a legal sign-off, not a developer task, and it is where a surprising number of these builds sit for weeks.

The seven event types, and which responses Google calls mandatory

Google distinguishes between responses that are Required and responses that are Suggested. Subscribing to an event and then ignoring the required response is a policy problem, not just an engineering shortcut.

Event type (short name)Google’s stated response
sessions-revokedRequired: end the user’s currently open sessions in your app.
tokens-revokedRequired: if the token was for Google Sign-in, terminate open sessions and consider prompting the user to set up an alternate sign-in method. Suggested: delete any stored OAuth tokens for that user.
token-revokedRequired: if you store the corresponding refresh token, delete it and force re-consent the next time an access token is needed.
account-disabledRequired when reason=hijacking: end open sessions. Suggested for reason=bulk-account or no reason: analyse activity, disable Google sign-in and email-based account recovery, offer an alternate sign-in method.
account-enabledSuggested: re-enable Google sign-in and email recovery for that user.
account-credential-change-requiredSuggested: watch for suspicious activity and act accordingly.
verificationSuggested: log that a test token arrived. This is the only event you can trigger on demand.

token-revoked is the event that dictates your database schema

Most of these events identify the user and stop there. token-revoked is different: it identifies a specific token, and it does so without ever showing you the token.

The token subject identifier carries three fields. token_type — only refresh_token is supported. token_identifier_alg — either prefix or hash_base64_sha512_sha512. And token — which is either the first 16 characters of the token, or the double SHA-512 hash of it, depending on which algorithm the event used.

Google’s own guidance is to index your stored tokens by both of these possible values so a match is fast when the event arrives. That is a schema decision. A service that stored refresh tokens encrypted at rest with no prefix column and no double-hash column cannot answer the question the event is asking, and adding those columns later means re-deriving them across the entire token table.

This is also the point where the event stops being an abstract security nicety and starts touching the same operational surface as ordinary token lifecycle work — the reason a Google integration goes quiet without any code change. We cover the non-security version of that failure in Google deletes OAuth clients after six months of inactivity.

Why stream:update keeps returning 403

Registration is a single authorised POST to the RISC stream configuration API. It is also where the integration fails most often, and the error list reads like a checklist of assumptions people make:

  • The delivery endpoint is not HTTPS. Google does not send RISC events to plain HTTP, even in testing.
  • The endpoint’s domain is not in your project’s authorized domains. A receiver hosted on a vendor subdomain or a serverless default URL will be rejected until that domain is added.
  • The calling identity is a user account rather than a service account. Stream management is service-account only.
  • The service account lacks roles/riscconfigs.admin.
  • The project has no OAuth client configured at all, which Google treats as evidence RISC will not be useful to you.
  • Firebase is managing RISC for the project because Google Sign-In is enabled there — in which case a custom configuration is blocked until Google Sign-In is disabled in Firebase and an hour has passed.

None of these are bugs. Every one is a configuration or eligibility decision surfacing at the worst moment, which is the same pattern behind most consent-screen failures. The equivalent catalogue for the sign-in side is in Google OAuth consent screen errors and what each one really means.

Duplicate events are expected. Missed events are possible.

Cross-Account Protection will attempt to redeliver events it believes were not delivered, so your endpoint will sometimes see the same event more than once. If your response to an event is user-visible — force sign-out, credential reset, recovery-email lockout — repeated delivery becomes repeated disruption. The documented mitigation is de-duplicating on the jti claim, which is unique per event within the stream.

The inverse is the harder problem. Delivery is retried a limited number of times. If your receiver is down for an extended period, some events are permanently lost, and there is no backfill endpoint to catch up. A security posture that assumes the stream is complete is a security posture with a silent gap in it.

What a competent build actually has to get right

  1. Validate before you decode

    Fetch the issuer and signing-key URI from Google’s RISC discovery document, match the key ID from the token header, verify the signature, the iss, and that aud is one of your own client IDs. Validating after decoding is the mistake that turns a security feature into an attack surface.

  2. Handle expiry correctly by not checking it

    Security event tokens describe historical events and do not expire. A JWT library configured with default expiry checking will reject perfectly valid tokens, and the symptom looks like a signing problem.

  3. Return the right status codes

    An unknown key ID or a malformed token is a 400. A valid token is a 202 — acknowledged — before you act on the event. Getting this inverted causes Google to retry tokens you already processed.

  4. Index tokens for both identifier algorithms

    Prefix and double-SHA-512 lookups both need to be O(1) against your token store, decided before the first token is written.

  5. Store and delete under the RISC Terms

    Google requires that received information is stored in a way that abides by the RISC Terms and deleted within a reasonable timeframe — including during testing. Your retention policy is part of the integration, not an afterthought.

Where these projects usually go wrong

  • Subscribing to every event type, then implementing none of the required responses — which is worse than not subscribing, because the obligation now exists.
  • Building the receiver on a staging domain that was never added to the project’s authorized domains, then debugging a 403 as if it were an auth-token problem.
  • Treating RISC as a general account-activity feed and using the signals for analytics or product decisions, which the RISC Terms do not permit.
  • Assuming Workspace users are covered. They are not — Google does not currently send security events for Workspace accounts.
  • No jti de-duplication, so a redelivered account-disabled event signs the same user out twice and generates a support ticket instead of a security win.
  • Refresh tokens stored without a prefix or double-hash index, making token-revoked unactionable on arrival.
  • Requesting a custom scope list that omits profile and email, so the stream registers cleanly and then delivers nothing forever.

How this connects to verification

Cross-Account Protection is not part of OAuth verification and passing one has no bearing on the other. But they draw on the same evidence: what scopes you request, what user data you store, how long you keep it, and whether your privacy policy describes the real data flows. A team that has already assembled that material for a sensitive-scope review has most of what a RISC integration’s data-handling review needs. A team that has not is now assembling it twice. The verification side of that work is set out in our Google OAuth verification guide for sensitive and restricted scopes.

An honest note on outcomes. Whether Google enables an API, accepts terms on an organisation’s behalf, or approves a verification submission is decided by Google. Nothing here is promised or assured on your behalf. What preparation changes is the number of cycles — an integration where the scope list, the authorized domains, the service-account roles and the data-retention policy all describe the same product tends to resolve in far fewer rounds than one assembled reactively after a rejection. Our work is technical implementation support, review preparation, and policy-aligned guidance.

Every mechanic described above is taken from Google’s official Cross-Account Protection documentation, verified current in August 2026 (page last updated 22 March 2026). Google changes these details without notice — verify against the official page before you build around them.