@venturekit/auth 0.0.0-dev.20260701100017 → 0.0.0-dev.20260704225856
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/{migrations → dist/migrations}/vk_auth_001_verification_codes.sql +7 -4
- package/dist/migrations/vk_auth_003_role_scopes.sql +43 -0
- package/dist/roles/index.d.ts +5 -1
- package/dist/roles/index.d.ts.map +1 -1
- package/dist/roles/index.js +4 -1
- package/dist/roles/index.js.map +1 -1
- package/dist/roles/role-scopes.d.ts +92 -0
- package/dist/roles/role-scopes.d.ts.map +1 -0
- package/dist/roles/role-scopes.js +122 -0
- package/dist/roles/role-scopes.js.map +1 -0
- package/dist/server/cookies.d.ts +98 -13
- package/dist/server/cookies.d.ts.map +1 -1
- package/dist/server/cookies.js +77 -19
- package/dist/server/cookies.js.map +1 -1
- package/dist/server/federated-routes.d.ts +29 -22
- package/dist/server/federated-routes.d.ts.map +1 -1
- package/dist/server/federated-routes.js +31 -4
- package/dist/server/federated-routes.js.map +1 -1
- package/dist/server/federated.d.ts.map +1 -1
- package/dist/server/federated.js +7 -11
- package/dist/server/federated.js.map +1 -1
- package/dist/server/forgot-password.js +0 -1
- package/dist/server/forgot-password.js.map +1 -1
- package/dist/server/handoff-routes.d.ts +130 -0
- package/dist/server/handoff-routes.d.ts.map +1 -0
- package/dist/server/handoff-routes.js +178 -0
- package/dist/server/handoff-routes.js.map +1 -0
- package/dist/server/handoff.d.ts +112 -0
- package/dist/server/handoff.d.ts.map +1 -0
- package/dist/server/handoff.js +102 -0
- package/dist/server/handoff.js.map +1 -0
- package/dist/server/index.d.ts +11 -4
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +9 -3
- package/dist/server/index.js.map +1 -1
- package/dist/server/middleware.d.ts +35 -0
- package/dist/server/middleware.d.ts.map +1 -1
- package/dist/server/middleware.js +50 -10
- package/dist/server/middleware.js.map +1 -1
- package/dist/server/passwordless.d.ts +68 -0
- package/dist/server/passwordless.d.ts.map +1 -0
- package/dist/server/passwordless.js +136 -0
- package/dist/server/passwordless.js.map +1 -0
- package/dist/server/revoke.d.ts +10 -0
- package/dist/server/revoke.d.ts.map +1 -1
- package/dist/server/revoke.js +19 -2
- package/dist/server/revoke.js.map +1 -1
- package/dist/server/store/postgres.d.ts +35 -0
- package/dist/server/store/postgres.d.ts.map +1 -0
- package/dist/server/store/postgres.js +88 -0
- package/dist/server/store/postgres.js.map +1 -0
- package/dist/server/token-utils.d.ts +12 -2
- package/dist/server/token-utils.d.ts.map +1 -1
- package/dist/server/token-utils.js +9 -4
- package/dist/server/token-utils.js.map +1 -1
- package/package.json +21 -8
- package/src/migrations/vk_auth_001_verification_codes.sql +55 -0
- package/src/migrations/vk_auth_003_role_scopes.sql +43 -0
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session handoff route factory — the SSO flow that moves a session
|
|
3
|
+
* between hosts that cannot share cookies (different registrable
|
|
4
|
+
* domains, e.g. a platform subdomain and a white-label custom domain).
|
|
5
|
+
*
|
|
6
|
+
* Emits two Lambda handlers:
|
|
7
|
+
*
|
|
8
|
+
* - `start` — `POST /auth/handoff/start` on the host WITH a session.
|
|
9
|
+
* Body `{ target: "https://tenant.io/dashboard" }`.
|
|
10
|
+
* Verifies the session cookies, runs the app's
|
|
11
|
+
* `authorize` hook (membership check — THE gate that
|
|
12
|
+
* keeps foreign domains out), stashes a single-use
|
|
13
|
+
* code via the app's `HandoffStore`, and returns
|
|
14
|
+
* `{ url }` for the SPA to navigate to.
|
|
15
|
+
* - `complete` — `GET /auth/handoff?code=…&next=…` on the TARGET
|
|
16
|
+
* host (a top-level browser navigation, typically
|
|
17
|
+
* through the app's same-origin API proxy). Redeems
|
|
18
|
+
* the code, mints fresh tokens from the stashed
|
|
19
|
+
* refresh token, sets first-party session cookies for
|
|
20
|
+
* THIS host, and 302-redirects to `next`. Every
|
|
21
|
+
* failure redirects to `failurePath` — the user is
|
|
22
|
+
* mid-navigation, JSON errors would dead-end them.
|
|
23
|
+
*
|
|
24
|
+
* The refresh token moves server→store→server; only the opaque
|
|
25
|
+
* single-use code (256-bit, 60s TTL, host-bound, hashed at rest)
|
|
26
|
+
* transits the browser. See `handoff.ts` for the security model.
|
|
27
|
+
*/
|
|
28
|
+
import { type APIGatewayProxyEventV2, type APIGatewayProxyResultV2, type Context as LambdaContext } from 'aws-lambda';
|
|
29
|
+
import { type RequestContext } from '@venturekit/runtime';
|
|
30
|
+
import { type CookieOptions } from './cookies.js';
|
|
31
|
+
import { type AuthServerConfig } from './config.js';
|
|
32
|
+
import { type HandoffStore } from './handoff.js';
|
|
33
|
+
/** Body shape for the `start` route. */
|
|
34
|
+
export interface HandoffStartBody {
|
|
35
|
+
/**
|
|
36
|
+
* Absolute URL the user wants to land on, on the other host —
|
|
37
|
+
* `https://tenant.io/dashboard`. The host is what `authorize` gets
|
|
38
|
+
* to approve; path + query become the post-handoff redirect.
|
|
39
|
+
*/
|
|
40
|
+
target: string;
|
|
41
|
+
}
|
|
42
|
+
/** Argument bag for the app's `authorize` hook. */
|
|
43
|
+
export interface HandoffAuthorizeArgs {
|
|
44
|
+
/** Cognito `sub` from the verified id token. */
|
|
45
|
+
userSub: string;
|
|
46
|
+
/** All claims of the verified id token. */
|
|
47
|
+
claims: Record<string, unknown>;
|
|
48
|
+
/** Host (lowercased, port included) the user wants a session on. */
|
|
49
|
+
targetHost: string;
|
|
50
|
+
/** Live request context (tenancy middleware output, headers, …). */
|
|
51
|
+
ctx: RequestContext;
|
|
52
|
+
}
|
|
53
|
+
export interface SessionHandoffRoutesOptions {
|
|
54
|
+
/** App-provided single-use code storage. See {@link HandoffStore}. */
|
|
55
|
+
store: HandoffStore;
|
|
56
|
+
/**
|
|
57
|
+
* THE authorization gate: may this user get a session on
|
|
58
|
+
* `targetHost`? Apps typically map the host to a tenant and check
|
|
59
|
+
* membership. Return `false` to refuse (403). Unknown hosts MUST
|
|
60
|
+
* return `false` — this hook is the only thing standing between a
|
|
61
|
+
* session and an arbitrary domain.
|
|
62
|
+
*/
|
|
63
|
+
authorize: (args: HandoffAuthorizeArgs) => Promise<boolean>;
|
|
64
|
+
/**
|
|
65
|
+
* Builds the absolute URL of the `complete` endpoint on a target
|
|
66
|
+
* host, WITHOUT query string. Defaults to
|
|
67
|
+
* `https://<targetHost><completePath>`. Override in dev (http, ports)
|
|
68
|
+
* or when the browser-visible API prefix differs per host.
|
|
69
|
+
*/
|
|
70
|
+
completeUrl?: (targetHost: string, ctx: RequestContext) => string;
|
|
71
|
+
/**
|
|
72
|
+
* Browser-visible path of the `complete` route used by the default
|
|
73
|
+
* `completeUrl`. Include the app's proxy prefix if there is one
|
|
74
|
+
* (mycohort: `/api/auth/handoff`). Default `/auth/handoff`.
|
|
75
|
+
*/
|
|
76
|
+
completePath?: string;
|
|
77
|
+
/**
|
|
78
|
+
* Where `complete` redirects when redemption fails (missing /
|
|
79
|
+
* expired / wrong-host code). Relative path on the target host.
|
|
80
|
+
* Default `/login`.
|
|
81
|
+
*/
|
|
82
|
+
failurePath?: string;
|
|
83
|
+
/**
|
|
84
|
+
* Cookie attributes for the session cookies minted on the target
|
|
85
|
+
* host — same semantics as the federated factory: pass a function
|
|
86
|
+
* to resolve per request (Domain depends on the host being landed
|
|
87
|
+
* on). Defaults to VK's host-only defaults.
|
|
88
|
+
*/
|
|
89
|
+
cookieOptions?: CookieOptions | ((ctx: RequestContext) => CookieOptions);
|
|
90
|
+
/**
|
|
91
|
+
* Resolve the current (browser-facing) host on the `complete` leg.
|
|
92
|
+
*
|
|
93
|
+
* NOTE: `complete` is a top-level GET navigation — there is no
|
|
94
|
+
* `origin` header and `referer` still points at the ISSUING host,
|
|
95
|
+
* so the general-purpose `getRequestHost()` chain would pick the
|
|
96
|
+
* wrong host. The default here reads `x-forwarded-host` (set by
|
|
97
|
+
* same-origin proxies) then `host`. Override when your proxy uses
|
|
98
|
+
* a different header.
|
|
99
|
+
*/
|
|
100
|
+
resolveRequestHost?: (ctx: RequestContext) => string;
|
|
101
|
+
/** Code lifetime in seconds. Default {@link DEFAULT_HANDOFF_TTL_SECONDS}. */
|
|
102
|
+
ttlSeconds?: number;
|
|
103
|
+
/** Explicit Cognito config; defaults to env-var loading per request. */
|
|
104
|
+
config?: AuthServerConfig;
|
|
105
|
+
/** Extra middleware for both routes (tenancy, rate limiting, …). */
|
|
106
|
+
middleware?: import('@venturekit/runtime').Middleware<RequestContext>[];
|
|
107
|
+
}
|
|
108
|
+
/** Route bundle returned by {@link createSessionHandoffRoutes}. */
|
|
109
|
+
export interface SessionHandoffRoutes {
|
|
110
|
+
start: (event: APIGatewayProxyEventV2, context: LambdaContext) => Promise<APIGatewayProxyResultV2>;
|
|
111
|
+
complete: (event: APIGatewayProxyEventV2, context: LambdaContext) => Promise<APIGatewayProxyResultV2>;
|
|
112
|
+
}
|
|
113
|
+
/** Default browser-facing path of the `complete` route. */
|
|
114
|
+
export declare const DEFAULT_HANDOFF_COMPLETE_PATH = "/auth/handoff";
|
|
115
|
+
/**
|
|
116
|
+
* Build the session-handoff route pair. See module docs for the flow.
|
|
117
|
+
*
|
|
118
|
+
* @example
|
|
119
|
+
* ```typescript
|
|
120
|
+
* export const handoffRoutes = createSessionHandoffRoutes({
|
|
121
|
+
* store: postgresHandoffStore,
|
|
122
|
+
* authorize: async ({ userSub, targetHost }) =>
|
|
123
|
+
* isApprovedMemberOfTenantOwning(targetHost, userSub),
|
|
124
|
+
* completeUrl: (host) => `https://${host}/api/auth/handoff`,
|
|
125
|
+
* cookieOptions: (ctx) => sessionCookieOptions(getRequestHost(ctx)),
|
|
126
|
+
* });
|
|
127
|
+
* ```
|
|
128
|
+
*/
|
|
129
|
+
export declare function createSessionHandoffRoutes(options: SessionHandoffRoutesOptions): SessionHandoffRoutes;
|
|
130
|
+
//# sourceMappingURL=handoff-routes.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"handoff-routes.d.ts","sourceRoot":"","sources":["../../src/server/handoff-routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,OAAO,EACL,KAAK,sBAAsB,EAC3B,KAAK,uBAAuB,EAC5B,KAAK,OAAO,IAAI,aAAa,EAC9B,MAAM,YAAY,CAAC;AAEpB,OAAO,EACL,KAAK,cAAc,EAQpB,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EACL,KAAK,aAAa,EAKnB,MAAM,cAAc,CAAC;AACtB,OAAO,EAAwB,KAAK,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAC1E,OAAO,EACL,KAAK,YAAY,EAIlB,MAAM,cAAc,CAAC;AAItB,wCAAwC;AACxC,MAAM,WAAW,gBAAgB;IAC/B;;;;OAIG;IACH,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,mDAAmD;AACnD,MAAM,WAAW,oBAAoB;IACnC,gDAAgD;IAChD,OAAO,EAAE,MAAM,CAAC;IAChB,2CAA2C;IAC3C,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAChC,oEAAoE;IACpE,UAAU,EAAE,MAAM,CAAC;IACnB,oEAAoE;IACpE,GAAG,EAAE,cAAc,CAAC;CACrB;AAED,MAAM,WAAW,2BAA2B;IAC1C,sEAAsE;IACtE,KAAK,EAAE,YAAY,CAAC;IACpB;;;;;;OAMG;IACH,SAAS,EAAE,CAAC,IAAI,EAAE,oBAAoB,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAC5D;;;;;OAKG;IACH,WAAW,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,EAAE,GAAG,EAAE,cAAc,KAAK,MAAM,CAAC;IAClE;;;;OAIG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;;OAKG;IACH,aAAa,CAAC,EAAE,aAAa,GAAG,CAAC,CAAC,GAAG,EAAE,cAAc,KAAK,aAAa,CAAC,CAAC;IACzE;;;;;;;;;OASG;IACH,kBAAkB,CAAC,EAAE,CAAC,GAAG,EAAE,cAAc,KAAK,MAAM,CAAC;IACrD,6EAA6E;IAC7E,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,wEAAwE;IACxE,MAAM,CAAC,EAAE,gBAAgB,CAAC;IAC1B,oEAAoE;IACpE,UAAU,CAAC,EAAE,OAAO,qBAAqB,EAAE,UAAU,CAAC,cAAc,CAAC,EAAE,CAAC;CACzE;AAED,mEAAmE;AACnE,MAAM,WAAW,oBAAoB;IACnC,KAAK,EAAE,CACL,KAAK,EAAE,sBAAsB,EAC7B,OAAO,EAAE,aAAa,KACnB,OAAO,CAAC,uBAAuB,CAAC,CAAC;IACtC,QAAQ,EAAE,CACR,KAAK,EAAE,sBAAsB,EAC7B,OAAO,EAAE,aAAa,KACnB,OAAO,CAAC,uBAAuB,CAAC,CAAC;CACvC;AAED,2DAA2D;AAC3D,eAAO,MAAM,6BAA6B,kBAAkB,CAAC;AA6D7D;;;;;;;;;;;;;GAaG;AACH,wBAAgB,0BAA0B,CACxC,OAAO,EAAE,2BAA2B,GACnC,oBAAoB,CAgJtB"}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { BadRequestError, ForbiddenError, UnauthorizedError, handler, setCookie, rawResult, redirect, } from '@venturekit/runtime';
|
|
2
|
+
import { buildSessionCookies, readCookieFromHeader, ID_TOKEN_COOKIE, REFRESH_TOKEN_COOKIE, } from './cookies.js';
|
|
3
|
+
import { loadAuthServerConfig } from './config.js';
|
|
4
|
+
import { DEFAULT_HANDOFF_TTL_SECONDS, issueHandoffCode, redeemHandoffCode, } from './handoff.js';
|
|
5
|
+
import { refreshSession } from './refresh.js';
|
|
6
|
+
import { verifyAndDecode } from './verify.js';
|
|
7
|
+
/** Default browser-facing path of the `complete` route. */
|
|
8
|
+
export const DEFAULT_HANDOFF_COMPLETE_PATH = '/auth/handoff';
|
|
9
|
+
function headersOf(ctx) {
|
|
10
|
+
return (ctx.rawEvent
|
|
11
|
+
?.headers ?? {});
|
|
12
|
+
}
|
|
13
|
+
function headerValue(headers, name) {
|
|
14
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
15
|
+
if (key.toLowerCase() === name && value)
|
|
16
|
+
return value;
|
|
17
|
+
}
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
/** Default current-host resolution for the `complete` navigation leg. */
|
|
21
|
+
function defaultResolveRequestHost(ctx) {
|
|
22
|
+
const headers = headersOf(ctx);
|
|
23
|
+
return (headerValue(headers, 'x-forwarded-host') ??
|
|
24
|
+
headerValue(headers, 'host') ??
|
|
25
|
+
'');
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* `next` must be a same-host path: absolute (`/dashboard`), not
|
|
29
|
+
* protocol-relative (`//evil.io`), not an absolute URL, and not a
|
|
30
|
+
* backslash variant (`/\evil.io` — browsers normalize `\` to `/` in
|
|
31
|
+
* URLs, turning it protocol-relative). Otherwise `complete` would be
|
|
32
|
+
* an open redirector.
|
|
33
|
+
*/
|
|
34
|
+
function sanitizeNextPath(next, fallback) {
|
|
35
|
+
if (!next)
|
|
36
|
+
return fallback;
|
|
37
|
+
if (!/^\/(?![/\\])/.test(next))
|
|
38
|
+
return fallback;
|
|
39
|
+
return next;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Redirect for the browser-navigation legs, with cache/referrer
|
|
43
|
+
* hygiene: the request URL carries the one-time code, so the response
|
|
44
|
+
* must never be cached by shared caches (`no-store`) and the landing
|
|
45
|
+
* page must not leak the code onward via `Referer`.
|
|
46
|
+
*/
|
|
47
|
+
function navRedirect(location) {
|
|
48
|
+
const base = redirect(location);
|
|
49
|
+
const result = typeof base === 'string' ? { statusCode: 302, body: '' } : base;
|
|
50
|
+
return rawResult({
|
|
51
|
+
...result,
|
|
52
|
+
headers: {
|
|
53
|
+
...(typeof base === 'string' ? {} : base.headers),
|
|
54
|
+
'Cache-Control': 'no-store',
|
|
55
|
+
'Referrer-Policy': 'no-referrer',
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Build the session-handoff route pair. See module docs for the flow.
|
|
61
|
+
*
|
|
62
|
+
* @example
|
|
63
|
+
* ```typescript
|
|
64
|
+
* export const handoffRoutes = createSessionHandoffRoutes({
|
|
65
|
+
* store: postgresHandoffStore,
|
|
66
|
+
* authorize: async ({ userSub, targetHost }) =>
|
|
67
|
+
* isApprovedMemberOfTenantOwning(targetHost, userSub),
|
|
68
|
+
* completeUrl: (host) => `https://${host}/api/auth/handoff`,
|
|
69
|
+
* cookieOptions: (ctx) => sessionCookieOptions(getRequestHost(ctx)),
|
|
70
|
+
* });
|
|
71
|
+
* ```
|
|
72
|
+
*/
|
|
73
|
+
export function createSessionHandoffRoutes(options) {
|
|
74
|
+
const { store, authorize, completePath = DEFAULT_HANDOFF_COMPLETE_PATH, failurePath = '/login', ttlSeconds = DEFAULT_HANDOFF_TTL_SECONDS, resolveRequestHost = defaultResolveRequestHost, middleware: extraMiddleware = [], } = options;
|
|
75
|
+
const cookieOptionsFor = (ctx) => {
|
|
76
|
+
if (!options.cookieOptions)
|
|
77
|
+
return {};
|
|
78
|
+
return typeof options.cookieOptions === 'function'
|
|
79
|
+
? options.cookieOptions(ctx)
|
|
80
|
+
: options.cookieOptions;
|
|
81
|
+
};
|
|
82
|
+
const buildCompleteUrl = (targetHost, ctx) => options.completeUrl
|
|
83
|
+
? options.completeUrl(targetHost, ctx)
|
|
84
|
+
: `https://${targetHost}${completePath}`;
|
|
85
|
+
// ─── start ──────────────────────────────────────────────
|
|
86
|
+
const start = handler(async (body, ctx) => {
|
|
87
|
+
if (!body?.target) {
|
|
88
|
+
throw new BadRequestError('target is required');
|
|
89
|
+
}
|
|
90
|
+
let target;
|
|
91
|
+
try {
|
|
92
|
+
target = new URL(body.target);
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
throw new BadRequestError('target must be an absolute URL');
|
|
96
|
+
}
|
|
97
|
+
if (target.protocol !== 'https:' && target.protocol !== 'http:') {
|
|
98
|
+
throw new BadRequestError('target must be http(s)');
|
|
99
|
+
}
|
|
100
|
+
const targetHost = target.host.toLowerCase();
|
|
101
|
+
const nextPath = sanitizeNextPath(`${target.pathname}${target.search}`, '/');
|
|
102
|
+
// The session being handed off — both cookies are required: the
|
|
103
|
+
// id token proves WHO is asking, the refresh token is what the
|
|
104
|
+
// target host will mint its session from.
|
|
105
|
+
const cookieHeader = ctx.rawEvent.headers?.['cookie'] ?? ctx.rawEvent.headers?.['Cookie'] ?? null;
|
|
106
|
+
const idToken = readCookieFromHeader(cookieHeader, ID_TOKEN_COOKIE);
|
|
107
|
+
const refreshToken = readCookieFromHeader(cookieHeader, REFRESH_TOKEN_COOKIE);
|
|
108
|
+
if (!idToken || !refreshToken) {
|
|
109
|
+
throw new UnauthorizedError('No session to hand off');
|
|
110
|
+
}
|
|
111
|
+
const config = options.config ?? loadAuthServerConfig();
|
|
112
|
+
const claims = await verifyAndDecode(idToken, {
|
|
113
|
+
userPoolId: config.userPoolId,
|
|
114
|
+
clientId: config.appClientId,
|
|
115
|
+
tokenUse: 'id',
|
|
116
|
+
endpoint: config.endpoint,
|
|
117
|
+
});
|
|
118
|
+
const userSub = claims && typeof claims['sub'] === 'string' ? claims['sub'] : null;
|
|
119
|
+
if (!userSub) {
|
|
120
|
+
throw new UnauthorizedError('Invalid session');
|
|
121
|
+
}
|
|
122
|
+
const allowed = await authorize({
|
|
123
|
+
userSub,
|
|
124
|
+
claims: claims,
|
|
125
|
+
targetHost,
|
|
126
|
+
ctx,
|
|
127
|
+
});
|
|
128
|
+
if (!allowed) {
|
|
129
|
+
throw new ForbiddenError('Not authorized for this destination');
|
|
130
|
+
}
|
|
131
|
+
const code = await issueHandoffCode(store, { sub: userSub, refreshToken, targetHost }, ttlSeconds);
|
|
132
|
+
const url = `${buildCompleteUrl(targetHost, ctx)}?code=${encodeURIComponent(code)}` +
|
|
133
|
+
`&next=${encodeURIComponent(nextPath)}`;
|
|
134
|
+
return { url };
|
|
135
|
+
}, {
|
|
136
|
+
status: 200,
|
|
137
|
+
middleware: extraMiddleware,
|
|
138
|
+
});
|
|
139
|
+
// ─── complete ───────────────────────────────────────────
|
|
140
|
+
const complete = handler(async (_body, ctx) => {
|
|
141
|
+
const code = ctx.queryParams?.['code'] ?? '';
|
|
142
|
+
const next = sanitizeNextPath(ctx.queryParams?.['next'], '/');
|
|
143
|
+
const currentHost = resolveRequestHost(ctx);
|
|
144
|
+
const payload = await redeemHandoffCode(store, code, currentHost);
|
|
145
|
+
if (!payload) {
|
|
146
|
+
// Missing / expired / wrong-host code. The user is mid-
|
|
147
|
+
// navigation on the target host: land them on the login page,
|
|
148
|
+
// never on a JSON error.
|
|
149
|
+
return navRedirect(failurePath);
|
|
150
|
+
}
|
|
151
|
+
const config = options.config ?? loadAuthServerConfig();
|
|
152
|
+
let tokens;
|
|
153
|
+
try {
|
|
154
|
+
tokens = await refreshSession(payload.refreshToken, config);
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
return navRedirect(failurePath);
|
|
158
|
+
}
|
|
159
|
+
// `refreshSession` never returns a refresh token (Cognito does
|
|
160
|
+
// not rotate it) — carry the original over so the target host
|
|
161
|
+
// can refresh on its own later.
|
|
162
|
+
const cookieOptions = cookieOptionsFor(ctx);
|
|
163
|
+
const sessionCookies = buildSessionCookies({
|
|
164
|
+
idToken: tokens.idToken,
|
|
165
|
+
accessToken: tokens.accessToken,
|
|
166
|
+
refreshToken: payload.refreshToken,
|
|
167
|
+
expiresIn: tokens.expiresIn,
|
|
168
|
+
}, cookieOptions);
|
|
169
|
+
for (const cookie of sessionCookies) {
|
|
170
|
+
setCookie(ctx, cookie);
|
|
171
|
+
}
|
|
172
|
+
return navRedirect(next);
|
|
173
|
+
}, {
|
|
174
|
+
middleware: extraMiddleware,
|
|
175
|
+
});
|
|
176
|
+
return { start, complete };
|
|
177
|
+
}
|
|
178
|
+
//# sourceMappingURL=handoff-routes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"handoff-routes.js","sourceRoot":"","sources":["../../src/server/handoff-routes.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-domain session handoff — core primitives.
|
|
3
|
+
*
|
|
4
|
+
* **Why this exists.** Browser cookies cannot span registrable domains:
|
|
5
|
+
* a session on `app.platform.com` can never be read by `tenant.io`, no
|
|
6
|
+
* matter what `Domain=` attribute it carries. Multi-tenant products
|
|
7
|
+
* with white-label custom domains therefore need an SSO-style handoff
|
|
8
|
+
* to "move" a session between hosts: the host WITH a session mints a
|
|
9
|
+
* short-lived, single-use **handoff code** bound to the target host;
|
|
10
|
+
* the target host redeems it server-side and mints its own first-party
|
|
11
|
+
* cookies. The user's refresh token travels server-to-server through
|
|
12
|
+
* the app's {@link HandoffStore} — only the opaque code ever appears
|
|
13
|
+
* in a URL.
|
|
14
|
+
*
|
|
15
|
+
* **Security properties**
|
|
16
|
+
* - Codes are 256-bit `crypto.randomBytes` values — unguessable.
|
|
17
|
+
* - The store is keyed by the **SHA-256 hash** of the code, so a
|
|
18
|
+
* leaked store (DB dump, logs) cannot be replayed into sessions.
|
|
19
|
+
* - Codes are single-use: {@link HandoffStore.take} must delete
|
|
20
|
+
* atomically (e.g. SQL `DELETE … RETURNING`).
|
|
21
|
+
* - Codes expire after {@link DEFAULT_HANDOFF_TTL_SECONDS}; expiry is
|
|
22
|
+
* enforced BOTH by the store's TTL mechanism and re-checked here on
|
|
23
|
+
* redeem, so a lagging store cleanup can't extend the window.
|
|
24
|
+
* - Codes are bound to the target host at issue time; redeeming on
|
|
25
|
+
* any other host fails — and a wrong-host attempt BURNS the code,
|
|
26
|
+
* so it cannot be probed and then replayed on the right host.
|
|
27
|
+
* Combined with the route factory's `authorize` hook this
|
|
28
|
+
* guarantees a session can only ever materialize on hosts the app
|
|
29
|
+
* explicitly approved for THIS user.
|
|
30
|
+
* - The `complete` redirects carry `Cache-Control: no-store` and
|
|
31
|
+
* `Referrer-Policy: no-referrer` so the code-bearing URL never
|
|
32
|
+
* sits in a shared cache and never leaks via `Referer`.
|
|
33
|
+
*
|
|
34
|
+
* **Residual risks — know what this does NOT protect against**
|
|
35
|
+
* - *Login CSRF*: like every URL-based SSO handoff (OAuth code flow
|
|
36
|
+
* included), an attacker can mint a code for THEIR OWN session and
|
|
37
|
+
* lure a victim into opening the complete URL, logging the victim
|
|
38
|
+
* in as the attacker on that host. The 60s single-use window and
|
|
39
|
+
* the membership-gated `authorize` hook narrow this to hosts the
|
|
40
|
+
* ATTACKER belongs to; apps for which this matters should show the
|
|
41
|
+
* signed-in identity prominently post-handoff.
|
|
42
|
+
* - *Store contents*: payloads hold live refresh tokens. The hashed
|
|
43
|
+
* key prevents lookup-by-code, rows die in ≤60s, but a hostile DBA
|
|
44
|
+
* window exists — encrypt payloads at rest if that is in scope.
|
|
45
|
+
*
|
|
46
|
+
* The route-level flow lives in `handoff-routes.ts`
|
|
47
|
+
* ({@link createSessionHandoffRoutes}); apps supply the storage
|
|
48
|
+
* (Postgres / DynamoDB / Redis — anything with atomic take).
|
|
49
|
+
*/
|
|
50
|
+
/** Default lifetime of a handoff code: one browser redirect, not more. */
|
|
51
|
+
export declare const DEFAULT_HANDOFF_TTL_SECONDS = 60;
|
|
52
|
+
/**
|
|
53
|
+
* What the issuing host stashes for the target host to redeem.
|
|
54
|
+
*
|
|
55
|
+
* Contains the user's **refresh token** — treat stored payloads as
|
|
56
|
+
* secrets (the code-hash key already prevents lookup-by-code, and rows
|
|
57
|
+
* are deleted on redeem / expiry, but encrypt at rest if your store's
|
|
58
|
+
* threat model calls for it).
|
|
59
|
+
*/
|
|
60
|
+
export interface HandoffPayload {
|
|
61
|
+
/** Cognito subject of the user the session belongs to. */
|
|
62
|
+
sub: string;
|
|
63
|
+
/** The session's refresh token — used by the target host to mint fresh tokens. */
|
|
64
|
+
refreshToken: string;
|
|
65
|
+
/** Host (including port, lowercased) the code was issued FOR. */
|
|
66
|
+
targetHost: string;
|
|
67
|
+
/** Epoch millis after which the code is dead even if the store still has it. */
|
|
68
|
+
expiresAt: number;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Storage the app provides for in-flight handoff codes.
|
|
72
|
+
*
|
|
73
|
+
* Keys are code **hashes** (see module docs). Implementations must make
|
|
74
|
+
* `take` atomic — two concurrent redeems of the same code must never
|
|
75
|
+
* both succeed:
|
|
76
|
+
*
|
|
77
|
+
* ```sql
|
|
78
|
+
* -- Postgres example
|
|
79
|
+
* DELETE FROM auth_handoff_codes WHERE code_hash = $1 RETURNING payload;
|
|
80
|
+
* ```
|
|
81
|
+
*/
|
|
82
|
+
export interface HandoffStore {
|
|
83
|
+
/** Persist a payload under `codeHash`, expiring after `ttlSeconds`. */
|
|
84
|
+
put(codeHash: string, payload: HandoffPayload, ttlSeconds: number): Promise<void>;
|
|
85
|
+
/** Atomically fetch AND delete. Returns null when absent (or already taken). */
|
|
86
|
+
take(codeHash: string): Promise<HandoffPayload | null>;
|
|
87
|
+
}
|
|
88
|
+
/** Generate an unguessable handoff code (256-bit, base64url). */
|
|
89
|
+
export declare function generateHandoffCode(): string;
|
|
90
|
+
/** Hash a handoff code for use as a store key. */
|
|
91
|
+
export declare function hashHandoffCode(code: string): string;
|
|
92
|
+
/**
|
|
93
|
+
* Issue a handoff code for `targetHost`.
|
|
94
|
+
*
|
|
95
|
+
* @returns the RAW code — put it in the redirect URL. Only its hash
|
|
96
|
+
* touches the store.
|
|
97
|
+
*/
|
|
98
|
+
export declare function issueHandoffCode(store: HandoffStore, input: {
|
|
99
|
+
sub: string;
|
|
100
|
+
refreshToken: string;
|
|
101
|
+
targetHost: string;
|
|
102
|
+
}, ttlSeconds?: number): Promise<string>;
|
|
103
|
+
/**
|
|
104
|
+
* Redeem a handoff code on `currentHost`.
|
|
105
|
+
*
|
|
106
|
+
* Returns the payload when the code exists, hasn't expired, and was
|
|
107
|
+
* issued for exactly this host — null otherwise. The code is consumed
|
|
108
|
+
* either way (single-use: a failed redeem must burn it too, so an
|
|
109
|
+
* attacker can't probe).
|
|
110
|
+
*/
|
|
111
|
+
export declare function redeemHandoffCode(store: HandoffStore, code: string, currentHost: string): Promise<HandoffPayload | null>;
|
|
112
|
+
//# sourceMappingURL=handoff.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"handoff.d.ts","sourceRoot":"","sources":["../../src/server/handoff.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgDG;AAIH,0EAA0E;AAC1E,eAAO,MAAM,2BAA2B,KAAK,CAAC;AAE9C;;;;;;;GAOG;AACH,MAAM,WAAW,cAAc;IAC7B,0DAA0D;IAC1D,GAAG,EAAE,MAAM,CAAC;IACZ,kFAAkF;IAClF,YAAY,EAAE,MAAM,CAAC;IACrB,iEAAiE;IACjE,UAAU,EAAE,MAAM,CAAC;IACnB,gFAAgF;IAChF,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,YAAY;IAC3B,uEAAuE;IACvE,GAAG,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAClF,gFAAgF;IAChF,IAAI,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC,CAAC;CACxD;AAED,iEAAiE;AACjE,wBAAgB,mBAAmB,IAAI,MAAM,CAE5C;AAED,kDAAkD;AAClD,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEpD;AAOD;;;;;GAKG;AACH,wBAAsB,gBAAgB,CACpC,KAAK,EAAE,YAAY,EACnB,KAAK,EAAE;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,EAChE,UAAU,GAAE,MAAoC,GAC/C,OAAO,CAAC,MAAM,CAAC,CAUjB;AAED;;;;;;;GAOG;AACH,wBAAsB,iBAAiB,CACrC,KAAK,EAAE,YAAY,EACnB,IAAI,EAAE,MAAM,EACZ,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC,CAOhC"}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-domain session handoff — core primitives.
|
|
3
|
+
*
|
|
4
|
+
* **Why this exists.** Browser cookies cannot span registrable domains:
|
|
5
|
+
* a session on `app.platform.com` can never be read by `tenant.io`, no
|
|
6
|
+
* matter what `Domain=` attribute it carries. Multi-tenant products
|
|
7
|
+
* with white-label custom domains therefore need an SSO-style handoff
|
|
8
|
+
* to "move" a session between hosts: the host WITH a session mints a
|
|
9
|
+
* short-lived, single-use **handoff code** bound to the target host;
|
|
10
|
+
* the target host redeems it server-side and mints its own first-party
|
|
11
|
+
* cookies. The user's refresh token travels server-to-server through
|
|
12
|
+
* the app's {@link HandoffStore} — only the opaque code ever appears
|
|
13
|
+
* in a URL.
|
|
14
|
+
*
|
|
15
|
+
* **Security properties**
|
|
16
|
+
* - Codes are 256-bit `crypto.randomBytes` values — unguessable.
|
|
17
|
+
* - The store is keyed by the **SHA-256 hash** of the code, so a
|
|
18
|
+
* leaked store (DB dump, logs) cannot be replayed into sessions.
|
|
19
|
+
* - Codes are single-use: {@link HandoffStore.take} must delete
|
|
20
|
+
* atomically (e.g. SQL `DELETE … RETURNING`).
|
|
21
|
+
* - Codes expire after {@link DEFAULT_HANDOFF_TTL_SECONDS}; expiry is
|
|
22
|
+
* enforced BOTH by the store's TTL mechanism and re-checked here on
|
|
23
|
+
* redeem, so a lagging store cleanup can't extend the window.
|
|
24
|
+
* - Codes are bound to the target host at issue time; redeeming on
|
|
25
|
+
* any other host fails — and a wrong-host attempt BURNS the code,
|
|
26
|
+
* so it cannot be probed and then replayed on the right host.
|
|
27
|
+
* Combined with the route factory's `authorize` hook this
|
|
28
|
+
* guarantees a session can only ever materialize on hosts the app
|
|
29
|
+
* explicitly approved for THIS user.
|
|
30
|
+
* - The `complete` redirects carry `Cache-Control: no-store` and
|
|
31
|
+
* `Referrer-Policy: no-referrer` so the code-bearing URL never
|
|
32
|
+
* sits in a shared cache and never leaks via `Referer`.
|
|
33
|
+
*
|
|
34
|
+
* **Residual risks — know what this does NOT protect against**
|
|
35
|
+
* - *Login CSRF*: like every URL-based SSO handoff (OAuth code flow
|
|
36
|
+
* included), an attacker can mint a code for THEIR OWN session and
|
|
37
|
+
* lure a victim into opening the complete URL, logging the victim
|
|
38
|
+
* in as the attacker on that host. The 60s single-use window and
|
|
39
|
+
* the membership-gated `authorize` hook narrow this to hosts the
|
|
40
|
+
* ATTACKER belongs to; apps for which this matters should show the
|
|
41
|
+
* signed-in identity prominently post-handoff.
|
|
42
|
+
* - *Store contents*: payloads hold live refresh tokens. The hashed
|
|
43
|
+
* key prevents lookup-by-code, rows die in ≤60s, but a hostile DBA
|
|
44
|
+
* window exists — encrypt payloads at rest if that is in scope.
|
|
45
|
+
*
|
|
46
|
+
* The route-level flow lives in `handoff-routes.ts`
|
|
47
|
+
* ({@link createSessionHandoffRoutes}); apps supply the storage
|
|
48
|
+
* (Postgres / DynamoDB / Redis — anything with atomic take).
|
|
49
|
+
*/
|
|
50
|
+
import { createHash, randomBytes } from 'node:crypto';
|
|
51
|
+
/** Default lifetime of a handoff code: one browser redirect, not more. */
|
|
52
|
+
export const DEFAULT_HANDOFF_TTL_SECONDS = 60;
|
|
53
|
+
/** Generate an unguessable handoff code (256-bit, base64url). */
|
|
54
|
+
export function generateHandoffCode() {
|
|
55
|
+
return randomBytes(32).toString('base64url');
|
|
56
|
+
}
|
|
57
|
+
/** Hash a handoff code for use as a store key. */
|
|
58
|
+
export function hashHandoffCode(code) {
|
|
59
|
+
return createHash('sha256').update(code).digest('base64url');
|
|
60
|
+
}
|
|
61
|
+
/** Lowercase a host for comparison. Ports are significant (dev hosts). */
|
|
62
|
+
function normalizeHost(host) {
|
|
63
|
+
return host.trim().toLowerCase();
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Issue a handoff code for `targetHost`.
|
|
67
|
+
*
|
|
68
|
+
* @returns the RAW code — put it in the redirect URL. Only its hash
|
|
69
|
+
* touches the store.
|
|
70
|
+
*/
|
|
71
|
+
export async function issueHandoffCode(store, input, ttlSeconds = DEFAULT_HANDOFF_TTL_SECONDS) {
|
|
72
|
+
const code = generateHandoffCode();
|
|
73
|
+
const payload = {
|
|
74
|
+
sub: input.sub,
|
|
75
|
+
refreshToken: input.refreshToken,
|
|
76
|
+
targetHost: normalizeHost(input.targetHost),
|
|
77
|
+
expiresAt: Date.now() + ttlSeconds * 1000,
|
|
78
|
+
};
|
|
79
|
+
await store.put(hashHandoffCode(code), payload, ttlSeconds);
|
|
80
|
+
return code;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Redeem a handoff code on `currentHost`.
|
|
84
|
+
*
|
|
85
|
+
* Returns the payload when the code exists, hasn't expired, and was
|
|
86
|
+
* issued for exactly this host — null otherwise. The code is consumed
|
|
87
|
+
* either way (single-use: a failed redeem must burn it too, so an
|
|
88
|
+
* attacker can't probe).
|
|
89
|
+
*/
|
|
90
|
+
export async function redeemHandoffCode(store, code, currentHost) {
|
|
91
|
+
if (!code)
|
|
92
|
+
return null;
|
|
93
|
+
const payload = await store.take(hashHandoffCode(code));
|
|
94
|
+
if (!payload)
|
|
95
|
+
return null;
|
|
96
|
+
if (Date.now() > payload.expiresAt)
|
|
97
|
+
return null;
|
|
98
|
+
if (payload.targetHost !== normalizeHost(currentHost))
|
|
99
|
+
return null;
|
|
100
|
+
return payload;
|
|
101
|
+
}
|
|
102
|
+
//# sourceMappingURL=handoff.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"handoff.js","sourceRoot":"","sources":["../../src/server/handoff.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgDG;AAEH,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAEtD,0EAA0E;AAC1E,MAAM,CAAC,MAAM,2BAA2B,GAAG,EAAE,CAAC;AAwC9C,iEAAiE;AACjE,MAAM,UAAU,mBAAmB;IACjC,OAAO,WAAW,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;AAC/C,CAAC;AAED,kDAAkD;AAClD,MAAM,UAAU,eAAe,CAAC,IAAY;IAC1C,OAAO,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC;AAC/D,CAAC;AAED,0EAA0E;AAC1E,SAAS,aAAa,CAAC,IAAY;IACjC,OAAO,IAAI,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;AACnC,CAAC;AAED;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACpC,KAAmB,EACnB,KAAgE,EAChE,aAAqB,2BAA2B;IAEhD,MAAM,IAAI,GAAG,mBAAmB,EAAE,CAAC;IACnC,MAAM,OAAO,GAAmB;QAC9B,GAAG,EAAE,KAAK,CAAC,GAAG;QACd,YAAY,EAAE,KAAK,CAAC,YAAY;QAChC,UAAU,EAAE,aAAa,CAAC,KAAK,CAAC,UAAU,CAAC;QAC3C,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,UAAU,GAAG,IAAI;KAC1C,CAAC;IACF,MAAM,KAAK,CAAC,GAAG,CAAC,eAAe,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,UAAU,CAAC,CAAC;IAC5D,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,iBAAiB,CACrC,KAAmB,EACnB,IAAY,EACZ,WAAmB;IAEnB,IAAI,CAAC,IAAI;QAAE,OAAO,IAAI,CAAC;IACvB,MAAM,OAAO,GAAG,MAAM,KAAK,CAAC,IAAI,CAAC,eAAe,CAAC,IAAI,CAAC,CAAC,CAAC;IACxD,IAAI,CAAC,OAAO;QAAE,OAAO,IAAI,CAAC;IAC1B,IAAI,IAAI,CAAC,GAAG,EAAE,GAAG,OAAO,CAAC,SAAS;QAAE,OAAO,IAAI,CAAC;IAChD,IAAI,OAAO,CAAC,UAAU,KAAK,aAAa,CAAC,WAAW,CAAC;QAAE,OAAO,IAAI,CAAC;IACnE,OAAO,OAAO,CAAC;AACjB,CAAC"}
|
package/dist/server/index.d.ts
CHANGED
|
@@ -34,19 +34,26 @@ export type { AdminInviteUserInput, AdminInviteUserResult, AdminUpdateAttributes
|
|
|
34
34
|
export { adminInviteUser, adminUpdateUserAttributes, adminDisableUser, adminEnableUser, adminDeleteUser, } from './admin-users.js';
|
|
35
35
|
export type { RefreshResult } from './refresh.js';
|
|
36
36
|
export { refreshSession } from './refresh.js';
|
|
37
|
-
export { revokeRefreshToken } from './revoke.js';
|
|
37
|
+
export { revokeRefreshToken, globalSignOut } from './revoke.js';
|
|
38
|
+
export type { EnsureUserForVerifiedEmailInput, EnsureUserForVerifiedEmailResult, } from './passwordless.js';
|
|
39
|
+
export { ensureUserForVerifiedEmail, mintSessionForVerifiedEmail, } from './passwordless.js';
|
|
38
40
|
export type { ChangePasswordInput } from './change-password.js';
|
|
39
41
|
export { changePassword } from './change-password.js';
|
|
40
42
|
export type { ForgotPasswordInput, ForgotPasswordResult, ConfirmForgotPasswordInput, CodeDeliveryDetails, } from './forgot-password.js';
|
|
41
43
|
export { forgotPassword, confirmForgotPassword } from './forgot-password.js';
|
|
42
44
|
export type { VerifyOptions } from './verify.js';
|
|
43
45
|
export { verifyAndDecode } from './verify.js';
|
|
44
|
-
export
|
|
45
|
-
export {
|
|
46
|
+
export { decodeJwtClaims } from './token-utils.js';
|
|
47
|
+
export type { SessionTokens, CookieOptions, CookieDomainOptions, OAuthStateCookieOptions, } from './cookies.js';
|
|
48
|
+
export { ID_TOKEN_COOKIE, ACCESS_TOKEN_COOKIE, REFRESH_TOKEN_COOKIE, buildSessionCookies, buildClearSessionCookies, readCookieFromHeader, buildOAuthStateCookie, clearOAuthStateCookie, oauthStateCookieName, resolveCookieDomain, } from './cookies.js';
|
|
46
49
|
export type { CookieAuthMiddlewareOptions } from './middleware.js';
|
|
47
|
-
export { cookieAuthMiddleware, extractToken } from './middleware.js';
|
|
50
|
+
export { cookieAuthMiddleware, extractToken, defaultScopeMapper } from './middleware.js';
|
|
48
51
|
export type { FederatedAuthRoutes, FederatedAuthRoutesOptions, FederatedCallbackBody, FederatedCallbackResult, FederatedOnSignInArgs, FederatedOnSignInResult, FederatedStartBody, } from './federated-routes.js';
|
|
49
52
|
export { createFederatedAuthRoutes } from './federated-routes.js';
|
|
53
|
+
export type { HandoffPayload, HandoffStore } from './handoff.js';
|
|
54
|
+
export { DEFAULT_HANDOFF_TTL_SECONDS, generateHandoffCode, hashHandoffCode, issueHandoffCode, redeemHandoffCode, } from './handoff.js';
|
|
55
|
+
export type { HandoffAuthorizeArgs, HandoffStartBody, SessionHandoffRoutes, SessionHandoffRoutesOptions, } from './handoff-routes.js';
|
|
56
|
+
export { createSessionHandoffRoutes, DEFAULT_HANDOFF_COMPLETE_PATH, } from './handoff-routes.js';
|
|
50
57
|
export type { FederatedProvider, FederatedProfile, FederatedProviderCredentials, SignInAsFederatedUserInput, BuildAuthorizeUrlInput, ExchangeAuthorizationCodeInput, } from './federated.js';
|
|
51
58
|
export { loadFederatedProviderCredentials, generateOAuthState, verifyOAuthState, buildAuthorizeUrl, exchangeAuthorizationCode, signInAsFederatedUser, } from './federated.js';
|
|
52
59
|
export type { VerificationChannel, VerificationCodeStore, VerificationCodeRecord, RequestVerificationCodeInput, RequestVerificationCodeResult, VerifyVerificationCodeInput, } from './verification.js';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EAAE,SAAS,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAC1D,YAAY,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AACpD,OAAO,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAEnD,YAAY,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AACpE,OAAO,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAElD,YAAY,EACV,eAAe,EACf,uBAAuB,EACvB,uBAAuB,GACxB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EACL,6BAA6B,EAC7B,6BAA6B,GAC9B,MAAM,gBAAgB,CAAC;AAExB,YAAY,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC9D,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAE1C,YAAY,EACV,oBAAoB,EACpB,qBAAqB,EACrB,0BAA0B,GAC3B,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACL,eAAe,EACf,yBAAyB,EACzB,gBAAgB,EAChB,eAAe,EACf,eAAe,GAChB,MAAM,kBAAkB,CAAC;AAE1B,YAAY,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAClD,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAE9C,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"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"}
|
package/dist/server/index.js
CHANGED
|
@@ -28,13 +28,19 @@ export { signInWithPasswordOrChallenge, respondToNewPasswordChallenge, } from '.
|
|
|
28
28
|
export { signUpUser } from './sign-up.js';
|
|
29
29
|
export { adminInviteUser, adminUpdateUserAttributes, adminDisableUser, adminEnableUser, adminDeleteUser, } from './admin-users.js';
|
|
30
30
|
export { refreshSession } from './refresh.js';
|
|
31
|
-
export { revokeRefreshToken } from './revoke.js';
|
|
31
|
+
export { revokeRefreshToken, globalSignOut } from './revoke.js';
|
|
32
|
+
export { ensureUserForVerifiedEmail, mintSessionForVerifiedEmail, } from './passwordless.js';
|
|
32
33
|
export { changePassword } from './change-password.js';
|
|
33
34
|
export { forgotPassword, confirmForgotPassword } from './forgot-password.js';
|
|
34
35
|
export { verifyAndDecode } from './verify.js';
|
|
35
|
-
|
|
36
|
-
|
|
36
|
+
// Unverified claim extraction — ONLY for tokens received directly
|
|
37
|
+
// from the provider (e.g. reading profile claims right after sign-in).
|
|
38
|
+
export { decodeJwtClaims } from './token-utils.js';
|
|
39
|
+
export { ID_TOKEN_COOKIE, ACCESS_TOKEN_COOKIE, REFRESH_TOKEN_COOKIE, buildSessionCookies, buildClearSessionCookies, readCookieFromHeader, buildOAuthStateCookie, clearOAuthStateCookie, oauthStateCookieName, resolveCookieDomain, } from './cookies.js';
|
|
40
|
+
export { cookieAuthMiddleware, extractToken, defaultScopeMapper } from './middleware.js';
|
|
37
41
|
export { createFederatedAuthRoutes } from './federated-routes.js';
|
|
42
|
+
export { DEFAULT_HANDOFF_TTL_SECONDS, generateHandoffCode, hashHandoffCode, issueHandoffCode, redeemHandoffCode, } from './handoff.js';
|
|
43
|
+
export { createSessionHandoffRoutes, DEFAULT_HANDOFF_COMPLETE_PATH, } from './handoff-routes.js';
|
|
38
44
|
export { loadFederatedProviderCredentials, generateOAuthState, verifyOAuthState, buildAuthorizeUrl, exchangeAuthorizationCode, signInAsFederatedUser, } from './federated.js';
|
|
39
45
|
export { generateVerificationCode, hashVerificationCode, requestVerificationCode, verifyVerificationCode, createInMemoryVerificationCodeStore, } from './verification.js';
|
|
40
46
|
//# sourceMappingURL=index.js.map
|
package/dist/server/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EAAE,SAAS,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAE1D,OAAO,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAGnD,OAAO,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAOlD,OAAO,EACL,6BAA6B,EAC7B,6BAA6B,GAC9B,MAAM,gBAAgB,CAAC;AAGxB,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAO1C,OAAO,EACL,eAAe,EACf,yBAAyB,EACzB,gBAAgB,EAChB,eAAe,EACf,eAAe,GAChB,MAAM,kBAAkB,CAAC;AAG1B,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAE9C,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EAAE,SAAS,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAE1D,OAAO,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAGnD,OAAO,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAOlD,OAAO,EACL,6BAA6B,EAC7B,6BAA6B,GAC9B,MAAM,gBAAgB,CAAC;AAGxB,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAO1C,OAAO,EACL,eAAe,EACf,yBAAyB,EACzB,gBAAgB,EAChB,eAAe,EACf,eAAe,GAChB,MAAM,kBAAkB,CAAC;AAG1B,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAE9C,OAAO,EAAE,kBAAkB,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAQhE,OAAO,EACL,0BAA0B,EAC1B,2BAA2B,GAC5B,MAAM,mBAAmB,CAAC;AAG3B,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAQtD,OAAO,EAAE,cAAc,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAG7E,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAE9C,kEAAkE;AAClE,uEAAuE;AACvE,OAAO,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAQnD,OAAO,EACL,eAAe,EACf,mBAAmB,EACnB,oBAAoB,EACpB,mBAAmB,EACnB,wBAAwB,EACxB,oBAAoB,EACpB,qBAAqB,EACrB,qBAAqB,EACrB,oBAAoB,EACpB,mBAAmB,GACpB,MAAM,cAAc,CAAC;AAGtB,OAAO,EAAE,oBAAoB,EAAE,YAAY,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAWzF,OAAO,EAAE,yBAAyB,EAAE,MAAM,uBAAuB,CAAC;AAGlE,OAAO,EACL,2BAA2B,EAC3B,mBAAmB,EACnB,eAAe,EACf,gBAAgB,EAChB,iBAAiB,GAClB,MAAM,cAAc,CAAC;AAQtB,OAAO,EACL,0BAA0B,EAC1B,6BAA6B,GAC9B,MAAM,qBAAqB,CAAC;AAU7B,OAAO,EACL,gCAAgC,EAChC,kBAAkB,EAClB,gBAAgB,EAChB,iBAAiB,EACjB,yBAAyB,EACzB,qBAAqB,GACtB,MAAM,gBAAgB,CAAC;AAUxB,OAAO,EACL,wBAAwB,EACxB,oBAAoB,EACpB,uBAAuB,EACvB,sBAAsB,EACtB,mCAAmC,GACpC,MAAM,mBAAmB,CAAC"}
|