Skip to content

Commit a00cd7e

Browse files
authored
Add returnTo argument to signOut method (#13)
* add returnTo argument to signOut method * update signOut docs * pass options object rather than single param
1 parent 67d278c commit a00cd7e

File tree

5 files changed

+55
-6
lines changed

5 files changed

+55
-6
lines changed

README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,15 @@ Enabling `ensureSignedIn` will redirect users to AuthKit if they attempt to acce
219219

220220
Use the `signOut` method to sign out the current logged in user, end the session, and redirect to your app's homepage. The homepage redirect is set in your WorkOS dashboard settings under "Redirect".
221221

222+
If you would like to specify where a user is redirected, an optional `returnTo` argument can be passed. Allowed values are configured in the WorkOS Dashboard under _[Logout redirects](https://workos.com/docs/user-management/sessions/configuring-sessions/logout-redirect)_.
223+
224+
```ts
225+
export async function action({ request }: ActionFunctionArgs) {
226+
// Called when the form in SignInButton is submitted
227+
return await signOut(request, { returnTo: 'https://example.com' });
228+
}
229+
```
230+
222231
### Get the access token
223232

224233
Sometimes it is useful to obtain the access token directly, for instance to make API requests to another service.

src/auth.spec.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,15 @@ describe('auth', () => {
6161
const request = new Request('https://example.com');
6262
const response = await signOut(request);
6363
expect(response).toBeInstanceOf(Response);
64-
expect(terminateSession).toHaveBeenCalledWith(request);
64+
expect(terminateSession).toHaveBeenCalledWith(request, undefined);
65+
});
66+
67+
it('should return a response with returnTo', async () => {
68+
const request = new Request('https://example.com');
69+
const returnTo = '/dashboard';
70+
const response = await signOut(request, { returnTo });
71+
expect(response).toBeInstanceOf(Response);
72+
expect(terminateSession).toHaveBeenCalledWith(request, { returnTo });
6573
});
6674
});
6775

src/auth.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ export async function getSignUpUrl(returnPathname?: string) {
1010
return getAuthorizationUrl({ returnPathname, screenHint: 'sign-up' });
1111
}
1212

13-
export async function signOut(request: Request) {
14-
return await terminateSession(request);
13+
export async function signOut(request: Request, options?: { returnTo?: string }) {
14+
return await terminateSession(request, options);
1515
}
1616

1717
/**

src/session.spec.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,36 @@ describe('session', () => {
188188
expect(getLogoutUrl).not.toHaveBeenCalled();
189189
});
190190

191+
it('Should redirect to the provided returnTo if no session exists', async () => {
192+
const mockSession = createMockSession({
193+
has: jest.fn().mockReturnValue(true),
194+
get: jest.fn().mockReturnValue('encrypted-jwt'),
195+
});
196+
197+
getSession.mockResolvedValueOnce(mockSession);
198+
199+
// Mock session data with a token that will decode to no sessionId
200+
const mockSessionData = {
201+
accessToken: 'token.without.sessionid',
202+
refreshToken: 'refresh-token',
203+
user: { id: 'user-id' },
204+
impersonator: null,
205+
};
206+
unsealData.mockResolvedValueOnce(mockSessionData);
207+
208+
// Mock decodeJwt to return no sessionId
209+
(jose.decodeJwt as jest.Mock).mockReturnValueOnce({});
210+
211+
const response = await terminateSession(createMockRequest(), { returnTo: '/login' });
212+
213+
expect(response instanceof Response).toBe(true);
214+
expect(response.status).toBe(302);
215+
expect(response.headers.get('Location')).toBe('/login');
216+
expect(response.headers.get('Set-Cookie')).toBe('destroyed-session-cookie');
217+
expect(destroySession).toHaveBeenCalledWith(mockSession);
218+
expect(getLogoutUrl).not.toHaveBeenCalled();
219+
});
220+
191221
it('should redirect to WorkOS logout URL when valid session exists', async () => {
192222
// Setup a session with jwt
193223
const mockSession = createMockSession({
@@ -217,11 +247,13 @@ describe('session', () => {
217247
expect(destroySession).toHaveBeenCalledWith(mockSession);
218248
expect(getLogoutUrl).toHaveBeenCalledWith({
219249
sessionId: 'test-session-id',
250+
returnTo: undefined,
220251
});
221252
expect(mockSession.has).toHaveBeenCalledWith('jwt');
222253
expect(mockSession.get).toHaveBeenCalledWith('jwt');
223254
});
224255
});
256+
225257
describe('authkitLoader', () => {
226258
const createLoaderArgs = (request: Request): LoaderFunctionArgs => ({
227259
request,

src/session.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -393,7 +393,7 @@ async function handleAuthLoader(
393393
return data({ ...loaderResult, ...auth }, session ? { headers: { ...session.headers } } : undefined);
394394
}
395395

396-
export async function terminateSession(request: Request) {
396+
export async function terminateSession(request: Request, { returnTo }: { returnTo?: string } = {}) {
397397
const { getSession, destroySession } = await getSessionStorage();
398398
const encryptedSession = await getSession(request.headers.get('Cookie'));
399399
const { accessToken } = (await getSessionFromCookie(
@@ -408,12 +408,12 @@ export async function terminateSession(request: Request) {
408408
};
409409

410410
if (sessionId) {
411-
return redirect(getWorkOS().userManagement.getLogoutUrl({ sessionId }), {
411+
return redirect(getWorkOS().userManagement.getLogoutUrl({ sessionId, returnTo }), {
412412
headers,
413413
});
414414
}
415415

416-
return redirect('/', {
416+
return redirect(returnTo ?? '/', {
417417
headers,
418418
});
419419
}

0 commit comments

Comments
 (0)