Skip to content

Commit 9fa68b8

Browse files
committed
feat(store): accept a tokenMaxAgeResolver in the session settings
This allows session lengths based on application parameters.
1 parent 3b81c14 commit 9fa68b8

File tree

3 files changed

+84
-34
lines changed

3 files changed

+84
-34
lines changed

packages/store/src/session-store.js

Lines changed: 80 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,18 @@ import { queueWorkerAddJob } from "./queue-worker.js";
2020
*/
2121

2222
/**
23+
* Session store settings. Either the `tokenMaxAgeResolver` is required or the resolved
24+
* fields.
25+
*
2326
* @typedef {object} SessionStoreSettings
24-
* @property {number} accessTokenMaxAgeInSeconds
25-
* @property {number} refreshTokenMaxAgeInSeconds
27+
* @property {(sql: import("postgres").Sql<{}>, session:
28+
* import("./generated/common/types.d.ts").QueryResultStoreSessionStore) => (Promise<{
29+
* accessTokenMaxAgeInSeconds: number, refreshTokenMaxAgeInSeconds: number,
30+
* }>|{
31+
* accessTokenMaxAgeInSeconds: number, refreshTokenMaxAgeInSeconds: number,
32+
* })} [tokenMaxAgeResolver]
33+
* @property {number} [accessTokenMaxAgeInSeconds]
34+
* @property {number} [refreshTokenMaxAgeInSeconds]
2635
* @property {string} signingKey
2736
*/
2837

@@ -105,7 +114,7 @@ export async function sessionStoreGet(
105114

106115
const token = await sessionStoreVerifyAndDecodeJWT(
107116
newEventFromEvent(event),
108-
sessionSettings,
117+
sessionSettings.signingKey,
109118
accessTokenString,
110119
);
111120

@@ -290,7 +299,7 @@ export async function sessionStoreRefreshTokens(
290299

291300
const token = await sessionStoreVerifyAndDecodeJWT(
292301
newEventFromEvent(event),
293-
sessionSettings,
302+
sessionSettings.signingKey,
294303
refreshTokenString,
295304
);
296305

@@ -509,19 +518,20 @@ export async function sessionStoreCreateTokenPair(
509518
return validateResult;
510519
}
511520

521+
const { accessTokenMaxAgeInSeconds, refreshTokenMaxAgeInSeconds } =
522+
await sessionStoreResolveTokenMaxAge(sql, session, sessionSettings);
523+
512524
const accessTokenId = uuid();
513525
const refreshTokenId = uuid();
514526

515527
const accessTokenExpireDate = new Date();
516528
const refreshTokenExpireDate = new Date();
517529

518530
accessTokenExpireDate.setSeconds(
519-
accessTokenExpireDate.getSeconds() +
520-
sessionSettings.accessTokenMaxAgeInSeconds,
531+
accessTokenExpireDate.getSeconds() + accessTokenMaxAgeInSeconds,
521532
);
522533
refreshTokenExpireDate.setSeconds(
523-
refreshTokenExpireDate.getSeconds() +
524-
sessionSettings.refreshTokenMaxAgeInSeconds,
534+
refreshTokenExpireDate.getSeconds() + refreshTokenMaxAgeInSeconds,
525535
);
526536

527537
await queries.sessionStoreTokenInsert(
@@ -630,7 +640,7 @@ export function sessionStoreCreateJWT(
630640
* Verify and decode a JWT token
631641
*
632642
* @param {import("@compas/stdlib").InsightEvent} event
633-
* @param {SessionStoreSettings} sessionSettings
643+
* @param {string} signingKey
634644
* @param {string} [tokenString]
635645
* @returns {Promise<Either<{
636646
* header: object,
@@ -643,7 +653,7 @@ export function sessionStoreCreateJWT(
643653
*/
644654
export async function sessionStoreVerifyAndDecodeJWT(
645655
event,
646-
sessionSettings,
656+
signingKey,
647657
tokenString,
648658
) {
649659
eventStart(event, "sessionStore.verifyAndDecodeJWT");
@@ -666,7 +676,8 @@ export async function sessionStoreVerifyAndDecodeJWT(
666676

667677
tokenString = tokenString.trim();
668678

669-
// Arbitrary length check, jws verify checks the contents but expects 3 parts for a valid JWT
679+
// Arbitrary length check, jws verify checks the contents but expects 3 parts for a
680+
// valid JWT
670681
if (tokenString.length < 6 || tokenString.split(".").length !== 3) {
671682
eventStop(event);
672683

@@ -678,7 +689,7 @@ export async function sessionStoreVerifyAndDecodeJWT(
678689
const { value, error } = await new Promise((resolve) => {
679690
createVerify({
680691
signature: tokenString,
681-
secret: sessionSettings.signingKey,
692+
secret: signingKey,
682693
algorithm: "HS256",
683694
})
684695
.once("error", (error) => {
@@ -728,6 +739,44 @@ export async function sessionStoreVerifyAndDecodeJWT(
728739
};
729740
}
730741

742+
/**
743+
* Resolve token lifetimes for the provided session.
744+
*
745+
* @param {import("postgres").Sql<{}>} sql
746+
* @param {import("./generated/common/types.d.ts").QueryResultStoreSessionStore} session
747+
* @param {SessionStoreSettings} settings
748+
* @returns {Promise<{
749+
* accessTokenMaxAgeInSeconds: number,
750+
* refreshTokenMaxAgeInSeconds: number
751+
* }>}
752+
*/
753+
export async function sessionStoreResolveTokenMaxAge(sql, session, settings) {
754+
let { accessTokenMaxAgeInSeconds, refreshTokenMaxAgeInSeconds } = settings;
755+
756+
if (typeof settings.tokenMaxAgeResolver === "function") {
757+
const result = await settings.tokenMaxAgeResolver(sql, session);
758+
accessTokenMaxAgeInSeconds = result.accessTokenMaxAgeInSeconds;
759+
refreshTokenMaxAgeInSeconds = result.refreshTokenMaxAgeInSeconds;
760+
}
761+
762+
accessTokenMaxAgeInSeconds ??= 0;
763+
refreshTokenMaxAgeInSeconds ??= 0;
764+
765+
if (accessTokenMaxAgeInSeconds >= refreshTokenMaxAgeInSeconds) {
766+
throw AppError.validationError("validator.error", {
767+
"$.sessionStoreSettings.accessTokenMaxAgeInSeconds": {
768+
message:
769+
"Max age of refresh token should be longer than the max age of an access token",
770+
},
771+
});
772+
}
773+
774+
return {
775+
accessTokenMaxAgeInSeconds,
776+
refreshTokenMaxAgeInSeconds,
777+
};
778+
}
779+
731780
/**
732781
* Create a fast checksum for the data object
733782
*
@@ -747,15 +796,27 @@ function sessionStoreChecksumForData(data) {
747796
function validateSessionStoreSettings(input) {
748797
const errObject = {};
749798

750-
if (typeof input.accessTokenMaxAgeInSeconds !== "number") {
751-
errObject["$.sessionStoreSettings.accessTokenMaxAgeInSeconds"] = {
752-
key: "validator.number.type",
799+
if (
800+
typeof input.tokenMaxAgeResolver !== "function" &&
801+
(typeof input.accessTokenMaxAgeInSeconds !== "number" ||
802+
typeof input.refreshTokenMaxAgeInSeconds !== "number")
803+
) {
804+
errObject["$.tokenMaxAgeResolver"] = {
805+
key: "validator.function.type",
753806
};
754807
}
755-
if (typeof input.refreshTokenMaxAgeInSeconds !== "number") {
756-
errObject["$.sessionStoreSettings.refreshTokenMaxAgeInSeconds"] = {
757-
key: "validator.number.type",
758-
};
808+
809+
if (typeof input.tokenMaxAgeResolver !== "function") {
810+
if (typeof input.accessTokenMaxAgeInSeconds !== "number") {
811+
errObject["$.sessionStoreSettings.accessTokenMaxAgeInSeconds"] = {
812+
key: "validator.number.type",
813+
};
814+
}
815+
if (typeof input.refreshTokenMaxAgeInSeconds !== "number") {
816+
errObject["$.sessionStoreSettings.refreshTokenMaxAgeInSeconds"] = {
817+
key: "validator.number.type",
818+
};
819+
}
759820
}
760821

761822
if (typeof input.signingKey !== "string") {
@@ -777,17 +838,6 @@ function validateSessionStoreSettings(input) {
777838
};
778839
}
779840

780-
if (input.accessTokenMaxAgeInSeconds >= input.refreshTokenMaxAgeInSeconds) {
781-
return {
782-
error: AppError.validationError("validator.error", {
783-
"$.sessionStoreSettings.accessTokenMaxAgeInSeconds": {
784-
message:
785-
"Max age of refresh token should be longer than the max age of an access token",
786-
},
787-
}),
788-
};
789-
}
790-
791841
return {
792842
value: undefined,
793843
};

packages/store/src/session-store.test.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ test("store/session-store", (t) => {
4040

4141
const accessTokenResult = await sessionStoreVerifyAndDecodeJWT(
4242
newTestEvent(t),
43-
sessionSettings,
43+
sessionSettings.signingKey,
4444
tokenResult.value.accessToken,
4545
);
4646

@@ -110,7 +110,7 @@ test("store/session-store", (t) => {
110110

111111
const accessTokenPayload = await sessionStoreVerifyAndDecodeJWT(
112112
newTestEvent(t),
113-
sessionSettings,
113+
sessionSettings.signingKey,
114114
result.value.accessToken,
115115
);
116116
t.ok(isNil(accessTokenPayload.error));
@@ -631,7 +631,7 @@ test("store/session-store", (t) => {
631631

632632
const accessTokenPayload = await sessionStoreVerifyAndDecodeJWT(
633633
newTestEvent(t),
634-
sessionSettings,
634+
sessionSettings.signingKey,
635635
sessionRefreshResult.value.accessToken,
636636
);
637637
t.ok(isNil(accessTokenPayload.error));

packages/store/src/session-transport.test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ test("store/session-transport", (t) => {
5555

5656
const accessTokenResult = await sessionStoreVerifyAndDecodeJWT(
5757
newTestEvent(t),
58-
sessionStoreSettings,
58+
sessionStoreSettings.signingKey,
5959
tokenResult.value.accessToken,
6060
);
6161

0 commit comments

Comments
 (0)