[Design Discussion] Per-Step Challenge Tokens for Flow Execution Security #1940
Replies: 3 comments 3 replies
-
|
Note: Need to consider how this will work with link sharing flows. I.e. invite user registration, magic link, etc. One option is to include this token also in the invite url. This raises the following concerns.
Alternatively we can avoid using challenge token for this exact step. I.e. challenge token will not be validated for the step just after generating the invitation/ url. But will be required for the steps before and after that. But still we'll not be able to retry the same scenario (by clicking on the same url) once the challenge token is generated for the flow. For example, after clicking on the invite link, flow execution goes through the invite executor verification path. Step will be successful and goes to the next node. If the flow require additional inputs before completing, this will return a challenge token. If the user doesn't complete the registration and clicks on the invite link again after some time, flow execution will fail as a challenge token is generated for the flow.
However this is not a blocker for flows with one time links. I.e. magic link. If the user doesn't continue, it is a valid scenario to ask the user to reinitiate the flow again. cc: @thiva-k @darshanasbg |
Beta Was this translation helpful? Give feedback.
-
Handling link-sharing flows (Invite, email verification, magic link)Here's a suggestion to handle link sharing flows (admin invite, email-link self-registration, magic link, etc.). Approach: Link token as persistent challenge substitute Executors that generate shareable links already produce a cryptographic token embedded in the URL. We can extend the challenge mechanism to accept this link token as a challenge substitute at the step where the flow resumes from the link. How it works:
This means:
|
Beta Was this translation helpful? Give feedback.
-
|
Proposed approach is implemented with; |
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
#2008
Problem Summary
The flow execution engine currently uses
flowIdas the sole identifier for continuing multi-step flows (authentication, registration, user onboarding). To improve the security posture of the flow execution API, we want to introduce a per-step challenge-response mechanism that ensures each step in a flow can only be continued by the client that initiated it.This design proposes per-step challenge token rotation — a mechanism where the server generates a new cryptographically random token with every flow response, and the client must present it in the next request to prove continuity.
High-Level Approach
Introduce per-step challenge token rotation on the flow execution API:
FlowResponse; the client must echo it back in the nextFlowRequest.SHA-256hash of the token is stored in the database — raw tokens only appear in HTTP responses.crypto/rand, hex-encoded (64 characters).Architecture Overview
The server generates a new cryptographically random challenge token with every flow response and expects the client to send it back in the next request. The token is rotated on every step, so even if the
flowIdis exposed, someone cannot continue the flow without the current challenge token.Flow with challenge tokens
sequenceDiagram participant C as Client participant S as Server participant DB as FLOW_CONTEXT DB Note over C,S: Step 1 — Flow Initialization C->>S: POST /flow/execute<br/>{applicationId, flowType} S->>S: Generate flowId + challengeToken₀ S->>DB: Store flowId + SHA256(challengeToken₀) S-->>C: {flowId, challengeToken: token₀, data: {...}} Note over C,S: Step 2 — Continue Flow C->>S: POST /flow/execute<br/>{flowId, challengeToken: token₀, action, inputs} S->>DB: Load flow, get stored hash S->>S: Verify SHA256(token₀) == stored hash ✅ S->>S: Generate challengeToken₁ S->>DB: Replace hash with SHA256(challengeToken₁) S-->>C: {flowId, challengeToken: token₁, data: {...}} Note over C,S: Step 3 — Invalid token presented C->>S: POST /flow/execute<br/>{flowId, challengeToken: token₀ (stale)} S->>S: SHA256(token₀) ≠ stored hash ❌ S->>DB: Invalidate flow S-->>C: 400 Bad RequestChallenge token lifecycle
stateDiagram-v2 [*] --> Generated: Server generates token₀<br/>on flow creation Generated --> Stored: SHA256(token₀) stored in DB<br/>Raw token₀ sent to client Stored --> Validated: Client returns token₀<br/>in next request Validated --> Rotated: SHA256(token₀) verified ✅<br/>New token₁ generated Rotated --> Stored: SHA256(token₁) replaces old hash<br/>Raw token₁ sent to client Validated --> Invalidated: Verification failed ❌ Invalidated --> [*]: Flow removed from DB Stored --> Expired: Flow TTL exceeded Expired --> [*]: Cleanup job removes flowDesign decisions
SHA-256(challengeToken). Even if the database is compromised, tokens can't be reconstructed.flowIdis present (i.e., not a new flow),challengeTokenis required. Missing or invalid → flow is invalidated.challengeTokenis included in the JSON request/response body alongsideflowId, consistent with the existing API structure.InitiateFlowpath — For the OAuth authorization flow, the challenge token is generated on the firstExecutecall (not duringInitiateFlow). The pre-created flow has no token until the client callsPOST /flow/execute, at which point the server generates and returns the first token.challengeToken. This is a required security upgrade.Security Considerations
crypto/rand) — computationally infeasible to guess.SHA-256(token)is persisted. Raw tokens exist only in HTTP responses.Impacted Areas
Backend
flowexec/model.goChallengeTokentoFlowRequest,FlowResponse,FlowContextWithUserDataDB,EngineContextflowexec/handler.gochallengeTokenfrom request; include new token in responseflowexec/service.goflowexec/store.gochallenge_token_hashcolumnflowexec/queries.godbscripts/runtimedb/postgres.sqlCHALLENGE_TOKEN_HASH VARCHAR(64)toFLOW_CONTEXTdbscripts/runtimedb/sqlite.sqlsystem/utils/orsystem/crypto/GenerateSecureToken()andHashToken()utilitiesClient SDKs
@asgardeo/javascriptchallengeTokenand include it in subsequent requests@asgardeo/reactchallengeTokenthroughFlowContext/AsgardeoProvider@asgardeo/nextjsAlternatives Considered
Alternative 1: Single Secret Per Flow (Static Token)
Generate one secret when the flow starts and require it in every subsequent request.
Alternative 2: HMAC-Signed Flow Responses
The server signs each response using HMAC with a server-side secret. The client returns the signature with the next request, proving receipt of the genuine response.
How HMAC works here
HMAC takes a secret key + message data and produces a fixed-size signature. The server computes
HMAC(serverSecret, flowId + stepNumber + nodeId + timestamp)and includes the signature in the response. The client echoes it back, and the server recomputes to verify.sequenceDiagram participant C as Client participant S as Server Note over S: Server holds secret key K<br/>(never shared with client) C->>S: POST /flow/execute {applicationId, flowType} S->>S: sig₀ = HMAC(K, flowId + step=1 + nodeId + ts) S-->>C: {flowId, signature: sig₀, data: {...}} C->>S: POST /flow/execute {flowId, signature: sig₀, action, inputs} S->>S: Recompute expected = HMAC(K, flowId + step=1 + nodeId + ts) S->>S: Compare sig₀ == expected ✅ S->>S: sig₁ = HMAC(K, flowId + step=2 + nodeId + ts) S-->>C: {flowId, signature: sig₁, data: {...}} Note over C,S: ✅ No DB read needed to validate!<br/>Server just recomputes the HMACWhy per-step challenge tokens are preferred
Alternative 3: Traditional CSRF Tokens (Double-Submit Cookie)
Set a CSRF token in a cookie and require it in the request body.
Alternative 4: Client Fingerprint Binding
Bind the flow to client characteristics (IP address, User-Agent, TLS session).
Alternative 5: Short-Lived Flows + Aggressive Expiry
Reduce flow TTL significantly (e.g., 5 minutes for auth).
Questions for Community Input
No response
Beta Was this translation helpful? Give feedback.
All reactions