Skip to content

Commit 4954a3e

Browse files
authored
fix: allow signOut to work outside middleware coverage (#296)
1 parent 825eeb3 commit 4954a3e

File tree

3 files changed

+72
-3
lines changed

3 files changed

+72
-3
lines changed

__tests__/auth.spec.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,5 +285,63 @@ describe('auth.ts', () => {
285285
});
286286
});
287287
});
288+
289+
describe('when called outside of middleware', () => {
290+
it('should fall back to reading session from cookie and redirect to logout URL', async () => {
291+
const nextCookies = await cookies();
292+
293+
// Don't set x-workos-middleware header to simulate being outside middleware
294+
// This will cause withAuth to throw
295+
296+
// Set up a session cookie with a valid access token
297+
const mockSession = {
298+
accessToken: await generateTestToken(),
299+
refreshToken: 'refresh_token',
300+
user: { id: 'user_123' },
301+
};
302+
303+
const encryptedSession = await sealData(mockSession, {
304+
password: process.env.WORKOS_COOKIE_PASSWORD as string,
305+
});
306+
307+
nextCookies.set('wos-session', encryptedSession);
308+
309+
jest
310+
.spyOn(workos.userManagement, 'getLogoutUrl')
311+
.mockReturnValue('https://api.workos.com/user_management/sessions/logout?session_id=session_123');
312+
313+
await signOut();
314+
315+
// Cookie should be deleted
316+
const sessionCookie = nextCookies.get('wos-session');
317+
expect(sessionCookie).toBeUndefined();
318+
319+
// Should redirect to WorkOS logout URL with session ID
320+
expect(redirect).toHaveBeenCalledTimes(1);
321+
expect(redirect).toHaveBeenCalledWith(
322+
'https://api.workos.com/user_management/sessions/logout?session_id=session_123',
323+
);
324+
expect(workos.userManagement.getLogoutUrl).toHaveBeenCalledWith(
325+
expect.objectContaining({
326+
sessionId: expect.stringMatching(/^session_/),
327+
}),
328+
);
329+
});
330+
331+
it('should throw the original error when no session cookie exists outside middleware', async () => {
332+
const nextCookies = await cookies();
333+
334+
// Don't set x-workos-middleware header to simulate being outside middleware
335+
// Set a cookie to verify it gets deleted
336+
nextCookies.set('wos-session', 'dummy-value');
337+
338+
// Should throw the error from withAuth since we can't recover
339+
await expect(signOut()).rejects.toThrow(/You are calling 'withAuth'/);
340+
341+
// Cookie should still be deleted even though it throws
342+
const sessionCookie = nextCookies.get('wos-session');
343+
expect(sessionCookie).toBeUndefined();
344+
});
345+
});
288346
});
289347
});

src/auth.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
'use server';
22

3+
import { decodeJwt } from 'jose';
34
import { revalidatePath, revalidateTag } from 'next/cache';
45
import { cookies, headers } from 'next/headers';
56
import { redirect } from 'next/navigation';
67
import { WORKOS_COOKIE_NAME } from './env-variables.js';
78
import { getCookieOptions } from './cookie.js';
89
import { getAuthorizationUrl } from './get-authorization-url.js';
9-
import { SwitchToOrganizationOptions, UserInfo } from './interfaces.js';
10-
import { refreshSession, withAuth } from './session.js';
10+
import type { AccessToken, SwitchToOrganizationOptions, UserInfo } from './interfaces.js';
11+
import { getSessionFromCookie, refreshSession, withAuth } from './session.js';
1112
import { getWorkOS } from './workos.js';
1213
export async function getSignInUrl({
1314
organizationId,
@@ -38,6 +39,16 @@ export async function signOut({ returnTo }: { returnTo?: string } = {}) {
3839
try {
3940
const { sessionId: sid } = await withAuth();
4041
sessionId = sid;
42+
} catch (error) {
43+
// Fall back to reading session directly from cookie when middleware isn't available
44+
const session = await getSessionFromCookie();
45+
if (session && session.accessToken) {
46+
const { sid } = decodeJwt<AccessToken>(session.accessToken);
47+
sessionId = sid;
48+
} else {
49+
// can't recover - throw the original error.
50+
throw error;
51+
}
4152
} finally {
4253
const nextCookies = await cookies();
4354
const cookieName = WORKOS_COOKIE_NAME || 'wos-session';

src/session.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -415,7 +415,7 @@ async function verifyAccessToken(accessToken: string) {
415415
}
416416
}
417417

418-
async function getSessionFromCookie(request?: NextRequest) {
418+
export async function getSessionFromCookie(request?: NextRequest) {
419419
const cookieName = WORKOS_COOKIE_NAME || 'wos-session';
420420
let cookie;
421421

0 commit comments

Comments
 (0)