@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/client.js
CHANGED
|
@@ -2,7 +2,27 @@
|
|
|
2
2
|
|
|
3
3
|
// src/nextjs/components/oauth-callback.tsx
|
|
4
4
|
import { useEffect, useState } from "react";
|
|
5
|
+
|
|
6
|
+
// src/lib/return-path.ts
|
|
7
|
+
var URL_STRIPPED_CHARACTER = /[\t\n\r]/;
|
|
8
|
+
function isSafeReturnPath(returnPath) {
|
|
9
|
+
if (!returnPath.startsWith("/")) {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
if (returnPath.startsWith("//") || returnPath.includes("\\")) {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
if (returnPath.includes("..") || URL_STRIPPED_CHARACTER.test(returnPath)) {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
return !/^\/[^/?#]*:/.test(returnPath);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// src/nextjs/components/oauth-callback.tsx
|
|
5
22
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
23
|
+
function toSafePath(value) {
|
|
24
|
+
return value && isSafeReturnPath(value) ? value : "/";
|
|
25
|
+
}
|
|
6
26
|
function OAuthCallback({
|
|
7
27
|
apiBasePath = "/api/rpc",
|
|
8
28
|
loadingComponent,
|
|
@@ -18,7 +38,7 @@ function OAuthCallback({
|
|
|
18
38
|
const params = new URLSearchParams(window.location.search);
|
|
19
39
|
const userId = params.get("userId");
|
|
20
40
|
const keyId = params.get("keyId");
|
|
21
|
-
const returnUrl = params.get("returnUrl")
|
|
41
|
+
const returnUrl = toSafePath(params.get("returnUrl"));
|
|
22
42
|
const errorParam = params.get("error");
|
|
23
43
|
if (errorParam) {
|
|
24
44
|
throw new Error(errorParam);
|
|
@@ -46,7 +66,7 @@ function OAuthCallback({
|
|
|
46
66
|
}
|
|
47
67
|
const data = await response.json();
|
|
48
68
|
onSuccess?.(userId);
|
|
49
|
-
window.location.href = data.returnUrl || returnUrl;
|
|
69
|
+
window.location.href = toSafePath(data.returnUrl || returnUrl);
|
|
50
70
|
} catch (err) {
|
|
51
71
|
const message = err instanceof Error ? err.message : "OAuth failed";
|
|
52
72
|
setError(message);
|
|
@@ -75,6 +95,7 @@ function OAuthCallback({
|
|
|
75
95
|
return null;
|
|
76
96
|
}
|
|
77
97
|
export {
|
|
78
|
-
OAuthCallback
|
|
98
|
+
OAuthCallback,
|
|
99
|
+
isSafeReturnPath
|
|
79
100
|
};
|
|
80
101
|
//# sourceMappingURL=client.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/nextjs/components/oauth-callback.tsx"],"sourcesContent":["'use client';\n\n/**\n * OAuthCallback Component\n *\n * OAuth 콜백 페이지용 클라이언트 컴포넌트\n * URL params에서 userId, keyId를 추출하여 oauthFinalize API 호출 후 returnUrl로 리다이렉트\n *\n * @example\n * ```tsx\n * // app/auth/callback/page.tsx\n * export { OAuthCallback as default } from '@spfn/auth/nextjs/client';\n * ```\n */\n\nimport { useEffect, useState } from 'react';\n\nexport interface OAuthCallbackProps\n{\n /**\n * API base path for RPC calls\n * @default '/api/rpc'\n */\n apiBasePath?: string;\n\n /**\n * Custom loading component\n */\n loadingComponent?: React.ReactNode;\n\n /**\n * Custom error component\n */\n errorComponent?: (error: string) => React.ReactNode;\n\n /**\n * Callback after successful OAuth\n */\n onSuccess?: (userId: string) => void;\n\n /**\n * Callback on error\n */\n onError?: (error: string) => void;\n}\n\nexport function OAuthCallback({\n apiBasePath = '/api/rpc',\n loadingComponent,\n errorComponent,\n onSuccess,\n onError,\n}: OAuthCallbackProps)\n{\n const [error, setError] = useState<string | null>(null);\n const [isLoading, setIsLoading] = useState(true);\n\n useEffect(() =>\n {\n async function finalizeOAuth()\n {\n try\n {\n const params = new URLSearchParams(window.location.search);\n const userId = params.get('userId');\n const keyId = params.get('keyId');\n const returnUrl = params.get('returnUrl')
|
|
1
|
+
{"version":3,"sources":["../../src/nextjs/components/oauth-callback.tsx","../../src/lib/return-path.ts"],"sourcesContent":["'use client';\n\n/**\n * OAuthCallback Component\n *\n * OAuth 콜백 페이지용 클라이언트 컴포넌트\n * URL params에서 userId, keyId를 추출하여 oauthFinalize API 호출 후 returnUrl로 리다이렉트\n *\n * @example\n * ```tsx\n * // app/auth/callback/page.tsx\n * export { OAuthCallback as default } from '@spfn/auth/nextjs/client';\n * ```\n */\n\nimport { useEffect, useState } from 'react';\n\nimport { isSafeReturnPath } from '../../lib/return-path';\n\n/**\n * The destination to navigate to, or `/` when the value would leave the app.\n *\n * Both the query parameter and the value echoed by `oauthFinalize` pass through\n * here. A callback URL is something a user can be handed, so neither is trusted\n * to be a path inside the app — without this an absolute URL in `?returnUrl=`\n * would make a genuine login end on someone else's origin.\n */\nfunction toSafePath(value: string | null | undefined): string\n{\n return value && isSafeReturnPath(value) ? value : '/';\n}\n\nexport interface OAuthCallbackProps\n{\n /**\n * API base path for RPC calls\n * @default '/api/rpc'\n */\n apiBasePath?: string;\n\n /**\n * Custom loading component\n */\n loadingComponent?: React.ReactNode;\n\n /**\n * Custom error component\n */\n errorComponent?: (error: string) => React.ReactNode;\n\n /**\n * Callback after successful OAuth\n */\n onSuccess?: (userId: string) => void;\n\n /**\n * Callback on error\n */\n onError?: (error: string) => void;\n}\n\nexport function OAuthCallback({\n apiBasePath = '/api/rpc',\n loadingComponent,\n errorComponent,\n onSuccess,\n onError,\n}: OAuthCallbackProps)\n{\n const [error, setError] = useState<string | null>(null);\n const [isLoading, setIsLoading] = useState(true);\n\n useEffect(() =>\n {\n async function finalizeOAuth()\n {\n try\n {\n const params = new URLSearchParams(window.location.search);\n const userId = params.get('userId');\n const keyId = params.get('keyId');\n const returnUrl = toSafePath(params.get('returnUrl'));\n const errorParam = params.get('error');\n\n // Handle error from backend\n if (errorParam)\n {\n throw new Error(errorParam);\n }\n\n if (!userId || !keyId)\n {\n throw new Error('Missing required parameters');\n }\n\n // Call oauthFinalize API\n const response = await fetch(`${apiBasePath}/oauthFinalize`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n credentials: 'include',\n body: JSON.stringify({\n body: {\n userId,\n keyId,\n returnUrl,\n },\n }),\n });\n\n if (!response.ok)\n {\n const data = await response.json().catch(() => ({}));\n throw new Error(data.message || 'Failed to finalize OAuth');\n }\n\n const data = await response.json();\n\n onSuccess?.(userId);\n\n // Redirect to returnUrl\n window.location.href = toSafePath(data.returnUrl || returnUrl);\n }\n catch (err)\n {\n const message = err instanceof Error ? err.message : 'OAuth failed';\n setError(message);\n setIsLoading(false);\n onError?.(message);\n }\n }\n\n finalizeOAuth();\n }, [apiBasePath, onSuccess, onError]);\n\n if (error)\n {\n if (errorComponent)\n {\n return <>{errorComponent(error)}</>;\n }\n\n return (\n <div style={{ padding: '20px', textAlign: 'center' }}>\n <h2>Authentication Error</h2>\n <p style={{ color: 'red' }}>{error}</p>\n <button onClick={() => window.location.href = '/'}>\n Go Home\n </button>\n </div>\n );\n }\n\n if (isLoading)\n {\n if (loadingComponent)\n {\n return <>{loadingComponent}</>;\n }\n\n return (\n <div style={{ padding: '20px', textAlign: 'center' }}>\n <p>Completing authentication...</p>\n </div>\n );\n }\n\n return null;\n}\n\nexport default OAuthCallback;\n","/**\n * @spfn/auth - Return-path validation\n *\n * One rule for every flow that hands a caller-supplied destination back to the\n * browser: the verified-email signup link, the password reset link, and the\n * OAuth start/callback seams. Apps that build their own destination before\n * calling an auth route import the same function rather than writing a second\n * rule that drifts from this one.\n *\n * The module imports nothing on purpose — it is part of the client bundle\n * (`@spfn/auth/nextjs/client`), which must not pull server code in behind it.\n */\n\n/**\n * The characters a URL parser deletes from anywhere in its input before it reads\n * the input as a URL: ASCII tab, LF and CR (WHATWG URL, \"remove all ASCII tab or\n * newline\"). The rule below reads the value as written, so a value holding one of\n * them is not the value the browser parses — `/<tab>/evil.com` is read as the\n * protocol-relative `//evil.com` and lands on another origin. Refusing the three\n * outright also keeps a raw CR or LF out of any `Location` header the value\n * reaches, which is what would split that header in two.\n */\nconst URL_STRIPPED_CHARACTER = /[\\t\\n\\r]/;\n\n/**\n * Whether a return path can be handed back to the browser.\n *\n * Only a path within the app is allowed. The rejected shapes are the ones that\n * turn a return path into an open redirect: an absolute URL, a protocol-relative\n * `//host` that a browser reads as another origin, a backslash that some\n * browsers normalize into a slash, any `..` traversal, and any character a URL\n * parser strips before parsing (see above).\n *\n * The value is judged exactly as written: nothing is percent-decoded here. A\n * `/a%0d%0a` is therefore a path containing those six literal characters and is\n * accepted — no decoder downstream turns it back into header bytes.\n */\nexport function isSafeReturnPath(returnPath: string): boolean\n{\n if (!returnPath.startsWith('/'))\n {\n return false;\n }\n\n if (returnPath.startsWith('//') || returnPath.includes('\\\\'))\n {\n return false;\n }\n\n if (returnPath.includes('..') || URL_STRIPPED_CHARACTER.test(returnPath))\n {\n return false;\n }\n\n // A path cannot carry a protocol prefix; `/\\thttps:` and friends are caught\n // above, this catches `/foo:bar` forms that some parsers read as an authority.\n return !/^\\/[^/?#]*:/.test(returnPath);\n}\n"],"mappings":";;;AAeA,SAAS,WAAW,gBAAgB;;;ACOpC,IAAM,yBAAyB;AAexB,SAAS,iBAAiB,YACjC;AACI,MAAI,CAAC,WAAW,WAAW,GAAG,GAC9B;AACI,WAAO;AAAA,EACX;AAEA,MAAI,WAAW,WAAW,IAAI,KAAK,WAAW,SAAS,IAAI,GAC3D;AACI,WAAO;AAAA,EACX;AAEA,MAAI,WAAW,SAAS,IAAI,KAAK,uBAAuB,KAAK,UAAU,GACvE;AACI,WAAO;AAAA,EACX;AAIA,SAAO,CAAC,cAAc,KAAK,UAAU;AACzC;;;ADmFmB,wBAIP,YAJO;AAjHnB,SAAS,WAAW,OACpB;AACI,SAAO,SAAS,iBAAiB,KAAK,IAAI,QAAQ;AACtD;AA+BO,SAAS,cAAc;AAAA,EAC1B,cAAc;AAAA,EACd;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACJ,GACA;AACI,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAwB,IAAI;AACtD,QAAM,CAAC,WAAW,YAAY,IAAI,SAAS,IAAI;AAE/C,YAAU,MACV;AACI,mBAAe,gBACf;AACI,UACA;AACI,cAAM,SAAS,IAAI,gBAAgB,OAAO,SAAS,MAAM;AACzD,cAAM,SAAS,OAAO,IAAI,QAAQ;AAClC,cAAM,QAAQ,OAAO,IAAI,OAAO;AAChC,cAAM,YAAY,WAAW,OAAO,IAAI,WAAW,CAAC;AACpD,cAAM,aAAa,OAAO,IAAI,OAAO;AAGrC,YAAI,YACJ;AACI,gBAAM,IAAI,MAAM,UAAU;AAAA,QAC9B;AAEA,YAAI,CAAC,UAAU,CAAC,OAChB;AACI,gBAAM,IAAI,MAAM,6BAA6B;AAAA,QACjD;AAGA,cAAM,WAAW,MAAM,MAAM,GAAG,WAAW,kBAAkB;AAAA,UACzD,QAAQ;AAAA,UACR,SAAS;AAAA,YACL,gBAAgB;AAAA,UACpB;AAAA,UACA,aAAa;AAAA,UACb,MAAM,KAAK,UAAU;AAAA,YACjB,MAAM;AAAA,cACF;AAAA,cACA;AAAA,cACA;AAAA,YACJ;AAAA,UACJ,CAAC;AAAA,QACL,CAAC;AAED,YAAI,CAAC,SAAS,IACd;AACI,gBAAMA,QAAO,MAAM,SAAS,KAAK,EAAE,MAAM,OAAO,CAAC,EAAE;AACnD,gBAAM,IAAI,MAAMA,MAAK,WAAW,0BAA0B;AAAA,QAC9D;AAEA,cAAM,OAAO,MAAM,SAAS,KAAK;AAEjC,oBAAY,MAAM;AAGlB,eAAO,SAAS,OAAO,WAAW,KAAK,aAAa,SAAS;AAAA,MACjE,SACO,KACP;AACI,cAAM,UAAU,eAAe,QAAQ,IAAI,UAAU;AACrD,iBAAS,OAAO;AAChB,qBAAa,KAAK;AAClB,kBAAU,OAAO;AAAA,MACrB;AAAA,IACJ;AAEA,kBAAc;AAAA,EAClB,GAAG,CAAC,aAAa,WAAW,OAAO,CAAC;AAEpC,MAAI,OACJ;AACI,QAAI,gBACJ;AACI,aAAO,gCAAG,yBAAe,KAAK,GAAE;AAAA,IACpC;AAEA,WACI,qBAAC,SAAI,OAAO,EAAE,SAAS,QAAQ,WAAW,SAAS,GAC/C;AAAA,0BAAC,QAAG,kCAAoB;AAAA,MACxB,oBAAC,OAAE,OAAO,EAAE,OAAO,MAAM,GAAI,iBAAM;AAAA,MACnC,oBAAC,YAAO,SAAS,MAAM,OAAO,SAAS,OAAO,KAAK,qBAEnD;AAAA,OACJ;AAAA,EAER;AAEA,MAAI,WACJ;AACI,QAAI,kBACJ;AACI,aAAO,gCAAG,4BAAiB;AAAA,IAC/B;AAEA,WACI,oBAAC,SAAI,OAAO,EAAE,SAAS,QAAQ,WAAW,SAAS,GAC/C,8BAAC,OAAE,0CAA4B,GACnC;AAAA,EAER;AAEA,SAAO;AACX;","names":["data"]}
|
package/dist/nextjs/server.d.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
2
|
import { ReactNode } from 'react';
|
|
3
|
-
import { S as SessionData } from '../session-
|
|
3
|
+
import { S as SessionData } from '../session-Dfwu5g2W.js';
|
|
4
|
+
export { i as isSafeReturnPath } from '../session-Dfwu5g2W.js';
|
|
4
5
|
import { K as KeyAlgorithmType } from '../types-DYyhze28.js';
|
|
5
|
-
import {
|
|
6
|
+
import { NextResponse, NextRequest } from 'next/server';
|
|
6
7
|
|
|
7
8
|
interface RequireAuthProps {
|
|
8
9
|
/**
|
|
@@ -278,6 +279,61 @@ declare function getPendingSession(): Promise<PendingSessionData | null>;
|
|
|
278
279
|
*/
|
|
279
280
|
declare function clearPendingSession(): Promise<void>;
|
|
280
281
|
|
|
282
|
+
/**
|
|
283
|
+
* Session cookie names for Next.js
|
|
284
|
+
*
|
|
285
|
+
* The names carry the `SPFN_PORT` suffix, so they are only knowable at call
|
|
286
|
+
* time — this module is the one place an app reads them from.
|
|
287
|
+
*/
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* The cookie names that make up a browser session
|
|
291
|
+
*/
|
|
292
|
+
interface SessionCookieNames {
|
|
293
|
+
/** Encrypted session data */
|
|
294
|
+
session: string;
|
|
295
|
+
/** Current key ID (for key rotation) */
|
|
296
|
+
keyId: string;
|
|
297
|
+
/** Pending OAuth session — present only mid-flow */
|
|
298
|
+
oauthPending: string;
|
|
299
|
+
/** CSRF token — the only one the browser can read */
|
|
300
|
+
csrf: string;
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* Names of the cookies that make up a browser session
|
|
304
|
+
*
|
|
305
|
+
* Read at call time, never at import: the names carry the `SPFN_PORT` suffix,
|
|
306
|
+
* and an app that spells them itself keeps clearing the old name after a
|
|
307
|
+
* release renames one.
|
|
308
|
+
*
|
|
309
|
+
* @example
|
|
310
|
+
* ```typescript
|
|
311
|
+
* const names = sessionCookieNames();
|
|
312
|
+
* const raw = request.cookies.get(names.session);
|
|
313
|
+
* ```
|
|
314
|
+
*/
|
|
315
|
+
declare function sessionCookieNames(): SessionCookieNames;
|
|
316
|
+
/**
|
|
317
|
+
* Expire every session cookie on a response
|
|
318
|
+
*
|
|
319
|
+
* For the route handler or middleware that answers "the API refused your
|
|
320
|
+
* session" — it empties the jar so the next request arrives anonymous. The
|
|
321
|
+
* path matches the one the setters use, because a delete under a different
|
|
322
|
+
* path leaves the cookie in place. Absent cookies are not an error.
|
|
323
|
+
*
|
|
324
|
+
* @param response - Response to expire the cookies on
|
|
325
|
+
* @returns The same response, so the call chains
|
|
326
|
+
*
|
|
327
|
+
* @example
|
|
328
|
+
* ```typescript
|
|
329
|
+
* export function GET(): NextResponse
|
|
330
|
+
* {
|
|
331
|
+
* return clearSessionCookies(NextResponse.redirect(new URL('/login', request.url)));
|
|
332
|
+
* }
|
|
333
|
+
* ```
|
|
334
|
+
*/
|
|
335
|
+
declare function clearSessionCookies(response: NextResponse): NextResponse;
|
|
336
|
+
|
|
281
337
|
/**
|
|
282
338
|
* OAuth Handlers for Next.js
|
|
283
339
|
*
|
|
@@ -314,4 +370,118 @@ interface OAuthCallbackOptions {
|
|
|
314
370
|
*/
|
|
315
371
|
declare function createOAuthCallbackHandler(options?: OAuthCallbackOptions): (request: NextRequest) => Promise<NextResponse>;
|
|
316
372
|
|
|
317
|
-
|
|
373
|
+
/**
|
|
374
|
+
* @spfn/auth - OAuth 2.1 consent screen (Next.js route handlers)
|
|
375
|
+
*
|
|
376
|
+
* The authorization server lives on the API origin; this is the one piece of it
|
|
377
|
+
* that cannot, because consent is a decision only the signed-in person can make
|
|
378
|
+
* and the session cookie is on the web app. `GET` draws the screen, `POST` takes
|
|
379
|
+
* the answer, and neither of them decides anything: both forward the request to
|
|
380
|
+
* `/_auth/oauth2/authorize`, which validates it against the registration and
|
|
381
|
+
* hands back either what to draw or the refusal to act on.
|
|
382
|
+
*
|
|
383
|
+
* Three rules shape everything below, and each of them is an attack that would
|
|
384
|
+
* otherwise work:
|
|
385
|
+
*
|
|
386
|
+
* - **The only URLs this file redirects to are `loginPath` and a URI the API
|
|
387
|
+
* returned.** The request's own `redirect_uri` is forwarded and never built
|
|
388
|
+
* into a `Location` — an unregistered one is exactly the open redirect the
|
|
389
|
+
* registration check exists to close, and the API is the only side that can
|
|
390
|
+
* tell the two apart.
|
|
391
|
+
* - **Everything interpolated into the page is escaped.** `client_name` arrives
|
|
392
|
+
* from unauthenticated dynamic registration, and `state` and `resource` come
|
|
393
|
+
* from the query string of a link somebody was sent.
|
|
394
|
+
* - **The POST carries its own CSRF token.** The handler's server-side call to
|
|
395
|
+
* the API mints the CSRF header itself and so would always pass; the check
|
|
396
|
+
* that matters is the browser form's, and it is made before the API is called
|
|
397
|
+
* at all.
|
|
398
|
+
*/
|
|
399
|
+
|
|
400
|
+
/** One scope, with the sentence the consent screen reads aloud for it. */
|
|
401
|
+
interface OAuth2ConsentScope {
|
|
402
|
+
name: string;
|
|
403
|
+
description: string;
|
|
404
|
+
}
|
|
405
|
+
/**
|
|
406
|
+
* Everything a consent screen needs, raw and unescaped.
|
|
407
|
+
*
|
|
408
|
+
* A custom `render` receives this and owns the whole body, so it must echo
|
|
409
|
+
* `fields` and `csrfToken` back as hidden inputs: the POST is refused without
|
|
410
|
+
* the token, and the API re-validates the request from the fields rather than
|
|
411
|
+
* trusting what the GET was once shown.
|
|
412
|
+
*
|
|
413
|
+
* Every string here is caller-supplied. Put each one through {@link escapeHtml}.
|
|
414
|
+
*/
|
|
415
|
+
interface OAuth2ConsentView {
|
|
416
|
+
/** Registered name of the client asking. Unauthenticated input. */
|
|
417
|
+
clientName: string;
|
|
418
|
+
/** Host the code would be sent to — the one fact about the client that is checkable. */
|
|
419
|
+
redirectHost: string;
|
|
420
|
+
scopes: OAuth2ConsentScope[];
|
|
421
|
+
/** RFC 8707 target the token would be good against. */
|
|
422
|
+
resource: string;
|
|
423
|
+
/** Every authorize parameter the request carried, verbatim, to echo as hidden inputs. */
|
|
424
|
+
fields: Record<string, string>;
|
|
425
|
+
/** Value the POST's `csrf` field must carry. */
|
|
426
|
+
csrfToken: string;
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* Options for {@link createOAuth2AuthorizeHandlers}
|
|
430
|
+
*/
|
|
431
|
+
interface OAuth2AuthorizeHandlerOptions {
|
|
432
|
+
/**
|
|
433
|
+
* Where to send a visitor with no session, e.g. `/login`
|
|
434
|
+
*
|
|
435
|
+
* The handler appends `?returnUrl=` pointing at this request, so the login
|
|
436
|
+
* lands back on the consent screen with its parameters intact.
|
|
437
|
+
*/
|
|
438
|
+
loginPath: string;
|
|
439
|
+
/**
|
|
440
|
+
* Replace the default consent page body
|
|
441
|
+
*
|
|
442
|
+
* Status, headers and the field set stay the handler's; this owns the HTML.
|
|
443
|
+
*/
|
|
444
|
+
render?: (view: OAuth2ConsentView) => string;
|
|
445
|
+
}
|
|
446
|
+
/** The pair a route file re-exports as `export const { GET, POST } = ...`. */
|
|
447
|
+
interface OAuth2AuthorizeHandlers {
|
|
448
|
+
GET: (request: NextRequest) => Promise<NextResponse>;
|
|
449
|
+
POST: (request: NextRequest) => Promise<NextResponse>;
|
|
450
|
+
}
|
|
451
|
+
/**
|
|
452
|
+
* Escape a string for interpolation into HTML text or a quoted attribute.
|
|
453
|
+
*
|
|
454
|
+
* Exported because a custom `render` needs the same escaping the default body
|
|
455
|
+
* applies: `client_name` comes from unauthenticated dynamic registration, and
|
|
456
|
+
* `state` is whatever was in the link the browser followed.
|
|
457
|
+
*
|
|
458
|
+
* @param value - Raw string
|
|
459
|
+
* @returns The same string with `& < > " '` replaced by entities
|
|
460
|
+
*/
|
|
461
|
+
declare function escapeHtml(value: string): string;
|
|
462
|
+
/**
|
|
463
|
+
* Create the consent screen's route handlers
|
|
464
|
+
*
|
|
465
|
+
* `GET` renders the screen for an `/oauth/authorize` request and `POST` takes
|
|
466
|
+
* the form it submits. Mount both at the path published as
|
|
467
|
+
* `authorization_endpoint` in the authorization server metadata —
|
|
468
|
+
* `/oauth/authorize` unless `authorizationServer.authorizeUrl` says otherwise.
|
|
469
|
+
*
|
|
470
|
+
* Every answer carries `Cache-Control: no-store`; every page also carries
|
|
471
|
+
* `Content-Security-Policy: frame-ancestors 'none'`, because a consent screen
|
|
472
|
+
* that can be framed is a consent screen that can be clickjacked.
|
|
473
|
+
*
|
|
474
|
+
* @param options - Where to send an unauthenticated visitor, and an optional renderer
|
|
475
|
+
* @returns `{ GET, POST }`, ready to re-export from a route file
|
|
476
|
+
*
|
|
477
|
+
* @example
|
|
478
|
+
* ```typescript
|
|
479
|
+
* // app/oauth/authorize/route.ts
|
|
480
|
+
* import { createOAuth2AuthorizeHandlers } from '@spfn/auth/nextjs/server';
|
|
481
|
+
*
|
|
482
|
+
* export const { GET, POST } = createOAuth2AuthorizeHandlers({ loginPath: '/login' });
|
|
483
|
+
* ```
|
|
484
|
+
*/
|
|
485
|
+
declare function createOAuth2AuthorizeHandlers(options: OAuth2AuthorizeHandlerOptions): OAuth2AuthorizeHandlers;
|
|
486
|
+
|
|
487
|
+
export { type OAuth2AuthorizeHandlerOptions, type OAuth2AuthorizeHandlers, type OAuth2ConsentScope, type OAuth2ConsentView, type OAuthCallbackOptions, type PendingSessionData, type PublicSession, RequireAuth, type RequireAuthProps, RequirePermission, type RequirePermissionProps, RequireRole, type RequireRoleProps, type SaveSessionOptions, type SessionCookieNames, SessionData, clearPendingSession, clearSession, clearSessionCookies, createOAuth2AuthorizeHandlers, createOAuthCallbackHandler, escapeHtml, getAuthSessionData, getPendingSession, getSession, getUserPermissions, getUserRole, hasAnyPermission, hasAnyRole, saveSession, sealPendingSession, sessionCookieNames, unsealPendingSession };
|