/** * OAuth Callback Endpoint — GET /oauth/callback * * Who calls: Browser after user completes login form * * When: After the user submits the login form from /oauth/authorize * * Purpose: Creates an authorization code and redirects back to the client's redirect_uri * * Notes: This is a simple "demo" login callback. In production, this would integrate * with a real identity provider or user database. */ import { z } from '@frontmcp/lazy-zod'; import { FlowBase, type FlowRunOptions } from '../../common'; declare const inputSchema: import("@frontmcp/lazy-zod").ZodObject<{ request: import("@frontmcp/lazy-zod").ZodObject<{}, import("zod/v4/core").$loose>; response: import("@frontmcp/lazy-zod").ZodObject<{}, import("zod/v4/core").$loose>; next: import("@frontmcp/lazy-zod").ZodOptional>; }, import("zod/v4/core").$strip>; declare const stateSchema: import("@frontmcp/lazy-zod").ZodObject<{ pendingAuthId: import("@frontmcp/lazy-zod").ZodOptional; email: import("@frontmcp/lazy-zod").ZodOptional; name: import("@frontmcp/lazy-zod").ZodOptional; clientId: import("@frontmcp/lazy-zod").ZodOptional; redirectUri: import("@frontmcp/lazy-zod").ZodOptional; scopes: import("@frontmcp/lazy-zod").ZodOptional>; codeChallenge: import("@frontmcp/lazy-zod").ZodOptional; originalState: import("@frontmcp/lazy-zod").ZodOptional; resource: import("@frontmcp/lazy-zod").ZodOptional; authorizationCode: import("@frontmcp/lazy-zod").ZodOptional; userSub: import("@frontmcp/lazy-zod").ZodOptional; loginFields: import("@frontmcp/lazy-zod").ZodOptional>; customClaims: import("@frontmcp/lazy-zod").ZodOptional>; credentials: import("@frontmcp/lazy-zod").ZodOptional>; }, import("zod/v4/core").$strip>>>; isIncremental: import("@frontmcp/lazy-zod").ZodDefault; targetAppId: import("@frontmcp/lazy-zod").ZodOptional; targetToolId: import("@frontmcp/lazy-zod").ZodOptional; existingSessionId: import("@frontmcp/lazy-zod").ZodOptional; existingAuthorizationId: import("@frontmcp/lazy-zod").ZodOptional; priorAuthorizedAppIds: import("@frontmcp/lazy-zod").ZodOptional>; authorizedAppIds: import("@frontmcp/lazy-zod").ZodOptional>; isFederated: import("@frontmcp/lazy-zod").ZodDefault; selectedProviders: import("@frontmcp/lazy-zod").ZodOptional>; skippedProviders: import("@frontmcp/lazy-zod").ZodOptional>; consentEnabled: import("@frontmcp/lazy-zod").ZodDefault; selectedTools: import("@frontmcp/lazy-zod").ZodOptional>; consentSubmitted: import("@frontmcp/lazy-zod").ZodDefault; csrf: import("@frontmcp/lazy-zod").ZodOptional; }, import("zod/v4/core").$strip>; declare const outputSchema: import("@frontmcp/lazy-zod").ZodUnion; status: import("@frontmcp/lazy-zod").ZodDefault>; location: import("@frontmcp/lazy-zod").ZodString; headers: import("@frontmcp/lazy-zod").ZodOptional]>]>>>>; cookies: import("@frontmcp/lazy-zod").ZodOptional; domain: import("@frontmcp/lazy-zod").ZodOptional; httpOnly: import("@frontmcp/lazy-zod").ZodDefault; secure: import("@frontmcp/lazy-zod").ZodOptional; sameSite: import("@frontmcp/lazy-zod").ZodOptional>; maxAge: import("@frontmcp/lazy-zod").ZodOptional; expires: import("@frontmcp/lazy-zod").ZodOptional; }, import("zod/v4/core").$strip>>>>; }, import("zod/v4/core").$strip>, import("@frontmcp/lazy-zod").ZodObject<{ status: import("@frontmcp/lazy-zod").ZodNumber; body: import("@frontmcp/lazy-zod").ZodString; headers: import("@frontmcp/lazy-zod").ZodOptional]>]>>>>; cookies: import("@frontmcp/lazy-zod").ZodOptional; domain: import("@frontmcp/lazy-zod").ZodOptional; httpOnly: import("@frontmcp/lazy-zod").ZodDefault; secure: import("@frontmcp/lazy-zod").ZodOptional; sameSite: import("@frontmcp/lazy-zod").ZodOptional>; maxAge: import("@frontmcp/lazy-zod").ZodOptional; expires: import("@frontmcp/lazy-zod").ZodOptional; }, import("zod/v4/core").$strip>>>>; kind: import("@frontmcp/lazy-zod").ZodLiteral<"html">; contentType: import("@frontmcp/lazy-zod").ZodDefault>; }, import("zod/v4/core").$strip>]>; declare const plan: { readonly pre: ["parseInput", "validatePendingAuth"]; readonly execute: ["handleIncrementalAuth", "handleFederatedAuth", "createAuthorizationCode", "redirectToClient"]; }; declare global { interface ExtendFlows { 'oauth:callback': FlowRunOptions; } } declare const name: "oauth:callback"; export default class OauthCallbackFlow extends FlowBase { private logger; parseInput(): Promise; validatePendingAuth(): Promise; /** * Compute the progressive/incremental authorization grant for the minted * token (the `authorized_apps` claim). * * EXPANSION model (claim-based, enforceable — mirrors how consent/federated * grants already work in this codebase): the authorized-app set lives in the * minted token's `authorized_apps` claim, which `checkToolAuthorization` * enforces. Both the initial login and an incremental authorize mint a token * via the normal code exchange; the difference is the computed grant: * * - INITIAL login: grant = the apps the client requested via `apps=` * (`priorAuthorizedAppIds`); if none requested, grant ALL scope apps (a * plain login keeps working for everything). * - INCREMENTAL authorize (`mode=incremental&app=B`): grant = the prior apps * (carried forward via `apps=`) UNION the newly-authorized `targetAppId`. * So after authorizing app B, a `tools/call` on B's tool succeeds while app * A keeps working — WITHOUT re-authorizing A. * * This stage ONLY emits a grant when `incrementalAuth.enabled` is true for an * orchestrated scope. When incremental auth is disabled or the scope is not * orchestrated, it leaves `authorizedAppIds` undefined → NO `authorized_apps` * claim is minted → NO app-level gating (the historical allow-all behavior is * preserved exactly). Federated logins mint their token in the provider * callback flow, so this stage is a no-op for them. * * Unknown app ids (not present in the scope) are dropped so a client can never * forge a grant to a non-existent app; app-level gating is per-real-app, so a * bogus id is harmless anyway. */ handleIncrementalAuth(): Promise; /** * Handle federated authentication - start provider chain * When user selects providers on federated login page, we need to: * 1. Create a federated session to track progress * 2. Start OAuth flow with the first selected provider * 3. Chain through remaining providers */ handleFederatedAuth(): Promise; createAuthorizationCode(): Promise; redirectToClient(): Promise; /** * Reserved OAuth/flow control params that must never be treated as custom * login fields when collecting input for a custom `authenticate` verifier. */ private static readonly RESERVED_LOGIN_PARAMS; /** * Collect submitted login-field values (built-in `email`/`name` plus any * custom `login.fields`) from the GET query and a urlencoded POST body. * Reserved OAuth/flow control params are excluded so they cannot be forwarded * to the verifier as if they were login fields. Only string values are kept. */ /** * Defensively read a single param from a urlencoded POST body (some adapters * parse the body into `request.body`). The consent form uses a GET round-trip * (mirroring the federated page) so query params are the primary source; this * is a fallback for adapters that route a POST body instead. Multi-valued * keys keep all values so checkbox groups (`tools`) survive a POST. */ private readBodyParam; /** * Same-origin (CSRF) check for a state-changing login/consent submission. * * Returns true when the request carries an `Origin` or `Referer` header whose * host does NOT match the request's own host — the signature of a cross-site * login-CSRF/fixation. `Origin`/`Referer` are set by the browser and cannot * be forged by a malicious page, so this reliably distinguishes a legitimate * same-origin submission (the built-in form is served by THIS server) from a * cross-site one. Returns false (do NOT block) when neither header is present * or the request's own host cannot be determined — defense-in-depth, not a * hard gate, so header-stripped GET navigations and existing flows keep working. */ private isCrossOriginSubmission; private collectLoginFields; /** * Build the {@link AuthenticateContext} from the flow scope and invoke the * configured verifier. Verifier exceptions are caught and converted to a * generic failure so a throwing verifier never produces a 500. */ private runAuthenticate; /** * Outbound fetch handed to the verifier. Routes through the auth instance's * `fetch` when available (so tests / adapters can intercept it), else the * global fetch. */ private contextFetch; /** * Resolve a human-readable client name for the verifier context. Uses the * CIMD `client_name` when the client_id is a CIMD URL and resolvable; falls * back to the raw client_id. */ private resolveClientName; /** * Derive a subject from the configured login subject strategy. * * - `per-account`: hash the value of `login.subject.fromField` into a stable * subject (same account → same `sub`). * - otherwise (`per-session` / unset): return undefined so the caller falls * back to the anonymous subject. */ private deriveSubjectFromStrategy; /** * Re-render the local login page after a failed authenticate(), surfacing the * verifier's message (and pre-selecting `retryField`). Submitted values are * preserved on the form so the user does not have to re-enter everything. */ private renderLoginRetryPage; /** * Render a custom `@AuthUi({ slot: 'consent' })` page when one is registered. * * Mints + persists the per-pending-auth CSRF token (reusing any token a prior * custom page already set on the record, so a consent re-render keeps the same * token), builds the consent {@link AuthFlowState} from the SAME tool * projection the built-in screen uses, SSRs the component, and responds with * the assembled page + CSP headers. Returns `true` when handled. */ private tryRenderCustomConsent; /** * Render the tool-consent screen. * * The form GETs back to `/oauth/callback` carrying `pending_auth_id`, * `consent_submitted=1`, the resolved identity (`email`/`name` + any custom * `login.fields`), and the chosen `tools=` checkboxes — so the resubmit * re-derives the SAME subject and proceeds to mint. * * Honors the `auth.consent` flags: `groupByApp`, `showDescriptions`, * `customMessage`, `allowSelectAll`, `requireSelection`, `defaultSelectedTools` * (pre-checked), and `excludedTools` (filtered out of the offered set by * {@link projectConsentTools}). `error` re-renders the screen with a banner * (used for an empty `requireSelection` submit). */ private renderConsentScreen; /** * Generate a stable user sub from email * In production, this would be the user's ID from the database */ private generateUserSub; /** * Render an error page */ private renderErrorPage; } export {}; //# sourceMappingURL=oauth.callback.flow.d.ts.map