Replies: 7 comments 13 replies
-
|
Consider CIBA as a first-class use case for mobile attestation The mobile attestation path proposed here has a direct intersection with the CIBA design in #2740. In CIBA, the "Async User Auth" sub-flow — where the user authenticates on their device after receiving a notification — is essentially an app-native flow execution via A few things worth considering while this is still in design:
Referencing #2740 from this discussion would help keep the two designs aligned. |
Beta Was this translation helpful? Give feedback.
-
|
I had a discussion with @darshanasbg regarding the approach two weeks ago and he has suggested to block the flows API for the redirect based applications (for those use Restricting /flow/execute for redirect-based applicationsFor redirect-based applications (those configured with the This means we can enforce a simple rule: if an application has The check would live in Effect on the paths:
This narrows the scope of the cookie/attestation work proposed here: those mechanisms only need to address app-native |
Beta Was this translation helpful? Give feedback.
-
|
Please correct me if I misunderstood. I found the description to be a bit confusing. High-Level Approach says
But Architecture Overview says
Can you clarify whether we are moving the challengeToken to a cookie or keeping challengeToken in the body and introducing a new nonce as the cookie ? |
Beta Was this translation helpful? Give feedback.
-
For customer apps deployed on different domains, won't this create a vulnerability to CSRF attacks? We cross check the origin against an allowlist for the initial request. But once the cookie is set, a forged request from a malicious site will also have the cookie attached to it. A correctly timed such request (eg: before a user attribute updating step) may be harmful. Thinking along that, I'm thinking whether we should re-evaluate
The proposed approach is also vulnerable to this as the initial request validation is purely based on this
This is valid. My suggestion is to rely on this only for browser apps
As I understood from #2008, the purpose of the challenge token is to ensure that that client that initiated the flow and the one advancing it are the same. By cross checking each request against the |
Beta Was this translation helpful? Give feedback.
-
Update from review callWe held a call to review the security model for the Flow API. Here's a summary of what was discussed and decided. Key decisions reached:
On challenge tokens: On SPAs and CSRF: On mobile: |
Beta Was this translation helpful? Give feedback.
-
Update: Decision Summary and Implementation DirectionFollowing further design discussions and review, we've reached agreement on the approach for the secured-by-default flow initiation model. This post summarizes the decisions and how we are moving forward with implementation. Decision 1 — Browser SPAs restricted to redirect-based flowsAfter evaluating the native SPA flow pattern (SPA calling Rationale: In a native SPA flow, the Implication: Any application configured with the Decision 2 — Origin header validation for native SPA flows is not being pursuedWe evaluated adding Rationale: Rather than offering a partial control that developers might rely on as sufficient protection, we've agreed it's a cleaner and more consistent approach to simply not support the native SPA pattern and direct developers toward the redirect-based flow. The SDK will be updated to reflect this. Implementation PlanThe secured-by-default initiation model will be implemented in the following order:
Next Steps
Please comment if you have questions or alternative perspectives. |
Beta Was this translation helpful? Give feedback.
-
Follow-up: decouple the flow-execution guard from protocol (OAuth) specificsThis came out of the review on #3451 / PR #3476 (App Secret verification at flow initiation). Capturing the plan here since it touches the broader app-native flow design and a future protocol (e.g. SAML) would hit the same path. ProblemThe flow-execution guard currently reaches into protocol internals: Proposed approachMove the classification out of flow-exec and into the actor/application layer (the
Why this is protocol-agnosticThe detection is expressed in OAuth terms today only because OAuth is the single redirect-based inbound protocol. With this shape, adding SAML (or any other redirect protocol) only touches the actor-layer derivation; flow-exec is untouched because it just reads Scope / sequencingThis is a cross-cutting refactor of the shared actor model and revisits structures from the App Secret PR, so I'd suggest landing it as its own follow-up rather than expanding #3476 (which is feature-complete and tested). Ideally done when a second protocol makes the abstraction concrete, so we generalise against a real second consumer rather than a hypothetical one. The smaller Open question to evaluate: whether the resolved model should also expose enough for the App Secret presence/requirement, while still keeping verification itself in the entity service. |
Beta Was this translation helpful? Give feedback.

Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
Related Feature Issue
#2009
Problem Summary
Thunder's app-native flow execution lets first-party clients call
/flow/executedirectly using an applicationId, which is a public identifier rather than a credential. As a defense-in-depth improvement, we want to strengthen client authenticity at flow initiation so that Thunder has positive verification that the calling client is the one the application was registered for, rather than relying on the identifier alone. The same hardening applies to gate, and we are also taking the opportunity to improve the in-flow challengeToken by moving it out of the JavaScript-readable response body for web clients and giving it an explicit lifetime, bringing the runtime token handling in line with modern best practice.High-Level Approach
We split the problem along the two client form factors, because the strongest mechanism differs for each.
For browser-based clients (gate and customer-hosted SPAs), Thunder relocates challengeToken from the response body into an HttpOnly, Secure, SameSite cookie set on the first /flow/execute response — no separate pre-flight endpoint. The first call's Origin and Sec-Fetch-Site headers, checked against the application's allowed origins, are what admit the flow; from the second call onward the browser attaches the cookie automatically and Thunder uses it to bind every subsequent request. SameSite is Strict for same-domain deployments and None for cross-domain, paired with POST-only and strict origin checks. For gate specifically, the first /flow/execute is made by the OAuth component server-side, so it must forward the resulting Set-Cookie through its /authorize response to the browser — a small but deliberate piece of wiring.
For mobile clients, cookies are not a good fit. We require platform attestation — Apple App Attest on iOS, Play Integrity on Android — on the first /flow/execute call. The app obtains a signed assertion proving its binary matches the registered app identity, Thunder verifies it with Apple's and Google's APIs, and executionId and challengeToken are then issued in the body as today.
In both cases, the existing executionId and challengeToken continue to drive subsequent flow steps, with the addition of an explicit TTL replacing the current valid-until-consumed behavior.
Architecture Overview
The diagram above shows both client paths converging on the same verification gate inside Thunder, with the verification folded into the first
/flow/executecall rather than a separate endpoint.The
/flow/executeendpoint gains a verification step at flow initiation, Thunder runs the verification path: check Origin against the application's allowedOrigins, check Sec-Fetch-Site to confirm the request is a same-origin or known cross-origin fetch, and for mobile requests verify the platform attestation token. If all checks pass, Thunder issues a server-side session record bound to the applicationId, generates a cookie value, sets Set-Cookie on the response (HttpOnly, Secure, SameSite per the rules above, with an explicit Max-Age), and returns executionId in the body and challengeToken as a cookie. From the second call onward, Thunder requires the cookie to be present on the request and to map to the same applicationId carried in the body.For browser clients calling
/flow/executedirectly (customer SPAs), the Set-Cookie lands on the browser naturally because the browser issued the request.For gate, the flow is one hop longer. The OAuth
/authorizehandler creates the flow by calling/flow/executeinternally. The response from/flow/executecarries Set-Cookie, which the OAuth handler must explicitly include in the response it sends back to the browser alongside the existing flowId, appId, authId payload. The OAuth component effectively acts as a transparent forwarder of the cookie. From that point on, the browser-side gate client drives/flow/executecalls directly and the cookie is sent automatically.The attestation verifier is a new internal component that holds Apple's and Google's public keys (or calls their verification APIs) and validates the assertion against the registered app ID, team ID, and bundle ID stored on the application configuration. Per-app configuration adds an attestation block listing the platform, app IDs, and key IDs that Thunder will accept.
The application configuration model gains two fields: allowedOrigins (list, used by the cookie path) and attestation (object, used by the mobile path). Both are optional — an application can be configured for one path, the other, or both.
Security Considerations
What relocating the token to a cookie actually buys us. The cookie itself is not a new authenticator, it carries the same challengeToken we already issue. The win is the transport: HttpOnly removes the token from anything JavaScript can reach, which closes off XSS exfiltration, browser-extension exfiltration, and accidental logging by the client. The token is no less secret than before; it is just no longer handed to code that has no business reading it. Secure keeps it off plaintext channels, and SameSite keeps it off cross-site requests.
Trust on the first call. The first
/flow/executecall has no cookie yet, that's the call that establishes the cookie. Its protection rests on Origin and Sec-Fetch-Site, both browser-set and unforgeable from JavaScript, plus the POST-only requirement. This is sound, but it is the most security-critical moment in the flow and the checks must be strict. Any application without a valid allowedOrigins entry must be refused, not allowed through with a warning.Challenge token lifetime — explicit TTL, not until-consumed. Today the token is effectively valid until consumed. The proposal adds an explicit expiration enforced on every check, on both transports. The reasoning:
Concrete recommendation: a default of 15 minutes, re-issued on each /flow/execute response with a sliding 15-minute window. The exact value should be a deployment configuration so operators can tune it. Expired tokens get a distinct error code so SDKs can restart the flow gracefully.
Thunder OAuth-component-as-forwarder. Because Thunder OAuth component is the one that receives Set-Cookie and must forward it, an implementation bug there (dropping the header, mangling attributes) results in gate failing on the second /flow/execute. This is observable rather than exploitable, but it means the OAuth component now has a small but real responsibility for security headers. Worth a focused unit test asserting Set-Cookie is present on /authorize with the expected flags.
Mobile transport remains body-based. Mobile clients still receive challengeToken in the body, which means it sits in process memory accessible to the app's own code. This is acceptable because,
The threat model on mobile is genuinely different from the browser, and a body-borne token is appropriate there.
Impacted Areas
Alternatives Considered
Backend-for-Frontend (BFF) pattern
Route all flow execution through a server-side proxy that holds client credentials, turning a public web client into a confidential one. Partially adopted as opt-in. Genuinely effective for web and we should document it as a stronger posture for customers who want it. Rejected as the baseline because it forces every web customer to stand up server-side infrastructure, adds a latency hop, and does nothing for standalone mobile apps that don't route through a web server.
Trusted domains / origin allowlist
Per-application list of allowed origins; reject /flow/execute requests whose Origin is not on the list. Partially adopted. Folded into the proposed solution as the gate on the first call. Rejected as a standalone solution because Origin is spoofable by non-browser clients, doesn't apply to mobile, and on its own doesn't protect the in-flow challengeToken from XSS — the cookie relocation is what addresses that.
DPoP-style proof of possession with a browser-generated key
SPA generates a non-extractable key in SubtleCrypto and signs each request. Rejected. The key would still be reachable by JS in the page, so XSS can use it freely. HttpOnly cookies are strictly stronger against the XSS vector, which is the most likely attack surface on a SPA. Worth revisiting if hardware-bound browser keys become broadly available.
Reverse proxy with secret injection
Developer-operated proxy injects a client_secret into requests before forwarding to Thunder. Rejected as a distinct option. Effectively the BFF pattern framed as a proxy configuration; the security properties and cost trade-offs are identical. Covered by adopting BFF as an opt-in.
Questions for Community Input
No response
Beta Was this translation helpful? Give feedback.
All reactions