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
57 changes: 53 additions & 4 deletions src/user-management/session.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -820,8 +820,6 @@ describe('Session', () => {

describe('getLogoutUrl', () => {
it('returns a logout URL for the user', async () => {
jest.mocked(jose.jwtVerify).mockResolvedValue(verifiedUserAccessToken);

const cookiePassword = 'alongcookiesecretmadefortestingsessions';

const sessionData = await sealData(
Expand Down Expand Up @@ -850,6 +848,43 @@ describe('Session', () => {
);
});

it('returns a logout URL when the access token has expired', async () => {
jest
.mocked(jose.jwtVerify)
.mockRejectedValue(
new jose.errors.JWTExpired('"exp" claim timestamp check failed', {}),
);

const cookiePassword = 'alongcookiesecretmadefortestingsessions';

const sessionData = await sealData(
{
accessToken:
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.ewogICJzdWIiOiAiMTIzNDU2Nzg5MCIsCiAgIm5hbWUiOiAiSm9obiBEb2UiLAogICJpYXQiOiAxNTE2MjM5MDIyLAogICJzaWQiOiAic2Vzc2lvbl8xMjMiLAogICJvcmdfaWQiOiAib3JnXzEyMyIsCiAgInJvbGUiOiAibWVtYmVyIiwKICAicGVybWlzc2lvbnMiOiBbInBvc3RzOmNyZWF0ZSIsICJwb3N0czpkZWxldGUiXQp9.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c',
refreshToken: 'def456',
user: {
object: 'user',
id: 'user_01H5JQDV7R7ATEYZDEG0W5PRYS',
email: 'test01@example.com',
},
},
{ password: cookiePassword },
);

const session = workos.userManagement.loadSealedSession({
sessionData,
cookiePassword,
});

await expect(session.authenticate()).resolves.toEqual({
authenticated: false,
reason: 'invalid_jwt',
});
await expect(session.getLogoutUrl()).resolves.toEqual(
'https://api.workos.com/user_management/sessions/logout?session_id=session_123',
);
});

it('returns an error if the session is invalid', async () => {
const session = workos.userManagement.loadSealedSession({
sessionData: '',
Expand All @@ -861,10 +896,24 @@ describe('Session', () => {
);
});

it('returns an error if the session cookie cannot be unsealed', async () => {
const sessionData = await sealData(
{ accessToken: 'abc123', refreshToken: 'def456' },
{ password: 'alongcookiesecretmadefortestingsessions' },
);

const session = workos.userManagement.loadSealedSession({
sessionData,
cookiePassword: 'adifferentcookiesecretmadefortestingsessions',
});

await expect(session.getLogoutUrl()).rejects.toThrow(
'Failed to extract session ID for logout URL: invalid_session_cookie',
);
});

describe('when a returnTo URL is provided', () => {
it('returns a logout URL for the user', async () => {
jest.mocked(jose.jwtVerify).mockResolvedValue(verifiedUserAccessToken);

const cookiePassword = 'alongcookiesecretmadefortestingsessions';

const sessionData = await sealData(
Expand Down
28 changes: 23 additions & 5 deletions src/user-management/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -227,20 +227,38 @@ export class CookieSession {
/**
* Gets the URL to redirect the user to for logging out.
*
* Works for sessions whose access token has expired. The sealed cookie is
* already authenticated by its encryption, so the session ID is read from
* the access token without verifying it.
*
* @returns The URL to redirect the user to for logging out.
*/
async getLogoutUrl({
returnTo,
}: { returnTo?: string } = {}): Promise<string> {
const authenticationResponse = await this.authenticate();
if (!this.sessionData) {
throw new Error(
`Failed to extract session ID for logout URL: ${AuthenticateWithSessionCookieFailureReason.NO_SESSION_COOKIE_PROVIDED}`,
);
}

const { accessToken } = await unsealData<SealedSessionCookieData>(
this.sessionData,
{ password: this.cookiePassword },
);

if (!authenticationResponse.authenticated) {
const { reason } = authenticationResponse;
throw new Error(`Failed to extract session ID for logout URL: ${reason}`);
if (!accessToken) {
throw new Error(
`Failed to extract session ID for logout URL: ${AuthenticateWithSessionCookieFailureReason.INVALID_SESSION_COOKIE}`,
);
}

const { decodeJwt } = await getJose();
const { sid: sessionId } =
decodeJwt<UserManagementAccessToken>(accessToken);

return this.userManagement.getLogoutUrl({
sessionId: authenticationResponse.sessionId,
sessionId,
returnTo,
});
}
Expand Down
Loading