@spfn/auth 0.3.0-beta.16 → 0.3.0-beta.18
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 +31 -0
- package/dist/index.d.ts +2 -2
- package/dist/{machine-principals-Dn-7BqYZ.d.ts → machine-principals-nrpFSvvB.d.ts} +1 -10
- package/dist/nextjs/api.js +36 -0
- 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 +59 -3
- package/dist/nextjs/server.js +40 -1
- package/dist/nextjs/server.js.map +1 -1
- package/dist/server.d.ts +3 -3
- package/dist/server.js +17 -2
- package/dist/server.js.map +1 -1
- package/dist/{session-DTHahDQ9.d.ts → session-Dfwu5g2W.d.ts} +28 -1
- package/package.json +1 -1
package/dist/nextjs/client.d.ts
CHANGED
|
@@ -25,4 +25,31 @@ interface OAuthCallbackProps {
|
|
|
25
25
|
}
|
|
26
26
|
declare function OAuthCallback({ apiBasePath, loadingComponent, errorComponent, onSuccess, onError, }: OAuthCallbackProps): react_jsx_runtime.JSX.Element | null;
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
/**
|
|
29
|
+
* @spfn/auth - Return-path validation
|
|
30
|
+
*
|
|
31
|
+
* One rule for every flow that hands a caller-supplied destination back to the
|
|
32
|
+
* browser: the verified-email signup link, the password reset link, and the
|
|
33
|
+
* OAuth start/callback seams. Apps that build their own destination before
|
|
34
|
+
* calling an auth route import the same function rather than writing a second
|
|
35
|
+
* rule that drifts from this one.
|
|
36
|
+
*
|
|
37
|
+
* The module imports nothing on purpose — it is part of the client bundle
|
|
38
|
+
* (`@spfn/auth/nextjs/client`), which must not pull server code in behind it.
|
|
39
|
+
*/
|
|
40
|
+
/**
|
|
41
|
+
* Whether a return path can be handed back to the browser.
|
|
42
|
+
*
|
|
43
|
+
* Only a path within the app is allowed. The rejected shapes are the ones that
|
|
44
|
+
* turn a return path into an open redirect: an absolute URL, a protocol-relative
|
|
45
|
+
* `//host` that a browser reads as another origin, a backslash that some
|
|
46
|
+
* browsers normalize into a slash, any `..` traversal, and any character a URL
|
|
47
|
+
* parser strips before parsing (see above).
|
|
48
|
+
*
|
|
49
|
+
* The value is judged exactly as written: nothing is percent-decoded here. A
|
|
50
|
+
* `/a%0d%0a` is therefore a path containing those six literal characters and is
|
|
51
|
+
* accepted — no decoder downstream turns it back into header bytes.
|
|
52
|
+
*/
|
|
53
|
+
declare function isSafeReturnPath(returnPath: string): boolean;
|
|
54
|
+
|
|
55
|
+
export { OAuthCallback, type OAuthCallbackProps, isSafeReturnPath };
|
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,4 @@ interface OAuthCallbackOptions {
|
|
|
314
370
|
*/
|
|
315
371
|
declare function createOAuthCallbackHandler(options?: OAuthCallbackOptions): (request: NextRequest) => Promise<NextResponse>;
|
|
316
372
|
|
|
317
|
-
export { type OAuthCallbackOptions, type PendingSessionData, type PublicSession, RequireAuth, type RequireAuthProps, RequirePermission, type RequirePermissionProps, RequireRole, type RequireRoleProps, type SaveSessionOptions, SessionData, clearPendingSession, clearSession, createOAuthCallbackHandler, getAuthSessionData, getPendingSession, getSession, getUserPermissions, getUserRole, hasAnyPermission, hasAnyRole, saveSession, sealPendingSession, unsealPendingSession };
|
|
373
|
+
export { type OAuthCallbackOptions, type PendingSessionData, type PublicSession, RequireAuth, type RequireAuthProps, RequirePermission, type RequirePermissionProps, RequireRole, type RequireRoleProps, type SaveSessionOptions, type SessionCookieNames, SessionData, clearPendingSession, clearSession, clearSessionCookies, createOAuthCallbackHandler, getAuthSessionData, getPendingSession, getSession, getUserPermissions, getUserRole, hasAnyPermission, hasAnyRole, saveSession, sealPendingSession, sessionCookieNames, unsealPendingSession };
|
package/dist/nextjs/server.js
CHANGED
|
@@ -404,11 +404,47 @@ async function RequirePermission({
|
|
|
404
404
|
return /* @__PURE__ */ jsx3(Fragment3, { children });
|
|
405
405
|
}
|
|
406
406
|
|
|
407
|
+
// src/nextjs/cookie-names.ts
|
|
408
|
+
function sessionCookieNames() {
|
|
409
|
+
return {
|
|
410
|
+
session: COOKIE_NAMES.SESSION,
|
|
411
|
+
keyId: COOKIE_NAMES.SESSION_KEY_ID,
|
|
412
|
+
oauthPending: COOKIE_NAMES.OAUTH_PENDING,
|
|
413
|
+
csrf: COOKIE_NAMES.CSRF
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
function clearSessionCookies(response) {
|
|
417
|
+
for (const name of Object.values(sessionCookieNames())) {
|
|
418
|
+
response.cookies.delete({ name, path: "/" });
|
|
419
|
+
}
|
|
420
|
+
return response;
|
|
421
|
+
}
|
|
422
|
+
|
|
407
423
|
// src/nextjs/oauth-handlers.ts
|
|
408
424
|
import { NextResponse } from "next/server";
|
|
409
425
|
import { cookies as cookies2 } from "next/headers.js";
|
|
410
426
|
import { env as env5 } from "@spfn/core/config";
|
|
411
427
|
import { logger as logger2 } from "@spfn/core/logger";
|
|
428
|
+
|
|
429
|
+
// src/lib/return-path.ts
|
|
430
|
+
var URL_STRIPPED_CHARACTER = /[\t\n\r]/;
|
|
431
|
+
function isSafeReturnPath(returnPath) {
|
|
432
|
+
if (!returnPath.startsWith("/")) {
|
|
433
|
+
return false;
|
|
434
|
+
}
|
|
435
|
+
if (returnPath.startsWith("//") || returnPath.includes("\\")) {
|
|
436
|
+
return false;
|
|
437
|
+
}
|
|
438
|
+
if (returnPath.includes("..") || URL_STRIPPED_CHARACTER.test(returnPath)) {
|
|
439
|
+
return false;
|
|
440
|
+
}
|
|
441
|
+
return !/^\/[^/?#]*:/.test(returnPath);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// src/nextjs/oauth-handlers.ts
|
|
445
|
+
function safeReturnUrl(requested, defaultRedirect) {
|
|
446
|
+
return requested && isSafeReturnPath(requested) ? requested : defaultRedirect;
|
|
447
|
+
}
|
|
412
448
|
function createOAuthCallbackHandler(options) {
|
|
413
449
|
const defaultRedirect = options?.defaultRedirectUrl || "/";
|
|
414
450
|
const errorRedirect = options?.errorRedirectUrl || "/auth/error";
|
|
@@ -416,7 +452,7 @@ function createOAuthCallbackHandler(options) {
|
|
|
416
452
|
const searchParams = request.nextUrl.searchParams;
|
|
417
453
|
const userId = searchParams.get("userId");
|
|
418
454
|
const keyId = searchParams.get("keyId");
|
|
419
|
-
const returnUrl = searchParams.get("returnUrl")
|
|
455
|
+
const returnUrl = safeReturnUrl(searchParams.get("returnUrl"), defaultRedirect);
|
|
420
456
|
const error = searchParams.get("error");
|
|
421
457
|
if (error) {
|
|
422
458
|
const errorUrl = new URL(errorRedirect, request.url);
|
|
@@ -487,6 +523,7 @@ export {
|
|
|
487
523
|
RequireRole,
|
|
488
524
|
clearPendingSession,
|
|
489
525
|
clearSession,
|
|
526
|
+
clearSessionCookies,
|
|
490
527
|
createOAuthCallbackHandler,
|
|
491
528
|
getAuthSessionData,
|
|
492
529
|
getPendingSession,
|
|
@@ -495,8 +532,10 @@ export {
|
|
|
495
532
|
getUserRole,
|
|
496
533
|
hasAnyPermission,
|
|
497
534
|
hasAnyRole,
|
|
535
|
+
isSafeReturnPath,
|
|
498
536
|
saveSession,
|
|
499
537
|
sealPendingSession,
|
|
538
|
+
sessionCookieNames,
|
|
500
539
|
unsealPendingSession
|
|
501
540
|
};
|
|
502
541
|
//# sourceMappingURL=server.js.map
|