Skip to content

Commit afb35d7

Browse files
nicknisidandorman
andauthored
Allow for configurable Session Storage (#40)
* add configuration option for session storage Use a promise to ensure the value is set * fix tests * rename cookie to sessionStorage * adjust build to minimum supported version * fixes and adjustments * Update src/sessionStorage.spec.ts Co-authored-by: Dan Dorman <[email protected]> * fix typo * allow for configurable cookie name * go deeper on testing session storage configuration * remove unused import * fix issue where cookieName isn't always used, fix types --------- Co-authored-by: Dan Dorman <[email protected]>
1 parent 9bf0ec3 commit afb35d7

13 files changed

+380
-94
lines changed

src/authkit-callback-route.spec.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
createAuthWithCodeResponse,
77
assertIsResponse,
88
} from './test-utils/test-helpers.js';
9+
import { configureSessionStorage } from './sessionStorage.js';
910

1011
// Mock dependencies
1112
jest.mock('../src/workos.js', () => ({
@@ -25,6 +26,7 @@ describe('authLoader', () => {
2526
beforeAll(() => {
2627
// Silence console.error during tests
2728
jest.spyOn(console, 'error').mockImplementation(() => {});
29+
configureSessionStorage();
2830
});
2931

3032
beforeEach(async () => {

src/authkit-callback-route.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,12 @@ import { HandleAuthOptions } from './interfaces.js';
22
import { WORKOS_CLIENT_ID } from './env-variables.js';
33
import { workos } from './workos.js';
44
import { encryptSession } from './session.js';
5-
import { getSession, commitSession, cookieName } from './cookie.js';
5+
import { getSessionStorage } from './sessionStorage.js';
66
import { redirect, json, LoaderFunctionArgs } from '@remix-run/node';
77

88
export function authLoader(options: HandleAuthOptions = {}) {
99
return async function loader({ request }: LoaderFunctionArgs) {
10+
const { getSession, commitSession, cookieName } = await getSessionStorage();
1011
const { returnPathname: returnPathnameOption = '/', onSuccess } = options;
1112

1213
const url = new URL(request.url);

src/cookie.spec.ts

Lines changed: 0 additions & 38 deletions
This file was deleted.

src/cookie.ts

Lines changed: 0 additions & 26 deletions
This file was deleted.

src/env-variables.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ const WORKOS_API_HOSTNAME = getOptionalEnvVariable('WORKOS_API_HOSTNAME');
2020
const WORKOS_API_HTTPS = getOptionalEnvVariable('WORKOS_API_HTTPS');
2121
const WORKOS_API_PORT = getOptionalEnvVariable('WORKOS_API_PORT');
2222
const WORKOS_COOKIE_MAX_AGE = getOptionalEnvVariable('WORKOS_COOKIE_MAX_AGE');
23+
const WORKOS_COOKIE_NAME = getOptionalEnvVariable('WORKOS_COOKIE_NAME');
2324

2425
if (WORKOS_COOKIE_PASSWORD.length < 32) {
2526
throw new Error('WORKOS_COOKIE_PASSWORD must be at least 32 characters long');
@@ -34,4 +35,5 @@ export {
3435
WORKOS_API_HTTPS,
3536
WORKOS_API_PORT,
3637
WORKOS_COOKIE_MAX_AGE,
38+
WORKOS_COOKIE_NAME,
3739
};

src/index.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
1+
import { getSignInUrl, getSignUpUrl, signOut } from './auth.js';
12
import { authLoader } from './authkit-callback-route.js';
23
import { authkitLoader } from './session.js';
3-
import { getSignInUrl, getSignUpUrl, signOut } from './auth.js';
44

55
export {
66
authLoader,
77
//
8+
authkitLoader,
9+
//
810
getSignInUrl,
911
getSignUpUrl,
1012
signOut,
11-
//
12-
authkitLoader,
1313
};

src/interfaces.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { OauthTokens, User } from '@workos-inc/node';
1+
import type { SessionStorage, SessionIdStorageStrategy } from '@remix-run/node';
2+
import type { OauthTokens, User } from '@workos-inc/node';
23

34
export interface HandleAuthOptions {
45
returnPathname?: string;
@@ -39,10 +40,19 @@ export interface GetAuthURLOptions {
3940
returnPathname?: string;
4041
}
4142

42-
export interface AuthKitLoaderOptions {
43+
export type AuthKitLoaderOptions = {
4344
ensureSignedIn?: boolean;
4445
debug?: boolean;
45-
}
46+
} & (
47+
| {
48+
storage?: never;
49+
cookie?: SessionIdStorageStrategy['cookie'];
50+
}
51+
| {
52+
storage: SessionStorage;
53+
cookie: SessionIdStorageStrategy['cookie'];
54+
}
55+
);
4656

4757
export interface AuthorizedData {
4858
user: User;

src/session.spec.ts

Lines changed: 38 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,28 @@
11
import { LoaderFunctionArgs, Session as RemixSession, redirect } from '@remix-run/node';
2+
import { AuthenticationResponse } from '@workos-inc/node';
23
import * as ironSession from 'iron-session';
3-
import * as cookie from './cookie.js';
4+
import * as jose from 'jose';
5+
import {
6+
configureSessionStorage as configureSessionStorageMock,
7+
getSessionStorage as getSessionStorageMock,
8+
} from './sessionStorage.js';
49
import { WORKOS_COOKIE_PASSWORD } from './env-variables.js';
510
import { Session } from './interfaces.js';
6-
import { encryptSession, terminateSession, authkitLoader } from './session.js';
7-
import { workos } from './workos.js';
8-
import * as jose from 'jose';
9-
import { AuthenticationResponse } from '@workos-inc/node';
11+
import { authkitLoader, encryptSession, terminateSession } from './session.js';
1012
import { assertIsResponse } from './test-utils/test-helpers.js';
13+
import { workos } from './workos.js';
1114

12-
const getSession = jest.mocked(cookie.getSession);
13-
const destroySession = jest.mocked(cookie.destroySession);
1415
const unsealData = jest.mocked(ironSession.unsealData);
1516
const sealData = jest.mocked(ironSession.sealData);
1617
const getLogoutUrl = jest.mocked(workos.userManagement.getLogoutUrl);
1718
const authenticateWithRefreshToken = jest.mocked(workos.userManagement.authenticateWithRefreshToken);
19+
const getSessionStorage = jest.mocked(getSessionStorageMock);
20+
const configureSessionStorage = jest.mocked(configureSessionStorageMock);
1821
const jwtVerify = jest.mocked(jose.jwtVerify);
1922

20-
jest.mock('./cookie', () => ({
21-
getSession: jest.fn(),
22-
destroySession: jest.fn().mockResolvedValue('destroyed-session-cookie'),
23-
commitSession: jest.fn(),
23+
jest.mock('./sessionStorage.js', () => ({
24+
configureSessionStorage: jest.fn(),
25+
getSessionStorage: jest.fn(),
2426
}));
2527

2628
jest.mock('./workos.js', () => ({
@@ -68,6 +70,30 @@ describe('session', () => {
6870
}),
6971
});
7072

73+
let getSession: jest.Mock;
74+
let destroySession: jest.Mock;
75+
let commitSession: jest.Mock;
76+
77+
beforeEach(async () => {
78+
getSession = jest.fn();
79+
destroySession = jest.fn().mockResolvedValue('destroyed-session-cookie');
80+
commitSession = jest.fn();
81+
82+
getSessionStorage.mockResolvedValue({
83+
cookieName: 'wos-cookie',
84+
getSession,
85+
destroySession,
86+
commitSession,
87+
});
88+
89+
configureSessionStorage.mockResolvedValue({
90+
cookieName: 'wos-cookie',
91+
getSession,
92+
destroySession,
93+
commitSession,
94+
});
95+
});
96+
7197
describe('encryptSession', () => {
7298
it('should encrypt session data with correct parameters', async () => {
7399
const mockSession = {
@@ -371,7 +397,7 @@ describe('session', () => {
371397
};
372398
unsealData.mockResolvedValue(expiredSessionData);
373399
sealData.mockResolvedValue('new-encrypted-jwt');
374-
(cookie.commitSession as jest.Mock).mockResolvedValue('new-session-cookie');
400+
commitSession.mockResolvedValue('new-session-cookie');
375401

376402
// Token verification fails
377403
jwtVerify.mockRejectedValue(new Error('Token expired'));

src/session.ts

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,19 @@
1-
import { json, redirect } from '@remix-run/node';
21
import type { LoaderFunctionArgs, SessionData, TypedResponse } from '@remix-run/node';
3-
import { WORKOS_CLIENT_ID, WORKOS_COOKIE_PASSWORD } from './env-variables.js';
4-
import type { AccessToken, AuthorizedData, UnauthorizedData, AuthKitLoaderOptions, Session } from './interfaces.js';
5-
import { getSession, destroySession, commitSession } from './cookie.js';
2+
import { json, redirect } from '@remix-run/node';
3+
import { WORKOS_CLIENT_ID, WORKOS_COOKIE_NAME, WORKOS_COOKIE_PASSWORD } from './env-variables.js';
64
import { getAuthorizationUrl } from './get-authorization-url.js';
5+
import type { AccessToken, AuthKitLoaderOptions, AuthorizedData, Session, UnauthorizedData } from './interfaces.js';
76
import { workos } from './workos.js';
87

98
import { sealData, unsealData } from 'iron-session';
10-
import { jwtVerify, createRemoteJWKSet, decodeJwt } from 'jose';
9+
import { createRemoteJWKSet, decodeJwt, jwtVerify } from 'jose';
10+
import { configureSessionStorage, getSessionStorage } from './sessionStorage.js';
1111

1212
const JWKS = createRemoteJWKSet(new URL(workos.userManagement.getJwksUrl(WORKOS_CLIENT_ID)));
1313

1414
async function updateSession(request: Request, debug: boolean) {
1515
const session = await getSessionFromCookie(request.headers.get('Cookie') as string);
16+
const { commitSession, getSession, destroySession } = await getSessionStorage();
1617

1718
// If no session, just continue
1819
if (!session) {
@@ -115,7 +116,15 @@ async function authkitLoader<Data = unknown>(
115116
options: AuthKitLoaderOptions = {},
116117
) {
117118
const loader = typeof loaderOrOptions === 'function' ? loaderOrOptions : undefined;
118-
const { ensureSignedIn = false, debug = false } = typeof loaderOrOptions === 'object' ? loaderOrOptions : options;
119+
const {
120+
ensureSignedIn = false,
121+
debug = false,
122+
storage,
123+
cookie,
124+
} = typeof loaderOrOptions === 'object' ? loaderOrOptions : options;
125+
126+
const cookieName = cookie?.name ?? WORKOS_COOKIE_NAME ?? 'wos-session';
127+
const { getSession, destroySession } = await configureSessionStorage({ storage, cookieName });
119128

120129
const { request } = loaderArgs;
121130
const session = await updateSession(request, debug);
@@ -215,6 +224,7 @@ async function handleAuthLoader(
215224
}
216225

217226
async function terminateSession(request: Request) {
227+
const { getSession, destroySession } = await getSessionStorage();
218228
const encryptedSession = await getSession(request.headers.get('Cookie'));
219229
const { accessToken } = (await getSessionFromCookie(
220230
request.headers.get('Cookie') as string,
@@ -257,6 +267,7 @@ function getClaimsFromAccessToken(accessToken: string) {
257267
}
258268

259269
async function getSessionFromCookie(cookie: string, session?: SessionData) {
270+
const { getSession } = await getSessionStorage();
260271
if (!session) {
261272
session = await getSession(cookie);
262273
}
@@ -286,4 +297,4 @@ function getReturnPathname(url: string): string {
286297
return `${newUrl.pathname}${newUrl.searchParams.size > 0 ? '?' + newUrl.searchParams.toString() : ''}`;
287298
}
288299

289-
export { encryptSession, terminateSession, authkitLoader };
300+
export { authkitLoader, encryptSession, terminateSession };

0 commit comments

Comments
 (0)