Skip to content

Commit

Permalink
Fix iron-session worker support (#1091)
Browse files Browse the repository at this point in the history
  • Loading branch information
mattgd authored Jul 30, 2024
1 parent 7248def commit fbc1e31
Show file tree
Hide file tree
Showing 9 changed files with 137 additions and 15 deletions.
9 changes: 8 additions & 1 deletion setup-jest.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
import { enableFetchMocks } from 'jest-fetch-mock';
import { Crypto } from '@peculiar/webcrypto';
import { WorkOS } from './src/workos';
import { WebIronSessionProvider } from './src/common/iron-session/web-iron-session-provider';

enableFetchMocks();

// Assign Node's Crypto to global.crypto if it is not already present
if (!global.crypto) {
global.crypto = new Crypto();
global.crypto = new Crypto();
}

// For tests, we can use the WebIronSessionProvider
WorkOS.prototype.createIronSessionProvider = jest
.fn()
.mockReturnValue(new WebIronSessionProvider());
24 changes: 24 additions & 0 deletions src/common/iron-session/edge-iron-session-provider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { sealData, unsealData } from 'iron-session/edge';
import {
IronSessionProvider,
SealDataOptions,
UnsealedDataType,
} from './iron-session-provider';

/**
* EdgeIronSessionProvider which uses the base iron-session seal/unseal methods.
*/
export class EdgeIronSessionProvider extends IronSessionProvider {
/** @override */
async sealData(data: unknown, options: SealDataOptions): Promise<string> {
return sealData(data, options);
}

/** @override */
async unsealData<T = UnsealedDataType>(
seal: string,
options: SealDataOptions,
): Promise<T> {
return unsealData<T>(seal, options);
}
}
27 changes: 27 additions & 0 deletions src/common/iron-session/iron-session-provider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
export type SealDataOptions = {
password:
| string
| {
[id: string]: string;
};
ttl?: number | undefined;
};

export type UnsealedDataType = Record<string, unknown>;

/**
* Interface encapsulating the sealData/unsealData methods for separate iron-session implementations.
*
* This allows for different implementations of the iron-session library to be used in
* worker/edge vs. regular web environments, which is required because of the different crypto APIs available.
* Once we drop support for Node 16 and upgrade to iron-session 8+, we can remove this abstraction as iron-session 8+
* handles this on its own.
*/
export abstract class IronSessionProvider {
abstract sealData(data: unknown, options: SealDataOptions): Promise<string>;

abstract unsealData<T = UnsealedDataType>(
seal: string,
options: SealDataOptions,
): Promise<T>;
}
24 changes: 24 additions & 0 deletions src/common/iron-session/web-iron-session-provider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { sealData, unsealData } from 'iron-session';
import {
IronSessionProvider,
SealDataOptions,
UnsealedDataType,
} from './iron-session-provider';

/**
* WebIronSessionProvider which uses the base iron-session seal/unseal methods.
*/
export class WebIronSessionProvider extends IronSessionProvider {
/** @override */
async sealData(data: unknown, options: SealDataOptions): Promise<string> {
return sealData(data, options);
}

/** @override */
async unsealData<T = UnsealedDataType>(
seal: string,
options: SealDataOptions,
): Promise<T> {
return unsealData<T>(seal, options);
}
}
2 changes: 1 addition & 1 deletion src/common/net/fetch-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export class FetchHttpClient extends HttpClient implements HttpClientInterface {
fetchFn = globalThis.fetch;
}

this._fetchFn = fetchFn;
this._fetchFn = fetchFn.bind(globalThis);
}

/** @override */
Expand Down
7 changes: 7 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import { NodeHttpClient } from './common/net/node-client';
import { Webhooks } from './webhooks/webhooks';
import { WorkOS } from './workos';
import { WorkOSOptions } from './common/interfaces';
import { WebIronSessionProvider } from './common/iron-session/web-iron-session-provider';
import { IronSessionProvider } from './common/iron-session/iron-session-provider';

export * from './audit-logs/interfaces';
export * from './common/exceptions';
Expand Down Expand Up @@ -57,6 +59,11 @@ class WorkOSNode extends WorkOS {

return new Webhooks(cryptoProvider);
}

/** @override */
createIronSessionProvider(): IronSessionProvider {
return new WebIronSessionProvider();
}
}

export { WorkOSNode as WorkOS };
7 changes: 7 additions & 0 deletions src/index.worker.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { SubtleCryptoProvider } from './common/crypto/subtle-crypto-provider';
import { EdgeIronSessionProvider } from './common/iron-session/edge-iron-session-provider';
import { IronSessionProvider } from './common/iron-session/iron-session-provider';
import { FetchHttpClient } from './common/net/fetch-client';
import { HttpClient } from './common/net/http-client';
import { WorkOSOptions } from './index.worker';
Expand Down Expand Up @@ -37,6 +39,11 @@ class WorkOSWorker extends WorkOS {

return new Webhooks(cryptoProvider);
}

/** @override */
createIronSessionProvider(): IronSessionProvider {
return new EdgeIronSessionProvider();
}
}

export { WorkOSWorker as WorkOS };
40 changes: 28 additions & 12 deletions src/user-management/user-management.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { sealData, unsealData } from 'iron-session';
import { createRemoteJWKSet, decodeJwt, jwtVerify } from 'jose';
import { OauthException } from '../common/exceptions/oauth.exception';
import { fetchAndDeserialize } from '../common/utils/fetch-and-deserialize';
Expand Down Expand Up @@ -136,6 +135,7 @@ import { serializeListUsersOptions } from './serializers/list-users-options.seri
import { deserializeOrganizationMembership } from './serializers/organization-membership.serializer';
import { serializeSendInvitationOptions } from './serializers/send-invitation-options.serializer';
import { serializeUpdateOrganizationMembershipOptions } from './serializers/update-organization-membership-options.serializer';
import { IronSessionProvider } from '../common/iron-session/iron-session-provider';

const toQueryString = (options: Record<string, string | undefined>): string => {
const searchParams = new URLSearchParams();
Expand All @@ -155,7 +155,10 @@ const toQueryString = (options: Record<string, string | undefined>): string => {
export class UserManagement {
private jwks: ReturnType<typeof createRemoteJWKSet> | undefined;

constructor(private readonly workos: WorkOS) {
constructor(
private readonly workos: WorkOS,
private readonly ironSessionProvider: IronSessionProvider,
) {
const { clientId } = workos.options;

// Set the JWKS URL. This is used to verify if the JWT is still valid
Expand Down Expand Up @@ -377,9 +380,13 @@ export class UserManagement {
};
}

const session = await unsealData<SessionCookieData>(sessionData, {
password: cookiePassword,
});
const session =
await this.ironSessionProvider.unsealData<SessionCookieData>(
sessionData,
{
password: cookiePassword,
},
);

if (!session.accessToken) {
return {
Expand Down Expand Up @@ -441,9 +448,13 @@ export class UserManagement {
};
}

const session = await unsealData<SessionCookieData>(sessionData, {
password: cookiePassword,
});
const session =
await this.ironSessionProvider.unsealData<SessionCookieData>(
sessionData,
{
password: cookiePassword,
},
);

if (!session.refreshToken || !session.user) {
return {
Expand Down Expand Up @@ -523,7 +534,9 @@ export class UserManagement {
impersonator: authenticationResponse.impersonator,
};

return sealData(sessionData, { password: cookiePassword });
return this.ironSessionProvider.sealData(sessionData, {
password: cookiePassword,
});
}

async getSessionFromCookie({
Expand All @@ -535,9 +548,12 @@ export class UserManagement {
}

if (sessionData) {
return unsealData<SessionCookieData>(sessionData, {
password: cookiePassword,
});
return this.ironSessionProvider.unsealData<SessionCookieData>(
sessionData,
{
password: cookiePassword,
},
);
}

return undefined;
Expand Down
12 changes: 11 additions & 1 deletion src/workos.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import { BadRequestException } from './common/exceptions/bad-request.exception';
import { HttpClient, HttpClientError } from './common/net/http-client';
import { SubtleCryptoProvider } from './common/crypto/subtle-crypto-provider';
import { FetchHttpClient } from './common/net/fetch-client';
import { IronSessionProvider } from './common/iron-session/iron-session-provider';

const VERSION = '7.18.0';

Expand Down Expand Up @@ -94,7 +95,10 @@ export class WorkOS {
this.webhooks = this.createWebhookClient();

// Must initialize UserManagement after baseURL is configured
this.userManagement = new UserManagement(this);
this.userManagement = new UserManagement(
this,
this.createIronSessionProvider(),
);

this.client = this.createHttpClient(options, userAgent);
}
Expand All @@ -114,6 +118,12 @@ export class WorkOS {
}) as HttpClient;
}

createIronSessionProvider(): IronSessionProvider {
throw new Error(
'IronSessionProvider not implemented. Use WorkOSNode or WorkOSWorker instead.',
);
}

get version() {
return VERSION;
}
Expand Down

0 comments on commit fbc1e31

Please sign in to comment.