Skip to content

Commit

Permalink
Better cookie error handling (#153)
Browse files Browse the repository at this point in the history
* Added tests and made a start to auth.ts

* Add tests for cookie and callback route

* Tests for session and actions

* Add jsdom tests for tsx files

* Add new workflow

* Clean up jest config file

* Didn't mean to add this

* Add jest config and setup scripts to ts exclude

* Impersonation shouldn't be a client component for now

* 100% test coverage

* Add debug flag

* Add another test and change coverage engine to have local and github show the same results

* Should actually add the test

* Address feedback

* Also run prettier on test files

* Throw if no cookie password or if it's too short

* Add onSuccess test

* Address feedback
  • Loading branch information
PaulAsjes authored Jan 2, 2025
1 parent 88a3b84 commit a9d77b8
Show file tree
Hide file tree
Showing 3 changed files with 94 additions and 46 deletions.
88 changes: 42 additions & 46 deletions __tests__/authkit-callback-route.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,28 @@ jest.mock('../src/workos', () => ({
}));

describe('authkit-callback-route', () => {
const mockAuthResponse = {
accessToken: 'access123',
refreshToken: 'refresh123',
user: {
id: 'user_123',
email: '[email protected]',
emailVerified: true,
profilePictureUrl: 'https://example.com/photo.jpg',
firstName: 'Test',
lastName: 'User',
object: 'user' as const,
createdAt: '2024-01-01T00:00:00Z',
updatedAt: '2024-01-01T00:00:00Z',
},
oauthTokens: {
accessToken: 'access123',
refreshToken: 'refresh123',
expiresAt: 1719811200,
scopes: ['foo', 'bar'],
},
};

describe('handleAuth', () => {
let request: NextRequest;

Expand All @@ -42,14 +64,7 @@ describe('authkit-callback-route', () => {
});

it('should handle successful authentication', async () => {
// Mock successful authentication response
const mockAuthResponse = {
accessToken: 'access123',
refreshToken: 'refresh123',
user: { id: 'user_123' },
};

(workos.userManagement.authenticateWithCode as jest.Mock).mockResolvedValue(mockAuthResponse);
jest.mocked(workos.userManagement.authenticateWithCode).mockResolvedValue(mockAuthResponse);

// Set up request with code
request.nextUrl.searchParams.set('code', 'test-code');
Expand Down Expand Up @@ -80,7 +95,7 @@ describe('authkit-callback-route', () => {

it('should handle authentication failure if a non-Error object is thrown', async () => {
// Mock authentication failure
(workos.userManagement.authenticateWithCode as jest.Mock).mockRejectedValue('Auth failed');
jest.mocked(workos.userManagement.authenticateWithCode).mockRejectedValue('Auth failed');

request.nextUrl.searchParams.set('code', 'invalid-code');

Expand All @@ -102,13 +117,7 @@ describe('authkit-callback-route', () => {
});

it('should respect custom returnPathname', async () => {
const mockAuthResponse = {
accessToken: 'access123',
refreshToken: 'refresh123',
user: { id: 'user1' },
};

(workos.userManagement.authenticateWithCode as jest.Mock).mockResolvedValue(mockAuthResponse);
jest.mocked(workos.userManagement.authenticateWithCode).mockResolvedValue(mockAuthResponse);

request.nextUrl.searchParams.set('code', 'test-code');

Expand All @@ -119,13 +128,7 @@ describe('authkit-callback-route', () => {
});

it('should handle state parameter with returnPathname', async () => {
const mockAuthResponse = {
accessToken: 'access123',
refreshToken: 'refresh123',
user: { id: 'user1' },
};

(workos.userManagement.authenticateWithCode as jest.Mock).mockResolvedValue(mockAuthResponse);
jest.mocked(workos.userManagement.authenticateWithCode).mockResolvedValue(mockAuthResponse);

const state = btoa(JSON.stringify({ returnPathname: '/custom-path' }));
request.nextUrl.searchParams.set('code', 'test-code');
Expand All @@ -138,13 +141,7 @@ describe('authkit-callback-route', () => {
});

it('should extract custom search params from returnPathname', async () => {
const mockAuthResponse = {
accessToken: 'access123',
refreshToken: 'refresh123',
user: { id: 'user1' },
};

(workos.userManagement.authenticateWithCode as jest.Mock).mockResolvedValue(mockAuthResponse);
jest.mocked(workos.userManagement.authenticateWithCode).mockResolvedValue(mockAuthResponse);

const state = btoa(JSON.stringify({ returnPathname: '/custom-path?foo=bar&baz=qux' }));
request.nextUrl.searchParams.set('code', 'test-code');
Expand All @@ -160,14 +157,7 @@ describe('authkit-callback-route', () => {
const originalRedirect = NextResponse.redirect;
(NextResponse as Partial<typeof NextResponse>).redirect = undefined;

// Mock successful authentication response
const mockAuthResponse = {
accessToken: 'access123',
refreshToken: 'refresh123',
user: { id: 'user_123' },
};

(workos.userManagement.authenticateWithCode as jest.Mock).mockResolvedValue(mockAuthResponse);
jest.mocked(workos.userManagement.authenticateWithCode).mockResolvedValue(mockAuthResponse);

// Set up request with code
request.nextUrl.searchParams.set('code', 'test-code');
Expand Down Expand Up @@ -199,14 +189,7 @@ describe('authkit-callback-route', () => {
});

it('should use baseURL if provided', async () => {
// Mock successful authentication response
const mockAuthResponse = {
accessToken: 'access123',
refreshToken: 'refresh123',
user: { id: 'user_123' },
};

(workos.userManagement.authenticateWithCode as jest.Mock).mockResolvedValue(mockAuthResponse);
jest.mocked(workos.userManagement.authenticateWithCode).mockResolvedValue(mockAuthResponse);

// Set up request with code
request.nextUrl.searchParams.set('code', 'test-code');
Expand All @@ -232,5 +215,18 @@ describe('authkit-callback-route', () => {

expect(response.status).toBe(500);
});

it('should call onSuccess if provided', async () => {
jest.mocked(workos.userManagement.authenticateWithCode).mockResolvedValue(mockAuthResponse);

// Set up request with code
request.nextUrl.searchParams.set('code', 'test-code');

const onSuccess = jest.fn();
const handler = handleAuth({ onSuccess: onSuccess });
await handler(request);

expect(onSuccess).toHaveBeenCalledWith(mockAuthResponse);
});
});
});
46 changes: 46 additions & 0 deletions __tests__/session.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,52 @@ describe('session.ts', () => {
jest.replaceProperty(envVariables, 'WORKOS_REDIRECT_URI', originalWorkosRedirectUri);
});

it('should throw an error if the cookie password is not set', async () => {
const originalWorkosCookiePassword = envVariables.WORKOS_COOKIE_PASSWORD;

jest.replaceProperty(envVariables, 'WORKOS_COOKIE_PASSWORD', '');

await expect(async () => {
await updateSession(
new NextRequest(new URL('http://example.com')),
false,
{
enabled: false,
unauthenticatedPaths: [],
},
'',
[],
);
}).rejects.toThrow(
'You must provide a valid cookie password that is at least 32 characters in the environment variables.',
);

jest.replaceProperty(envVariables, 'WORKOS_COOKIE_PASSWORD', originalWorkosCookiePassword);
});

it('should throw an error if the cookie password is less than 32 characters', async () => {
const originalWorkosCookiePassword = envVariables.WORKOS_COOKIE_PASSWORD;

jest.replaceProperty(envVariables, 'WORKOS_COOKIE_PASSWORD', 'short');

await expect(async () => {
await updateSession(
new NextRequest(new URL('http://example.com')),
false,
{
enabled: false,
unauthenticatedPaths: [],
},
'',
[],
);
}).rejects.toThrow(
'You must provide a valid cookie password that is at least 32 characters in the environment variables.',
);

jest.replaceProperty(envVariables, 'WORKOS_COOKIE_PASSWORD', originalWorkosCookiePassword);
});

it('should return early if there is no session', async () => {
const request = new NextRequest(new URL('http://example.com'));
const result = await updateSession(
Expand Down
6 changes: 6 additions & 0 deletions src/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,12 @@ async function updateSession(
throw new Error('You must provide a redirect URI in the AuthKit middleware or in the environment variables.');
}

if (!WORKOS_COOKIE_PASSWORD || WORKOS_COOKIE_PASSWORD.length < 32) {
throw new Error(
'You must provide a valid cookie password that is at least 32 characters in the environment variables.',
);
}

const session = await getSessionFromCookie();
const newRequestHeaders = new Headers(request.headers);

Expand Down

0 comments on commit a9d77b8

Please sign in to comment.