Skip to content

feat(apps): add multi-flow OAuth lifecycle - #744

Open
Mehak Bindra (MehakBindra) wants to merge 5 commits into
mehakbindra-add-per-turn-statefrom
mehakbindra-oauth-lifecycle
Open

feat(apps): add multi-flow OAuth lifecycle#744
Mehak Bindra (MehakBindra) wants to merge 5 commits into
mehakbindra-add-per-turn-statefrom
mehakbindra-oauth-lifecycle

Conversation

@MehakBindra

@MehakBindra Mehak Bindra (MehakBindra) commented Aug 21, 2026

Copy link
Copy Markdown
Member

Summary

Adds a first-class, per-connection OAuth lifecycle to @microsoft/teams.apps, modeled on the current microsoft/teams.net implementation and stacked on #729.

  • introduces OAuthFlow for silent token lookup, interactive sign-in, sign-out, connection status, and completion/failure callbacks
  • supports declarative registration with AppOptions.oauthFlows and imperative registration with app.addOAuthFlow(...)
  • adds app-level getOAuthFlow(...) lookup with case-insensitive connection matching
  • dispatches token exchange, verification, and failure invokes across multiple registered flows
  • tracks pending sign-in attribution and exchange deduplication in turn state
  • deduplicates token exchanges, including concurrent requests and retries after a completed exchange
  • adds OAuth lifecycle telemetry and realistic multi-flow, failure, routing, and compatibility coverage
  • replaces the Graph-only example with an OAuth example covering Graph and GitHub connections

Public API

const app = new App({
  oauthFlows: ['graph', 'github'],
});

const graph = app.getOAuthFlow('graph');

graph
  .onSignInComplete(async (ctx, token) => {
    // Use the returned connection token.
  })
  .onSignInFailure(async (ctx, failure) => {
    // Handle interactive or SSO failure.
  });

await graph.signIn(ctx);
await graph.signOut(ctx);
const token = await graph.getToken(ctx);
const status = await graph.getConnectionStatus(ctx);

Flows may also be registered and configured directly:

const github = app.addOAuthFlow('github', {
  oauthCardText: 'Connect GitHub',
  signInButtonText: 'Connect',
});

Lifecycle behavior

  1. getToken() performs silent token lookup and reuses the existing token models.
  2. signIn() returns a cached token immediately or emits an OAuth card and records pending flow attribution.
  3. signin/tokenExchange resolves the named flow, performs exchange, suppresses duplicates, invokes the completion callback, and emits the existing signin event.
  4. signin/verifyState tries pending flows in attribution order until a connection redeems the code.
  5. signin/failure routes the failure to the most recent pending SSO-capable flow.
  6. signOut() and getConnectionStatus() expose the remaining per-connection operations.

Compatibility

  • retains AppOptions.oauth, ctx.signin(), ctx.signout(), ctx.userToken, ctx.isSignedIn, ctx.userGraph, existing OAuth events, invoke routes, and response shapes
  • preserves the configured default connection only for legacy apps without registered flows
  • rejects combining oauth.defaultConnectionName with registered flows; once flows are registered, deprecated context OAuth helpers must name a connection; a configured default may be omitted or named exactly
  • keeps the TypeScript-only sign-in activity override and existing OAuth card customization options
  • automatically enables turn state when OAuth flows are configured or added, while leaving implicit-default and legacy-only apps state-free; state: false with registered flows is rejected
  • does not enable eager per-turn token lookup when flows are registered; eager lookup remains limited to deprecated context OAuth fields
  • moves app option declarations to app.options.ts, while re-exporting them from app.ts so existing imports continue to work
  • marks legacy OAuth surfaces deprecated only where the flow API provides a replacement
  • represents the legacy default connection as a normal OAuthFlow while keeping legacy-default and registered-flow modes mutually exclusive

Intentional semantics

  • successful token exchange is marked complete before callbacks run; if a completion callback throws, a retry is acknowledged as a duplicate because the exchange itself already completed
  • multi-flow verification returns 404 when no flow redeems the code, matching teams.net, even when an earlier candidate produced a token-service 5xx
  • OAuthFlow.signIn() converts expected token-miss responses (400, 404, and 412) into card initiation and propagates unexpected service or transport errors
  • the legacy default uses the same completion and error semantics as registered flows rather than maintaining a separate handler path
  • telemetry operation and result values are aligned with the .NET OAuth lifecycle
  • the OAuth example uses the token returned by OAuthFlow with a token-specific GraphClient instead of deprecated ctx.userGraph

Deliberate TypeScript differences

  • retains the existing TypeScript sign-in URL and activity override APIs
  • uses per-turn user and conversation state from Add per-turn conversation and user state #729 for pending attribution and exchange deduplication
  • continues emitting the existing TypeScript OAuth events and populating deprecated context token and Graph fields on invoke completion
  • preserves plugin-provided context fields in onSignInComplete and onSignInFailure callback types

Validation

  • npm test --workspace @microsoft/teams.apps -- --runInBand
  • npm run lint --workspace @microsoft/teams.apps
  • npm run build --workspace @microsoft/teams.apps
  • npm run lint --workspace @examples/oauth
  • npm run build --workspace @examples/oauth

Stack

Add per-connection OAuth flows, lifecycle callbacks, multi-flow invoke routing, pending attribution, exchange deduplication, telemetry, compatibility fallbacks, and the OAuth example.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 0d88953d-cdee-4a28-ac2d-36a5fee72da5
Validate deprecated context sign-in connections before initiation, record pending attribution for the selected flow, expose the completed connection on signin events, and keep internal OAuth helpers out of the package barrel.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 0d88953d-cdee-4a28-ac2d-36a5fee72da5
Treat the implicit default as a normal flow, standardize completion error semantics, and preserve plugin-provided context in OAuth lifecycle callback types.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 0d88953d-cdee-4a28-ac2d-36a5fee72da5
Automatically enable turn state for explicit OAuth flows, reject state: false, store pending attribution and bounded exchange deduplication in state, and require exact token-exchange routing.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 0d88953d-cdee-4a28-ac2d-36a5fee72da5
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 0d88953d-cdee-4a28-ac2d-36a5fee72da5
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant