@spfn/auth 0.3.0-beta.24 → 0.3.0-beta.25
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/README.md +163 -5
- package/dist/client-proof.d.ts +10 -1
- package/dist/client-proof.js +126 -9
- package/dist/client-proof.js.map +1 -1
- package/dist/client.d.ts +50 -1
- package/dist/client.js +25 -0
- package/dist/client.js.map +1 -1
- package/dist/config.d.ts +40 -0
- package/dist/config.js +16 -0
- package/dist/config.js.map +1 -1
- package/dist/errors.d.ts +48 -2
- package/dist/errors.js +27 -1
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +23 -16
- package/dist/index.js +31 -1
- package/dist/index.js.map +1 -1
- package/dist/{machine-principals-ZJd9anVT.d.ts → machine-principals-CdEgxOB1.d.ts} +1397 -886
- package/dist/nextjs/api.js +182 -40
- package/dist/nextjs/api.js.map +1 -1
- package/dist/nextjs/server.d.ts +17 -0
- package/dist/nextjs/server.js +33 -6
- package/dist/nextjs/server.js.map +1 -1
- package/dist/server.d.ts +256 -96
- package/dist/server.js +1328 -676
- package/dist/server.js.map +1 -1
- package/migrations/20260919023107_even_mikhail_rasputin/migration.sql +20 -0
- package/migrations/20260919023107_even_mikhail_rasputin/snapshot.json +6300 -0
- package/package.json +1 -1
package/dist/nextjs/server.d.ts
CHANGED
|
@@ -369,6 +369,18 @@ interface OAuthCallbackOptions {
|
|
|
369
369
|
* @default '/auth/error'
|
|
370
370
|
*/
|
|
371
371
|
errorRedirectUrl?: string;
|
|
372
|
+
/**
|
|
373
|
+
* App page that asks for the second factor, when the callback carries a
|
|
374
|
+
* step-up challenge instead of a session (#95).
|
|
375
|
+
*
|
|
376
|
+
* An override for `SPFN_AUTH_MFA_CONFIRM_PATH`, which is where every other
|
|
377
|
+
* app-page path in this package lives; the env var is the one to set, and
|
|
378
|
+
* this exists for an app mounting two handlers on different screens. The
|
|
379
|
+
* handler redirects to it with `?challenge=` and `?returnUrl=`.
|
|
380
|
+
*
|
|
381
|
+
* @default env SPFN_AUTH_MFA_CONFIRM_PATH, then '/auth/mfa'
|
|
382
|
+
*/
|
|
383
|
+
mfaPath?: string;
|
|
372
384
|
}
|
|
373
385
|
/**
|
|
374
386
|
* Create OAuth callback handler for Next.js API Route
|
|
@@ -379,6 +391,11 @@ interface OAuthCallbackOptions {
|
|
|
379
391
|
* 3. Creates full session and saves to cookie
|
|
380
392
|
* 4. Redirects to returnUrl
|
|
381
393
|
*
|
|
394
|
+
* When the account has a second factor and this device is new to it (#95) the
|
|
395
|
+
* backend sends `mfaChallenge` in place of `userId` and `keyId`. No session is
|
|
396
|
+
* sealed; the browser goes to `SPFN_AUTH_MFA_CONFIRM_PATH` with the challenge,
|
|
397
|
+
* and the session is sealed by `mfaVerifyInterceptor` once the page proves it.
|
|
398
|
+
*
|
|
382
399
|
* @example
|
|
383
400
|
* ```typescript
|
|
384
401
|
* // /api/auth/callback/route.ts
|
package/dist/nextjs/server.js
CHANGED
|
@@ -158,6 +158,18 @@ var COOKIE_NAMES = {
|
|
|
158
158
|
get OAUTH_PENDING() {
|
|
159
159
|
return `spfn_oauth_pending${getCookieSuffix()}`;
|
|
160
160
|
},
|
|
161
|
+
/**
|
|
162
|
+
* Pending second-factor session (privateKey, keyId, challengeHash) (#95)
|
|
163
|
+
*
|
|
164
|
+
* Its own name and its own audience, separate from OAUTH_PENDING. The two
|
|
165
|
+
* coexist: a person who starts a social login in one tab while a password
|
|
166
|
+
* step-up is outstanding in another has both flows live, and one name would
|
|
167
|
+
* mean the second overwrote the first — sealing a session with a private key
|
|
168
|
+
* that does not match the key being activated.
|
|
169
|
+
*/
|
|
170
|
+
get MFA_PENDING() {
|
|
171
|
+
return `spfn_mfa_pending${getCookieSuffix()}`;
|
|
172
|
+
},
|
|
161
173
|
/** OAuth CSRF nonce — double-submit against the (encrypted) state.nonce at callback */
|
|
162
174
|
get OAUTH_CSRF() {
|
|
163
175
|
return `spfn_oauth_csrf${getCookieSuffix()}`;
|
|
@@ -272,20 +284,21 @@ async function clearSession() {
|
|
|
272
284
|
cookieStore.delete(COOKIE_NAMES.SESSION_KEY_ID);
|
|
273
285
|
cookieStore.delete(COOKIE_NAMES.CSRF);
|
|
274
286
|
}
|
|
275
|
-
async function getPendingSessionKey() {
|
|
287
|
+
async function getPendingSessionKey(purpose) {
|
|
276
288
|
const secret = env4.SPFN_AUTH_SESSION_SECRET;
|
|
277
289
|
const encoder = new TextEncoder();
|
|
278
|
-
const data = encoder.encode(`oauth-pending:${secret}`);
|
|
290
|
+
const data = encoder.encode(purpose === "oauth" ? `oauth-pending:${secret}` : `mfa-pending:${secret}`);
|
|
279
291
|
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
|
|
280
292
|
return new Uint8Array(hashBuffer);
|
|
281
293
|
}
|
|
282
294
|
async function sealPendingSession(data, ttl = 600) {
|
|
283
|
-
|
|
284
|
-
|
|
295
|
+
return await sealFor("oauth", data, ttl);
|
|
296
|
+
}
|
|
297
|
+
async function sealFor(purpose, data, ttl) {
|
|
298
|
+
return await new jose2.EncryptJWT({ data }).setProtectedHeader({ alg: "dir", enc: "A256GCM" }).setIssuedAt().setExpirationTime(`${ttl}s`).setIssuer("spfn-auth").setAudience(purpose === "oauth" ? "spfn-oauth" : "spfn-mfa").encrypt(await getPendingSessionKey(purpose));
|
|
285
299
|
}
|
|
286
300
|
async function unsealPendingSession(jwt) {
|
|
287
|
-
const
|
|
288
|
-
const { payload } = await jose2.jwtDecrypt(jwt, key, {
|
|
301
|
+
const { payload } = await jose2.jwtDecrypt(jwt, await getPendingSessionKey("oauth"), {
|
|
289
302
|
issuer: "spfn-auth",
|
|
290
303
|
audience: "spfn-oauth"
|
|
291
304
|
});
|
|
@@ -461,6 +474,7 @@ function clearSessionCookies(response) {
|
|
|
461
474
|
import { NextResponse } from "next/server";
|
|
462
475
|
import { cookies as cookies2 } from "next/headers.js";
|
|
463
476
|
import { env as env5 } from "@spfn/core/config";
|
|
477
|
+
import { env as authEnv } from "@spfn/auth/config";
|
|
464
478
|
import { logger as logger2 } from "@spfn/core/logger";
|
|
465
479
|
|
|
466
480
|
// src/lib/return-path.ts
|
|
@@ -528,6 +542,15 @@ function bindingFromQuery(searchParams) {
|
|
|
528
542
|
}
|
|
529
543
|
return { sessionBinding: "passkey", keyExpiresAtMillis: expiresAt };
|
|
530
544
|
}
|
|
545
|
+
function mfaRedirect(request, challenge, returnUrl, configured) {
|
|
546
|
+
const path = configured || authEnv.SPFN_AUTH_MFA_CONFIRM_PATH || DEFAULT_MFA_CONFIRM_PATH;
|
|
547
|
+
const target = new URL(path, request.url);
|
|
548
|
+
target.searchParams.set("challenge", challenge);
|
|
549
|
+
target.searchParams.set("returnUrl", returnUrl);
|
|
550
|
+
logger2.debug("OAuth callback needs a second factor", { path });
|
|
551
|
+
return NextResponse.redirect(target);
|
|
552
|
+
}
|
|
553
|
+
var DEFAULT_MFA_CONFIRM_PATH = "/auth/mfa";
|
|
531
554
|
function createOAuthCallbackHandler(options) {
|
|
532
555
|
const defaultRedirect = options?.defaultRedirectUrl || "/";
|
|
533
556
|
const errorRedirect = options?.errorRedirectUrl || "/auth/error";
|
|
@@ -542,6 +565,10 @@ function createOAuthCallbackHandler(options) {
|
|
|
542
565
|
errorUrl.searchParams.set("error", error);
|
|
543
566
|
return NextResponse.redirect(errorUrl);
|
|
544
567
|
}
|
|
568
|
+
const mfaChallenge = searchParams.get("mfaChallenge");
|
|
569
|
+
if (mfaChallenge) {
|
|
570
|
+
return mfaRedirect(request, mfaChallenge, returnUrl, options?.mfaPath);
|
|
571
|
+
}
|
|
545
572
|
if (!userId || !keyId) {
|
|
546
573
|
logger2.error("OAuth callback missing required params", { userId: !!userId, keyId: !!keyId });
|
|
547
574
|
const errorUrl = new URL(errorRedirect, request.url);
|