Skip to content

Post the flow's SSO sessions on the authorization callback - #147

Merged
brionmario merged 1 commit into
thunder-id:mainfrom
ImalshaD:post-session-data-on-callback
Oct 30, 2026
Merged

brionmario merged 1 commit into
thunder-id:mainfrom
ImalshaD:post-session-data-on-callback

Conversation

@ImalshaD

Copy link
Copy Markdown
Contributor

Purpose

A sign-in or sign-up that completes an authorization request posts its assertion to /oauth2/auth/callback; this is how the ThunderID Gate finishes a browser sign-in. The flow's responses also return the SSO sessions its steps start, in sessionData. With thunder-id/thunderid#6227, the server keeps the sessions posted on the callback with the code when the application asked for the device_sso scope, and the code exchange returns them as a device_secret that another application can exchange to sign the user in without a redirect. Until the SDK posts them, a sign-in through the Gate receives no device secret.

Approach

  • Collection: api/auth/flowSessions.ts keeps, per authId, the sessions every flow response returned, INCOMPLETE ones included, since a session can start on any step. A session a later step lists in clearedSessions is dropped. They are held in memory only and never persisted or logged, so a reload in the middle of the flow drops them.
  • Sign-in: executeEmbeddedSignInFlow posts the collected sessions with the assertion on a completed sign-in. A failed sign-in, which posts an error assertion and issues no code, discards them.
  • Sign-up: executeEmbeddedSignUpFlow does the same, since a registration flow that signs the user in also completes the authorization request through the callback.
  • Types: sessionData and clearedSessions are added to EmbeddedSignInFlowResponse and EmbeddedSignUpFlowResponse, and EmbeddedFlowSessionData is exported.
  • Nothing changes for callers: the sessions are posted only when the flow returned any, and a server that predates #6227 ignores the extra field.

Not in this PR: keeping the device_secret a code exchange returns, and exchanging it from the SDK. Those follow separately.

Related Issues

Related PRs

SDK parity

  • ios-sdks: not applicable, the SDK never posts a flow assertion to /oauth2/auth/callback.
  • android-sdks: not applicable, the SDK never posts a flow assertion to /oauth2/auth/callback.
  • flutter-sdks: not applicable, the SDK never posts a flow assertion to /oauth2/auth/callback.

The SDK development specification does not describe the authorization callback path yet, so an update to spec.md and threat-model.md in thunder-id/thunderid follows.

Checklist

  • Followed the contribution guidelines.
  • Manual test round performed and verified.
  • Documentation provided. (Add links if there are any)
  • Tests provided. (Add links if there are any)
    • Unit Tests (executeEmbeddedSignInFlow.test.ts, executeEmbeddedSignUpFlow.test.ts)
    • Integration Tests
  • Breaking changes. (Fill if applicable)
    • Breaking changes section filled.
    • breaking change label added.
  • Cross-SDK parity. Exactly one of parity/prs-raised or parity/prs-not-needed added.
    • If parity/prs-raised, the port links are posted as a reply on the parity check's comment.

Security checks

  • Followed secure coding standards.
  • Confirmed that this PR doesn't commit any keys, passwords, tokens, usernames, or other secrets.

A session handle resumes the session without authentication, so the collected sessions are held only in memory, for the authorization request they belong to, and are sent only to the server's own callback.

🤖 Generated with Claude Code

A sign-in or sign-up that completes an authorization request posts its
assertion to /oauth2/auth/callback. The flow's responses also return the SSO
sessions its steps start, and the server keeps them with the code when the
application asked for the device_sso scope, so the code exchange can return
them as a device secret. Collect the sessions from every response of the
authorization request, in memory only, drop the ones a later step ends, and
post them with the assertion. A failed sign-in posts none.

Refs thunder-id/thunderid#6144
@coderabbitai

coderabbitai Bot commented Oct 30, 2026

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

Run configuration
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@ImalshaD ImalshaD added the parity/prs-not-needed Nothing here reaches another SDK label Oct 30, 2026
@github-actions

github-actions Bot commented Oct 30, 2026 •

Copy link
Copy Markdown

🔀 Cross-SDK feature parity, no ports needed

This pull request is labelled parity/prs-not-needed: nothing here has to ship in the other ThunderID SDKs. If that changes, swap the label for parity/prs-raised.

// The SSO sessions each authorization request's flow responses returned, held in memory until the
// callback posts them. A session can start on any step, not only the last, so every response is read.
// They are never persisted, so a reload in the middle of the flow drops them.
const collectedSessions = new Map<string, Map<string, EmbeddedFlowSessionData>>();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@coderabbitai Check whether this can cause any issues when the code is run on a SSR app.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
fd -H '^(AGENTS\.md|flowSessions\.ts)$' . --exclude node_modules --exclude .git
ast-grep outline packages/javascript/src/api/auth/flowSessions.ts --items all
rg -n -C 4 'flowSessions|collectedSessions' packages

Length of output: 5677


🏁 Script executed:

#!/bin/bash
cat AGENTS.md
ast-grep outline packages/javascript/src/api/auth/executeEmbeddedSignInFlow.ts --items all
ast-grep outline packages/javascript/src/api/auth/executeEmbeddedSignUpFlow.ts --items all
nl -ba packages/javascript/src/api/auth/flowSessions.ts
nl -ba packages/javascript/src/api/auth/executeEmbeddedSignInFlow.ts | sed -n '1,260p'
nl -ba packages/javascript/src/api/auth/executeEmbeddedSignUpFlow.ts | sed -n '1,270p'
rg -n -C 3 '\bexecuteEmbeddedSign(In|Up)Flow\s*\(|\binterface EmbeddedFlowSessionData\b|authId:' packages --glob '*.{ts,tsx}' | head -240

Length of output: 54630


🏁 Script executed:

#!/bin/bash
ast-grep outline packages/nuxt/src/runtime/server/ThunderIDNuxtClient.ts --items all
nl -ba packages/nuxt/src/runtime/server/ThunderIDNuxtClient.ts | sed -n '75,155p'
rg -n -C 3 'executeEmbeddedSignInFlow|executeEmbeddedSignUpFlow' packages/javascript/src --glob '*.ts' --glob '!**/__tests__/**'
rg -n -C 3 'EmbeddedFlowSessionData|authId\??:' packages/javascript/src/models/embedded-flow* 
rg -n -C 3 'executeEmbeddedSignInFlow|executeEmbeddedSignUpFlow' packages/node packages/nextjs packages/nuxt --glob '*.{ts,tsx}' --glob '!**/__tests__/**'

Length of output: 18676


@brionmario Yes—if the embedded flow runs on the server with authId, this module-level Map can cause issues. Importing the module during SSR does not itself cause a browser-API error.

I checked the collection logic and both flow executors.

Risk Finding
Shared server state In flowSessions.ts, Line 17, the Map belongs to the loaded module, not an HTTP request or SDK client. All requests in that server instance share it. Distinct authId values separate entries, but the key has no server or tenant namespace. This alone does not prove cross-user leakage.
Unbounded credential retention Cleanup depends on takeFlowSessions(). Abandoned flows and some error paths leave session handles in memory without expiry or a size limit. A browser reload does not clear the server’s map, despite the comment at Line 16.
Missing sessions across instances If later flow steps reach another worker or serverless instance, that instance cannot retrieve earlier sessions. The callback can then omit sessions collected on earlier steps.
No SSR-to-browser transfer Server memory and browser memory are separate. If a flow starts during SSR and continues after hydration, the browser cannot access the server’s collected sessions.

There is also a retry issue that is not specific to SSR: both executors call takeFlowSessions() before the callback succeeds. If the callback fails, the collected sessions have already been deleted.

Current Nuxt behavior: the inspected server-side signIn() call in packages/nuxt/src/runtime/server/ThunderIDNuxtClient.ts, Line 121, does not pass authId. That path does not populate this map. The exported core APIs still permit server callers to pass authId.

Recommendation: make the collection explicit and scoped to the authorization flow. If flows span server requests or instances, use a protected server-side store with tenant/server namespacing, expiry, and terminal cleanup. A request-local map alone will not preserve a multi-request flow. If collection is intended only for browser execution, enforce that boundary explicitly.

This assessment is based on source inspection. I did not run runtime tests.

You are interacting with an AI system.

@brionmario
brionmario merged commit 5caca27 into thunder-id:main Oct 30, 2026
6 of 8 checks passed
lashinijay pushed a commit to lashinijay/thunderid that referenced this pull request Oct 30, 2026
Moves @thunderid/react to 1.11.1 and @thunderid/react-router to 1.10.1
(@thunderid/browser 1.9.1 and @thunderid/javascript 1.10.1 follow
transitively). These releases post the SSO sessions a flow's responses
return on the authorization callback (thunder-id/javascript-sdks#147), so a
sign-in through the Gate that asks for the device_sso scope now receives a
device secret (thunder-id#6227). They also bring Device Grant support
(thunder-id/javascript-sdks#140), an ouId prop on GroupMembers
(thunder-id/javascript-sdks#146), and granted permissions read from the
backend session in backend mode (thunder-id/javascript-sdks#148).

Refs thunder-id#6144

Signed-off-by: ImalshaD <plid475@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

parity/prs-not-needed Nothing here reaches another SDK

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants