Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions src/core/AuthKitCore.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,42 @@ describe('AuthKitCore', () => {
SessionEncryptionError,
);
});

it.each([
['string', 'hello'],
['number', 42],
['null', null],
['array', [1, 2, 3]],
['empty object', {}],
['missing user', { accessToken: 'at', refreshToken: 'rt' }],
['null user', { accessToken: 'at', refreshToken: 'rt', user: null }],
['missing refreshToken', { accessToken: 'at', user: { id: 'user_123' } }],
])(
'throws SessionEncryptionError for invalid shape: %s',
async (_label, badValue) => {
const badEncryption = {
sealData: async () => 'encrypted',
unsealData: async () => badValue,
};
const badCore = new AuthKitCore(
mockConfig as any,
mockClient as any,
badEncryption as any,
);

await expect(badCore.decryptSession('data')).rejects.toThrow(
SessionEncryptionError,
);
},
);

it('accepts valid session shape', async () => {
const result = await core.decryptSession('encrypted-data');

expect(result.accessToken).toBe('test-access-token');
expect(result.refreshToken).toBe('test-refresh-token');
expect(result.user).toEqual(mockUser);
});
});

describe('refreshTokens()', () => {
Expand Down
20 changes: 20 additions & 0 deletions src/core/AuthKitCore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,8 +147,16 @@ export class AuthKitCore {
encryptedSession,
{ password: this.config.cookiePassword },
);

if (!isSessionLike(session)) {
throw new Error('Decoded value is not a valid Session object');
}

return session;
} catch (error) {
if (error instanceof SessionEncryptionError) {
throw error;
}
throw new SessionEncryptionError('Failed to decrypt session', error);
}
}
Expand Down Expand Up @@ -303,3 +311,15 @@ export class AuthKitCore {
};
}
}

function isSessionLike(value: unknown): value is Session {
return (
typeof value === 'object' &&
value !== null &&
!Array.isArray(value) &&
typeof (value as Record<string, unknown>).accessToken === 'string' &&
typeof (value as Record<string, unknown>).refreshToken === 'string' &&
typeof (value as Record<string, unknown>).user === 'object' &&
(value as Record<string, unknown>).user !== null
);
}
17 changes: 12 additions & 5 deletions src/core/config.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,16 +108,26 @@ describe('config', () => {
const config = getFullConfig();
expect(config).toMatchObject({ clientId: 'test-client' });
});

it('derives cookiePassword when not provided', () => {
configure({
clientId: 'test-client',
apiKey: 'test-api-key',
redirectUri: 'http://localhost:3000/callback',
});

const config = getFullConfig();
expect(config.cookiePassword).toBeDefined();
expect(config.cookiePassword).toContain('test-api-key');
});
});

describe('validateConfig()', () => {
it('passes with all required config', () => {
const validPassword = 'a'.repeat(32);
configure({
clientId: 'test-client',
apiKey: 'test-api-key',
redirectUri: 'http://localhost:3000/callback',
cookiePassword: validPassword,
});

expect(() => validateConfig()).not.toThrow();
Expand All @@ -133,9 +143,6 @@ describe('config', () => {
expect(() => validateConfig()).toThrow(/WORKOS_CLIENT_ID is required/);
expect(() => validateConfig()).toThrow(/WORKOS_API_KEY is required/);
expect(() => validateConfig()).toThrow(/WORKOS_REDIRECT_URI is required/);
expect(() => validateConfig()).toThrow(
/WORKOS_COOKIE_PASSWORD is required/,
);
});

it('throws for short cookie password', () => {
Expand Down
48 changes: 47 additions & 1 deletion src/core/config/ConfigurationProvider.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,42 @@ describe('ConfigurationProvider', () => {
const config = provider.getConfig();
expect(config.cookieName).toBe('test-cookie');
});

it('uses explicit cookiePassword when provided', () => {
const validPassword = 'a'.repeat(32);
provider.configure({
clientId: 'test-client',
apiKey: 'test-api-key',
redirectUri: 'http://localhost:3000/callback',
cookiePassword: validPassword,
});

const config = provider.getConfig();
expect(config.cookiePassword).toBe(validPassword);
});

it('derives cookiePassword from apiKey and clientId when not provided', () => {
provider.configure({
clientId: 'test-client',
apiKey: 'test-api-key',
redirectUri: 'http://localhost:3000/callback',
});

const config = provider.getConfig();
expect(config.cookiePassword).toBe('wos-ck:test-api-key:test-client');
});

it('derives a deterministic password', () => {
provider.configure({
clientId: 'test-client',
apiKey: 'test-api-key',
redirectUri: 'http://localhost:3000/callback',
});

const config1 = provider.getConfig();
const config2 = provider.getConfig();
expect(config1.cookiePassword).toBe(config2.cookiePassword);
});
});

describe('validate()', () => {
Expand All @@ -135,7 +171,7 @@ describe('ConfigurationProvider', () => {

it('throws with all missing required fields at once', () => {
expect(() => provider.validate()).toThrow(
/AuthKit configuration error\. Missing or invalid environment variables:\n\n • WORKOS_CLIENT_ID is required\n • WORKOS_API_KEY is required\n • WORKOS_REDIRECT_URI is required\n • WORKOS_COOKIE_PASSWORD is required/,
/AuthKit configuration error\. Missing or invalid environment variables:\n\n • WORKOS_CLIENT_ID is required\n • WORKOS_API_KEY is required\n • WORKOS_REDIRECT_URI is required/,
);
});

Expand Down Expand Up @@ -185,6 +221,16 @@ describe('ConfigurationProvider', () => {
);
});

it('passes without cookiePassword when apiKey and clientId are set', () => {
provider.configure({
clientId: 'test-client',
apiKey: 'test-api-key',
redirectUri: 'http://localhost:3000/callback',
});

expect(() => provider.validate()).not.toThrow();
});

it('prefers environment values over config in validation', () => {
const source = vi.fn((key: string) => {
if (key === 'WORKOS_COOKIE_PASSWORD') return 'a'.repeat(32);
Expand Down
33 changes: 24 additions & 9 deletions src/core/config/ConfigurationProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ export class ConfigurationProvider {
'clientId',
'apiKey',
'redirectUri',
'cookiePassword',
];

/**
Expand Down Expand Up @@ -155,14 +154,19 @@ export class ConfigurationProvider {

if (!value) {
errors.push(`${envKey} is required`);
} else if (key === 'cookiePassword') {
// Special validation for cookiePassword length
const password = String(value);
if (password.length < 32) {
errors.push(
`${envKey} must be at least 32 characters (currently ${password.length})`,
);
}
}
}

// Validate cookiePassword length when explicitly provided
const passwordEnvKey = this.getEnvironmentVariableName('cookiePassword');
const explicitPassword =
this.getEnvironmentValue(passwordEnvKey) ?? this.config.cookiePassword;
if (explicitPassword != null) {
const password = String(explicitPassword);
if (password.length < 32) {
errors.push(
`${passwordEnvKey} must be at least 32 characters (currently ${password.length})`,
);
}
}

Expand Down Expand Up @@ -202,6 +206,17 @@ export class ConfigurationProvider {
}
}

if (!fullConfig.cookiePassword) {
fullConfig.cookiePassword = deriveCookiePassword(
fullConfig.apiKey,
fullConfig.clientId,
);
}

return fullConfig;
}
}

function deriveCookiePassword(apiKey: string, clientId: string): string {
return `wos-ck:${apiKey}:${clientId}`;
}
12 changes: 9 additions & 3 deletions src/core/config/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,15 @@ export interface AuthKitConfig {
redirectUri: string;

/**
* The password used to encrypt the session cookie
* Equivalent to the WORKOS_COOKIE_PASSWORD environment variable
* Must be at least 32 characters long
* The password used to seal PKCE verifier cookies and (in sealed mode)
* session cookies.
*
* Optional. If not provided, a password is derived from `apiKey` and
* `clientId`. Set this explicitly if you need cookie continuity across
* API key rotations.
*
* Equivalent to the WORKOS_COOKIE_PASSWORD environment variable.
* Must be at least 32 characters long when explicitly provided.
*/
cookiePassword: string;

Expand Down
Loading
Loading