@spfn/auth 0.3.0-beta.2 → 0.3.0-beta.20
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 +1382 -23
- package/dist/client-proof.d.ts +45 -15
- package/dist/client-proof.js +198 -4
- package/dist/client-proof.js.map +1 -1
- package/dist/client.d.ts +92 -1
- package/dist/client.js +58 -0
- package/dist/client.js.map +1 -1
- package/dist/config.d.ts +302 -0
- package/dist/config.js +134 -4
- package/dist/config.js.map +1 -1
- package/dist/errors.d.ts +370 -3
- package/dist/errors.js +245 -2
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +185 -2
- package/dist/index.js +256 -2
- package/dist/index.js.map +1 -1
- package/dist/machine-principals-BD4tnASp.d.ts +2739 -0
- package/dist/nextjs/api.js +350 -12
- package/dist/nextjs/api.js.map +1 -1
- package/dist/nextjs/client.d.ts +28 -1
- package/dist/nextjs/client.js +24 -3
- package/dist/nextjs/client.js.map +1 -1
- package/dist/nextjs/server.d.ts +173 -3
- package/dist/nextjs/server.js +372 -10
- package/dist/nextjs/server.js.map +1 -1
- package/dist/server.d.ts +3761 -414
- package/dist/server.js +5865 -1043
- package/dist/server.js.map +1 -1
- package/dist/{session-DTHahDQ9.d.ts → session-Dfwu5g2W.d.ts} +28 -1
- package/migrations/20260810112144_colorful_tomorrow_man/migration.sql +18 -0
- package/migrations/20260810112144_colorful_tomorrow_man/snapshot.json +3576 -0
- package/migrations/20260901091716_fine_arclight/migration.sql +21 -0
- package/migrations/20260901091716_fine_arclight/snapshot.json +3849 -0
- package/migrations/20260906155957_natural_moonstone/migration.sql +33 -0
- package/migrations/20260906155957_natural_moonstone/snapshot.json +4275 -0
- package/migrations/20260907020904_giant_eternals/migration.sql +21 -0
- package/migrations/20260907020904_giant_eternals/snapshot.json +4561 -0
- package/migrations/20260907044807_eminent_angel/migration.sql +2 -0
- package/migrations/20260907044807_eminent_angel/snapshot.json +4561 -0
- package/migrations/20260918083158_foamy_roughhouse/migration.sql +55 -0
- package/migrations/20260918083158_foamy_roughhouse/snapshot.json +5271 -0
- package/package.json +9 -6
- package/dist/authenticate-55LeXHqZ.d.ts +0 -1447
package/dist/nextjs/server.js
CHANGED
|
@@ -22,7 +22,8 @@ var authLogger = {
|
|
|
22
22
|
general: rootLogger.child("@spfn/auth:interceptor:general"),
|
|
23
23
|
login: rootLogger.child("@spfn/auth:interceptor:login"),
|
|
24
24
|
keyRotation: rootLogger.child("@spfn/auth:interceptor:key-rotation"),
|
|
25
|
-
oauth: rootLogger.child("@spfn/auth:interceptor:oauth")
|
|
25
|
+
oauth: rootLogger.child("@spfn/auth:interceptor:oauth"),
|
|
26
|
+
csrf: rootLogger.child("@spfn/auth:interceptor:csrf")
|
|
26
27
|
},
|
|
27
28
|
session: rootLogger.child("@spfn/auth:session"),
|
|
28
29
|
service: rootLogger.child("@spfn/auth:service"),
|
|
@@ -89,10 +90,59 @@ async function unsealSession(jwt) {
|
|
|
89
90
|
}
|
|
90
91
|
}
|
|
91
92
|
|
|
92
|
-
// src/server/lib/
|
|
93
|
+
// src/server/lib/csrf.ts
|
|
93
94
|
import { env as env2 } from "@spfn/auth/config";
|
|
95
|
+
var CSRF_SUBKEY_LABEL = "spfn-auth-csrf-token-v1";
|
|
96
|
+
var MAX_CANDIDATES = 32;
|
|
97
|
+
function sessionSecret() {
|
|
98
|
+
const secret = env2.SPFN_AUTH_SESSION_SECRET;
|
|
99
|
+
if (!secret) {
|
|
100
|
+
throw new Error(
|
|
101
|
+
"SPFN_AUTH_SESSION_SECRET is required for CSRF protection. Set it (sessions need it anyway), or set SPFN_AUTH_CSRF=off."
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
return secret;
|
|
105
|
+
}
|
|
106
|
+
async function hmacSha256(key, message) {
|
|
107
|
+
const cryptoKey = await crypto.subtle.importKey(
|
|
108
|
+
"raw",
|
|
109
|
+
key.buffer,
|
|
110
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
111
|
+
false,
|
|
112
|
+
["sign"]
|
|
113
|
+
);
|
|
114
|
+
const signature = await crypto.subtle.sign("HMAC", cryptoKey, new TextEncoder().encode(message));
|
|
115
|
+
return new Uint8Array(signature);
|
|
116
|
+
}
|
|
117
|
+
function toHex(bytes) {
|
|
118
|
+
return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
119
|
+
}
|
|
120
|
+
async function deriveCsrfToken(keyId) {
|
|
121
|
+
const subkey = await hmacSha256(new TextEncoder().encode(sessionSecret()), CSRF_SUBKEY_LABEL);
|
|
122
|
+
return toHex(await hmacSha256(subkey, keyId));
|
|
123
|
+
}
|
|
124
|
+
function timingSafeEqualString(a, b) {
|
|
125
|
+
if (a.length !== b.length) {
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
let difference = 0;
|
|
129
|
+
for (let i = 0; i < a.length; i++) {
|
|
130
|
+
difference |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
131
|
+
}
|
|
132
|
+
return difference === 0;
|
|
133
|
+
}
|
|
134
|
+
function matchesCsrfToken(expected, presented) {
|
|
135
|
+
if (!presented) {
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
return presented.split(",", MAX_CANDIDATES).some((candidate) => timingSafeEqualString(expected, candidate.trim()));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// src/server/lib/config.ts
|
|
142
|
+
import { env as env3 } from "@spfn/auth/config";
|
|
143
|
+
import { PasskeyConfigError } from "@spfn/auth/errors";
|
|
94
144
|
function getCookieSuffix() {
|
|
95
|
-
const port = process.env.
|
|
145
|
+
const port = process.env.SPFN_PORT;
|
|
96
146
|
return port ? `_${port}` : "";
|
|
97
147
|
}
|
|
98
148
|
var COOKIE_NAMES = {
|
|
@@ -111,6 +161,18 @@ var COOKIE_NAMES = {
|
|
|
111
161
|
/** OAuth CSRF nonce — double-submit against the (encrypted) state.nonce at callback */
|
|
112
162
|
get OAUTH_CSRF() {
|
|
113
163
|
return `spfn_oauth_csrf${getCookieSuffix()}`;
|
|
164
|
+
},
|
|
165
|
+
/** Password-setup session for verified-email signup — temporary, single-purpose */
|
|
166
|
+
get SIGNUP_SETUP() {
|
|
167
|
+
return `spfn_signup_setup${getCookieSuffix()}`;
|
|
168
|
+
},
|
|
169
|
+
/** Password-setup session for a password reset — temporary, single-purpose */
|
|
170
|
+
get PASSWORD_RESET_SETUP() {
|
|
171
|
+
return `spfn_password_reset_setup${getCookieSuffix()}`;
|
|
172
|
+
},
|
|
173
|
+
/** CSRF token — the only cookie here the browser can read */
|
|
174
|
+
get CSRF() {
|
|
175
|
+
return `spfn_csrf${getCookieSuffix()}`;
|
|
114
176
|
}
|
|
115
177
|
};
|
|
116
178
|
function parseDuration(duration) {
|
|
@@ -147,7 +209,7 @@ function getSessionTtl(override) {
|
|
|
147
209
|
if (globalConfig.sessionTtl !== void 0) {
|
|
148
210
|
return parseDuration(globalConfig.sessionTtl);
|
|
149
211
|
}
|
|
150
|
-
const envTtl =
|
|
212
|
+
const envTtl = env3.SPFN_AUTH_SESSION_TTL;
|
|
151
213
|
if (envTtl) {
|
|
152
214
|
return parseDuration(envTtl);
|
|
153
215
|
}
|
|
@@ -155,7 +217,7 @@ function getSessionTtl(override) {
|
|
|
155
217
|
}
|
|
156
218
|
|
|
157
219
|
// src/nextjs/session-helpers.ts
|
|
158
|
-
import { env as
|
|
220
|
+
import { env as env4 } from "@spfn/auth/config";
|
|
159
221
|
import { logger } from "@spfn/core/logger";
|
|
160
222
|
async function saveSession(data, options) {
|
|
161
223
|
let maxAge;
|
|
@@ -173,6 +235,13 @@ async function saveSession(data, options) {
|
|
|
173
235
|
path: "/",
|
|
174
236
|
maxAge
|
|
175
237
|
});
|
|
238
|
+
cookieStore.set(COOKIE_NAMES.CSRF, await deriveCsrfToken(data.keyId), {
|
|
239
|
+
httpOnly: false,
|
|
240
|
+
secure: process.env.NODE_ENV === "production",
|
|
241
|
+
sameSite: "lax",
|
|
242
|
+
path: "/",
|
|
243
|
+
maxAge
|
|
244
|
+
});
|
|
176
245
|
}
|
|
177
246
|
async function getSession() {
|
|
178
247
|
const cookieStore = await cookies();
|
|
@@ -197,9 +266,10 @@ async function clearSession() {
|
|
|
197
266
|
const cookieStore = await cookies();
|
|
198
267
|
cookieStore.delete(COOKIE_NAMES.SESSION);
|
|
199
268
|
cookieStore.delete(COOKIE_NAMES.SESSION_KEY_ID);
|
|
269
|
+
cookieStore.delete(COOKIE_NAMES.CSRF);
|
|
200
270
|
}
|
|
201
271
|
async function getPendingSessionKey() {
|
|
202
|
-
const secret =
|
|
272
|
+
const secret = env4.SPFN_AUTH_SESSION_SECRET;
|
|
203
273
|
const encoder = new TextEncoder();
|
|
204
274
|
const data = encoder.encode(`oauth-pending:${secret}`);
|
|
205
275
|
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
|
|
@@ -351,11 +421,47 @@ async function RequirePermission({
|
|
|
351
421
|
return /* @__PURE__ */ jsx3(Fragment3, { children });
|
|
352
422
|
}
|
|
353
423
|
|
|
424
|
+
// src/nextjs/cookie-names.ts
|
|
425
|
+
function sessionCookieNames() {
|
|
426
|
+
return {
|
|
427
|
+
session: COOKIE_NAMES.SESSION,
|
|
428
|
+
keyId: COOKIE_NAMES.SESSION_KEY_ID,
|
|
429
|
+
oauthPending: COOKIE_NAMES.OAUTH_PENDING,
|
|
430
|
+
csrf: COOKIE_NAMES.CSRF
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
function clearSessionCookies(response) {
|
|
434
|
+
for (const name of Object.values(sessionCookieNames())) {
|
|
435
|
+
response.cookies.delete({ name, path: "/" });
|
|
436
|
+
}
|
|
437
|
+
return response;
|
|
438
|
+
}
|
|
439
|
+
|
|
354
440
|
// src/nextjs/oauth-handlers.ts
|
|
355
441
|
import { NextResponse } from "next/server";
|
|
356
442
|
import { cookies as cookies2 } from "next/headers.js";
|
|
357
|
-
import { env as
|
|
443
|
+
import { env as env5 } from "@spfn/core/config";
|
|
358
444
|
import { logger as logger2 } from "@spfn/core/logger";
|
|
445
|
+
|
|
446
|
+
// src/lib/return-path.ts
|
|
447
|
+
var URL_STRIPPED_CHARACTER = /[\t\n\r]/;
|
|
448
|
+
function isSafeReturnPath(returnPath) {
|
|
449
|
+
if (!returnPath.startsWith("/")) {
|
|
450
|
+
return false;
|
|
451
|
+
}
|
|
452
|
+
if (returnPath.startsWith("//") || returnPath.includes("\\")) {
|
|
453
|
+
return false;
|
|
454
|
+
}
|
|
455
|
+
if (returnPath.includes("..") || URL_STRIPPED_CHARACTER.test(returnPath)) {
|
|
456
|
+
return false;
|
|
457
|
+
}
|
|
458
|
+
return !/^\/[^/?#]*:/.test(returnPath);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// src/nextjs/oauth-handlers.ts
|
|
462
|
+
function safeReturnUrl(requested, defaultRedirect) {
|
|
463
|
+
return requested && isSafeReturnPath(requested) ? requested : defaultRedirect;
|
|
464
|
+
}
|
|
359
465
|
function createOAuthCallbackHandler(options) {
|
|
360
466
|
const defaultRedirect = options?.defaultRedirectUrl || "/";
|
|
361
467
|
const errorRedirect = options?.errorRedirectUrl || "/auth/error";
|
|
@@ -363,7 +469,7 @@ function createOAuthCallbackHandler(options) {
|
|
|
363
469
|
const searchParams = request.nextUrl.searchParams;
|
|
364
470
|
const userId = searchParams.get("userId");
|
|
365
471
|
const keyId = searchParams.get("keyId");
|
|
366
|
-
const returnUrl = searchParams.get("returnUrl")
|
|
472
|
+
const returnUrl = safeReturnUrl(searchParams.get("returnUrl"), defaultRedirect);
|
|
367
473
|
const error = searchParams.get("error");
|
|
368
474
|
if (error) {
|
|
369
475
|
const errorUrl = new URL(errorRedirect, request.url);
|
|
@@ -397,14 +503,21 @@ function createOAuthCallbackHandler(options) {
|
|
|
397
503
|
const response = NextResponse.redirect(redirectUrl);
|
|
398
504
|
response.cookies.set(COOKIE_NAMES.SESSION, sessionToken, {
|
|
399
505
|
httpOnly: true,
|
|
400
|
-
secure:
|
|
506
|
+
secure: env5.NODE_ENV === "production",
|
|
401
507
|
sameSite: "lax",
|
|
402
508
|
maxAge: ttl,
|
|
403
509
|
path: "/"
|
|
404
510
|
});
|
|
405
511
|
response.cookies.set(COOKIE_NAMES.SESSION_KEY_ID, keyId, {
|
|
406
512
|
httpOnly: true,
|
|
407
|
-
secure:
|
|
513
|
+
secure: env5.NODE_ENV === "production",
|
|
514
|
+
sameSite: "lax",
|
|
515
|
+
maxAge: ttl,
|
|
516
|
+
path: "/"
|
|
517
|
+
});
|
|
518
|
+
response.cookies.set(COOKIE_NAMES.CSRF, await deriveCsrfToken(keyId), {
|
|
519
|
+
httpOnly: false,
|
|
520
|
+
secure: env5.NODE_ENV === "production",
|
|
408
521
|
sameSite: "lax",
|
|
409
522
|
maxAge: ttl,
|
|
410
523
|
path: "/"
|
|
@@ -421,13 +534,260 @@ function createOAuthCallbackHandler(options) {
|
|
|
421
534
|
}
|
|
422
535
|
};
|
|
423
536
|
}
|
|
537
|
+
|
|
538
|
+
// src/nextjs/oauth2-authorize-handlers.ts
|
|
539
|
+
import { cookies as cookies3 } from "next/headers.js";
|
|
540
|
+
import { NextResponse as NextResponse2 } from "next/server";
|
|
541
|
+
import { authApi as authApi2 } from "@spfn/auth";
|
|
542
|
+
import { logger as logger3 } from "@spfn/core/logger";
|
|
543
|
+
var AUTHORIZE_PARAMETERS = [
|
|
544
|
+
"client_id",
|
|
545
|
+
"redirect_uri",
|
|
546
|
+
"code_challenge",
|
|
547
|
+
"code_challenge_method",
|
|
548
|
+
"resource",
|
|
549
|
+
"scope",
|
|
550
|
+
"state"
|
|
551
|
+
];
|
|
552
|
+
var NON_REDIRECTABLE = /* @__PURE__ */ new Set(["unknown_client", "redirect_uri_mismatch"]);
|
|
553
|
+
var FORM_CONTENT_TYPES = ["application/x-www-form-urlencoded", "multipart/form-data"];
|
|
554
|
+
function escapeHtml(value) {
|
|
555
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
556
|
+
}
|
|
557
|
+
function authorizeFields(read) {
|
|
558
|
+
const fields = {};
|
|
559
|
+
for (const name of AUTHORIZE_PARAMETERS) {
|
|
560
|
+
const value = read(name);
|
|
561
|
+
if (value) {
|
|
562
|
+
fields[name] = value;
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
return fields;
|
|
566
|
+
}
|
|
567
|
+
function screen(status, body) {
|
|
568
|
+
return new NextResponse2(body, {
|
|
569
|
+
status,
|
|
570
|
+
headers: {
|
|
571
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
572
|
+
"Content-Security-Policy": "frame-ancestors 'none'",
|
|
573
|
+
"Cache-Control": "no-store"
|
|
574
|
+
}
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
function refusalScreen(status, heading, message) {
|
|
578
|
+
return screen(status, [
|
|
579
|
+
"<!DOCTYPE html>",
|
|
580
|
+
'<html lang="en"><head><meta charset="utf-8"><title>Authorization request refused</title></head>',
|
|
581
|
+
`<body><h1>${heading}</h1><p>${message}</p></body></html>`
|
|
582
|
+
].join("\n"));
|
|
583
|
+
}
|
|
584
|
+
function unknownClientScreen() {
|
|
585
|
+
return refusalScreen(
|
|
586
|
+
400,
|
|
587
|
+
"Unrecognized application",
|
|
588
|
+
"The application that sent you here is not registered with this service, so the request cannot be completed. Nothing was shared."
|
|
589
|
+
);
|
|
590
|
+
}
|
|
591
|
+
function redirectMismatchScreen() {
|
|
592
|
+
return refusalScreen(
|
|
593
|
+
400,
|
|
594
|
+
"Address not recognized",
|
|
595
|
+
"The application asked for the authorization to be returned to an address it never registered. Nothing was shared, and you were not sent there."
|
|
596
|
+
);
|
|
597
|
+
}
|
|
598
|
+
function unavailableScreen() {
|
|
599
|
+
return refusalScreen(
|
|
600
|
+
500,
|
|
601
|
+
"Authorization unavailable",
|
|
602
|
+
"This authorization request could not be checked. Nothing was shared. Please try again."
|
|
603
|
+
);
|
|
604
|
+
}
|
|
605
|
+
function noSessionScreen() {
|
|
606
|
+
return refusalScreen(
|
|
607
|
+
403,
|
|
608
|
+
"Sign-in required",
|
|
609
|
+
"This authorization request needs a signed-in session and yours is not available. Start the request again from the application."
|
|
610
|
+
);
|
|
611
|
+
}
|
|
612
|
+
function redirect4(url) {
|
|
613
|
+
return NextResponse2.redirect(url, { status: 302, headers: { "Cache-Control": "no-store" } });
|
|
614
|
+
}
|
|
615
|
+
function safeUrl(value) {
|
|
616
|
+
try {
|
|
617
|
+
return new URL(value);
|
|
618
|
+
} catch {
|
|
619
|
+
return null;
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
function loginRedirect(request, loginPath) {
|
|
623
|
+
const returnPath = `${request.nextUrl.pathname}${request.nextUrl.search}`;
|
|
624
|
+
if (!isSafeReturnPath(returnPath)) {
|
|
625
|
+
return refusalScreen(
|
|
626
|
+
400,
|
|
627
|
+
"Malformed authorization request",
|
|
628
|
+
"This authorization request cannot be signed in to. Start it again from the application."
|
|
629
|
+
);
|
|
630
|
+
}
|
|
631
|
+
const url = new URL(loginPath, request.url);
|
|
632
|
+
url.searchParams.set("returnUrl", returnPath);
|
|
633
|
+
return redirect4(url);
|
|
634
|
+
}
|
|
635
|
+
function detailsOf(thrown) {
|
|
636
|
+
const error = thrown;
|
|
637
|
+
return error?.details ?? error?.response?.error?.details ?? error?.response?.details ?? {};
|
|
638
|
+
}
|
|
639
|
+
function statusOf(thrown) {
|
|
640
|
+
const error = thrown;
|
|
641
|
+
return Number(error?.status ?? error?.statusCode ?? 0);
|
|
642
|
+
}
|
|
643
|
+
function refusalRedirect(details) {
|
|
644
|
+
const { error, redirectUri, state } = details;
|
|
645
|
+
if (typeof error !== "string" || NON_REDIRECTABLE.has(error) || typeof redirectUri !== "string") {
|
|
646
|
+
return null;
|
|
647
|
+
}
|
|
648
|
+
const url = safeUrl(redirectUri);
|
|
649
|
+
if (!url) {
|
|
650
|
+
return null;
|
|
651
|
+
}
|
|
652
|
+
url.searchParams.set("error", error);
|
|
653
|
+
if (typeof state === "string") {
|
|
654
|
+
url.searchParams.set("state", state);
|
|
655
|
+
}
|
|
656
|
+
return redirect4(url);
|
|
657
|
+
}
|
|
658
|
+
function answerRefusal(thrown, onStaleSession) {
|
|
659
|
+
const status = statusOf(thrown);
|
|
660
|
+
if (status === 401) {
|
|
661
|
+
return onStaleSession();
|
|
662
|
+
}
|
|
663
|
+
const details = detailsOf(thrown);
|
|
664
|
+
if (details.error === "unknown_client") {
|
|
665
|
+
return unknownClientScreen();
|
|
666
|
+
}
|
|
667
|
+
if (details.error === "redirect_uri_mismatch") {
|
|
668
|
+
return redirectMismatchScreen();
|
|
669
|
+
}
|
|
670
|
+
const redirectable = refusalRedirect(details);
|
|
671
|
+
if (redirectable) {
|
|
672
|
+
return redirectable;
|
|
673
|
+
}
|
|
674
|
+
logger3.error("OAuth2 consent request could not be answered", { status });
|
|
675
|
+
return unavailableScreen();
|
|
676
|
+
}
|
|
677
|
+
function hiddenFields(fields, csrfToken) {
|
|
678
|
+
return [...Object.entries(fields), ["csrf", csrfToken]].map(([name, value]) => `<input type="hidden" name="${escapeHtml(name)}" value="${escapeHtml(value)}">`).join("\n ");
|
|
679
|
+
}
|
|
680
|
+
function defaultRender(view) {
|
|
681
|
+
const scopes = view.scopes.map((scope) => `<li><strong>${escapeHtml(scope.name)}</strong> \u2014 ${escapeHtml(scope.description)}</li>`).join("\n ");
|
|
682
|
+
return `<!DOCTYPE html>
|
|
683
|
+
<html lang="en">
|
|
684
|
+
<head><meta charset="utf-8"><title>Authorize ${escapeHtml(view.clientName)}</title></head>
|
|
685
|
+
<body>
|
|
686
|
+
<h1>Authorize ${escapeHtml(view.clientName)}</h1>
|
|
687
|
+
<p><strong>${escapeHtml(view.clientName)}</strong> is asking to act on your behalf at
|
|
688
|
+
<code>${escapeHtml(view.resource)}</code>. The authorization would be returned to
|
|
689
|
+
<code>${escapeHtml(view.redirectHost)}</code>.</p>
|
|
690
|
+
<h2>It is asking for</h2>
|
|
691
|
+
<ul>
|
|
692
|
+
${scopes}
|
|
693
|
+
</ul>
|
|
694
|
+
<form method="post">
|
|
695
|
+
${hiddenFields(view.fields, view.csrfToken)}
|
|
696
|
+
<button type="submit" name="decision" value="approve">Approve</button>
|
|
697
|
+
<button type="submit" name="decision" value="deny">Deny</button>
|
|
698
|
+
</form>
|
|
699
|
+
</body>
|
|
700
|
+
</html>`;
|
|
701
|
+
}
|
|
702
|
+
async function csrfCookie() {
|
|
703
|
+
const cookieStore = await cookies3();
|
|
704
|
+
return cookieStore.get(sessionCookieNames().csrf)?.value ?? null;
|
|
705
|
+
}
|
|
706
|
+
async function renderConsent(request, options) {
|
|
707
|
+
const csrfToken = await csrfCookie();
|
|
708
|
+
if (!csrfToken) {
|
|
709
|
+
return loginRedirect(request, options.loginPath);
|
|
710
|
+
}
|
|
711
|
+
const fields = authorizeFields((name) => request.nextUrl.searchParams.get(name));
|
|
712
|
+
const described = await authApi2.getOAuth2Authorize.call({ query: fields });
|
|
713
|
+
const render = options.render ?? defaultRender;
|
|
714
|
+
return screen(200, render({ ...described, fields, csrfToken }));
|
|
715
|
+
}
|
|
716
|
+
async function refuseUnverifiedPost(form) {
|
|
717
|
+
const presented = form.get("csrf");
|
|
718
|
+
const expected = await csrfCookie();
|
|
719
|
+
if (!expected || !matchesCsrfToken(expected, typeof presented === "string" ? presented : null)) {
|
|
720
|
+
return refusalScreen(
|
|
721
|
+
403,
|
|
722
|
+
"Request could not be verified",
|
|
723
|
+
"This form did not carry a valid token for your session. Start the authorization again from the application."
|
|
724
|
+
);
|
|
725
|
+
}
|
|
726
|
+
return null;
|
|
727
|
+
}
|
|
728
|
+
function isFormPost(request) {
|
|
729
|
+
const contentType = request.headers.get("content-type") ?? "";
|
|
730
|
+
return FORM_CONTENT_TYPES.some((type) => contentType.startsWith(type));
|
|
731
|
+
}
|
|
732
|
+
async function recordDecision(fields, decision) {
|
|
733
|
+
const body = { ...fields, approve: decision === "approve" };
|
|
734
|
+
const issued = await authApi2.createOAuth2AuthorizationCode.call({ body });
|
|
735
|
+
const url = safeUrl(issued.redirectUri);
|
|
736
|
+
if (!url) {
|
|
737
|
+
return unavailableScreen();
|
|
738
|
+
}
|
|
739
|
+
url.searchParams.set("code", issued.code);
|
|
740
|
+
if (issued.state !== void 0) {
|
|
741
|
+
url.searchParams.set("state", issued.state);
|
|
742
|
+
}
|
|
743
|
+
return redirect4(url);
|
|
744
|
+
}
|
|
745
|
+
function createOAuth2AuthorizeHandlers(options) {
|
|
746
|
+
async function GET(request) {
|
|
747
|
+
if (!await getSession()) {
|
|
748
|
+
return loginRedirect(request, options.loginPath);
|
|
749
|
+
}
|
|
750
|
+
try {
|
|
751
|
+
return await renderConsent(request, options);
|
|
752
|
+
} catch (error) {
|
|
753
|
+
return answerRefusal(error, () => loginRedirect(request, options.loginPath));
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
async function POST(request) {
|
|
757
|
+
if (!await getSession()) {
|
|
758
|
+
return noSessionScreen();
|
|
759
|
+
}
|
|
760
|
+
if (!isFormPost(request)) {
|
|
761
|
+
return refusalScreen(
|
|
762
|
+
415,
|
|
763
|
+
"Unsupported request",
|
|
764
|
+
"The consent form is submitted as a form. Start the authorization again from the application."
|
|
765
|
+
);
|
|
766
|
+
}
|
|
767
|
+
const form = await request.formData();
|
|
768
|
+
const refusal = await refuseUnverifiedPost(form);
|
|
769
|
+
if (refusal) {
|
|
770
|
+
return refusal;
|
|
771
|
+
}
|
|
772
|
+
try {
|
|
773
|
+
const fields = authorizeFields((name) => form.get(name));
|
|
774
|
+
return await recordDecision(fields, form.get("decision"));
|
|
775
|
+
} catch (error) {
|
|
776
|
+
return answerRefusal(error, noSessionScreen);
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
return { GET, POST };
|
|
780
|
+
}
|
|
424
781
|
export {
|
|
425
782
|
RequireAuth,
|
|
426
783
|
RequirePermission,
|
|
427
784
|
RequireRole,
|
|
428
785
|
clearPendingSession,
|
|
429
786
|
clearSession,
|
|
787
|
+
clearSessionCookies,
|
|
788
|
+
createOAuth2AuthorizeHandlers,
|
|
430
789
|
createOAuthCallbackHandler,
|
|
790
|
+
escapeHtml,
|
|
431
791
|
getAuthSessionData,
|
|
432
792
|
getPendingSession,
|
|
433
793
|
getSession,
|
|
@@ -435,8 +795,10 @@ export {
|
|
|
435
795
|
getUserRole,
|
|
436
796
|
hasAnyPermission,
|
|
437
797
|
hasAnyRole,
|
|
798
|
+
isSafeReturnPath,
|
|
438
799
|
saveSession,
|
|
439
800
|
sealPendingSession,
|
|
801
|
+
sessionCookieNames,
|
|
440
802
|
unsealPendingSession
|
|
441
803
|
};
|
|
442
804
|
//# sourceMappingURL=server.js.map
|