@workos-inc/authkit-nextjs 3.0.0-beta.1 → 3.0.0
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 +276 -102
- package/dist/esm/actions.js +35 -4
- package/dist/esm/actions.js.map +1 -1
- package/dist/esm/auth.js +51 -20
- package/dist/esm/auth.js.map +1 -1
- package/dist/esm/authkit-callback-route.js +82 -93
- package/dist/esm/authkit-callback-route.js.map +1 -1
- package/dist/esm/components/authkit-provider.js +36 -15
- package/dist/esm/components/authkit-provider.js.map +1 -1
- package/dist/esm/components/impersonation.js +17 -15
- package/dist/esm/components/impersonation.js.map +1 -1
- package/dist/esm/components/min-max-button.js +1 -1
- package/dist/esm/components/min-max-button.js.map +1 -1
- package/dist/esm/components/tokenStore.js +28 -19
- package/dist/esm/components/tokenStore.js.map +1 -1
- package/dist/esm/components/useAccessToken.js +1 -1
- package/dist/esm/components/useAccessToken.js.map +1 -1
- package/dist/esm/components/useTokenClaims.js +1 -1
- package/dist/esm/components/useTokenClaims.js.map +1 -1
- package/dist/esm/cookie.js +16 -5
- package/dist/esm/cookie.js.map +1 -1
- package/dist/esm/env-variables.js +6 -6
- package/dist/esm/env-variables.js.map +1 -1
- package/dist/esm/errors.js +36 -0
- package/dist/esm/errors.js.map +1 -0
- package/dist/esm/get-authorization-url.js +51 -12
- package/dist/esm/get-authorization-url.js.map +1 -1
- package/dist/esm/index.js +5 -2
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/interfaces.js +7 -1
- package/dist/esm/interfaces.js.map +1 -1
- package/dist/esm/middleware-helpers.js +102 -0
- package/dist/esm/middleware-helpers.js.map +1 -0
- package/dist/esm/middleware.js +3 -1
- package/dist/esm/middleware.js.map +1 -1
- package/dist/esm/pkce.js +38 -0
- package/dist/esm/pkce.js.map +1 -0
- package/dist/esm/session.js +73 -35
- package/dist/esm/session.js.map +1 -1
- package/dist/esm/test-helpers.js +1 -1
- package/dist/esm/test-helpers.js.map +1 -1
- package/dist/esm/types/actions.d.ts +34 -5
- package/dist/esm/types/auth.d.ts +7 -15
- package/dist/esm/types/components/authkit-provider.d.ts +6 -2
- package/dist/esm/types/components/impersonation.d.ts +2 -1
- package/dist/esm/types/cookie.d.ts +8 -0
- package/dist/esm/types/env-variables.d.ts +2 -1
- package/dist/esm/types/errors.d.ts +15 -0
- package/dist/esm/types/get-authorization-url.d.ts +2 -2
- package/dist/esm/types/index.d.ts +5 -2
- package/dist/esm/types/interfaces.d.ts +12 -0
- package/dist/esm/types/jwt.d.ts +9 -9
- package/dist/esm/types/middleware-helpers.d.ts +27 -0
- package/dist/esm/types/middleware.d.ts +3 -1
- package/dist/esm/types/pkce.d.ts +12 -0
- package/dist/esm/types/session.d.ts +1 -1
- package/dist/esm/types/utils.d.ts +5 -0
- package/dist/esm/types/validate-api-key.d.ts +1 -0
- package/dist/esm/types/workos.d.ts +1 -1
- package/dist/esm/utils.js +10 -2
- package/dist/esm/utils.js.map +1 -1
- package/dist/esm/validate-api-key.js +16 -0
- package/dist/esm/validate-api-key.js.map +1 -0
- package/dist/esm/workos.js +1 -1
- package/package.json +32 -34
- package/src/actions.spec.ts +94 -17
- package/src/actions.ts +44 -5
- package/src/auth.spec.ts +60 -29
- package/src/auth.ts +55 -41
- package/src/authkit-callback-route.spec.ts +310 -58
- package/src/authkit-callback-route.ts +106 -103
- package/src/components/authkit-provider.spec.tsx +264 -70
- package/src/components/authkit-provider.tsx +40 -15
- package/src/components/button.spec.tsx +4 -6
- package/src/components/impersonation.spec.tsx +152 -35
- package/src/components/impersonation.tsx +37 -30
- package/src/components/min-max-button.spec.tsx +2 -1
- package/src/components/tokenStore.spec.ts +59 -44
- package/src/components/tokenStore.ts +11 -3
- package/src/components/useAccessToken.spec.tsx +82 -83
- package/src/components/useTokenClaims.spec.tsx +23 -22
- package/src/cookie.spec.ts +14 -9
- package/src/cookie.ts +29 -0
- package/src/env-variables.ts +2 -0
- package/src/errors.spec.ts +108 -0
- package/src/errors.ts +46 -0
- package/src/get-authorization-url.spec.ts +170 -15
- package/src/get-authorization-url.ts +69 -23
- package/src/index.ts +20 -2
- package/src/interfaces.ts +15 -0
- package/src/jwt.ts +9 -9
- package/src/middleware-helpers.spec.ts +238 -0
- package/src/middleware-helpers.ts +134 -0
- package/src/middleware.spec.ts +25 -0
- package/src/middleware.ts +4 -1
- package/src/pkce.spec.ts +125 -0
- package/src/pkce.ts +42 -0
- package/src/session.spec.ts +87 -89
- package/src/session.ts +91 -27
- package/src/test-helpers.ts +1 -1
- package/src/utils.spec.ts +14 -31
- package/src/utils.ts +9 -0
- package/src/validate-api-key.spec.ts +111 -0
- package/src/validate-api-key.ts +19 -0
- package/src/workos.spec.ts +2 -2
- package/src/workos.ts +1 -1
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { State } from './interfaces.js';
|
|
2
|
+
export declare const PKCE_COOKIE_NAME = "wos-auth-verifier";
|
|
3
|
+
/**
|
|
4
|
+
* Set the PKCE verifier cookie in server action context.
|
|
5
|
+
* In middleware context, callers must set the cookie via Set-Cookie headers instead.
|
|
6
|
+
*/
|
|
7
|
+
export declare function setPKCECookie(sealedState: string): Promise<void>;
|
|
8
|
+
/**
|
|
9
|
+
* Read and unseal the auth cookie containing PKCE code verifier and OAuth state.
|
|
10
|
+
* Throws if the cookie is not in the required state
|
|
11
|
+
*/
|
|
12
|
+
export declare function getStateFromPKCECookieValue(cookieValue: string): Promise<State>;
|
|
@@ -3,7 +3,7 @@ import { NextRequest } from 'next/server';
|
|
|
3
3
|
import { AuthkitMiddlewareAuth, AuthkitOptions, AuthkitResponse, NoUserInfo, Session, UserInfo } from './interfaces.js';
|
|
4
4
|
import type { AuthenticationResponse } from '@workos-inc/node';
|
|
5
5
|
declare function encryptSession(session: Session): Promise<string>;
|
|
6
|
-
declare function updateSessionMiddleware(request: NextRequest, debug: boolean, middlewareAuth: AuthkitMiddlewareAuth, redirectUri: string, signUpPaths: string[], eagerAuth?: boolean): Promise<
|
|
6
|
+
declare function updateSessionMiddleware(request: NextRequest, debug: boolean, middlewareAuth: AuthkitMiddlewareAuth, redirectUri: string, signUpPaths: string[], eagerAuth?: boolean): Promise<import("next/server.js").NextResponse<unknown>>;
|
|
7
7
|
declare function updateSession(request: NextRequest, options?: AuthkitOptions): Promise<AuthkitResponse>;
|
|
8
8
|
declare function refreshSession(options: {
|
|
9
9
|
organizationId?: string;
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sets cache prevention headers to prevent CDN/proxy caching.
|
|
3
|
+
* @param headers - The Headers object to set the cache prevention headers on.
|
|
4
|
+
*/
|
|
5
|
+
export declare function setCachePreventionHeaders(headers: Headers): void;
|
|
1
6
|
export declare function redirectWithFallback(redirectUri: string, headers?: Headers): Response;
|
|
2
7
|
export declare function errorResponseWithFallback(errorBody: {
|
|
3
8
|
error: {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function validateApiKey(): Promise<import("@workos-inc/node").ValidateApiKeyResponse>;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { WorkOS } from '@workos-inc/node';
|
|
2
|
-
export declare const VERSION = "2.
|
|
2
|
+
export declare const VERSION = "2.14.0";
|
|
3
3
|
/**
|
|
4
4
|
* Create a WorkOS instance with the provided API key and options.
|
|
5
5
|
* If an instance already exists, it returns the existing instance.
|
package/dist/esm/utils.js
CHANGED
|
@@ -1,17 +1,25 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server';
|
|
2
|
+
/**
|
|
3
|
+
* Sets cache prevention headers to prevent CDN/proxy caching.
|
|
4
|
+
* @param headers - The Headers object to set the cache prevention headers on.
|
|
5
|
+
*/
|
|
6
|
+
export function setCachePreventionHeaders(headers) {
|
|
7
|
+
headers.set('Cache-Control', 'private, no-cache, no-store, must-revalidate, max-age=0');
|
|
8
|
+
headers.set('x-middleware-cache', 'no-cache');
|
|
9
|
+
}
|
|
2
10
|
export function redirectWithFallback(redirectUri, headers) {
|
|
3
11
|
const newHeaders = headers ? new Headers(headers) : new Headers();
|
|
4
12
|
newHeaders.set('Location', redirectUri);
|
|
5
13
|
// Fall back to standard Response if NextResponse is not available.
|
|
6
14
|
// This is to support Next.js 13.
|
|
7
|
-
return
|
|
15
|
+
return NextResponse?.redirect
|
|
8
16
|
? NextResponse.redirect(redirectUri, { headers })
|
|
9
17
|
: new Response(null, { status: 307, headers: newHeaders });
|
|
10
18
|
}
|
|
11
19
|
export function errorResponseWithFallback(errorBody) {
|
|
12
20
|
// Fall back to standard Response if NextResponse is not available.
|
|
13
21
|
// This is to support Next.js 13.
|
|
14
|
-
return
|
|
22
|
+
return NextResponse?.json
|
|
15
23
|
? NextResponse.json(errorBody, { status: 500 })
|
|
16
24
|
: new Response(JSON.stringify(errorBody), {
|
|
17
25
|
status: 500,
|
package/dist/esm/utils.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"utils.js","sourceRoot":"","sources":["../../src/utils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE3C,MAAM,UAAU,oBAAoB,CAAC,WAAmB,EAAE,OAAiB;IACzE,MAAM,UAAU,GAAG,OAAO,CAAC,CAAC,CAAC,IAAI,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,IAAI,OAAO,EAAE,CAAC;IAClE,UAAU,CAAC,GAAG,CAAC,UAAU,EAAE,WAAW,CAAC,CAAC;IAExC,mEAAmE;IACnE,iCAAiC;IACjC,OAAO,
|
|
1
|
+
{"version":3,"file":"utils.js","sourceRoot":"","sources":["../../src/utils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE3C;;;GAGG;AACH,MAAM,UAAU,yBAAyB,CAAC,OAAgB;IACxD,OAAO,CAAC,GAAG,CAAC,eAAe,EAAE,yDAAyD,CAAC,CAAC;IACxF,OAAO,CAAC,GAAG,CAAC,oBAAoB,EAAE,UAAU,CAAC,CAAC;AAChD,CAAC;AAED,MAAM,UAAU,oBAAoB,CAAC,WAAmB,EAAE,OAAiB;IACzE,MAAM,UAAU,GAAG,OAAO,CAAC,CAAC,CAAC,IAAI,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,IAAI,OAAO,EAAE,CAAC;IAClE,UAAU,CAAC,GAAG,CAAC,UAAU,EAAE,WAAW,CAAC,CAAC;IAExC,mEAAmE;IACnE,iCAAiC;IACjC,OAAO,YAAY,EAAE,QAAQ;QAC3B,CAAC,CAAC,YAAY,CAAC,QAAQ,CAAC,WAAW,EAAE,EAAE,OAAO,EAAE,CAAC;QACjD,CAAC,CAAC,IAAI,QAAQ,CAAC,IAAI,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,OAAO,EAAE,UAAU,EAAE,CAAC,CAAC;AAC/D,CAAC;AAED,MAAM,UAAU,yBAAyB,CAAC,SAA8D;IACtG,mEAAmE;IACnE,iCAAiC;IACjC,OAAO,YAAY,EAAE,IAAI;QACvB,CAAC,CAAC,YAAY,CAAC,IAAI,CAAC,SAAS,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC;QAC/C,CAAC,CAAC,IAAI,QAAQ,CAAC,IAAI,CAAC,SAAS,CAAC,SAAS,CAAC,EAAE;YACtC,MAAM,EAAE,GAAG;YACX,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;SAChD,CAAC,CAAC;AACT,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,IAAI,CAAI,EAAW;IACjC,IAAI,MAAM,GAAG,KAAK,CAAC;IACnB,IAAI,MAAS,CAAC;IACd,OAAO,GAAG,EAAE;QACV,IAAI,CAAC,MAAM,EAAE,CAAC;YACZ,MAAM,GAAG,EAAE,EAAE,CAAC;YACd,MAAM,GAAG,IAAI,CAAC;QAChB,CAAC;QACD,OAAO,MAAM,CAAC;IAChB,CAAC,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
'use server';
|
|
2
|
+
import { getWorkOS } from './workos.js';
|
|
3
|
+
import { headers } from 'next/headers';
|
|
4
|
+
export async function validateApiKey() {
|
|
5
|
+
const headersList = await headers();
|
|
6
|
+
const authorizationHeader = headersList.get('authorization');
|
|
7
|
+
if (!authorizationHeader) {
|
|
8
|
+
return { apiKey: null };
|
|
9
|
+
}
|
|
10
|
+
const value = authorizationHeader.match(/Bearer\s+(.*)/i)?.[1];
|
|
11
|
+
if (!value) {
|
|
12
|
+
return { apiKey: null };
|
|
13
|
+
}
|
|
14
|
+
return getWorkOS().apiKeys.validateApiKey({ value });
|
|
15
|
+
}
|
|
16
|
+
//# sourceMappingURL=validate-api-key.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validate-api-key.js","sourceRoot":"","sources":["../../src/validate-api-key.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;AAEb,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAEvC,MAAM,CAAC,KAAK,UAAU,cAAc;IAClC,MAAM,WAAW,GAAG,MAAM,OAAO,EAAE,CAAC;IACpC,MAAM,mBAAmB,GAAG,WAAW,CAAC,GAAG,CAAC,eAAe,CAAC,CAAC;IAC7D,IAAI,CAAC,mBAAmB,EAAE,CAAC;QACzB,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC;IAC1B,CAAC;IAED,MAAM,KAAK,GAAG,mBAAmB,CAAC,KAAK,CAAC,gBAAgB,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;IAC/D,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC;IAC1B,CAAC;IAED,OAAO,SAAS,EAAE,CAAC,OAAO,CAAC,cAAc,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC;AACvD,CAAC"}
|
package/dist/esm/workos.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { WorkOS } from '@workos-inc/node';
|
|
2
2
|
import { WORKOS_API_HOSTNAME, WORKOS_API_KEY, WORKOS_API_HTTPS, WORKOS_API_PORT } from './env-variables.js';
|
|
3
3
|
import { lazy } from './utils.js';
|
|
4
|
-
export const VERSION = '2.
|
|
4
|
+
export const VERSION = '2.14.0';
|
|
5
5
|
const options = {
|
|
6
6
|
apiHostname: WORKOS_API_HOSTNAME,
|
|
7
7
|
https: WORKOS_API_HTTPS ? WORKOS_API_HTTPS === 'true' : true,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@workos-inc/authkit-nextjs",
|
|
3
|
-
"version": "3.0.0
|
|
3
|
+
"version": "3.0.0",
|
|
4
4
|
"description": "Authentication and session helpers for using WorkOS & AuthKit with Next.js",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"type": "module",
|
|
@@ -22,47 +22,33 @@
|
|
|
22
22
|
"import": "./dist/esm/index.js"
|
|
23
23
|
}
|
|
24
24
|
},
|
|
25
|
-
"scripts": {
|
|
26
|
-
"clean": "rm -rf dist",
|
|
27
|
-
"prebuild": "npm run clean",
|
|
28
|
-
"build": "tsc --project tsconfig.json",
|
|
29
|
-
"prepublishOnly": "npm run lint",
|
|
30
|
-
"lint": "eslint \"src/**/*.ts*\"",
|
|
31
|
-
"test": "jest",
|
|
32
|
-
"test:watch": "jest --watch",
|
|
33
|
-
"prettier": "prettier \"src/**/*.{js,ts,tsx}\" --check",
|
|
34
|
-
"format": "prettier \"src/**/*.{js,ts,tsx}\" --write",
|
|
35
|
-
"type-check": "tsc --project tsconfig.json --noEmit"
|
|
36
|
-
},
|
|
37
25
|
"dependencies": {
|
|
38
|
-
"@workos-inc/node": "^8.
|
|
39
|
-
"iron-session": "^8.0.
|
|
40
|
-
"jose": "^5.
|
|
41
|
-
"path-to-regexp": "^6.
|
|
26
|
+
"@workos-inc/node": "^8.9.0",
|
|
27
|
+
"iron-session": "^8.0.4",
|
|
28
|
+
"jose": "^5.10.0",
|
|
29
|
+
"path-to-regexp": "^6.3.0",
|
|
30
|
+
"valibot": "^1.2.0"
|
|
42
31
|
},
|
|
43
32
|
"peerDependencies": {
|
|
44
|
-
"next": "^13.5.9 || ^14.2.26 || ^15.2.3",
|
|
33
|
+
"next": "^13.5.9 || ^14.2.26 || ^15.2.3 || ^16",
|
|
45
34
|
"react": "^18.0 || ^19.0.0",
|
|
46
35
|
"react-dom": "^18.0 || ^19.0.0"
|
|
47
36
|
},
|
|
48
37
|
"devDependencies": {
|
|
49
|
-
"@testing-library/jest-dom": "^6.
|
|
50
|
-
"@testing-library/react": "^16.
|
|
51
|
-
"@types/
|
|
52
|
-
"@types/node": "^20.11.28",
|
|
38
|
+
"@testing-library/jest-dom": "^6.9.1",
|
|
39
|
+
"@testing-library/react": "^16.3.2",
|
|
40
|
+
"@types/node": "^20.19.35",
|
|
53
41
|
"@types/react": "18.2.67",
|
|
54
42
|
"@types/react-dom": "18.2.22",
|
|
55
|
-
"
|
|
56
|
-
"
|
|
57
|
-
"
|
|
58
|
-
"
|
|
59
|
-
"
|
|
60
|
-
"
|
|
61
|
-
"
|
|
62
|
-
"
|
|
63
|
-
"
|
|
64
|
-
"typescript": "5.4.2",
|
|
65
|
-
"typescript-eslint": "^7.2.0"
|
|
43
|
+
"@vitest/coverage-v8": "^3.2.4",
|
|
44
|
+
"jsdom": "^26.1.0",
|
|
45
|
+
"next": "^16.2.1",
|
|
46
|
+
"oxfmt": "^0.42.0",
|
|
47
|
+
"oxlint": "^1.57.0",
|
|
48
|
+
"tslib": "^2.8.1",
|
|
49
|
+
"typescript": "6.0.2",
|
|
50
|
+
"typescript-eslint": "^7.18.0",
|
|
51
|
+
"vitest": "^3.2.4"
|
|
66
52
|
},
|
|
67
53
|
"license": "MIT",
|
|
68
54
|
"homepage": "https://github.com/workos/authkit-nextjs#readme",
|
|
@@ -72,5 +58,17 @@
|
|
|
72
58
|
},
|
|
73
59
|
"bugs": {
|
|
74
60
|
"url": "https://github.com/workos/authkit-nextjs/issues"
|
|
61
|
+
},
|
|
62
|
+
"scripts": {
|
|
63
|
+
"clean": "rm -rf dist",
|
|
64
|
+
"prebuild": "pnpm run clean",
|
|
65
|
+
"build": "tsc --project tsconfig.app.json",
|
|
66
|
+
"test": "vitest run",
|
|
67
|
+
"test:watch": "vitest",
|
|
68
|
+
"test:coverage": "vitest run --coverage",
|
|
69
|
+
"lint": "oxlint",
|
|
70
|
+
"format": "oxfmt .",
|
|
71
|
+
"format:check": "oxfmt --check .",
|
|
72
|
+
"typecheck": "tsc --project tsconfig.app.json --noEmit && tsc --project tsconfig.test.json --noEmit"
|
|
75
73
|
}
|
|
76
|
-
}
|
|
74
|
+
}
|
package/src/actions.spec.ts
CHANGED
|
@@ -11,28 +11,51 @@ import {
|
|
|
11
11
|
import { signOut, switchToOrganization } from './auth.js';
|
|
12
12
|
import { getWorkOS } from '../src/workos.js';
|
|
13
13
|
import { withAuth, refreshSession } from '../src/session.js';
|
|
14
|
+
import { getAuthorizationUrl } from '../src/get-authorization-url.js';
|
|
14
15
|
|
|
15
|
-
|
|
16
|
-
signOut:
|
|
17
|
-
switchToOrganization:
|
|
16
|
+
vi.mock('../src/auth.js', () => ({
|
|
17
|
+
signOut: vi.fn().mockResolvedValue(true),
|
|
18
|
+
switchToOrganization: vi.fn().mockResolvedValue({ organizationId: 'org_123' }),
|
|
18
19
|
}));
|
|
19
20
|
|
|
20
|
-
const fakeWorkosInstance = {
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
const { fakeWorkosInstance } = vi.hoisted(() => ({
|
|
22
|
+
fakeWorkosInstance: {
|
|
23
|
+
organizations: {
|
|
24
|
+
getOrganization: vi.fn().mockResolvedValue({ id: 'org_123', name: 'Test Org' }),
|
|
25
|
+
},
|
|
23
26
|
},
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
getWorkOS:
|
|
27
|
+
}));
|
|
28
|
+
vi.mock('../src/workos.js', () => ({
|
|
29
|
+
getWorkOS: vi.fn(() => fakeWorkosInstance),
|
|
30
|
+
}));
|
|
31
|
+
|
|
32
|
+
vi.mock('../src/session.js', () => ({
|
|
33
|
+
withAuth: vi.fn().mockResolvedValue({ user: 'testUser', accessToken: 'access_token' }),
|
|
34
|
+
refreshSession: vi.fn().mockResolvedValue({ user: 'testUser', accessToken: 'refreshed_token' }),
|
|
27
35
|
}));
|
|
28
36
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
refreshSession: jest.fn().mockResolvedValue({ session: 'newSession', accessToken: 'refreshed_token' }),
|
|
37
|
+
vi.mock('../src/get-authorization-url.js', () => ({
|
|
38
|
+
getAuthorizationUrl: vi.fn().mockResolvedValue('https://api.workos.com/authorize?...'),
|
|
32
39
|
}));
|
|
33
40
|
|
|
34
41
|
describe('actions', () => {
|
|
35
42
|
const workos = getWorkOS();
|
|
43
|
+
|
|
44
|
+
beforeEach(() => {
|
|
45
|
+
vi.clearAllMocks();
|
|
46
|
+
// Restore default mock implementations
|
|
47
|
+
vi.mocked(withAuth).mockResolvedValue({
|
|
48
|
+
user: 'testUser' as never,
|
|
49
|
+
sessionId: 'session_123',
|
|
50
|
+
accessToken: 'access_token',
|
|
51
|
+
});
|
|
52
|
+
vi.mocked(refreshSession).mockResolvedValue({
|
|
53
|
+
user: 'testUser' as never,
|
|
54
|
+
sessionId: 'session_123',
|
|
55
|
+
accessToken: 'refreshed_token',
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
36
59
|
describe('checkSessionAction', () => {
|
|
37
60
|
it('should return true for authenticated users', async () => {
|
|
38
61
|
const result = await checkSessionAction();
|
|
@@ -60,16 +83,52 @@ describe('actions', () => {
|
|
|
60
83
|
it('should return auth details', async () => {
|
|
61
84
|
const result = await getAuthAction();
|
|
62
85
|
expect(withAuth).toHaveBeenCalled();
|
|
63
|
-
expect(result).toEqual({ user: 'testUser' });
|
|
86
|
+
expect(result).toEqual({ user: 'testUser', sessionId: 'session_123' });
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('should not pass ensureSignedIn to withAuth', async () => {
|
|
90
|
+
await getAuthAction({ ensureSignedIn: true });
|
|
91
|
+
expect(withAuth).toHaveBeenCalledWith();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('should return signInUrl when ensureSignedIn is true and no user', async () => {
|
|
95
|
+
vi.mocked(withAuth).mockResolvedValueOnce({ user: null });
|
|
96
|
+
const result = await getAuthAction({ ensureSignedIn: true });
|
|
97
|
+
expect(getAuthorizationUrl).toHaveBeenCalledWith({ screenHint: 'sign-in' });
|
|
98
|
+
expect(result).toEqual({ user: null, signInUrl: 'https://api.workos.com/authorize?...' });
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('should not return signInUrl when ensureSignedIn is true and user exists', async () => {
|
|
102
|
+
const result = await getAuthAction({ ensureSignedIn: true });
|
|
103
|
+
expect(getAuthorizationUrl).not.toHaveBeenCalled();
|
|
104
|
+
expect(result).toEqual({ user: 'testUser', sessionId: 'session_123' });
|
|
64
105
|
});
|
|
65
106
|
});
|
|
66
107
|
|
|
67
108
|
describe('refreshAuthAction', () => {
|
|
68
109
|
it('should refresh session', async () => {
|
|
69
|
-
const params = { ensureSignedIn:
|
|
110
|
+
const params = { ensureSignedIn: false, organizationId: 'org_123' };
|
|
70
111
|
const result = await refreshAuthAction(params);
|
|
71
|
-
expect(refreshSession).toHaveBeenCalledWith(
|
|
72
|
-
expect(result).toEqual({
|
|
112
|
+
expect(refreshSession).toHaveBeenCalledWith({ organizationId: 'org_123' });
|
|
113
|
+
expect(result).toEqual({ user: 'testUser', sessionId: 'session_123' });
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('should not pass ensureSignedIn to refreshSession', async () => {
|
|
117
|
+
await refreshAuthAction({ ensureSignedIn: true, organizationId: 'org_123' });
|
|
118
|
+
expect(refreshSession).toHaveBeenCalledWith({ organizationId: 'org_123' });
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('should return signInUrl when ensureSignedIn is true and no user', async () => {
|
|
122
|
+
vi.mocked(refreshSession).mockResolvedValueOnce({ user: null });
|
|
123
|
+
const result = await refreshAuthAction({ ensureSignedIn: true });
|
|
124
|
+
expect(getAuthorizationUrl).toHaveBeenCalledWith({ screenHint: 'sign-in' });
|
|
125
|
+
expect(result).toEqual({ user: null, signInUrl: 'https://api.workos.com/authorize?...' });
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('should not return signInUrl when ensureSignedIn is true and user exists', async () => {
|
|
129
|
+
const result = await refreshAuthAction({ ensureSignedIn: true });
|
|
130
|
+
expect(getAuthorizationUrl).not.toHaveBeenCalled();
|
|
131
|
+
expect(result).toEqual({ user: 'testUser', sessionId: 'session_123' });
|
|
73
132
|
});
|
|
74
133
|
});
|
|
75
134
|
|
|
@@ -94,7 +153,25 @@ describe('actions', () => {
|
|
|
94
153
|
it('should refresh access token', async () => {
|
|
95
154
|
const result = await refreshAccessTokenAction();
|
|
96
155
|
expect(refreshSession).toHaveBeenCalled();
|
|
97
|
-
expect(result).toEqual('refreshed_token');
|
|
156
|
+
expect(result).toEqual({ accessToken: 'refreshed_token' });
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('should catch errors and return a generic error instead of throwing', async () => {
|
|
160
|
+
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
161
|
+
vi.mocked(refreshSession).mockRejectedValueOnce(new Error('Rate limit exceeded'));
|
|
162
|
+
const result = await refreshAccessTokenAction();
|
|
163
|
+
expect(result).toEqual({ accessToken: undefined, error: 'Failed to refresh access token' });
|
|
164
|
+
expect(warnSpy).toHaveBeenCalledWith('Failed to refresh access token:', 'Rate limit exceeded');
|
|
165
|
+
warnSpy.mockRestore();
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('should handle non-Error objects in catch', async () => {
|
|
169
|
+
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
170
|
+
vi.mocked(refreshSession).mockRejectedValueOnce('string error');
|
|
171
|
+
const result = await refreshAccessTokenAction();
|
|
172
|
+
expect(result).toEqual({ accessToken: undefined, error: 'Failed to refresh access token' });
|
|
173
|
+
expect(warnSpy).toHaveBeenCalledWith('Failed to refresh access token:', 'string error');
|
|
174
|
+
warnSpy.mockRestore();
|
|
98
175
|
});
|
|
99
176
|
});
|
|
100
177
|
});
|
package/src/actions.ts
CHANGED
|
@@ -3,8 +3,14 @@
|
|
|
3
3
|
import { signOut, switchToOrganization } from './auth.js';
|
|
4
4
|
import { NoUserInfo, UserInfo, SwitchToOrganizationOptions } from './interfaces.js';
|
|
5
5
|
import { refreshSession, withAuth } from './session.js';
|
|
6
|
+
import { getAuthorizationUrl } from './get-authorization-url.js';
|
|
6
7
|
import { getWorkOS } from './workos.js';
|
|
7
8
|
|
|
9
|
+
export interface RefreshAccessTokenActionResult {
|
|
10
|
+
accessToken: string | undefined;
|
|
11
|
+
error?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
8
14
|
/**
|
|
9
15
|
* This function is used to sanitize the auth object.
|
|
10
16
|
* Remove the accessToken from the auth object as it is not needed on the client side.
|
|
@@ -35,7 +41,19 @@ export const getOrganizationAction = async (organizationId: string) => {
|
|
|
35
41
|
};
|
|
36
42
|
|
|
37
43
|
export const getAuthAction = async (options?: { ensureSignedIn?: boolean }) => {
|
|
38
|
-
|
|
44
|
+
// Never pass ensureSignedIn to withAuth from a server action, because withAuth
|
|
45
|
+
// would call redirect() to an external URL, which causes CORS errors when
|
|
46
|
+
// invoked via a client-side fetch. Instead, return the sign-in URL so the
|
|
47
|
+
// client can redirect via window.location.href.
|
|
48
|
+
const auth = await withAuth();
|
|
49
|
+
const sanitized = sanitize(auth);
|
|
50
|
+
|
|
51
|
+
if (options?.ensureSignedIn && !auth.user) {
|
|
52
|
+
const signInUrl = await getAuthorizationUrl({ screenHint: 'sign-in' });
|
|
53
|
+
return { ...sanitized, signInUrl };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return sanitized;
|
|
39
57
|
};
|
|
40
58
|
|
|
41
59
|
export const refreshAuthAction = async ({
|
|
@@ -45,7 +63,17 @@ export const refreshAuthAction = async ({
|
|
|
45
63
|
ensureSignedIn?: boolean;
|
|
46
64
|
organizationId?: string;
|
|
47
65
|
}) => {
|
|
48
|
-
|
|
66
|
+
// Never pass ensureSignedIn to refreshSession from a server action for the
|
|
67
|
+
// same CORS reason as getAuthAction above.
|
|
68
|
+
const auth = await refreshSession({ organizationId });
|
|
69
|
+
const sanitized = sanitize(auth);
|
|
70
|
+
|
|
71
|
+
if (ensureSignedIn && !auth.user) {
|
|
72
|
+
const signInUrl = await getAuthorizationUrl({ screenHint: 'sign-in' });
|
|
73
|
+
return { ...sanitized, signInUrl };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return sanitized;
|
|
49
77
|
};
|
|
50
78
|
|
|
51
79
|
export const switchToOrganizationAction = async (organizationId: string, options?: SwitchToOrganizationOptions) => {
|
|
@@ -64,8 +92,19 @@ export async function getAccessTokenAction() {
|
|
|
64
92
|
/**
|
|
65
93
|
* This action is used to refresh the access token from the auth object.
|
|
66
94
|
* It is used to fetch the access token from the server.
|
|
95
|
+
*
|
|
96
|
+
* Errors are caught and returned as data rather than thrown, to prevent
|
|
97
|
+
* Next.js from returning 500 responses for server action failures.
|
|
67
98
|
*/
|
|
68
|
-
export async function refreshAccessTokenAction() {
|
|
69
|
-
|
|
70
|
-
|
|
99
|
+
export async function refreshAccessTokenAction(): Promise<RefreshAccessTokenActionResult> {
|
|
100
|
+
try {
|
|
101
|
+
const auth = await refreshSession();
|
|
102
|
+
return { accessToken: auth.accessToken };
|
|
103
|
+
} catch (error) {
|
|
104
|
+
console.warn('Failed to refresh access token:', error instanceof Error ? error.message : String(error));
|
|
105
|
+
return {
|
|
106
|
+
accessToken: undefined,
|
|
107
|
+
error: 'Failed to refresh access token',
|
|
108
|
+
};
|
|
109
|
+
}
|
|
71
110
|
}
|
package/src/auth.spec.ts
CHANGED
|
@@ -1,57 +1,63 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, jest } from '@jest/globals';
|
|
2
|
-
|
|
3
1
|
import { getSignInUrl, getSignUpUrl, signOut, switchToOrganization } from './auth.js';
|
|
4
2
|
import * as session from './session.js';
|
|
5
3
|
import * as cache from 'next/cache';
|
|
6
4
|
import * as workosModule from './workos.js';
|
|
7
5
|
|
|
8
|
-
// These are mocked in
|
|
6
|
+
// These are mocked in vitest.setup.ts
|
|
9
7
|
import { cookies, headers } from 'next/headers';
|
|
10
8
|
import { redirect } from 'next/navigation';
|
|
11
9
|
import { generateSession, generateTestToken } from './test-helpers.js';
|
|
12
10
|
import { sealData } from 'iron-session';
|
|
13
11
|
import { getWorkOS } from './workos.js';
|
|
12
|
+
import { getStateFromPKCECookieValue } from './pkce.js';
|
|
14
13
|
|
|
15
14
|
const workos = getWorkOS();
|
|
16
15
|
|
|
17
|
-
|
|
18
|
-
const actual =
|
|
16
|
+
vi.mock('next/cache', async () => {
|
|
17
|
+
const actual = await vi.importActual<typeof cache>('next/cache');
|
|
19
18
|
return {
|
|
20
19
|
...actual,
|
|
21
|
-
revalidateTag:
|
|
22
|
-
revalidatePath:
|
|
20
|
+
revalidateTag: vi.fn(),
|
|
21
|
+
revalidatePath: vi.fn(),
|
|
23
22
|
};
|
|
24
23
|
});
|
|
25
24
|
|
|
26
25
|
// Create a fake WorkOS instance that will be used only in the "on error" tests
|
|
27
26
|
const fakeWorkosInstance = {
|
|
28
27
|
userManagement: {
|
|
29
|
-
authenticateWithRefreshToken:
|
|
30
|
-
getAuthorizationUrl:
|
|
31
|
-
getJwksUrl:
|
|
32
|
-
getLogoutUrl:
|
|
28
|
+
authenticateWithRefreshToken: vi.fn(),
|
|
29
|
+
getAuthorizationUrl: vi.fn(),
|
|
30
|
+
getJwksUrl: vi.fn(() => 'https://api.workos.com/sso/jwks/client_1234567890'),
|
|
31
|
+
getLogoutUrl: vi.fn(),
|
|
32
|
+
},
|
|
33
|
+
pkce: {
|
|
34
|
+
generate: vi.fn().mockResolvedValue({
|
|
35
|
+
codeVerifier: 'test-code-verifier',
|
|
36
|
+
codeChallenge: 'test-code-challenge',
|
|
37
|
+
codeChallengeMethod: 'S256' as const,
|
|
38
|
+
}),
|
|
33
39
|
},
|
|
34
40
|
};
|
|
35
41
|
|
|
36
|
-
const revalidatePath =
|
|
37
|
-
const revalidateTag =
|
|
42
|
+
const revalidatePath = vi.mocked(cache.revalidatePath);
|
|
43
|
+
const revalidateTag = vi.mocked(cache.revalidateTag);
|
|
38
44
|
// We'll only use these in the "on error" tests
|
|
39
45
|
const authenticateWithRefreshToken = fakeWorkosInstance.userManagement.authenticateWithRefreshToken;
|
|
40
46
|
const getAuthorizationUrl = fakeWorkosInstance.userManagement.getAuthorizationUrl;
|
|
41
47
|
|
|
42
|
-
|
|
43
|
-
const actual =
|
|
48
|
+
vi.mock('../src/session', async () => {
|
|
49
|
+
const actual = await vi.importActual<typeof session>('../src/session');
|
|
44
50
|
|
|
45
51
|
return {
|
|
46
52
|
...actual,
|
|
47
|
-
refreshSession:
|
|
53
|
+
refreshSession: vi.fn(actual.refreshSession),
|
|
48
54
|
};
|
|
49
55
|
});
|
|
50
56
|
|
|
51
57
|
describe('auth.ts', () => {
|
|
52
58
|
beforeEach(async () => {
|
|
53
59
|
// Clear all mocks between tests
|
|
54
|
-
|
|
60
|
+
vi.clearAllMocks();
|
|
55
61
|
|
|
56
62
|
// Reset the cookie store
|
|
57
63
|
const nextCookies = await cookies();
|
|
@@ -76,6 +82,15 @@ describe('auth.ts', () => {
|
|
|
76
82
|
expect(url).toBeDefined();
|
|
77
83
|
expect(() => new URL(url)).not.toThrow();
|
|
78
84
|
});
|
|
85
|
+
|
|
86
|
+
it('should include returnTo as returnPathname in the state parameter', async () => {
|
|
87
|
+
const url = await getSignInUrl({ returnTo: '/dashboard' });
|
|
88
|
+
const parsedUrl = new URL(url);
|
|
89
|
+
const state = parsedUrl.searchParams.get('state');
|
|
90
|
+
expect(state).toBeDefined();
|
|
91
|
+
const decoded = await getStateFromPKCECookieValue(state!);
|
|
92
|
+
expect(decoded.returnPathname).toBe('/dashboard');
|
|
93
|
+
});
|
|
79
94
|
});
|
|
80
95
|
|
|
81
96
|
it('should not include prompt when not specified for getSignInUrl', async () => {
|
|
@@ -103,6 +118,15 @@ describe('auth.ts', () => {
|
|
|
103
118
|
const url = await getSignUpUrl({ prompt: 'consent' });
|
|
104
119
|
expect(url).toContain('prompt=consent');
|
|
105
120
|
});
|
|
121
|
+
|
|
122
|
+
it('should include returnTo as returnPathname in the state parameter', async () => {
|
|
123
|
+
const url = await getSignUpUrl({ returnTo: '/welcome' });
|
|
124
|
+
const parsedUrl = new URL(url);
|
|
125
|
+
const state = parsedUrl.searchParams.get('state');
|
|
126
|
+
expect(state).toBeDefined();
|
|
127
|
+
const decoded = await getStateFromPKCECookieValue(state!);
|
|
128
|
+
expect(decoded.returnPathname).toBe('/welcome');
|
|
129
|
+
});
|
|
106
130
|
});
|
|
107
131
|
|
|
108
132
|
describe('switchToOrganization', () => {
|
|
@@ -135,14 +159,21 @@ describe('auth.ts', () => {
|
|
|
135
159
|
nextHeaders.set('x-url', 'http://localhost/test');
|
|
136
160
|
await generateSession();
|
|
137
161
|
|
|
162
|
+
fakeWorkosInstance.pkce.generate.mockResolvedValue({
|
|
163
|
+
codeVerifier: 'test-code-verifier',
|
|
164
|
+
codeChallenge: 'test-code-challenge',
|
|
165
|
+
codeChallengeMethod: 'S256' as const,
|
|
166
|
+
});
|
|
167
|
+
|
|
138
168
|
// Create a WorkOS-like object that matches what our tests need
|
|
139
169
|
const mockWorkOS = {
|
|
140
170
|
userManagement: fakeWorkosInstance.userManagement,
|
|
171
|
+
pkce: fakeWorkosInstance.pkce,
|
|
141
172
|
// Add minimal properties to satisfy TypeScript
|
|
142
|
-
createHttpClient:
|
|
143
|
-
createWebhookClient:
|
|
144
|
-
createActionsClient:
|
|
145
|
-
createIronSessionProvider:
|
|
173
|
+
createHttpClient: vi.fn(),
|
|
174
|
+
createWebhookClient: vi.fn(),
|
|
175
|
+
createActionsClient: vi.fn(),
|
|
176
|
+
createIronSessionProvider: vi.fn(),
|
|
146
177
|
apiKey: 'test',
|
|
147
178
|
clientId: 'test',
|
|
148
179
|
host: 'test',
|
|
@@ -154,12 +185,12 @@ describe('auth.ts', () => {
|
|
|
154
185
|
|
|
155
186
|
// Apply the mock for these tests only
|
|
156
187
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
157
|
-
|
|
188
|
+
vi.spyOn(workosModule, 'getWorkOS').mockImplementation(() => mockWorkOS as any);
|
|
158
189
|
});
|
|
159
190
|
|
|
160
191
|
afterEach(() => {
|
|
161
192
|
// Restore all mocks after each test
|
|
162
|
-
|
|
193
|
+
vi.restoreAllMocks();
|
|
163
194
|
});
|
|
164
195
|
|
|
165
196
|
it('should redirect to sign in when error is "sso_required"', async () => {
|
|
@@ -245,9 +276,9 @@ describe('auth.ts', () => {
|
|
|
245
276
|
|
|
246
277
|
describe('when given a `returnTo` parameter', () => {
|
|
247
278
|
it('passes the `returnTo` through to the `getLogoutUrl` call', async () => {
|
|
248
|
-
|
|
249
|
-
.
|
|
250
|
-
|
|
279
|
+
vi.spyOn(workos.userManagement, 'getLogoutUrl').mockReturnValue(
|
|
280
|
+
'https://user-management-logout.com/signed-out',
|
|
281
|
+
);
|
|
251
282
|
const mockSession = {
|
|
252
283
|
accessToken: await generateTestToken(),
|
|
253
284
|
sessionId: 'session_123',
|
|
@@ -306,9 +337,9 @@ describe('auth.ts', () => {
|
|
|
306
337
|
|
|
307
338
|
nextCookies.set('wos-session', encryptedSession);
|
|
308
339
|
|
|
309
|
-
|
|
310
|
-
.
|
|
311
|
-
|
|
340
|
+
vi.spyOn(workos.userManagement, 'getLogoutUrl').mockReturnValue(
|
|
341
|
+
'https://api.workos.com/user_management/sessions/logout?session_id=session_123',
|
|
342
|
+
);
|
|
312
343
|
|
|
313
344
|
await signOut();
|
|
314
345
|
|