@venturekit/auth 0.0.0-dev.20260507015944 → 0.0.0-dev.20260511023410

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.
@@ -0,0 +1,153 @@
1
+ /**
2
+ * Federated sign-in — server-side OAuth Authorization Code flow.
3
+ *
4
+ * VentureKit deliberately does NOT enable the Cognito Hosted UI: your
5
+ * application owns the login screen. The flow is:
6
+ *
7
+ * 1. **SPA** — user clicks "Sign in with Google".
8
+ * 2. **SPA → API** — `POST /auth/federated/google/start { redirectUri }`.
9
+ * The `redirectUri` is a page on the SPA itself (e.g.
10
+ * `https://app.example.com/auth/google/callback`) and must be
11
+ * registered with the IdP as an allowed redirect.
12
+ * 3. **API → SPA** — `{ authorizeUrl }` plus an HttpOnly cookie
13
+ * holding a CSRF `state`. The API computes the URL with
14
+ * {@link buildAuthorizeUrl}.
15
+ * 4. **SPA** navigates to `authorizeUrl`.
16
+ * 5. **IdP → SPA** — after authentication, the IdP 302s to
17
+ * `redirectUri?code=…&state=…`. The SPA reads the query, then
18
+ * calls the API again.
19
+ * 6. **SPA → API** — `POST /auth/federated/google/complete
20
+ * { code, state, redirectUri }`. The API:
21
+ * - re-checks `state` against the cookie ({@link verifyOAuthState});
22
+ * - exchanges the code for tokens at the IdP via
23
+ * {@link exchangeAuthorizationCode} (the
24
+ * `client_secret` lives in Secrets Manager, never in the SPA);
25
+ * - resolves the user's verified profile;
26
+ * - calls {@link signInAsFederatedUser} which mints a Cognito
27
+ * session for the same email (creating the user on first
28
+ * contact).
29
+ * 7. **API → SPA** — Set-Cookie session + JSON envelope.
30
+ *
31
+ * The SPA never sees the `client_secret`, the IdP `access_token`, or
32
+ * the OAuth `code` after step 6. Cookies stay HttpOnly + SameSite=Lax.
33
+ *
34
+ * The OAuth `client_id` / `client_secret` per provider live in the
35
+ * Secrets Manager placeholder VentureKit provisions when
36
+ * `AuthIntent.federated` is set; the ARN is exposed as
37
+ * `COGNITO_FEDERATED_<PROVIDER>_SECRET_ARN` on every Lambda.
38
+ */
39
+ import type { AuthServerConfig } from './config.js';
40
+ import { type SignInResult } from './tokens.js';
41
+ export type FederatedProvider = 'google' | 'facebook' | 'apple';
42
+ /**
43
+ * Verified federated profile — the shape every IdP normalizes to
44
+ * after `exchangeAuthorizationCode` runs.
45
+ */
46
+ export interface FederatedProfile {
47
+ /** Stable identifier from the IdP. Google `sub`, Facebook `id`. */
48
+ externalId: string;
49
+ /** Verified email. Required — Cognito uses email as the username. */
50
+ email: string;
51
+ /** Optional display name, passed through to Cognito's `name` attr. */
52
+ name?: string;
53
+ }
54
+ export interface FederatedProviderCredentials {
55
+ clientId: string;
56
+ clientSecret: string;
57
+ }
58
+ /**
59
+ * Read the OAuth client credentials for `provider` from the Secrets
60
+ * Manager placeholder VentureKit provisions. The ARN is read from
61
+ * `COGNITO_FEDERATED_<PROVIDER>_SECRET_ARN` on every Lambda; the
62
+ * secret value is JSON `{"clientId":"…","clientSecret":"…"}`.
63
+ *
64
+ * Cached for the lifetime of the Lambda container — IdP credentials
65
+ * rotate via Secrets Manager rotation + a redeploy, not per-request.
66
+ */
67
+ export declare function loadFederatedProviderCredentials(provider: FederatedProvider, env?: NodeJS.ProcessEnv | Record<string, string | undefined>): Promise<FederatedProviderCredentials>;
68
+ /** Test-only — drop the cached credentials between vitest runs. */
69
+ export declare function _resetFederatedCredentialsForTesting(): void;
70
+ /**
71
+ * Generate a 32-byte URL-safe random string used as the OAuth `state`
72
+ * parameter. The same value is returned to the SPA (so it can be
73
+ * appended to `authorizeUrl`) and pinned to the user's browser via an
74
+ * HttpOnly cookie. {@link verifyOAuthState} performs a constant-time
75
+ * comparison on the callback.
76
+ */
77
+ export declare function generateOAuthState(): string;
78
+ /**
79
+ * Constant-time equality check between the `state` echoed back by the
80
+ * IdP and the value that was set on the state cookie at `start` time.
81
+ * Returns `false` for any mismatch including length differences.
82
+ */
83
+ export declare function verifyOAuthState(fromQuery: string | undefined, fromCookie: string | undefined): boolean;
84
+ export interface BuildAuthorizeUrlInput {
85
+ provider: FederatedProvider;
86
+ /**
87
+ * Where the IdP should redirect after auth. Must be a page on your
88
+ * SPA and must be registered as an allowed redirect URI in the IdP
89
+ * console — Google / Facebook reject mismatches.
90
+ */
91
+ redirectUri: string;
92
+ /** Output of {@link generateOAuthState}. */
93
+ state: string;
94
+ /** Override default scopes if you need extra IdP permissions. */
95
+ scopes?: string[];
96
+ /**
97
+ * Extra query parameters appended verbatim. Useful for
98
+ * `prompt=select_account` (Google) or `auth_type=rerequest`
99
+ * (Facebook re-consent).
100
+ */
101
+ extraParams?: Record<string, string>;
102
+ }
103
+ /**
104
+ * Build the IdP authorize URL the SPA navigates to after `start`.
105
+ *
106
+ * Loads the OAuth `client_id` from Secrets Manager (cached). The
107
+ * `client_secret` is NOT used here — it's only needed at token
108
+ * exchange.
109
+ */
110
+ export declare function buildAuthorizeUrl(input: BuildAuthorizeUrlInput, env?: NodeJS.ProcessEnv | Record<string, string | undefined>): Promise<string>;
111
+ export interface ExchangeAuthorizationCodeInput {
112
+ provider: FederatedProvider;
113
+ /** The `code` query param the IdP appended to `redirectUri`. */
114
+ code: string;
115
+ /**
116
+ * Must match the `redirectUri` used in `buildAuthorizeUrl`. Both
117
+ * Google and Facebook compare these byte-for-byte and 400 on a
118
+ * mismatch.
119
+ */
120
+ redirectUri: string;
121
+ }
122
+ /**
123
+ * Exchange an OAuth `code` for tokens at the IdP, then resolve the
124
+ * verified `FederatedProfile`. Throws {@link AuthError}
125
+ * (`federated_token_invalid`, HTTP 401) on any IdP error.
126
+ */
127
+ export declare function exchangeAuthorizationCode(input: ExchangeAuthorizationCodeInput, env?: NodeJS.ProcessEnv | Record<string, string | undefined>): Promise<FederatedProfile>;
128
+ export interface SignInAsFederatedUserInput {
129
+ /** Verified profile from {@link exchangeAuthorizationCode}. */
130
+ profile: FederatedProfile;
131
+ /** Provider name — recorded as `custom:federated_provider`. */
132
+ provider: FederatedProvider;
133
+ /**
134
+ * Standard Cognito attributes to set on first creation only — e.g.
135
+ * `{ phone_number: '+212…' }`. Ignored for existing users.
136
+ */
137
+ initialAttributes?: Record<string, string>;
138
+ /**
139
+ * Custom attributes to set on first creation only — keyed without
140
+ * the `custom:` prefix (the helper prepends it).
141
+ */
142
+ initialCustomAttributes?: Record<string, string>;
143
+ }
144
+ /**
145
+ * Sign a verified federated user in. Creates the Cognito user on
146
+ * first contact (idempotent), rotates a server-side password, and
147
+ * mints session tokens via `ADMIN_USER_PASSWORD_AUTH`.
148
+ *
149
+ * Returns the same {@link SignInResult} shape `signInWithPassword`
150
+ * returns, so cookie / session helpers stay identical.
151
+ */
152
+ export declare function signInAsFederatedUser(input: SignInAsFederatedUserInput, config?: AuthServerConfig): Promise<SignInResult>;
153
+ //# sourceMappingURL=federated.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"federated.d.ts","sourceRoot":"","sources":["../../src/server/federated.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AAYH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAIpD,OAAO,EAAuB,KAAK,YAAY,EAAE,MAAM,aAAa,CAAC;AAErE,MAAM,MAAM,iBAAiB,GAAG,QAAQ,GAAG,UAAU,GAAG,OAAO,CAAC;AAEhE;;;GAGG;AACH,MAAM,WAAW,gBAAgB;IAC/B,mEAAmE;IACnE,UAAU,EAAE,MAAM,CAAC;IACnB,qEAAqE;IACrE,KAAK,EAAE,MAAM,CAAC;IACd,sEAAsE;IACtE,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,4BAA4B;IAC3C,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;CACtB;AAQD;;;;;;;;GAQG;AACH,wBAAsB,gCAAgC,CACpD,QAAQ,EAAE,iBAAiB,EAC3B,GAAG,GAAE,MAAM,CAAC,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAe,GACxE,OAAO,CAAC,4BAA4B,CAAC,CAgEvC;AAED,mEAAmE;AACnE,wBAAgB,oCAAoC,IAAI,IAAI,CAE3D;AAMD;;;;;;GAMG;AACH,wBAAgB,kBAAkB,IAAI,MAAM,CAE3C;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAC9B,SAAS,EAAE,MAAM,GAAG,SAAS,EAC7B,UAAU,EAAE,MAAM,GAAG,SAAS,GAC7B,OAAO,CAMT;AAyCD,MAAM,WAAW,sBAAsB;IACrC,QAAQ,EAAE,iBAAiB,CAAC;IAC5B;;;;OAIG;IACH,WAAW,EAAE,MAAM,CAAC;IACpB,4CAA4C;IAC5C,KAAK,EAAE,MAAM,CAAC;IACd,iEAAiE;IACjE,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACtC;AAED;;;;;;GAMG;AACH,wBAAsB,iBAAiB,CACrC,KAAK,EAAE,sBAAsB,EAC7B,GAAG,GAAE,MAAM,CAAC,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAe,GACxE,OAAO,CAAC,MAAM,CAAC,CAejB;AAMD,MAAM,WAAW,8BAA8B;IAC7C,QAAQ,EAAE,iBAAiB,CAAC;IAC5B,gEAAgE;IAChE,IAAI,EAAE,MAAM,CAAC;IACb;;;;OAIG;IACH,WAAW,EAAE,MAAM,CAAC;CACrB;AAED;;;;GAIG;AACH,wBAAsB,yBAAyB,CAC7C,KAAK,EAAE,8BAA8B,EACrC,GAAG,GAAE,MAAM,CAAC,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAe,GACxE,OAAO,CAAC,gBAAgB,CAAC,CA6B3B;AA2JD,MAAM,WAAW,0BAA0B;IACzC,+DAA+D;IAC/D,OAAO,EAAE,gBAAgB,CAAC;IAC1B,+DAA+D;IAC/D,QAAQ,EAAE,iBAAiB,CAAC;IAC5B;;;OAGG;IACH,iBAAiB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC3C;;;OAGG;IACH,uBAAuB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAClD;AAED;;;;;;;GAOG;AACH,wBAAsB,qBAAqB,CACzC,KAAK,EAAE,0BAA0B,EACjC,MAAM,GAAE,gBAAyC,GAChD,OAAO,CAAC,YAAY,CAAC,CA6HvB"}
@@ -0,0 +1,436 @@
1
+ /**
2
+ * Federated sign-in — server-side OAuth Authorization Code flow.
3
+ *
4
+ * VentureKit deliberately does NOT enable the Cognito Hosted UI: your
5
+ * application owns the login screen. The flow is:
6
+ *
7
+ * 1. **SPA** — user clicks "Sign in with Google".
8
+ * 2. **SPA → API** — `POST /auth/federated/google/start { redirectUri }`.
9
+ * The `redirectUri` is a page on the SPA itself (e.g.
10
+ * `https://app.example.com/auth/google/callback`) and must be
11
+ * registered with the IdP as an allowed redirect.
12
+ * 3. **API → SPA** — `{ authorizeUrl }` plus an HttpOnly cookie
13
+ * holding a CSRF `state`. The API computes the URL with
14
+ * {@link buildAuthorizeUrl}.
15
+ * 4. **SPA** navigates to `authorizeUrl`.
16
+ * 5. **IdP → SPA** — after authentication, the IdP 302s to
17
+ * `redirectUri?code=…&state=…`. The SPA reads the query, then
18
+ * calls the API again.
19
+ * 6. **SPA → API** — `POST /auth/federated/google/complete
20
+ * { code, state, redirectUri }`. The API:
21
+ * - re-checks `state` against the cookie ({@link verifyOAuthState});
22
+ * - exchanges the code for tokens at the IdP via
23
+ * {@link exchangeAuthorizationCode} (the
24
+ * `client_secret` lives in Secrets Manager, never in the SPA);
25
+ * - resolves the user's verified profile;
26
+ * - calls {@link signInAsFederatedUser} which mints a Cognito
27
+ * session for the same email (creating the user on first
28
+ * contact).
29
+ * 7. **API → SPA** — Set-Cookie session + JSON envelope.
30
+ *
31
+ * The SPA never sees the `client_secret`, the IdP `access_token`, or
32
+ * the OAuth `code` after step 6. Cookies stay HttpOnly + SameSite=Lax.
33
+ *
34
+ * The OAuth `client_id` / `client_secret` per provider live in the
35
+ * Secrets Manager placeholder VentureKit provisions when
36
+ * `AuthIntent.federated` is set; the ARN is exposed as
37
+ * `COGNITO_FEDERATED_<PROVIDER>_SECRET_ARN` on every Lambda.
38
+ */
39
+ import { AdminCreateUserCommand, AdminGetUserCommand, AdminInitiateAuthCommand, AdminSetUserPasswordCommand, AdminUpdateUserAttributesCommand, } from '@aws-sdk/client-cognito-identity-provider';
40
+ import { createHmac, randomBytes, timingSafeEqual } from 'node:crypto';
41
+ import { loadAuthServerConfig } from './config.js';
42
+ import { getCognitoClient } from './cognito-client.js';
43
+ import { AuthError, mapProviderError } from './errors.js';
44
+ import { extractSignInTokens } from './tokens.js';
45
+ // ────────────────────────────────────────────────────────────────────
46
+ // Provider-credential resolution (Secrets Manager → cached)
47
+ // ────────────────────────────────────────────────────────────────────
48
+ const credentialsCache = new Map();
49
+ /**
50
+ * Read the OAuth client credentials for `provider` from the Secrets
51
+ * Manager placeholder VentureKit provisions. The ARN is read from
52
+ * `COGNITO_FEDERATED_<PROVIDER>_SECRET_ARN` on every Lambda; the
53
+ * secret value is JSON `{"clientId":"…","clientSecret":"…"}`.
54
+ *
55
+ * Cached for the lifetime of the Lambda container — IdP credentials
56
+ * rotate via Secrets Manager rotation + a redeploy, not per-request.
57
+ */
58
+ export async function loadFederatedProviderCredentials(provider, env = process.env) {
59
+ const cached = credentialsCache.get(provider);
60
+ if (cached)
61
+ return cached;
62
+ const envVar = `COGNITO_FEDERATED_${provider.toUpperCase()}_SECRET_ARN`;
63
+ const arn = env[envVar];
64
+ if (!arn) {
65
+ throw new AuthError('federated_provider_not_configured', `[${provider}] missing env var ${envVar} — declare ` +
66
+ `\`federated: ['${provider}']\` on the auth intent in vk.config.ts ` +
67
+ `so VentureKit provisions the Secrets Manager placeholder, then ` +
68
+ `populate it with the OAuth client credentials from the IdP console.`, 500);
69
+ }
70
+ const { SecretsManagerClient, GetSecretValueCommand } = await import('@aws-sdk/client-secrets-manager');
71
+ const client = new SecretsManagerClient({
72
+ region: env['AWS_REGION'] ?? env['COGNITO_REGION'],
73
+ });
74
+ const res = await client.send(new GetSecretValueCommand({ SecretId: arn }));
75
+ if (!res.SecretString) {
76
+ throw new AuthError('federated_provider_not_configured', `[${provider}] Secrets Manager entry ${arn} is empty`, 500);
77
+ }
78
+ let parsed;
79
+ try {
80
+ parsed = JSON.parse(res.SecretString);
81
+ }
82
+ catch {
83
+ throw new AuthError('federated_provider_not_configured', `[${provider}] Secrets Manager entry ${arn} is not valid JSON`, 500);
84
+ }
85
+ if (!parsed.clientId ||
86
+ !parsed.clientSecret ||
87
+ parsed.clientId === 'PLACEHOLDER' ||
88
+ parsed.clientSecret === 'PLACEHOLDER') {
89
+ throw new AuthError('federated_provider_not_configured', `[${provider}] Secrets Manager entry ${arn} still holds the ` +
90
+ `placeholder. Populate it with the OAuth client id + secret ` +
91
+ `from the IdP console: aws secretsmanager put-secret-value ` +
92
+ `--secret-id ${arn} --secret-string '{"clientId":"…","clientSecret":"…"}'`, 500);
93
+ }
94
+ const creds = {
95
+ clientId: parsed.clientId,
96
+ clientSecret: parsed.clientSecret,
97
+ };
98
+ credentialsCache.set(provider, creds);
99
+ return creds;
100
+ }
101
+ /** Test-only — drop the cached credentials between vitest runs. */
102
+ export function _resetFederatedCredentialsForTesting() {
103
+ credentialsCache.clear();
104
+ }
105
+ // ────────────────────────────────────────────────────────────────────
106
+ // CSRF state — random token, signed cookie
107
+ // ────────────────────────────────────────────────────────────────────
108
+ /**
109
+ * Generate a 32-byte URL-safe random string used as the OAuth `state`
110
+ * parameter. The same value is returned to the SPA (so it can be
111
+ * appended to `authorizeUrl`) and pinned to the user's browser via an
112
+ * HttpOnly cookie. {@link verifyOAuthState} performs a constant-time
113
+ * comparison on the callback.
114
+ */
115
+ export function generateOAuthState() {
116
+ return randomBytes(32).toString('base64url');
117
+ }
118
+ /**
119
+ * Constant-time equality check between the `state` echoed back by the
120
+ * IdP and the value that was set on the state cookie at `start` time.
121
+ * Returns `false` for any mismatch including length differences.
122
+ */
123
+ export function verifyOAuthState(fromQuery, fromCookie) {
124
+ if (!fromQuery || !fromCookie)
125
+ return false;
126
+ const a = Buffer.from(fromQuery);
127
+ const b = Buffer.from(fromCookie);
128
+ if (a.length !== b.length)
129
+ return false;
130
+ return timingSafeEqual(a, b);
131
+ }
132
+ const PROVIDER_ENDPOINTS = {
133
+ google: {
134
+ authorize: 'https://accounts.google.com/o/oauth2/v2/auth',
135
+ token: 'https://oauth2.googleapis.com/token',
136
+ // `openid email profile` returns an `id_token` (JWT) we can decode
137
+ // for the verified profile without a second `/userinfo` call.
138
+ defaultScopes: ['openid', 'email', 'profile'],
139
+ },
140
+ facebook: {
141
+ authorize: 'https://www.facebook.com/v18.0/dialog/oauth',
142
+ token: 'https://graph.facebook.com/v18.0/oauth/access_token',
143
+ // `email` is required to read the address; `public_profile` is
144
+ // implicit but listing it makes the consent screen explicit.
145
+ defaultScopes: ['email', 'public_profile'],
146
+ },
147
+ apple: {
148
+ authorize: 'https://appleid.apple.com/auth/authorize',
149
+ token: 'https://appleid.apple.com/auth/token',
150
+ // Apple wants `name email`; `response_mode=form_post` is required
151
+ // when scopes include `name`/`email` — callers should set it.
152
+ defaultScopes: ['name', 'email'],
153
+ },
154
+ };
155
+ /**
156
+ * Build the IdP authorize URL the SPA navigates to after `start`.
157
+ *
158
+ * Loads the OAuth `client_id` from Secrets Manager (cached). The
159
+ * `client_secret` is NOT used here — it's only needed at token
160
+ * exchange.
161
+ */
162
+ export async function buildAuthorizeUrl(input, env = process.env) {
163
+ const { provider, redirectUri, state, scopes, extraParams } = input;
164
+ const { clientId } = await loadFederatedProviderCredentials(provider, env);
165
+ const endpoints = PROVIDER_ENDPOINTS[provider];
166
+ const url = new URL(endpoints.authorize);
167
+ url.searchParams.set('response_type', 'code');
168
+ url.searchParams.set('client_id', clientId);
169
+ url.searchParams.set('redirect_uri', redirectUri);
170
+ url.searchParams.set('scope', (scopes ?? endpoints.defaultScopes).join(' '));
171
+ url.searchParams.set('state', state);
172
+ for (const [k, v] of Object.entries(extraParams ?? {})) {
173
+ url.searchParams.set(k, v);
174
+ }
175
+ return url.toString();
176
+ }
177
+ /**
178
+ * Exchange an OAuth `code` for tokens at the IdP, then resolve the
179
+ * verified `FederatedProfile`. Throws {@link AuthError}
180
+ * (`federated_token_invalid`, HTTP 401) on any IdP error.
181
+ */
182
+ export async function exchangeAuthorizationCode(input, env = process.env) {
183
+ const { provider, code, redirectUri } = input;
184
+ const { clientId, clientSecret } = await loadFederatedProviderCredentials(provider, env);
185
+ switch (provider) {
186
+ case 'google':
187
+ return exchangeGoogle(code, redirectUri, clientId, clientSecret);
188
+ case 'facebook':
189
+ return exchangeFacebook(code, redirectUri, clientId, clientSecret);
190
+ case 'apple':
191
+ throw new AuthError('federated_provider_not_configured', 'Sign in with Apple requires a JWT-signed client_secret rotated ' +
192
+ 'every 6 months — implement the Apple-specific exchange in your ' +
193
+ 'application until VentureKit ships a built-in helper.', 500);
194
+ default: {
195
+ const _exhaustive = provider;
196
+ void _exhaustive;
197
+ throw new AuthError('federated_provider_not_configured', `Unknown federated provider: ${String(provider)}`, 500);
198
+ }
199
+ }
200
+ }
201
+ async function exchangeGoogle(code, redirectUri, clientId, clientSecret) {
202
+ // Google's token endpoint accepts application/x-www-form-urlencoded
203
+ // and returns `{ access_token, id_token, expires_in, ... }`. We only
204
+ // need the id_token: it's a signed JWT carrying `sub`, `email`,
205
+ // `email_verified`, `name`. Signature verification is unnecessary
206
+ // because we just received it from Google's TLS endpoint over a
207
+ // back-channel call we initiated — there's no way for an attacker
208
+ // to substitute it. (Per Google's docs:
209
+ // https://developers.google.com/identity/openid-connect/openid-connect#validatinganidtoken)
210
+ const body = new URLSearchParams({
211
+ code,
212
+ client_id: clientId,
213
+ client_secret: clientSecret,
214
+ redirect_uri: redirectUri,
215
+ grant_type: 'authorization_code',
216
+ });
217
+ const tokenRes = await fetch(PROVIDER_ENDPOINTS.google.token, {
218
+ method: 'POST',
219
+ headers: { 'content-type': 'application/x-www-form-urlencoded' },
220
+ body,
221
+ });
222
+ if (!tokenRes.ok) {
223
+ throw new AuthError('federated_token_invalid', `Google token exchange failed (HTTP ${tokenRes.status}): ` +
224
+ (await tokenRes.text().catch(() => '')), 401);
225
+ }
226
+ const tokenJson = (await tokenRes.json());
227
+ if (!tokenJson.id_token) {
228
+ throw new AuthError('federated_token_invalid', 'Google token exchange returned no id_token', 401);
229
+ }
230
+ const claims = decodeJwtPayload(tokenJson.id_token);
231
+ const sub = typeof claims['sub'] === 'string' ? claims['sub'] : null;
232
+ const email = typeof claims['email'] === 'string'
233
+ ? claims['email'].toLowerCase()
234
+ : null;
235
+ const emailVerified = claims['email_verified'] === true;
236
+ if (!sub || !email || !emailVerified) {
237
+ throw new AuthError('federated_token_invalid', 'Google id_token missing required verified email claim', 401);
238
+ }
239
+ const name = typeof claims['name'] === 'string' ? claims['name'] : undefined;
240
+ return { externalId: sub, email, name };
241
+ }
242
+ async function exchangeFacebook(code, redirectUri, clientId, clientSecret) {
243
+ // Facebook's token endpoint accepts the same params as a GET. The
244
+ // response is `{ access_token, token_type, expires_in }`. There's
245
+ // no id_token, so we follow up with a Graph `/me` call.
246
+ const tokenUrl = new URL(PROVIDER_ENDPOINTS.facebook.token);
247
+ tokenUrl.searchParams.set('code', code);
248
+ tokenUrl.searchParams.set('client_id', clientId);
249
+ tokenUrl.searchParams.set('client_secret', clientSecret);
250
+ tokenUrl.searchParams.set('redirect_uri', redirectUri);
251
+ const tokenRes = await fetch(tokenUrl);
252
+ if (!tokenRes.ok) {
253
+ throw new AuthError('federated_token_invalid', `Facebook token exchange failed (HTTP ${tokenRes.status}): ` +
254
+ (await tokenRes.text().catch(() => '')), 401);
255
+ }
256
+ const tokenJson = (await tokenRes.json());
257
+ if (!tokenJson.access_token) {
258
+ throw new AuthError('federated_token_invalid', 'Facebook token exchange returned no access_token', 401);
259
+ }
260
+ // appsecret_proof = HMAC_SHA256(access_token, app_secret) — Meta
261
+ // requires it for sensitive scopes and recommends it for everything.
262
+ const proof = createHmac('sha256', clientSecret)
263
+ .update(tokenJson.access_token)
264
+ .digest('hex');
265
+ const meUrl = new URL('https://graph.facebook.com/v18.0/me');
266
+ meUrl.searchParams.set('fields', 'id,email,name');
267
+ meUrl.searchParams.set('access_token', tokenJson.access_token);
268
+ meUrl.searchParams.set('appsecret_proof', proof);
269
+ const meRes = await fetch(meUrl);
270
+ if (!meRes.ok) {
271
+ throw new AuthError('federated_token_invalid', `Facebook /me failed (HTTP ${meRes.status})`, 401);
272
+ }
273
+ const me = (await meRes.json());
274
+ if (!me.id || !me.email) {
275
+ throw new AuthError('federated_token_invalid', 'Facebook profile missing id or email — request the `email` scope.', 401);
276
+ }
277
+ return {
278
+ externalId: me.id,
279
+ email: me.email.toLowerCase(),
280
+ name: me.name,
281
+ };
282
+ }
283
+ /**
284
+ * Decode a JWT payload **without** verifying the signature. Safe here
285
+ * because the caller just fetched the token over TLS from the IdP's
286
+ * own token endpoint; the decoder is internal-only.
287
+ */
288
+ function decodeJwtPayload(jwt) {
289
+ const parts = jwt.split('.');
290
+ if (parts.length < 2) {
291
+ throw new AuthError('federated_token_invalid', 'Malformed JWT', 401);
292
+ }
293
+ try {
294
+ const payload = Buffer.from(parts[1], 'base64url').toString('utf-8');
295
+ return JSON.parse(payload);
296
+ }
297
+ catch {
298
+ throw new AuthError('federated_token_invalid', 'Malformed JWT', 401);
299
+ }
300
+ }
301
+ /**
302
+ * Sign a verified federated user in. Creates the Cognito user on
303
+ * first contact (idempotent), rotates a server-side password, and
304
+ * mints session tokens via `ADMIN_USER_PASSWORD_AUTH`.
305
+ *
306
+ * Returns the same {@link SignInResult} shape `signInWithPassword`
307
+ * returns, so cookie / session helpers stay identical.
308
+ */
309
+ export async function signInAsFederatedUser(input, config = loadAuthServerConfig()) {
310
+ const client = getCognitoClient(config.region, config.endpoint);
311
+ const email = input.profile.email.toLowerCase();
312
+ // Generate a strong server-side password every sign-in so even if
313
+ // the value were ever exfiltrated it'd already be replaced. The
314
+ // user never sees this password — federated sign-in never exposes
315
+ // it; password sign-in for the same account is still possible
316
+ // after a `forgot-password` flow.
317
+ const password = generateStrongPassword();
318
+ let userExists;
319
+ try {
320
+ await client.send(new AdminGetUserCommand({
321
+ UserPoolId: config.userPoolId,
322
+ Username: email,
323
+ }));
324
+ userExists = true;
325
+ }
326
+ catch (err) {
327
+ if (err.name === 'UserNotFoundException') {
328
+ userExists = false;
329
+ }
330
+ else {
331
+ throw mapProviderError(err, 'federated_signin_failed');
332
+ }
333
+ }
334
+ if (!userExists) {
335
+ const attrs = [
336
+ { Name: 'email', Value: email },
337
+ { Name: 'email_verified', Value: 'true' },
338
+ ];
339
+ if (input.profile.name) {
340
+ attrs.push({ Name: 'name', Value: input.profile.name });
341
+ }
342
+ for (const [k, v] of Object.entries(input.initialAttributes ?? {})) {
343
+ if (k === 'email' || k === 'email_verified' || k === 'name')
344
+ continue;
345
+ attrs.push({ Name: k, Value: v });
346
+ }
347
+ attrs.push({
348
+ Name: 'custom:federated_provider',
349
+ Value: input.provider,
350
+ });
351
+ for (const [k, v] of Object.entries(input.initialCustomAttributes ?? {})) {
352
+ attrs.push({ Name: `custom:${k}`, Value: v });
353
+ }
354
+ try {
355
+ await client.send(new AdminCreateUserCommand({
356
+ UserPoolId: config.userPoolId,
357
+ Username: email,
358
+ UserAttributes: attrs,
359
+ // Suppress the welcome email — the user just authenticated
360
+ // through the IdP, they don't need a Cognito-generated one.
361
+ MessageAction: 'SUPPRESS',
362
+ TemporaryPassword: password,
363
+ }));
364
+ }
365
+ catch (err) {
366
+ // `UsernameExistsException` here is a race between two parallel
367
+ // federated sign-ins for the same email — fall through to the
368
+ // password reset + auth path; both will succeed.
369
+ if (err.name !== 'UsernameExistsException') {
370
+ throw mapProviderError(err, 'federated_signin_failed');
371
+ }
372
+ }
373
+ }
374
+ else {
375
+ // Existing user — make sure email is marked verified (older
376
+ // password-only signups may have `email_verified=false` if the
377
+ // confirmation email was never clicked) so `AdminInitiateAuth`
378
+ // doesn't raise a verification challenge.
379
+ try {
380
+ await client.send(new AdminUpdateUserAttributesCommand({
381
+ UserPoolId: config.userPoolId,
382
+ Username: email,
383
+ UserAttributes: [
384
+ { Name: 'email', Value: email },
385
+ { Name: 'email_verified', Value: 'true' },
386
+ ],
387
+ }));
388
+ }
389
+ catch (err) {
390
+ throw mapProviderError(err, 'federated_signin_failed');
391
+ }
392
+ }
393
+ // Set a fresh permanent password whether the user is new (the
394
+ // temporary one above lands them in `FORCE_CHANGE_PASSWORD`) or
395
+ // returning. `Permanent: true` skips the new-password challenge.
396
+ try {
397
+ await client.send(new AdminSetUserPasswordCommand({
398
+ UserPoolId: config.userPoolId,
399
+ Username: email,
400
+ Password: password,
401
+ Permanent: true,
402
+ }));
403
+ }
404
+ catch (err) {
405
+ throw mapProviderError(err, 'federated_signin_failed');
406
+ }
407
+ let res;
408
+ try {
409
+ res = await client.send(new AdminInitiateAuthCommand({
410
+ UserPoolId: config.userPoolId,
411
+ ClientId: config.appClientId,
412
+ AuthFlow: 'ADMIN_USER_PASSWORD_AUTH',
413
+ AuthParameters: { USERNAME: email, PASSWORD: password },
414
+ }));
415
+ }
416
+ catch (err) {
417
+ throw mapProviderError(err, 'federated_signin_failed');
418
+ }
419
+ if (res.ChallengeName) {
420
+ throw new AuthError('federated_signin_failed', `Cognito returned an unexpected challenge after federated sign-in: ${res.ChallengeName}`, 500);
421
+ }
422
+ return extractSignInTokens(res.AuthenticationResult);
423
+ }
424
+ /**
425
+ * Generate a 32-char password that satisfies the strongest password
426
+ * policy VentureKit ships (`passwordStrength: 'strong'` — 12+ chars,
427
+ * upper, lower, digit, symbol).
428
+ */
429
+ function generateStrongPassword() {
430
+ const base = randomBytes(24)
431
+ .toString('base64')
432
+ .replace(/[^A-Za-z0-9]/g, '')
433
+ .slice(0, 28);
434
+ return `Aa1!${base}`;
435
+ }
436
+ //# sourceMappingURL=federated.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"federated.js","sourceRoot":"","sources":["../../src/server/federated.ts"],"names":[],"mappings":"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"}
@@ -43,4 +43,8 @@ export type { SessionTokens, CookieOptions } from './cookies.js';
43
43
  export { ID_TOKEN_COOKIE, ACCESS_TOKEN_COOKIE, REFRESH_TOKEN_COOKIE, buildSessionCookies, buildClearSessionCookies, readCookieFromHeader, } from './cookies.js';
44
44
  export type { CookieAuthMiddlewareOptions } from './middleware.js';
45
45
  export { cookieAuthMiddleware, extractToken } from './middleware.js';
46
+ export type { FederatedProvider, FederatedProfile, FederatedProviderCredentials, SignInAsFederatedUserInput, BuildAuthorizeUrlInput, ExchangeAuthorizationCodeInput, } from './federated.js';
47
+ export { loadFederatedProviderCredentials, generateOAuthState, verifyOAuthState, buildAuthorizeUrl, exchangeAuthorizationCode, signInAsFederatedUser, } from './federated.js';
48
+ export type { VerificationChannel, VerificationCodeStore, VerificationCodeRecord, RequestVerificationCodeInput, RequestVerificationCodeResult, VerifyVerificationCodeInput, } from './verification.js';
49
+ export { generateVerificationCode, hashVerificationCode, requestVerificationCode, verifyVerificationCode, createInMemoryVerificationCodeStore, } from './verification.js';
46
50
  //# sourceMappingURL=index.d.ts.map
@@ -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,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;AAEjD,YAAY,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAChE,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAEtD,YAAY,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAE9C,YAAY,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AACjE,OAAO,EACL,eAAe,EACf,mBAAmB,EACnB,oBAAoB,EACpB,mBAAmB,EACnB,wBAAwB,EACxB,oBAAoB,GACrB,MAAM,cAAc,CAAC;AAEtB,YAAY,EAAE,2BAA2B,EAAE,MAAM,iBAAiB,CAAC;AACnE,OAAO,EAAE,oBAAoB,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC"}
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,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;AAEjD,YAAY,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAChE,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAEtD,YAAY,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAE9C,YAAY,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AACjE,OAAO,EACL,eAAe,EACf,mBAAmB,EACnB,oBAAoB,EACpB,mBAAmB,EACnB,wBAAwB,EACxB,oBAAoB,GACrB,MAAM,cAAc,CAAC;AAEtB,YAAY,EAAE,2BAA2B,EAAE,MAAM,iBAAiB,CAAC;AACnE,OAAO,EAAE,oBAAoB,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAErE,YAAY,EACV,iBAAiB,EACjB,gBAAgB,EAChB,4BAA4B,EAC5B,0BAA0B,EAC1B,sBAAsB,EACtB,8BAA8B,GAC/B,MAAM,gBAAgB,CAAC;AACxB,OAAO,EACL,gCAAgC,EAChC,kBAAkB,EAClB,gBAAgB,EAChB,iBAAiB,EACjB,yBAAyB,EACzB,qBAAqB,GACtB,MAAM,gBAAgB,CAAC;AAExB,YAAY,EACV,mBAAmB,EACnB,qBAAqB,EACrB,sBAAsB,EACtB,4BAA4B,EAC5B,6BAA6B,EAC7B,2BAA2B,GAC5B,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EACL,wBAAwB,EACxB,oBAAoB,EACpB,uBAAuB,EACvB,sBAAsB,EACtB,mCAAmC,GACpC,MAAM,mBAAmB,CAAC"}
@@ -33,4 +33,6 @@ export { changePassword } from './change-password.js';
33
33
  export { verifyAndDecode } from './verify.js';
34
34
  export { ID_TOKEN_COOKIE, ACCESS_TOKEN_COOKIE, REFRESH_TOKEN_COOKIE, buildSessionCookies, buildClearSessionCookies, readCookieFromHeader, } from './cookies.js';
35
35
  export { cookieAuthMiddleware, extractToken } from './middleware.js';
36
+ export { loadFederatedProviderCredentials, generateOAuthState, verifyOAuthState, buildAuthorizeUrl, exchangeAuthorizationCode, signInAsFederatedUser, } from './federated.js';
37
+ export { generateVerificationCode, hashVerificationCode, requestVerificationCode, verifyVerificationCode, createInMemoryVerificationCodeStore, } from './verification.js';
36
38
  //# sourceMappingURL=index.js.map
@@ -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,GAChB,MAAM,kBAAkB,CAAC;AAG1B,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAE9C,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAGjD,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAGtD,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAG9C,OAAO,EACL,eAAe,EACf,mBAAmB,EACnB,oBAAoB,EACpB,mBAAmB,EACnB,wBAAwB,EACxB,oBAAoB,GACrB,MAAM,cAAc,CAAC;AAGtB,OAAO,EAAE,oBAAoB,EAAE,YAAY,EAAE,MAAM,iBAAiB,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,GAChB,MAAM,kBAAkB,CAAC;AAG1B,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAE9C,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAGjD,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAGtD,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAG9C,OAAO,EACL,eAAe,EACf,mBAAmB,EACnB,oBAAoB,EACpB,mBAAmB,EACnB,wBAAwB,EACxB,oBAAoB,GACrB,MAAM,cAAc,CAAC;AAGtB,OAAO,EAAE,oBAAoB,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAUrE,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"}
@@ -0,0 +1,148 @@
1
+ /**
2
+ * One-time verification codes (OTP) for sign-up and sign-in.
3
+ *
4
+ * Generic primitives for the "we'll text/email you a 6-digit code"
5
+ * pattern that gates registration when an app wants to confirm an
6
+ * email or phone number before it ever lives in Cognito. The code
7
+ * channel is opaque to this module — the caller plugs in `email` /
8
+ * `whatsapp` / `sms` and decides how to deliver the code (typically
9
+ * via `@venturekit/notify`).
10
+ *
11
+ * Storage is also pluggable via {@link VerificationCodeStore} so
12
+ * applications can pick Postgres, DynamoDB, or in-memory (tests). The
13
+ * stored value is a SHA-256 hash of the code, never the code itself —
14
+ * if the table leaks the codes are still useless.
15
+ *
16
+ * Usage sketch (route handler):
17
+ *
18
+ * ```ts
19
+ * // Step 1 — user submits email, we send a code
20
+ * const { code } = await requestVerificationCode({
21
+ * channel: 'email',
22
+ * identifier: email,
23
+ * store,
24
+ * });
25
+ * await notify.sendEmail({ to: email, subject: 'Code', text: code });
26
+ *
27
+ * // Step 2 — user submits the code; we verify it
28
+ * await verifyVerificationCode({
29
+ * channel: 'email', identifier: email, code: req.body.code, store,
30
+ * });
31
+ * await signUpUser({ email, password });
32
+ * ```
33
+ */
34
+ /** A delivery channel — `email`, `sms`, `whatsapp`, or anything else. */
35
+ export type VerificationChannel = string;
36
+ /**
37
+ * Pluggable storage for outstanding codes. Implementations must be
38
+ * idempotent on `put` (overwrite previous code for the same key) and
39
+ * return `undefined` for unknown / expired keys on `get`.
40
+ *
41
+ * Keys are tuples of `(channel, identifier)`; e.g.
42
+ * `('email', 'foo@example.com')` and `('whatsapp', '+212600000000')`
43
+ * occupy distinct slots so a user with both can verify each one.
44
+ */
45
+ export interface VerificationCodeStore {
46
+ put(input: {
47
+ channel: VerificationChannel;
48
+ identifier: string;
49
+ /** SHA-256 hex digest of the plaintext code. */
50
+ codeHash: string;
51
+ /** Wall-clock expiry, ms epoch. */
52
+ expiresAt: number;
53
+ /** Number of failed `verify` attempts allowed before rotation. */
54
+ maxAttempts: number;
55
+ }): Promise<void>;
56
+ get(input: {
57
+ channel: VerificationChannel;
58
+ identifier: string;
59
+ }): Promise<VerificationCodeRecord | undefined>;
60
+ /**
61
+ * Increment the attempt counter for an outstanding code (after a
62
+ * mismatch). Returning the new counter lets the helper decide
63
+ * whether to delete the record on the spot once the limit is hit.
64
+ */
65
+ incrementAttempts(input: {
66
+ channel: VerificationChannel;
67
+ identifier: string;
68
+ }): Promise<number>;
69
+ delete(input: {
70
+ channel: VerificationChannel;
71
+ identifier: string;
72
+ }): Promise<void>;
73
+ }
74
+ export interface VerificationCodeRecord {
75
+ channel: VerificationChannel;
76
+ identifier: string;
77
+ codeHash: string;
78
+ expiresAt: number;
79
+ attempts: number;
80
+ maxAttempts: number;
81
+ }
82
+ /**
83
+ * Generate a random numeric code of `length` digits (default 6).
84
+ * Uses `crypto.randomInt` so each digit is uniformly distributed —
85
+ * `Math.random()` would NOT meet the bar for an authentication code.
86
+ */
87
+ export declare function generateVerificationCode(length?: number): string;
88
+ /**
89
+ * Hash a code with SHA-256. Stored hashes use this exact function so
90
+ * `verifyVerificationCode` can recompute the digest and
91
+ * constant-time-compare. We don't bother with bcrypt/scrypt: codes
92
+ * live for minutes, not years, and brute-forcing 6 digits in <5
93
+ * minutes is what `maxAttempts` is for.
94
+ */
95
+ export declare function hashVerificationCode(code: string): string;
96
+ export interface RequestVerificationCodeInput {
97
+ channel: VerificationChannel;
98
+ /** Email, phone number, etc. — opaque to the helper. */
99
+ identifier: string;
100
+ store: VerificationCodeStore;
101
+ /** Code length in digits. Default 6. */
102
+ length?: number;
103
+ /** TTL in seconds. Default 600 (10 minutes). */
104
+ ttlSeconds?: number;
105
+ /** Max wrong-attempts before the code self-destructs. Default 5. */
106
+ maxAttempts?: number;
107
+ }
108
+ export interface RequestVerificationCodeResult {
109
+ /** The plaintext code — caller is responsible for delivery. */
110
+ code: string;
111
+ /** Wall-clock expiry, ms epoch. */
112
+ expiresAt: number;
113
+ }
114
+ /**
115
+ * Mint a fresh code and store its hash. Overwrites any previous code
116
+ * for the same `(channel, identifier)` so a user who didn't receive
117
+ * the first email can hit "resend" without touching state manually.
118
+ *
119
+ * Returns the plaintext code so the caller can deliver it via
120
+ * email / WhatsApp / SMS. The plaintext never re-enters this module.
121
+ */
122
+ export declare function requestVerificationCode(input: RequestVerificationCodeInput): Promise<RequestVerificationCodeResult>;
123
+ export interface VerifyVerificationCodeInput {
124
+ channel: VerificationChannel;
125
+ identifier: string;
126
+ /** Plaintext code submitted by the user. */
127
+ code: string;
128
+ store: VerificationCodeStore;
129
+ }
130
+ /**
131
+ * Verify a submitted code. On success the record is deleted (codes
132
+ * are single-use). On mismatch the attempt counter is bumped; once
133
+ * `maxAttempts` is reached the record is wiped and the next call
134
+ * looks like "no code requested" — the user has to start over.
135
+ *
136
+ * Throws {@link AuthError} (`verification_failed`, HTTP 401) on any
137
+ * failure path so route handlers can surface a single error to the
138
+ * SPA without leaking which path was taken.
139
+ */
140
+ export declare function verifyVerificationCode(input: VerifyVerificationCodeInput): Promise<void>;
141
+ /**
142
+ * Process-local store. Useful in unit tests and local `vk dev` runs
143
+ * (single Lambda warm container); never use in production — codes
144
+ * vanish on cold start, and parallel containers don't see each
145
+ * other's records.
146
+ */
147
+ export declare function createInMemoryVerificationCodeStore(): VerificationCodeStore;
148
+ //# sourceMappingURL=verification.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"verification.d.ts","sourceRoot":"","sources":["../../src/server/verification.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAKH,yEAAyE;AACzE,MAAM,MAAM,mBAAmB,GAAG,MAAM,CAAC;AAEzC;;;;;;;;GAQG;AACH,MAAM,WAAW,qBAAqB;IACpC,GAAG,CAAC,KAAK,EAAE;QACT,OAAO,EAAE,mBAAmB,CAAC;QAC7B,UAAU,EAAE,MAAM,CAAC;QACnB,gDAAgD;QAChD,QAAQ,EAAE,MAAM,CAAC;QACjB,mCAAmC;QACnC,SAAS,EAAE,MAAM,CAAC;QAClB,kEAAkE;QAClE,WAAW,EAAE,MAAM,CAAC;KACrB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAClB,GAAG,CAAC,KAAK,EAAE;QACT,OAAO,EAAE,mBAAmB,CAAC;QAC7B,UAAU,EAAE,MAAM,CAAC;KACpB,GAAG,OAAO,CAAC,sBAAsB,GAAG,SAAS,CAAC,CAAC;IAChD;;;;OAIG;IACH,iBAAiB,CAAC,KAAK,EAAE;QACvB,OAAO,EAAE,mBAAmB,CAAC;QAC7B,UAAU,EAAE,MAAM,CAAC;KACpB,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACpB,MAAM,CAAC,KAAK,EAAE;QACZ,OAAO,EAAE,mBAAmB,CAAC;QAC7B,UAAU,EAAE,MAAM,CAAC;KACpB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACnB;AAED,MAAM,WAAW,sBAAsB;IACrC,OAAO,EAAE,mBAAmB,CAAC;IAC7B,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;CACrB;AAMD;;;;GAIG;AACH,wBAAgB,wBAAwB,CAAC,MAAM,SAAI,GAAG,MAAM,CAW3D;AAED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEzD;AAMD,MAAM,WAAW,4BAA4B;IAC3C,OAAO,EAAE,mBAAmB,CAAC;IAC7B,wDAAwD;IACxD,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,qBAAqB,CAAC;IAC7B,wCAAwC;IACxC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,gDAAgD;IAChD,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,oEAAoE;IACpE,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,6BAA6B;IAC5C,+DAA+D;IAC/D,IAAI,EAAE,MAAM,CAAC;IACb,mCAAmC;IACnC,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;;GAOG;AACH,wBAAsB,uBAAuB,CAC3C,KAAK,EAAE,4BAA4B,GAClC,OAAO,CAAC,6BAA6B,CAAC,CAexC;AAED,MAAM,WAAW,2BAA2B;IAC1C,OAAO,EAAE,mBAAmB,CAAC;IAC7B,UAAU,EAAE,MAAM,CAAC;IACnB,4CAA4C;IAC5C,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,qBAAqB,CAAC;CAC9B;AAED;;;;;;;;;GASG;AACH,wBAAsB,sBAAsB,CAC1C,KAAK,EAAE,2BAA2B,GACjC,OAAO,CAAC,IAAI,CAAC,CAsDf;AAMD;;;;;GAKG;AACH,wBAAgB,mCAAmC,IAAI,qBAAqB,CA2B3E"}
@@ -0,0 +1,177 @@
1
+ /**
2
+ * One-time verification codes (OTP) for sign-up and sign-in.
3
+ *
4
+ * Generic primitives for the "we'll text/email you a 6-digit code"
5
+ * pattern that gates registration when an app wants to confirm an
6
+ * email or phone number before it ever lives in Cognito. The code
7
+ * channel is opaque to this module — the caller plugs in `email` /
8
+ * `whatsapp` / `sms` and decides how to deliver the code (typically
9
+ * via `@venturekit/notify`).
10
+ *
11
+ * Storage is also pluggable via {@link VerificationCodeStore} so
12
+ * applications can pick Postgres, DynamoDB, or in-memory (tests). The
13
+ * stored value is a SHA-256 hash of the code, never the code itself —
14
+ * if the table leaks the codes are still useless.
15
+ *
16
+ * Usage sketch (route handler):
17
+ *
18
+ * ```ts
19
+ * // Step 1 — user submits email, we send a code
20
+ * const { code } = await requestVerificationCode({
21
+ * channel: 'email',
22
+ * identifier: email,
23
+ * store,
24
+ * });
25
+ * await notify.sendEmail({ to: email, subject: 'Code', text: code });
26
+ *
27
+ * // Step 2 — user submits the code; we verify it
28
+ * await verifyVerificationCode({
29
+ * channel: 'email', identifier: email, code: req.body.code, store,
30
+ * });
31
+ * await signUpUser({ email, password });
32
+ * ```
33
+ */
34
+ import { createHash, randomInt, timingSafeEqual } from 'node:crypto';
35
+ import { AuthError } from './errors.js';
36
+ // ────────────────────────────────────────────────────────────────────
37
+ // Primitives
38
+ // ────────────────────────────────────────────────────────────────────
39
+ /**
40
+ * Generate a random numeric code of `length` digits (default 6).
41
+ * Uses `crypto.randomInt` so each digit is uniformly distributed —
42
+ * `Math.random()` would NOT meet the bar for an authentication code.
43
+ */
44
+ export function generateVerificationCode(length = 6) {
45
+ if (length < 4 || length > 10) {
46
+ throw new Error(`[verification] code length ${length} outside the supported 4–10 range`);
47
+ }
48
+ let out = '';
49
+ for (let i = 0; i < length; i++) {
50
+ out += String(randomInt(0, 10));
51
+ }
52
+ return out;
53
+ }
54
+ /**
55
+ * Hash a code with SHA-256. Stored hashes use this exact function so
56
+ * `verifyVerificationCode` can recompute the digest and
57
+ * constant-time-compare. We don't bother with bcrypt/scrypt: codes
58
+ * live for minutes, not years, and brute-forcing 6 digits in <5
59
+ * minutes is what `maxAttempts` is for.
60
+ */
61
+ export function hashVerificationCode(code) {
62
+ return createHash('sha256').update(code).digest('hex');
63
+ }
64
+ /**
65
+ * Mint a fresh code and store its hash. Overwrites any previous code
66
+ * for the same `(channel, identifier)` so a user who didn't receive
67
+ * the first email can hit "resend" without touching state manually.
68
+ *
69
+ * Returns the plaintext code so the caller can deliver it via
70
+ * email / WhatsApp / SMS. The plaintext never re-enters this module.
71
+ */
72
+ export async function requestVerificationCode(input) {
73
+ const length = input.length ?? 6;
74
+ const ttlSeconds = input.ttlSeconds ?? 600;
75
+ const maxAttempts = input.maxAttempts ?? 5;
76
+ const code = generateVerificationCode(length);
77
+ const codeHash = hashVerificationCode(code);
78
+ const expiresAt = Date.now() + ttlSeconds * 1000;
79
+ await input.store.put({
80
+ channel: input.channel,
81
+ identifier: input.identifier,
82
+ codeHash,
83
+ expiresAt,
84
+ maxAttempts,
85
+ });
86
+ return { code, expiresAt };
87
+ }
88
+ /**
89
+ * Verify a submitted code. On success the record is deleted (codes
90
+ * are single-use). On mismatch the attempt counter is bumped; once
91
+ * `maxAttempts` is reached the record is wiped and the next call
92
+ * looks like "no code requested" — the user has to start over.
93
+ *
94
+ * Throws {@link AuthError} (`verification_failed`, HTTP 401) on any
95
+ * failure path so route handlers can surface a single error to the
96
+ * SPA without leaking which path was taken.
97
+ */
98
+ export async function verifyVerificationCode(input) {
99
+ const record = await input.store.get({
100
+ channel: input.channel,
101
+ identifier: input.identifier,
102
+ });
103
+ if (!record) {
104
+ throw new AuthError('verification_failed', 'No verification code outstanding — request a new one', 401);
105
+ }
106
+ if (record.expiresAt < Date.now()) {
107
+ await input.store.delete({
108
+ channel: input.channel,
109
+ identifier: input.identifier,
110
+ });
111
+ throw new AuthError('verification_failed', 'Verification code expired — request a new one', 401);
112
+ }
113
+ const submittedHash = hashVerificationCode(input.code);
114
+ const a = Buffer.from(submittedHash, 'hex');
115
+ const b = Buffer.from(record.codeHash, 'hex');
116
+ const matches = a.length === b.length && timingSafeEqual(a, b);
117
+ if (matches) {
118
+ await input.store.delete({
119
+ channel: input.channel,
120
+ identifier: input.identifier,
121
+ });
122
+ return;
123
+ }
124
+ // Wrong code — bump the counter; if we've hit the limit, wipe the
125
+ // record so the user has to request a new one (this is the
126
+ // brute-force defense, since 6-digit codes only have 1M states).
127
+ const attempts = await input.store.incrementAttempts({
128
+ channel: input.channel,
129
+ identifier: input.identifier,
130
+ });
131
+ if (attempts >= record.maxAttempts) {
132
+ await input.store.delete({
133
+ channel: input.channel,
134
+ identifier: input.identifier,
135
+ });
136
+ }
137
+ throw new AuthError('verification_failed', 'Invalid verification code', 401);
138
+ }
139
+ // ────────────────────────────────────────────────────────────────────
140
+ // In-memory store — for tests / local dev only
141
+ // ────────────────────────────────────────────────────────────────────
142
+ /**
143
+ * Process-local store. Useful in unit tests and local `vk dev` runs
144
+ * (single Lambda warm container); never use in production — codes
145
+ * vanish on cold start, and parallel containers don't see each
146
+ * other's records.
147
+ */
148
+ export function createInMemoryVerificationCodeStore() {
149
+ const map = new Map();
150
+ const key = (c, i) => `${c}\u0000${i}`;
151
+ return {
152
+ async put({ channel, identifier, codeHash, expiresAt, maxAttempts }) {
153
+ map.set(key(channel, identifier), {
154
+ channel,
155
+ identifier,
156
+ codeHash,
157
+ expiresAt,
158
+ attempts: 0,
159
+ maxAttempts,
160
+ });
161
+ },
162
+ async get({ channel, identifier }) {
163
+ return map.get(key(channel, identifier));
164
+ },
165
+ async incrementAttempts({ channel, identifier }) {
166
+ const rec = map.get(key(channel, identifier));
167
+ if (!rec)
168
+ return 0;
169
+ rec.attempts += 1;
170
+ return rec.attempts;
171
+ },
172
+ async delete({ channel, identifier }) {
173
+ map.delete(key(channel, identifier));
174
+ },
175
+ };
176
+ }
177
+ //# sourceMappingURL=verification.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"verification.js","sourceRoot":"","sources":["../../src/server/verification.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,52 @@
1
+ -- vk_auth_001_verification_codes.sql
2
+ --
3
+ -- Owned by `@venturekit/auth`. Backs `VerificationCodeStore` (used by
4
+ -- the OTP gating on self-service sign-up + sign-in / passwordless).
5
+ --
6
+ -- Rows are pre-Cognito state — created BEFORE a user exists in either
7
+ -- Cognito or any consumer-side `users` mirror. The `identifier` is the
8
+ -- email or E.164 phone number the code was sent to; `channel` is the
9
+ -- delivery surface.
10
+ --
11
+ -- Storage shape:
12
+ -- - `code_hash` is SHA-256 of the plaintext code (hex). Plaintext
13
+ -- never lands in the DB so a future leak still requires brute
14
+ -- force against the (short) TTL.
15
+ -- - `expires_at` is the wall-clock cutoff after which the
16
+ -- `verifyVerificationCode` helper deletes the row and surfaces
17
+ -- `verification_failed`.
18
+ -- - `attempts` is incremented on every wrong-code submission;
19
+ -- once it reaches `max_attempts` the row is deleted (forces the
20
+ -- user to request a fresh code).
21
+ --
22
+ -- Channel typing: stored as TEXT + CHECK rather than a Postgres enum
23
+ -- so this migration does not depend on `@venturekit/notify`'s
24
+ -- `notify_channel` enum (auth must be usable without notify — e.g. a
25
+ -- project that ships its own delivery layer). Add new values here when
26
+ -- the auth package learns to dispatch over them.
27
+ --
28
+ -- Idempotent: uses `CREATE TABLE IF NOT EXISTS` so projects that
29
+ -- previously created the table from their own migration (with a
30
+ -- compatible shape) can adopt this one without dropping data. The
31
+ -- expectation is that those projects will then leave their old
32
+ -- migration file in place but stop hand-maintaining the schema here.
33
+
34
+ CREATE TABLE IF NOT EXISTS verification_codes (
35
+ channel TEXT NOT NULL,
36
+ identifier TEXT NOT NULL,
37
+ code_hash TEXT NOT NULL,
38
+ expires_at TIMESTAMPTZ NOT NULL,
39
+ attempts INT NOT NULL DEFAULT 0,
40
+ max_attempts INT NOT NULL DEFAULT 5,
41
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
42
+ PRIMARY KEY (channel, identifier),
43
+ CONSTRAINT verification_codes_channel_chk
44
+ CHECK (channel IN ('email', 'whatsapp', 'sms')),
45
+ CONSTRAINT verification_codes_attempts_chk
46
+ CHECK (attempts >= 0),
47
+ CONSTRAINT verification_codes_max_attempts_chk
48
+ CHECK (max_attempts >= 1)
49
+ );
50
+
51
+ CREATE INDEX IF NOT EXISTS idx_verification_codes_expires_at
52
+ ON verification_codes (expires_at);
package/package.json CHANGED
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "name": "@venturekit/auth",
3
- "version": "0.0.0-dev.20260507015944",
3
+ "version": "0.0.0-dev.20260511023410",
4
4
  "description": "Authentication and authorization for VentureKit",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
7
7
  "types": "./dist/index.d.ts",
8
8
  "files": [
9
- "dist"
9
+ "dist",
10
+ "migrations"
10
11
  ],
11
12
  "repository": {
12
13
  "type": "git",
@@ -18,6 +19,9 @@
18
19
  "access": "public"
19
20
  },
20
21
  "license": "Apache-2.0",
22
+ "vk": {
23
+ "migrations": "migrations"
24
+ },
21
25
  "exports": {
22
26
  ".": {
23
27
  "import": "./dist/index.js",
@@ -29,12 +33,13 @@
29
33
  }
30
34
  },
31
35
  "dependencies": {
32
- "@venturekit/core": "0.0.0-dev.20260507015944",
36
+ "@venturekit/core": "0.0.0-dev.20260511023410",
33
37
  "@aws-sdk/client-cognito-identity-provider": "^3.668.0",
38
+ "@aws-sdk/client-secrets-manager": "^3.668.0",
34
39
  "aws-jwt-verify": "^4.0.1"
35
40
  },
36
41
  "peerDependencies": {
37
- "@venturekit/runtime": "0.0.0-dev.20260507015944"
42
+ "@venturekit/runtime": "0.0.0-dev.20260511023410"
38
43
  },
39
44
  "peerDependenciesMeta": {
40
45
  "@venturekit/runtime": {
@@ -42,7 +47,7 @@
42
47
  }
43
48
  },
44
49
  "devDependencies": {
45
- "@venturekit/runtime": "0.0.0-dev.20260507015944",
50
+ "@venturekit/runtime": "0.0.0-dev.20260511023410",
46
51
  "@types/aws-lambda": "^8.10.131",
47
52
  "@types/node": "^25.6.0",
48
53
  "typescript": "^5.3.0"