@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
|
@@ -45,6 +45,16 @@ export interface CookieAuthMiddlewareOptions {
|
|
|
45
45
|
* tests lets you construct a middleware without touching env.
|
|
46
46
|
*/
|
|
47
47
|
config?: AuthServerConfig;
|
|
48
|
+
/**
|
|
49
|
+
* Map verified JWT claims to the scope strings the
|
|
50
|
+
* `handler({ scopes })` gate checks. Defaults to
|
|
51
|
+
* {@link defaultScopeMapper}, which unions the access-token `scope`
|
|
52
|
+
* claim with Cognito `cognito:groups` and a `custom:scopes`
|
|
53
|
+
* attribute — so id-token browser sessions (which carry no `scope`
|
|
54
|
+
* claim) can still satisfy scoped routes via group / attribute
|
|
55
|
+
* membership. Provide your own to map app-specific claims.
|
|
56
|
+
*/
|
|
57
|
+
scopeMapper?: (claims: Record<string, unknown>) => string[];
|
|
48
58
|
}
|
|
49
59
|
/**
|
|
50
60
|
* Build a `@venturekit/runtime` middleware that verifies a session JWT
|
|
@@ -73,4 +83,29 @@ export declare function cookieAuthMiddleware(options?: CookieAuthMiddlewareOptio
|
|
|
73
83
|
* {@link cookieAuthMiddleware} instead.
|
|
74
84
|
*/
|
|
75
85
|
export declare function extractToken(event: APIGatewayProxyEventV2, cookieName?: string): string | null;
|
|
86
|
+
/**
|
|
87
|
+
* Default claims→scopes mapper.
|
|
88
|
+
*
|
|
89
|
+
* Unions three sources so BOTH access-token and id-token sessions can
|
|
90
|
+
* satisfy the `handler({ scopes })` gate:
|
|
91
|
+
* - `scope` — space-separated, present on Cognito ACCESS tokens.
|
|
92
|
+
* - `cognito:groups` — `string[]` on both id and access tokens; group
|
|
93
|
+
* names double as coarse scopes (e.g. a `cms.admin` group satisfies
|
|
94
|
+
* `scopes: ['cms.admin']`).
|
|
95
|
+
* - `custom:scopes` — a custom user-pool attribute holding a space- or
|
|
96
|
+
* comma-separated scope list; the recommended way to attach scopes
|
|
97
|
+
* to id-token browser sessions, which otherwise carry no `scope`.
|
|
98
|
+
*
|
|
99
|
+
* De-duplicates and drops empties. Override via
|
|
100
|
+
* {@link CookieAuthMiddlewareOptions.scopeMapper} for app-specific claims.
|
|
101
|
+
*
|
|
102
|
+
* # id-token vs access-token (the A2 decision)
|
|
103
|
+
* The default {@link cookieAuthMiddleware} verifies ID tokens because the
|
|
104
|
+
* browser session cookie is an id token (it carries `email` + profile).
|
|
105
|
+
* ID tokens have no OAuth `scope`, so scope gating on id-token sessions
|
|
106
|
+
* MUST come from Cognito groups or a `custom:scopes` attribute — which
|
|
107
|
+
* this mapper reads out of the box. For service-to-service callers, use
|
|
108
|
+
* `cookieAuthMiddleware({ tokenUse: 'access' })` and the `scope` claim.
|
|
109
|
+
*/
|
|
110
|
+
export declare function defaultScopeMapper(claims: Record<string, unknown>): string[];
|
|
76
111
|
//# sourceMappingURL=middleware.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"middleware.d.ts","sourceRoot":"","sources":["../../src/server/middleware.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAC;AAKzD,OAAO,KAAK,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAGtE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAGpD,MAAM,WAAW,2BAA2B;IAC1C;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,IAAI,GAAG,QAAQ,CAAC;IAC3B;;;;OAIG;IACH,MAAM,CAAC,EAAE,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"middleware.d.ts","sourceRoot":"","sources":["../../src/server/middleware.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAC;AAKzD,OAAO,KAAK,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAGtE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAGpD,MAAM,WAAW,2BAA2B;IAC1C;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,IAAI,GAAG,QAAQ,CAAC;IAC3B;;;;OAIG;IACH,MAAM,CAAC,EAAE,gBAAgB,CAAC;IAC1B;;;;;;;;OAQG;IACH,WAAW,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,MAAM,EAAE,CAAC;CAC7D;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,oBAAoB,CAClC,OAAO,GAAE,2BAAgC,GACxC,UAAU,CAAC,cAAc,CAAC,CAkC5B;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAC1B,KAAK,EAAE,sBAAsB,EAC7B,UAAU,GAAE,MAAwB,GACnC,MAAM,GAAG,IAAI,CAoBf;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC9B,MAAM,EAAE,CAiBV"}
|
|
@@ -46,6 +46,7 @@ import { loadAuthServerConfig } from './config.js';
|
|
|
46
46
|
export function cookieAuthMiddleware(options = {}) {
|
|
47
47
|
const cookieName = options.cookieName ?? ID_TOKEN_COOKIE;
|
|
48
48
|
const tokenUse = options.tokenUse ?? 'id';
|
|
49
|
+
const scopeMapper = options.scopeMapper ?? defaultScopeMapper;
|
|
49
50
|
// Resolve config lazily so a route module can import the middleware
|
|
50
51
|
// factory at file-load time without the env vars being set (tests,
|
|
51
52
|
// local type-only imports, etc.). The first actual request resolves
|
|
@@ -70,7 +71,7 @@ export function cookieAuthMiddleware(options = {}) {
|
|
|
70
71
|
...(cfg.endpoint ? { endpoint: cfg.endpoint } : {}),
|
|
71
72
|
});
|
|
72
73
|
if (claims) {
|
|
73
|
-
ctx.user = claimsToUserContext(claims);
|
|
74
|
+
ctx.user = claimsToUserContext(claims, scopeMapper);
|
|
74
75
|
}
|
|
75
76
|
}
|
|
76
77
|
return next();
|
|
@@ -103,17 +104,56 @@ export function extractToken(event, cookieName = ID_TOKEN_COOKIE) {
|
|
|
103
104
|
: (headers['cookie'] ?? headers['Cookie']);
|
|
104
105
|
return readCookieFromHeader(cookieHeader, cookieName);
|
|
105
106
|
}
|
|
106
|
-
|
|
107
|
+
/**
|
|
108
|
+
* Default claims→scopes mapper.
|
|
109
|
+
*
|
|
110
|
+
* Unions three sources so BOTH access-token and id-token sessions can
|
|
111
|
+
* satisfy the `handler({ scopes })` gate:
|
|
112
|
+
* - `scope` — space-separated, present on Cognito ACCESS tokens.
|
|
113
|
+
* - `cognito:groups` — `string[]` on both id and access tokens; group
|
|
114
|
+
* names double as coarse scopes (e.g. a `cms.admin` group satisfies
|
|
115
|
+
* `scopes: ['cms.admin']`).
|
|
116
|
+
* - `custom:scopes` — a custom user-pool attribute holding a space- or
|
|
117
|
+
* comma-separated scope list; the recommended way to attach scopes
|
|
118
|
+
* to id-token browser sessions, which otherwise carry no `scope`.
|
|
119
|
+
*
|
|
120
|
+
* De-duplicates and drops empties. Override via
|
|
121
|
+
* {@link CookieAuthMiddlewareOptions.scopeMapper} for app-specific claims.
|
|
122
|
+
*
|
|
123
|
+
* # id-token vs access-token (the A2 decision)
|
|
124
|
+
* The default {@link cookieAuthMiddleware} verifies ID tokens because the
|
|
125
|
+
* browser session cookie is an id token (it carries `email` + profile).
|
|
126
|
+
* ID tokens have no OAuth `scope`, so scope gating on id-token sessions
|
|
127
|
+
* MUST come from Cognito groups or a `custom:scopes` attribute — which
|
|
128
|
+
* this mapper reads out of the box. For service-to-service callers, use
|
|
129
|
+
* `cookieAuthMiddleware({ tokenUse: 'access' })` and the `scope` claim.
|
|
130
|
+
*/
|
|
131
|
+
export function defaultScopeMapper(claims) {
|
|
132
|
+
const out = new Set();
|
|
133
|
+
const addAll = (raw, splitter) => {
|
|
134
|
+
if (typeof raw === 'string') {
|
|
135
|
+
for (const s of raw.split(splitter))
|
|
136
|
+
if (s)
|
|
137
|
+
out.add(s);
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
// Access-token scope claim (space-separated).
|
|
141
|
+
addAll(claims['scope'], / +/);
|
|
142
|
+
// Cognito groups (array on id + access tokens).
|
|
143
|
+
const groups = claims['cognito:groups'];
|
|
144
|
+
if (Array.isArray(groups)) {
|
|
145
|
+
for (const g of groups)
|
|
146
|
+
if (typeof g === 'string' && g)
|
|
147
|
+
out.add(g);
|
|
148
|
+
}
|
|
149
|
+
// custom:scopes attribute (space- or comma-separated).
|
|
150
|
+
addAll(claims['custom:scopes'], /[\s,]+/);
|
|
151
|
+
return [...out];
|
|
152
|
+
}
|
|
153
|
+
function claimsToUserContext(claims, scopeMapper = defaultScopeMapper) {
|
|
107
154
|
const sub = claims['sub'];
|
|
108
155
|
const email = claims['email'];
|
|
109
|
-
|
|
110
|
-
// carry scopes; if the app uses ID tokens, scope-based route gating
|
|
111
|
-
// should be backed by `custom:*` role/scope attributes on the user
|
|
112
|
-
// pool and mapped in a downstream mapper.
|
|
113
|
-
const scopeClaim = claims['scope'];
|
|
114
|
-
const scopes = typeof scopeClaim === 'string'
|
|
115
|
-
? scopeClaim.split(' ').filter(Boolean)
|
|
116
|
-
: [];
|
|
156
|
+
const scopes = scopeMapper(claims);
|
|
117
157
|
return {
|
|
118
158
|
id: typeof sub === 'string' ? sub : '',
|
|
119
159
|
email: typeof email === 'string' ? email : undefined,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"middleware.js","sourceRoot":"","sources":["../../src/server/middleware.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAQH,OAAO,EAAE,eAAe,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC;AACrE,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAE9C,OAAO,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"middleware.js","sourceRoot":"","sources":["../../src/server/middleware.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server-verified (passwordless) identity flows.
|
|
3
|
+
*
|
|
4
|
+
* For UXes where the APP proves address ownership out-of-band — magic
|
|
5
|
+
* links, OAuth/OIDC callbacks, emailed one-time codes — and Cognito is
|
|
6
|
+
* only the identity/token backend, never the credential UI:
|
|
7
|
+
*
|
|
8
|
+
* 1. {@link ensureUserForVerifiedEmail} — idempotently create the
|
|
9
|
+
* Cognito user (invite email suppressed, `email_verified=true`)
|
|
10
|
+
* or fetch the existing one; the returned `sub` doubles as the
|
|
11
|
+
* app's `users.id`.
|
|
12
|
+
* 2. {@link mintSessionForVerifiedEmail} — rotate the user's
|
|
13
|
+
* password to a throwaway strong secret (`AdminSetUserPassword`,
|
|
14
|
+
* permanent) and immediately sign in via
|
|
15
|
+
* `InitiateAuth(USER_PASSWORD_AUTH)`. The password is discarded;
|
|
16
|
+
* only the tokens leave the function.
|
|
17
|
+
*
|
|
18
|
+
* Requirements:
|
|
19
|
+
* - the app client must allow `ALLOW_USER_PASSWORD_AUTH`;
|
|
20
|
+
* - the caller's IAM role needs the `cognito-idp:Admin*` actions
|
|
21
|
+
* used below (VentureKit's auth intent grants them to the API
|
|
22
|
+
* Lambdas).
|
|
23
|
+
*
|
|
24
|
+
* Security notes:
|
|
25
|
+
* - the rotated password never leaves the server and satisfies the
|
|
26
|
+
* default "strong" policy (length + upper/lower/digit/symbol);
|
|
27
|
+
* - replays are benign: re-minting just issues another valid token
|
|
28
|
+
* set for the same identity — revoke on sign-out;
|
|
29
|
+
* - concurrent mints race harmlessly (last rotation wins, every
|
|
30
|
+
* token set issued stays valid until expiry/revocation).
|
|
31
|
+
*/
|
|
32
|
+
import type { AuthServerConfig } from './config.js';
|
|
33
|
+
import { type SignInResult } from './tokens.js';
|
|
34
|
+
export interface EnsureUserForVerifiedEmailInput {
|
|
35
|
+
/** Address the app has already proven ownership of. */
|
|
36
|
+
email: string;
|
|
37
|
+
/** Optional standard attributes copied onto the Cognito record. */
|
|
38
|
+
firstName?: string;
|
|
39
|
+
lastName?: string;
|
|
40
|
+
/**
|
|
41
|
+
* Additional attributes (e.g. `{ 'custom:tenantRoles': '…' }`).
|
|
42
|
+
* Applied on create AND refreshed on subsequent calls.
|
|
43
|
+
*/
|
|
44
|
+
attributes?: Record<string, string>;
|
|
45
|
+
}
|
|
46
|
+
export interface EnsureUserForVerifiedEmailResult {
|
|
47
|
+
/** Cognito `sub` (UUID) — commonly reused as the app's user id. */
|
|
48
|
+
sub: string;
|
|
49
|
+
/** True when this call created the Cognito user. */
|
|
50
|
+
created: boolean;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Idempotently ensure a Cognito user exists for a server-verified
|
|
54
|
+
* email. New users are auto-confirmed with `email_verified=true` and
|
|
55
|
+
* the invite email suppressed — the app owns the UX. On subsequent
|
|
56
|
+
* calls the profile attributes are refreshed best-effort.
|
|
57
|
+
*/
|
|
58
|
+
export declare function ensureUserForVerifiedEmail(input: EnsureUserForVerifiedEmailInput, config?: AuthServerConfig): Promise<EnsureUserForVerifiedEmailResult>;
|
|
59
|
+
/**
|
|
60
|
+
* Mint a full token set (id + access + refresh) for a user whose
|
|
61
|
+
* email the app just verified: rotate the password server-side, sign
|
|
62
|
+
* in with it, discard it.
|
|
63
|
+
*
|
|
64
|
+
* Throws {@link AuthError} when the rotation or sign-in fails (e.g.
|
|
65
|
+
* `USER_PASSWORD_AUTH` not enabled on the app client).
|
|
66
|
+
*/
|
|
67
|
+
export declare function mintSessionForVerifiedEmail(email: string, config?: AuthServerConfig): Promise<SignInResult>;
|
|
68
|
+
//# sourceMappingURL=passwordless.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"passwordless.d.ts","sourceRoot":"","sources":["../../src/server/passwordless.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAaH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAIpD,OAAO,EAAuB,KAAK,YAAY,EAAE,MAAM,aAAa,CAAC;AAWrE,MAAM,WAAW,+BAA+B;IAC9C,uDAAuD;IACvD,KAAK,EAAE,MAAM,CAAC;IACd,mEAAmE;IACnE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACrC;AAED,MAAM,WAAW,gCAAgC;IAC/C,mEAAmE;IACnE,GAAG,EAAE,MAAM,CAAC;IACZ,oDAAoD;IACpD,OAAO,EAAE,OAAO,CAAC;CAClB;AAED;;;;;GAKG;AACH,wBAAsB,0BAA0B,CAC9C,KAAK,EAAE,+BAA+B,EACtC,MAAM,GAAE,gBAAyC,GAChD,OAAO,CAAC,gCAAgC,CAAC,CAgE3C;AAED;;;;;;;GAOG;AACH,wBAAsB,2BAA2B,CAC/C,KAAK,EAAE,MAAM,EACb,MAAM,GAAE,gBAAyC,GAChD,OAAO,CAAC,YAAY,CAAC,CAyBvB"}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server-verified (passwordless) identity flows.
|
|
3
|
+
*
|
|
4
|
+
* For UXes where the APP proves address ownership out-of-band — magic
|
|
5
|
+
* links, OAuth/OIDC callbacks, emailed one-time codes — and Cognito is
|
|
6
|
+
* only the identity/token backend, never the credential UI:
|
|
7
|
+
*
|
|
8
|
+
* 1. {@link ensureUserForVerifiedEmail} — idempotently create the
|
|
9
|
+
* Cognito user (invite email suppressed, `email_verified=true`)
|
|
10
|
+
* or fetch the existing one; the returned `sub` doubles as the
|
|
11
|
+
* app's `users.id`.
|
|
12
|
+
* 2. {@link mintSessionForVerifiedEmail} — rotate the user's
|
|
13
|
+
* password to a throwaway strong secret (`AdminSetUserPassword`,
|
|
14
|
+
* permanent) and immediately sign in via
|
|
15
|
+
* `InitiateAuth(USER_PASSWORD_AUTH)`. The password is discarded;
|
|
16
|
+
* only the tokens leave the function.
|
|
17
|
+
*
|
|
18
|
+
* Requirements:
|
|
19
|
+
* - the app client must allow `ALLOW_USER_PASSWORD_AUTH`;
|
|
20
|
+
* - the caller's IAM role needs the `cognito-idp:Admin*` actions
|
|
21
|
+
* used below (VentureKit's auth intent grants them to the API
|
|
22
|
+
* Lambdas).
|
|
23
|
+
*
|
|
24
|
+
* Security notes:
|
|
25
|
+
* - the rotated password never leaves the server and satisfies the
|
|
26
|
+
* default "strong" policy (length + upper/lower/digit/symbol);
|
|
27
|
+
* - replays are benign: re-minting just issues another valid token
|
|
28
|
+
* set for the same identity — revoke on sign-out;
|
|
29
|
+
* - concurrent mints race harmlessly (last rotation wins, every
|
|
30
|
+
* token set issued stays valid until expiry/revocation).
|
|
31
|
+
*/
|
|
32
|
+
import { AdminCreateUserCommand, AdminGetUserCommand, AdminSetUserPasswordCommand, AdminUpdateUserAttributesCommand, InitiateAuthCommand, MessageActionType, UsernameExistsException, } from '@aws-sdk/client-cognito-identity-provider';
|
|
33
|
+
import { randomBytes } from 'node:crypto';
|
|
34
|
+
import { loadAuthServerConfig } from './config.js';
|
|
35
|
+
import { getCognitoClient } from './cognito-client.js';
|
|
36
|
+
import { AuthError, mapProviderError } from './errors.js';
|
|
37
|
+
import { extractSignInTokens } from './tokens.js';
|
|
38
|
+
/**
|
|
39
|
+
* Cryptographically random password satisfying Cognito's default
|
|
40
|
+
* "strong" policy: 40 base64url chars + `Aa1!` to guarantee the
|
|
41
|
+
* character-class mix. Internal — the value is never surfaced.
|
|
42
|
+
*/
|
|
43
|
+
function generateRotatingPassword() {
|
|
44
|
+
return `${randomBytes(30).toString('base64url')}Aa1!`;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Idempotently ensure a Cognito user exists for a server-verified
|
|
48
|
+
* email. New users are auto-confirmed with `email_verified=true` and
|
|
49
|
+
* the invite email suppressed — the app owns the UX. On subsequent
|
|
50
|
+
* calls the profile attributes are refreshed best-effort.
|
|
51
|
+
*/
|
|
52
|
+
export async function ensureUserForVerifiedEmail(input, config = loadAuthServerConfig()) {
|
|
53
|
+
const client = getCognitoClient(config.region, config.endpoint);
|
|
54
|
+
const email = input.email.trim().toLowerCase();
|
|
55
|
+
const attributes = [
|
|
56
|
+
{ Name: 'email', Value: email },
|
|
57
|
+
{ Name: 'email_verified', Value: 'true' },
|
|
58
|
+
];
|
|
59
|
+
if (input.firstName)
|
|
60
|
+
attributes.push({ Name: 'given_name', Value: input.firstName });
|
|
61
|
+
if (input.lastName)
|
|
62
|
+
attributes.push({ Name: 'family_name', Value: input.lastName });
|
|
63
|
+
for (const [name, value] of Object.entries(input.attributes ?? {})) {
|
|
64
|
+
attributes.push({ Name: name, Value: value });
|
|
65
|
+
}
|
|
66
|
+
try {
|
|
67
|
+
const res = await client.send(new AdminCreateUserCommand({
|
|
68
|
+
UserPoolId: config.userPoolId,
|
|
69
|
+
Username: email,
|
|
70
|
+
UserAttributes: attributes,
|
|
71
|
+
MessageAction: MessageActionType.SUPPRESS,
|
|
72
|
+
}));
|
|
73
|
+
const sub = res.User?.Attributes?.find((a) => a.Name === 'sub')?.Value;
|
|
74
|
+
if (!sub) {
|
|
75
|
+
throw new AuthError('incomplete_auth_result', 'AdminCreateUser did not return a sub', 500);
|
|
76
|
+
}
|
|
77
|
+
return { sub, created: true };
|
|
78
|
+
}
|
|
79
|
+
catch (err) {
|
|
80
|
+
if (!(err instanceof UsernameExistsException)) {
|
|
81
|
+
throw err instanceof AuthError ? err : mapProviderError(err, 'sign_up_failed');
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
// Already present — fetch the sub and refresh mutable attributes.
|
|
85
|
+
const existing = await client.send(new AdminGetUserCommand({ UserPoolId: config.userPoolId, Username: email }));
|
|
86
|
+
const sub = existing.UserAttributes?.find((a) => a.Name === 'sub')?.Value;
|
|
87
|
+
if (!sub) {
|
|
88
|
+
throw new AuthError('incomplete_auth_result', 'AdminGetUser did not return a sub', 500);
|
|
89
|
+
}
|
|
90
|
+
const updates = attributes.filter((a) => a.Name !== 'email' && a.Name !== 'email_verified');
|
|
91
|
+
if (updates.length) {
|
|
92
|
+
try {
|
|
93
|
+
await client.send(new AdminUpdateUserAttributesCommand({
|
|
94
|
+
UserPoolId: config.userPoolId,
|
|
95
|
+
Username: email,
|
|
96
|
+
UserAttributes: updates,
|
|
97
|
+
}));
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
// Best-effort — stale profile attributes are not worth failing
|
|
101
|
+
// an authentication flow over.
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return { sub, created: false };
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Mint a full token set (id + access + refresh) for a user whose
|
|
108
|
+
* email the app just verified: rotate the password server-side, sign
|
|
109
|
+
* in with it, discard it.
|
|
110
|
+
*
|
|
111
|
+
* Throws {@link AuthError} when the rotation or sign-in fails (e.g.
|
|
112
|
+
* `USER_PASSWORD_AUTH` not enabled on the app client).
|
|
113
|
+
*/
|
|
114
|
+
export async function mintSessionForVerifiedEmail(email, config = loadAuthServerConfig()) {
|
|
115
|
+
const client = getCognitoClient(config.region, config.endpoint);
|
|
116
|
+
const normalized = email.trim().toLowerCase();
|
|
117
|
+
const password = generateRotatingPassword();
|
|
118
|
+
try {
|
|
119
|
+
await client.send(new AdminSetUserPasswordCommand({
|
|
120
|
+
UserPoolId: config.userPoolId,
|
|
121
|
+
Username: normalized,
|
|
122
|
+
Password: password,
|
|
123
|
+
Permanent: true,
|
|
124
|
+
}));
|
|
125
|
+
const res = await client.send(new InitiateAuthCommand({
|
|
126
|
+
AuthFlow: 'USER_PASSWORD_AUTH',
|
|
127
|
+
ClientId: config.appClientId,
|
|
128
|
+
AuthParameters: { USERNAME: normalized, PASSWORD: password },
|
|
129
|
+
}));
|
|
130
|
+
return extractSignInTokens(res.AuthenticationResult);
|
|
131
|
+
}
|
|
132
|
+
catch (err) {
|
|
133
|
+
throw err instanceof AuthError ? err : mapProviderError(err, 'sign_in_failed');
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
//# sourceMappingURL=passwordless.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"passwordless.js","sourceRoot":"","sources":["../../src/server/passwordless.ts"],"names":[],"mappings":"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"}
|
package/dist/server/revoke.d.ts
CHANGED
|
@@ -15,4 +15,14 @@ import type { AuthServerConfig } from './config.js';
|
|
|
15
15
|
* the function never throws.
|
|
16
16
|
*/
|
|
17
17
|
export declare function revokeRefreshToken(refreshToken: string, config?: AuthServerConfig): Promise<void>;
|
|
18
|
+
/**
|
|
19
|
+
* Invalidate EVERY outstanding refresh token for the caller via
|
|
20
|
+
* Cognito's `GlobalSignOut` (keyed by a valid access token, unlike
|
|
21
|
+
* {@link revokeRefreshToken} which revokes one refresh token). Already
|
|
22
|
+
* issued access tokens keep working until natural expiry.
|
|
23
|
+
*
|
|
24
|
+
* **Best-effort**, same contract as {@link revokeRefreshToken}: sign-out
|
|
25
|
+
* must never fail the user, so errors are logged and swallowed.
|
|
26
|
+
*/
|
|
27
|
+
export declare function globalSignOut(accessToken: string, config?: AuthServerConfig): Promise<void>;
|
|
18
28
|
//# sourceMappingURL=revoke.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"revoke.d.ts","sourceRoot":"","sources":["../../src/server/revoke.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;
|
|
1
|
+
{"version":3,"file":"revoke.d.ts","sourceRoot":"","sources":["../../src/server/revoke.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAMH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAIpD;;;;;GAKG;AACH,wBAAsB,kBAAkB,CACtC,YAAY,EAAE,MAAM,EACpB,MAAM,GAAE,gBAAyC,GAChD,OAAO,CAAC,IAAI,CAAC,CAaf;AAED;;;;;;;;GAQG;AACH,wBAAsB,aAAa,CACjC,WAAW,EAAE,MAAM,EACnB,MAAM,GAAE,gBAAyC,GAChD,OAAO,CAAC,IAAI,CAAC,CAQf"}
|
package/dist/server/revoke.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* because the server-side revoke had a hiccup — the cookies clear
|
|
8
8
|
* regardless.
|
|
9
9
|
*/
|
|
10
|
-
import { RevokeTokenCommand } from '@aws-sdk/client-cognito-identity-provider';
|
|
10
|
+
import { GlobalSignOutCommand, RevokeTokenCommand, } from '@aws-sdk/client-cognito-identity-provider';
|
|
11
11
|
import { loadAuthServerConfig } from './config.js';
|
|
12
12
|
import { getCognitoClient } from './cognito-client.js';
|
|
13
13
|
/**
|
|
@@ -25,8 +25,25 @@ export async function revokeRefreshToken(refreshToken, config = loadAuthServerCo
|
|
|
25
25
|
}));
|
|
26
26
|
}
|
|
27
27
|
catch (err) {
|
|
28
|
-
// eslint-disable-next-line no-console
|
|
29
28
|
console.warn('[@venturekit/auth/server] refresh-token revoke failed (ignored):', err);
|
|
30
29
|
}
|
|
31
30
|
}
|
|
31
|
+
/**
|
|
32
|
+
* Invalidate EVERY outstanding refresh token for the caller via
|
|
33
|
+
* Cognito's `GlobalSignOut` (keyed by a valid access token, unlike
|
|
34
|
+
* {@link revokeRefreshToken} which revokes one refresh token). Already
|
|
35
|
+
* issued access tokens keep working until natural expiry.
|
|
36
|
+
*
|
|
37
|
+
* **Best-effort**, same contract as {@link revokeRefreshToken}: sign-out
|
|
38
|
+
* must never fail the user, so errors are logged and swallowed.
|
|
39
|
+
*/
|
|
40
|
+
export async function globalSignOut(accessToken, config = loadAuthServerConfig()) {
|
|
41
|
+
const client = getCognitoClient(config.region, config.endpoint);
|
|
42
|
+
try {
|
|
43
|
+
await client.send(new GlobalSignOutCommand({ AccessToken: accessToken }));
|
|
44
|
+
}
|
|
45
|
+
catch (err) {
|
|
46
|
+
console.warn('[@venturekit/auth/server] global sign-out failed (ignored):', err);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
32
49
|
//# sourceMappingURL=revoke.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"revoke.js","sourceRoot":"","sources":["../../src/server/revoke.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,
|
|
1
|
+
{"version":3,"file":"revoke.js","sourceRoot":"","sources":["../../src/server/revoke.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EACL,oBAAoB,EACpB,kBAAkB,GACnB,MAAM,2CAA2C,CAAC;AAEnD,OAAO,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AACnD,OAAO,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAEvD;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,kBAAkB,CACtC,YAAoB,EACpB,SAA2B,oBAAoB,EAAE;IAEjD,MAAM,MAAM,GAAG,gBAAgB,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,QAAQ,CAAC,CAAC;IAChE,IAAI,CAAC;QACH,MAAM,MAAM,CAAC,IAAI,CACf,IAAI,kBAAkB,CAAC;YACrB,QAAQ,EAAE,MAAM,CAAC,WAAW;YAC5B,KAAK,EAAE,YAAY;SACpB,CAAC,CACH,CAAC;IACJ,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QAEb,OAAO,CAAC,IAAI,CAAC,kEAAkE,EAAE,GAAG,CAAC,CAAC;IACxF,CAAC;AACH,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CACjC,WAAmB,EACnB,SAA2B,oBAAoB,EAAE;IAEjD,MAAM,MAAM,GAAG,gBAAgB,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,QAAQ,CAAC,CAAC;IAChE,IAAI,CAAC;QACH,MAAM,MAAM,CAAC,IAAI,CAAC,IAAI,oBAAoB,CAAC,EAAE,WAAW,EAAE,WAAW,EAAE,CAAC,CAAC,CAAC;IAC5E,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QAEb,OAAO,CAAC,IAAI,CAAC,6DAA6D,EAAE,GAAG,CAAC,CAAC;IACnF,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @venturekit/auth — Postgres VerificationCodeStore
|
|
3
|
+
*
|
|
4
|
+
* Production {@link VerificationCodeStore} backing the OTP primitives
|
|
5
|
+
* ({@link requestVerificationCode} / {@link verifyVerificationCode}).
|
|
6
|
+
* Talks to the project's primary Postgres database via
|
|
7
|
+
* `@venturekit/data`'s `query()` — or any `Querier`, so the caller can
|
|
8
|
+
* enlist code writes in an already-open transaction (pass a
|
|
9
|
+
* `Transaction`'s `query`; the default is the global connection pool).
|
|
10
|
+
*
|
|
11
|
+
* The backing table `vk_verification_codes` is shipped by this package's
|
|
12
|
+
* `migrations/vk_auth_001_verification_codes.sql` and applied by the
|
|
13
|
+
* in-VPC migration runner `@venturekit/infra` provisions — so consumers
|
|
14
|
+
* get the table and this store from the same package.
|
|
15
|
+
*
|
|
16
|
+
* `@venturekit/data` is an OPTIONAL peer dependency: importing THIS
|
|
17
|
+
* module is what opts a project into the Postgres store, so apps on a
|
|
18
|
+
* different backend (DynamoDB, the in-memory dev store) never load it
|
|
19
|
+
* and `@venturekit/auth/server` stays storage-agnostic.
|
|
20
|
+
*
|
|
21
|
+
* `expires_at` crosses the store boundary as ms-epoch to satisfy
|
|
22
|
+
* {@link VerificationCodeRecord}'s numeric contract: `to_timestamp` on
|
|
23
|
+
* write, `EXTRACT(EPOCH …) * 1000` on read.
|
|
24
|
+
*/
|
|
25
|
+
import { type Querier } from '@venturekit/data';
|
|
26
|
+
import type { VerificationCodeStore } from '../verification.js';
|
|
27
|
+
/**
|
|
28
|
+
* Build a Postgres-backed {@link VerificationCodeStore} bound to `q`.
|
|
29
|
+
*
|
|
30
|
+
* Pass a transaction's `query` (see `beginTransaction` /
|
|
31
|
+
* `withTransaction` in `@venturekit/data`) to make code writes
|
|
32
|
+
* participate in an open transaction; omit it to use the global pool.
|
|
33
|
+
*/
|
|
34
|
+
export declare function createPostgresVerificationCodeStore(q?: Querier): VerificationCodeStore;
|
|
35
|
+
//# sourceMappingURL=postgres.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"postgres.d.ts","sourceRoot":"","sources":["../../../src/server/store/postgres.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,EAAS,KAAK,OAAO,EAAE,MAAM,kBAAkB,CAAC;AACvD,OAAO,KAAK,EAEV,qBAAqB,EACtB,MAAM,oBAAoB,CAAC;AAwB5B;;;;;;GAMG;AACH,wBAAgB,mCAAmC,CACjD,CAAC,GAAE,OAAe,GACjB,qBAAqB,CAgEvB"}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @venturekit/auth — Postgres VerificationCodeStore
|
|
3
|
+
*
|
|
4
|
+
* Production {@link VerificationCodeStore} backing the OTP primitives
|
|
5
|
+
* ({@link requestVerificationCode} / {@link verifyVerificationCode}).
|
|
6
|
+
* Talks to the project's primary Postgres database via
|
|
7
|
+
* `@venturekit/data`'s `query()` — or any `Querier`, so the caller can
|
|
8
|
+
* enlist code writes in an already-open transaction (pass a
|
|
9
|
+
* `Transaction`'s `query`; the default is the global connection pool).
|
|
10
|
+
*
|
|
11
|
+
* The backing table `vk_verification_codes` is shipped by this package's
|
|
12
|
+
* `migrations/vk_auth_001_verification_codes.sql` and applied by the
|
|
13
|
+
* in-VPC migration runner `@venturekit/infra` provisions — so consumers
|
|
14
|
+
* get the table and this store from the same package.
|
|
15
|
+
*
|
|
16
|
+
* `@venturekit/data` is an OPTIONAL peer dependency: importing THIS
|
|
17
|
+
* module is what opts a project into the Postgres store, so apps on a
|
|
18
|
+
* different backend (DynamoDB, the in-memory dev store) never load it
|
|
19
|
+
* and `@venturekit/auth/server` stays storage-agnostic.
|
|
20
|
+
*
|
|
21
|
+
* `expires_at` crosses the store boundary as ms-epoch to satisfy
|
|
22
|
+
* {@link VerificationCodeRecord}'s numeric contract: `to_timestamp` on
|
|
23
|
+
* write, `EXTRACT(EPOCH …) * 1000` on read.
|
|
24
|
+
*/
|
|
25
|
+
import { query } from '@venturekit/data';
|
|
26
|
+
function rowToRecord(r) {
|
|
27
|
+
return {
|
|
28
|
+
channel: r.channel,
|
|
29
|
+
identifier: r.identifier,
|
|
30
|
+
codeHash: r.code_hash,
|
|
31
|
+
expiresAt: Number(r.expires_at),
|
|
32
|
+
attempts: r.attempts,
|
|
33
|
+
maxAttempts: r.max_attempts,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Build a Postgres-backed {@link VerificationCodeStore} bound to `q`.
|
|
38
|
+
*
|
|
39
|
+
* Pass a transaction's `query` (see `beginTransaction` /
|
|
40
|
+
* `withTransaction` in `@venturekit/data`) to make code writes
|
|
41
|
+
* participate in an open transaction; omit it to use the global pool.
|
|
42
|
+
*/
|
|
43
|
+
export function createPostgresVerificationCodeStore(q = query) {
|
|
44
|
+
return {
|
|
45
|
+
async put({ channel, identifier, codeHash, expiresAt, maxAttempts }) {
|
|
46
|
+
// Opportunistic sweep of already-expired rows on every write —
|
|
47
|
+
// keeps the table bounded without a dedicated cron (the migration
|
|
48
|
+
// ships the `expires_at` index that backs this).
|
|
49
|
+
await q(`DELETE FROM vk_verification_codes WHERE expires_at < CURRENT_TIMESTAMP`);
|
|
50
|
+
// Idempotent per the store contract: a re-request overwrites the
|
|
51
|
+
// outstanding code for this (channel, identifier) and resets the
|
|
52
|
+
// attempt counter — a fresh code means "let me try again", not
|
|
53
|
+
// "reset the limit on the old one".
|
|
54
|
+
await q(`INSERT INTO vk_verification_codes
|
|
55
|
+
(channel, identifier, code_hash, expires_at, attempts, max_attempts)
|
|
56
|
+
VALUES ($1, $2, $3, to_timestamp($4 / 1000.0), 0, $5)
|
|
57
|
+
ON CONFLICT (channel, identifier) DO UPDATE
|
|
58
|
+
SET code_hash = EXCLUDED.code_hash,
|
|
59
|
+
expires_at = EXCLUDED.expires_at,
|
|
60
|
+
attempts = 0,
|
|
61
|
+
max_attempts = EXCLUDED.max_attempts,
|
|
62
|
+
created_at = NOW()`, [channel, identifier, codeHash, expiresAt, maxAttempts]);
|
|
63
|
+
},
|
|
64
|
+
async get({ channel, identifier }) {
|
|
65
|
+
// Return the row as-is, INCLUDING when expired — verifyVerificationCode
|
|
66
|
+
// performs the expiry check and deletes the row itself.
|
|
67
|
+
return q(`SELECT channel, identifier, code_hash,
|
|
68
|
+
(EXTRACT(EPOCH FROM expires_at) * 1000)::bigint AS expires_at,
|
|
69
|
+
attempts, max_attempts
|
|
70
|
+
FROM vk_verification_codes
|
|
71
|
+
WHERE channel = $1 AND identifier = $2
|
|
72
|
+
LIMIT 1`, [channel, identifier], (rows) => {
|
|
73
|
+
const r = rows[0];
|
|
74
|
+
return r ? rowToRecord(r) : undefined;
|
|
75
|
+
});
|
|
76
|
+
},
|
|
77
|
+
async incrementAttempts({ channel, identifier }) {
|
|
78
|
+
return q(`UPDATE vk_verification_codes
|
|
79
|
+
SET attempts = attempts + 1
|
|
80
|
+
WHERE channel = $1 AND identifier = $2
|
|
81
|
+
RETURNING attempts`, [channel, identifier], (rows) => rows[0]?.attempts ?? 0);
|
|
82
|
+
},
|
|
83
|
+
async delete({ channel, identifier }) {
|
|
84
|
+
await q(`DELETE FROM vk_verification_codes WHERE channel = $1 AND identifier = $2`, [channel, identifier]);
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
//# sourceMappingURL=postgres.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"postgres.js","sourceRoot":"","sources":["../../../src/server/store/postgres.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,EAAE,KAAK,EAAgB,MAAM,kBAAkB,CAAC;AAiBvD,SAAS,WAAW,CAAC,CAAsB;IACzC,OAAO;QACL,OAAO,EAAE,CAAC,CAAC,OAAO;QAClB,UAAU,EAAE,CAAC,CAAC,UAAU;QACxB,QAAQ,EAAE,CAAC,CAAC,SAAS;QACrB,SAAS,EAAE,MAAM,CAAC,CAAC,CAAC,UAAU,CAAC;QAC/B,QAAQ,EAAE,CAAC,CAAC,QAAQ;QACpB,WAAW,EAAE,CAAC,CAAC,YAAY;KAC5B,CAAC;AACJ,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,mCAAmC,CACjD,IAAa,KAAK;IAElB,OAAO;QACL,KAAK,CAAC,GAAG,CAAC,EAAE,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAE,SAAS,EAAE,WAAW,EAAE;YACjE,+DAA+D;YAC/D,kEAAkE;YAClE,iDAAiD;YACjD,MAAM,CAAC,CACL,wEAAwE,CACzE,CAAC;YACF,iEAAiE;YACjE,iEAAiE;YACjE,+DAA+D;YAC/D,oCAAoC;YACpC,MAAM,CAAC,CACL;;;;;;;;qCAQ6B,EAC7B,CAAC,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAE,SAAS,EAAE,WAAW,CAAC,CACxD,CAAC;QACJ,CAAC;QAED,KAAK,CAAC,GAAG,CAAC,EAAE,OAAO,EAAE,UAAU,EAAE;YAC/B,wEAAwE;YACxE,wDAAwD;YACxD,OAAO,CAAC,CACN;;;;;kBAKU,EACV,CAAC,OAAO,EAAE,UAAU,CAAC,EACrB,CAAC,IAAI,EAAE,EAAE;gBACP,MAAM,CAAC,GAAG,IAAI,CAAC,CAAC,CAA+C,CAAC;gBAChE,OAAO,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;YACxC,CAAC,CACF,CAAC;QACJ,CAAC;QAED,KAAK,CAAC,iBAAiB,CAAC,EAAE,OAAO,EAAE,UAAU,EAAE;YAC7C,OAAO,CAAC,CACN;;;6BAGqB,EACrB,CAAC,OAAO,EAAE,UAAU,CAAC,EACrB,CAAC,IAAI,EAAE,EAAE,CACN,IAAI,CAAC,CAAC,CAAsC,EAAE,QAAQ,IAAI,CAAC,CAC/D,CAAC;QACJ,CAAC;QAED,KAAK,CAAC,MAAM,CAAC,EAAE,OAAO,EAAE,UAAU,EAAE;YAClC,MAAM,CAAC,CACL,0EAA0E,EAC1E,CAAC,OAAO,EAAE,UAAU,CAAC,CACtB,CAAC;QACJ,CAAC;KACF,CAAC;AACJ,CAAC"}
|
|
@@ -8,8 +8,9 @@
|
|
|
8
8
|
* treat those responses as fatal, we derive the remaining lifetime
|
|
9
9
|
* from the access token itself.
|
|
10
10
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
11
|
+
* `decodeJwtClaims` IS part of the public surface: consumers that
|
|
12
|
+
* just minted tokens (sign-in, passwordless flows) read profile
|
|
13
|
+
* claims from the id token without a redundant verify round-trip.
|
|
13
14
|
*/
|
|
14
15
|
/** Cognito's default access-token lifetime (1 hour). */
|
|
15
16
|
export declare const DEFAULT_ACCESS_TOKEN_TTL_SECONDS = 3600;
|
|
@@ -24,4 +25,13 @@ export declare const DEFAULT_ACCESS_TOKEN_TTL_SECONDS = 3600;
|
|
|
24
25
|
* `InitiateAuthCommand`). Use `verifyAndDecode` for untrusted input.
|
|
25
26
|
*/
|
|
26
27
|
export declare function deriveExpiresInFromJwt(jwt: string, nowSeconds?: number): number;
|
|
28
|
+
/**
|
|
29
|
+
* Base64url-decode and JSON.parse a JWT payload. Returns `null` on any
|
|
30
|
+
* failure — callers fall back to a default.
|
|
31
|
+
*
|
|
32
|
+
* Does NOT verify the signature — only use on tokens received
|
|
33
|
+
* directly from the identity provider (e.g. right after sign-in).
|
|
34
|
+
* Use `verifyAndDecode` for anything that crossed a trust boundary.
|
|
35
|
+
*/
|
|
36
|
+
export declare function decodeJwtClaims<T extends Record<string, unknown> = Record<string, unknown>>(jwt: string): T | null;
|
|
27
37
|
//# sourceMappingURL=token-utils.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"token-utils.d.ts","sourceRoot":"","sources":["../../src/server/token-utils.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"token-utils.d.ts","sourceRoot":"","sources":["../../src/server/token-utils.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,wDAAwD;AACxD,eAAO,MAAM,gCAAgC,OAAO,CAAC;AAErD;;;;;;;;;GASG;AACH,wBAAgB,sBAAsB,CACpC,GAAG,EAAE,MAAM,EACX,UAAU,GAAE,MAAsC,GACjD,MAAM,CAgBR;AAED;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACzF,GAAG,EAAE,MAAM,GACV,CAAC,GAAG,IAAI,CAYV"}
|
|
@@ -8,8 +8,9 @@
|
|
|
8
8
|
* treat those responses as fatal, we derive the remaining lifetime
|
|
9
9
|
* from the access token itself.
|
|
10
10
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
11
|
+
* `decodeJwtClaims` IS part of the public surface: consumers that
|
|
12
|
+
* just minted tokens (sign-in, passwordless flows) read profile
|
|
13
|
+
* claims from the id token without a redundant verify round-trip.
|
|
13
14
|
*/
|
|
14
15
|
/** Cognito's default access-token lifetime (1 hour). */
|
|
15
16
|
export const DEFAULT_ACCESS_TOKEN_TTL_SECONDS = 3600;
|
|
@@ -24,7 +25,7 @@ export const DEFAULT_ACCESS_TOKEN_TTL_SECONDS = 3600;
|
|
|
24
25
|
* `InitiateAuthCommand`). Use `verifyAndDecode` for untrusted input.
|
|
25
26
|
*/
|
|
26
27
|
export function deriveExpiresInFromJwt(jwt, nowSeconds = Math.floor(Date.now() / 1000)) {
|
|
27
|
-
const payload =
|
|
28
|
+
const payload = decodeJwtClaims(jwt);
|
|
28
29
|
if (!payload)
|
|
29
30
|
return DEFAULT_ACCESS_TOKEN_TTL_SECONDS;
|
|
30
31
|
const exp = typeof payload.exp === 'number' ? payload.exp : undefined;
|
|
@@ -42,8 +43,12 @@ export function deriveExpiresInFromJwt(jwt, nowSeconds = Math.floor(Date.now() /
|
|
|
42
43
|
/**
|
|
43
44
|
* Base64url-decode and JSON.parse a JWT payload. Returns `null` on any
|
|
44
45
|
* failure — callers fall back to a default.
|
|
46
|
+
*
|
|
47
|
+
* Does NOT verify the signature — only use on tokens received
|
|
48
|
+
* directly from the identity provider (e.g. right after sign-in).
|
|
49
|
+
* Use `verifyAndDecode` for anything that crossed a trust boundary.
|
|
45
50
|
*/
|
|
46
|
-
function
|
|
51
|
+
export function decodeJwtClaims(jwt) {
|
|
47
52
|
if (typeof jwt !== 'string' || jwt.length === 0)
|
|
48
53
|
return null;
|
|
49
54
|
const parts = jwt.split('.');
|