Skip to content

Commit c61c76b

Browse files
authored
fix: Fix jwt refresh errors when using jwts in cookies RELEASE (#535)
* update ts-node to tsx to support all node versions add refresh route to es6 example fix missing cookies in refresh response fix session jwt empty causes error add tests * update deps
1 parent 9db508b commit c61c76b

File tree

10 files changed

+1156
-546
lines changed

10 files changed

+1156
-546
lines changed

examples/es6/package-lock.json

Lines changed: 758 additions & 307 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

examples/es6/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
},
1111
"scripts": {
1212
"build": "rollup -c",
13-
"start": "ts-node --esm src/index.ts",
13+
"start": "npx tsx src/index.ts",
1414
"generateCerts": "test -f ./server.key && test -f ./server.crt || openssl req -new -newkey rsa:2048 -days 365 -nodes -x509 -subj '/C=US/ST=California/L=San Francisco/O=Descope/CN=localhost' -keyout ./server.key -out ./server.crt"
1515
},
1616
"author": "descope",
@@ -31,6 +31,6 @@
3131
"@types/express": "^4.17.13",
3232
"dotenv": "^16.4.5",
3333
"rollup-plugin-delete": "^2.0.0",
34-
"ts-node": "^10.8.2"
34+
"tsx": "^4.20.3"
3535
}
3636
}

examples/es6/src/index.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -337,10 +337,24 @@ app.post('/api/private', authMiddleware, (_unused: Request, res: Response) => {
337337
res.sendStatus(200);
338338
});
339339

340+
app.post('/refresh', authMiddleware, async (req: Request, res: Response) => {
341+
try {
342+
const cookies = parseCookies(req);
343+
const out = await clientAuth.auth.refreshSession(cookies[DescopeClient.RefreshTokenCookieName]);
344+
if (out?.cookies) {
345+
res.set('Set-Cookie', out.cookies);
346+
}
347+
res.status(200).send(out);
348+
} catch (error) {
349+
console.log(error);
350+
res.sendStatus(401);
351+
}
352+
});
353+
340354
app.post('/logout', authMiddleware, async (req: Request, res: Response) => {
341355
try {
342356
const cookies = parseCookies(req);
343-
const out = await clientAuth.auth.logout(cookies.DS);
357+
const out = await clientAuth.auth.logout(cookies[DescopeClient.SessionTokenCookieName]);
344358
returnCookies(res, out);
345359
} catch (error) {
346360
console.log(error);

examples/managementCli/package-lock.json

Lines changed: 9 additions & 9 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

examples/managementCli/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import DescopeClient, { SdkResponse } from '@descope/node-sdk';
33
import { config } from 'dotenv';
44
import { writeFileSync, readFileSync } from 'fs';
55
import { Command } from 'commander';
6+
import { UserResponse } from '@descope/core-js-sdk';
67

78
config();
89

lib/helpers.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ const generateCookie = (name: string, value: string, options?: Record<string, st
2020
* @param name the name of the cookie to get value for
2121
* @returns the value of the given cookie
2222
*/
23-
const getCookieValue = (cookie: string | null | undefined, name: string) => {
23+
export const getCookieValue = (cookie: string | null | undefined, name: string) => {
2424
const match = cookie?.match(RegExp(`(?:^|;\\s*)${name}=([^;]*)`));
2525
return match ? match[1] : null;
2626
};

lib/index.test.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
authorizedTenantsClaimName,
88
permissionsClaimName,
99
rolesClaimName,
10+
sessionTokenCookieName,
1011
} from './constants';
1112

1213
let validToken: string;
@@ -195,6 +196,36 @@ describe('sdk', () => {
195196
const res = await sdk.refreshSession(validToken);
196197
expect(res.jwt).toBe(validToken);
197198
expect(res.refreshJwt).toBe('refresh-jwt');
199+
expect(res.cookies).toStrictEqual([]);
200+
expect(spyRefresh).toHaveBeenCalledWith(validToken);
201+
});
202+
it('should return only cookies when refresh call returns it', async () => {
203+
const expectedCookies = [
204+
`${sessionTokenCookieName}=${validToken}; Domain=; Max-Age=; Path=/; HttpOnly; SameSite=Strict`,
205+
`${refreshTokenCookieName}=${validToken}; Domain=; Max-Age=; Path=/; HttpOnly; SameSite=Strict`,
206+
];
207+
const spyRefresh = jest.spyOn(sdk, 'refresh').mockResolvedValueOnce({
208+
ok: true,
209+
data: { sessionJwt: '', refreshJwt: null, cookies: expectedCookies },
210+
} as unknown as SdkResponse<JWTResponse>);
211+
212+
const res = await sdk.refreshSession(validToken);
213+
expect(res.jwt).toBe(validToken);
214+
expect(res.cookies).toBe(expectedCookies);
215+
expect(spyRefresh).toHaveBeenCalledWith(validToken);
216+
});
217+
it('should return refrehs in cookie when refresh call returns it', async () => {
218+
const expectedCookies = [
219+
`${refreshTokenCookieName}=${validToken}; Domain=; Max-Age=; Path=/; HttpOnly; SameSite=Strict`,
220+
];
221+
const spyRefresh = jest.spyOn(sdk, 'refresh').mockResolvedValueOnce({
222+
ok: true,
223+
data: { sessionJwt: validToken, refreshJwt: null, cookies: expectedCookies },
224+
} as unknown as SdkResponse<JWTResponse>);
225+
226+
const res = await sdk.refreshSession(validToken);
227+
expect(res.jwt).toBe(validToken);
228+
expect(res.cookies).toBe(expectedCookies);
198229
expect(spyRefresh).toHaveBeenCalledWith(validToken);
199230
});
200231
it('should fail when refresh returns an error', async () => {

lib/index.ts

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import createSdk, {
22
AccessKeyLoginOptions,
33
ExchangeAccessKeyResponse,
44
SdkResponse,
5+
JWTResponse as CoreJWTResponse,
56
wrapWith,
67
} from '@descope/core-js-sdk';
78
import { JWK, JWTHeaderParameters, KeyLike, errors, importJWK, jwtVerify } from 'jose';
@@ -12,13 +13,23 @@ import {
1213
sessionTokenCookieName,
1314
} from './constants';
1415
import fetch from './fetch-polyfill';
15-
import { getAuthorizationClaimItems, isUserAssociatedWithTenant, withCookie } from './helpers';
16+
import {
17+
getAuthorizationClaimItems,
18+
getCookieValue,
19+
isUserAssociatedWithTenant,
20+
withCookie,
21+
} from './helpers';
1622
import withManagement from './management';
1723
import { AuthenticationInfo, RefreshAuthenticationInfo } from './types';
1824
import descopeErrors from './errors';
1925

2026
declare const BUILD_VERSION: string;
2127

28+
// Extend the type wrapped by withCookie
29+
type JWTResponseWithCookies = CoreJWTResponse & {
30+
cookies: string[];
31+
};
32+
2233
/** Configuration arguments which include the Descope core SDK args and an optional management key */
2334
type NodeSdkArgs = Parameters<typeof createSdk>[0] & {
2435
managementKey?: string;
@@ -155,8 +166,16 @@ const nodeSdk = ({ managementKey, publicKey, ...config }: NodeSdkArgs) => {
155166
await sdk.validateJwt(refreshToken);
156167
const jwtResp = await sdk.refresh(refreshToken);
157168
if (jwtResp.ok) {
158-
const token = await sdk.validateJwt(jwtResp.data?.sessionJwt);
159-
if (jwtResp.data.refreshJwt) {
169+
// if refresh was successful, validate the new session JWT
170+
const seesionJwt =
171+
getCookieValue(
172+
(jwtResp.data as JWTResponseWithCookies)?.cookies?.join(';'),
173+
sessionTokenCookieName,
174+
) || jwtResp.data?.sessionJwt;
175+
const token = await sdk.validateJwt(seesionJwt);
176+
// add cookies to the token response if they exist
177+
token.cookies = (jwtResp.data as JWTResponseWithCookies)?.cookies || [];
178+
if (jwtResp.data?.refreshJwt) {
160179
// if refresh returned a refresh JWT, add it to the response
161180
(token as RefreshAuthenticationInfo).refreshJwt = jwtResp.data.refreshJwt;
162181
}

0 commit comments

Comments
 (0)