Skip to content
Merged
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
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,15 @@ Enabling `ensureSignedIn` will redirect users to AuthKit if they attempt to acce

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".

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)_.

```ts
export async function action({ request }: ActionFunctionArgs) {
// Called when the form in SignInButton is submitted
return await signOut(request, { returnTo: 'https://example.com' });
}
```

### Get the access token

Sometimes it is useful to obtain the access token directly, for instance to make API requests to another service.
Expand Down
10 changes: 9 additions & 1 deletion src/auth.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,15 @@ describe('auth', () => {
const request = new Request('https://example.com');
const response = await signOut(request);
expect(response).toBeInstanceOf(Response);
expect(terminateSession).toHaveBeenCalledWith(request);
expect(terminateSession).toHaveBeenCalledWith(request, undefined);
});

it('should return a response with returnTo', async () => {
const request = new Request('https://example.com');
const returnTo = '/dashboard';
const response = await signOut(request, { returnTo });
expect(response).toBeInstanceOf(Response);
expect(terminateSession).toHaveBeenCalledWith(request, { returnTo });
});
});

Expand Down
4 changes: 2 additions & 2 deletions src/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ export async function getSignUpUrl(returnPathname?: string) {
return getAuthorizationUrl({ returnPathname, screenHint: 'sign-up' });
}

export async function signOut(request: Request) {
return await terminateSession(request);
export async function signOut(request: Request, options?: { returnTo?: string }) {
return await terminateSession(request, options);
}

/**
Expand Down
32 changes: 32 additions & 0 deletions src/session.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,36 @@ describe('session', () => {
expect(getLogoutUrl).not.toHaveBeenCalled();
});

it('Should redirect to the provided returnTo if no session exists', async () => {
const mockSession = createMockSession({
has: jest.fn().mockReturnValue(true),
get: jest.fn().mockReturnValue('encrypted-jwt'),
});

getSession.mockResolvedValueOnce(mockSession);

// Mock session data with a token that will decode to no sessionId
const mockSessionData = {
accessToken: 'token.without.sessionid',
refreshToken: 'refresh-token',
user: { id: 'user-id' },
impersonator: null,
};
unsealData.mockResolvedValueOnce(mockSessionData);

// Mock decodeJwt to return no sessionId
(jose.decodeJwt as jest.Mock).mockReturnValueOnce({});

const response = await terminateSession(createMockRequest(), { returnTo: '/login' });

expect(response instanceof Response).toBe(true);
expect(response.status).toBe(302);
expect(response.headers.get('Location')).toBe('/login');
expect(response.headers.get('Set-Cookie')).toBe('destroyed-session-cookie');
expect(destroySession).toHaveBeenCalledWith(mockSession);
expect(getLogoutUrl).not.toHaveBeenCalled();
});

it('should redirect to WorkOS logout URL when valid session exists', async () => {
// Setup a session with jwt
const mockSession = createMockSession({
Expand Down Expand Up @@ -217,11 +247,13 @@ describe('session', () => {
expect(destroySession).toHaveBeenCalledWith(mockSession);
expect(getLogoutUrl).toHaveBeenCalledWith({
sessionId: 'test-session-id',
returnTo: undefined,
});
expect(mockSession.has).toHaveBeenCalledWith('jwt');
expect(mockSession.get).toHaveBeenCalledWith('jwt');
});
});

describe('authkitLoader', () => {
const createLoaderArgs = (request: Request): LoaderFunctionArgs => ({
request,
Expand Down
6 changes: 3 additions & 3 deletions src/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -393,7 +393,7 @@ async function handleAuthLoader(
return data({ ...loaderResult, ...auth }, session ? { headers: { ...session.headers } } : undefined);
}

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

if (sessionId) {
return redirect(getWorkOS().userManagement.getLogoutUrl({ sessionId }), {
return redirect(getWorkOS().userManagement.getLogoutUrl({ sessionId, returnTo }), {
headers,
});
}

return redirect('/', {
return redirect(returnTo ?? '/', {
headers,
});
}
Expand Down