@@ -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 */
644654export 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) {
747796function 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 } ;
0 commit comments