1- import  {  logger  }  from  '@powersync/lib-services-framework' ; 
1+ import  {  logger ,   errors ,   AuthorizationError ,   ErrorCode  }  from  '@powersync/lib-services-framework' ; 
22import  *  as  jose  from  'jose' ; 
33import  secs  from  '../util/secs.js' ; 
44import  {  JwtPayload  }  from  './JwtPayload.js' ; 
55import  {  KeyCollector  }  from  './KeyCollector.js' ; 
66import  {  KeyOptions ,  KeySpec ,  SUPPORTED_ALGORITHMS  }  from  './KeySpec.js' ; 
7+ import  {  mapAuthError  }  from  './utils.js' ; 
78
89/** 
910 * KeyStore to get keys and verify tokens. 
@@ -49,7 +50,8 @@ export class KeyStore<Collector extends KeyCollector = KeyCollector> {
4950      clockTolerance : 60 , 
5051      // More specific algorithm checking is done when selecting the key to use. 
5152      algorithms : SUPPORTED_ALGORITHMS , 
52-       requiredClaims : [ 'aud' ,  'sub' ,  'iat' ,  'exp' ] 
53+       // 'aud' presence is checked below, so we can add more details to the error message. 
54+       requiredClaims : [ 'sub' ,  'iat' ,  'exp' ] 
5355    } ) ; 
5456
5557    let  audiences  =  options . defaultAudiences ; 
@@ -60,16 +62,24 @@ export class KeyStore<Collector extends KeyCollector = KeyCollector> {
6062
6163    const  tokenPayload  =  result . payload ; 
6264
63-     let  aud  =  tokenPayload . aud ! ; 
64-     if  ( ! Array . isArray ( aud ) )  { 
65+     let  aud  =  tokenPayload . aud ; 
66+     if  ( aud  ==  null )  { 
67+       throw  new  AuthorizationError ( ErrorCode . PSYNC_S2105 ,  `JWT payload is missing a required claim "aud"` ,  { 
68+         configurationDetails : `Current configuration allows these audience values: ${ JSON . stringify ( audiences ) }  
69+       } ) ; 
70+     }  else  if  ( ! Array . isArray ( aud ) )  { 
6571      aud  =  [ aud ] ; 
6672    } 
6773    if  ( 
6874      ! aud . some ( ( a )  =>  { 
6975        return  audiences . includes ( a ) ; 
7076      } ) 
7177    )  { 
72-       throw  new  jose . errors . JWTClaimValidationFailed ( 'unexpected "aud" claim value' ,  'aud' ,  'check_failed' ) ; 
78+       throw  new  AuthorizationError ( 
79+         ErrorCode . PSYNC_S2105 , 
80+         `Unexpected "aud" claim value: ${ JSON . stringify ( tokenPayload . aud ) }  , 
81+         {  configurationDetails : `Current configuration allows these audience values: ${ JSON . stringify ( audiences ) }   } 
82+       ) ; 
7383    } 
7484
7585    const  tokenDuration  =  tokenPayload . exp !  -  tokenPayload . iat ! ; 
@@ -78,29 +88,36 @@ export class KeyStore<Collector extends KeyCollector = KeyCollector> {
7888    // is too far into the future. 
7989    const  maxAge  =  keyOptions . maxLifetimeSeconds  ??  secs ( options . maxAge ) ; 
8090    if  ( tokenDuration  >  maxAge )  { 
81-       throw  new  jose . errors . JWTInvalid ( `Token must expire in a maximum of ${ maxAge } ${ tokenDuration }  ) ; 
91+       throw  new  AuthorizationError ( 
92+         ErrorCode . PSYNC_S2104 , 
93+         `Token must expire in a maximum of ${ maxAge } ${ tokenDuration }  
94+       ) ; 
8295    } 
8396
8497    const  parameters  =  tokenPayload . parameters ; 
8598    if  ( parameters  !=  null  &&  ( Array . isArray ( parameters )  ||  typeof  parameters  !=  'object' ) )  { 
86-       throw  new  jose . errors . JWTInvalid ( ' parameters must be an object' ) ; 
99+       throw  new  AuthorizationError ( ErrorCode . PSYNC_S2101 ,   `Payload  parameters must be an object` ) ; 
87100    } 
88101
89102    return  tokenPayload  as  JwtPayload ; 
90103  } 
91104
92105  private  async  verifyInternal ( token : string ,  options : jose . JWTVerifyOptions )  { 
93106    let  keyOptions : KeyOptions  |  undefined  =  undefined ; 
94-     const  result  =  await  jose . jwtVerify ( 
95-       token , 
96-       async  ( header )  =>  { 
97-         let  key  =  await  this . getCachedKey ( token ,  header ) ; 
98-         keyOptions  =  key . options ; 
99-         return  key . key ; 
100-       } , 
101-       options 
102-     ) ; 
103-     return  {  result,  keyOptions : keyOptions !  } ; 
107+     try  { 
108+       const  result  =  await  jose . jwtVerify ( 
109+         token , 
110+         async  ( header )  =>  { 
111+           let  key  =  await  this . getCachedKey ( token ,  header ) ; 
112+           keyOptions  =  key . options ; 
113+           return  key . key ; 
114+         } , 
115+         options 
116+       ) ; 
117+       return  {  result,  keyOptions : keyOptions !  } ; 
118+     }  catch  ( e )  { 
119+       throw  mapAuthError ( e ,  token ) ; 
120+     } 
104121  } 
105122
106123  private  async  getCachedKey ( token : string ,  header : jose . JWTHeaderParameters ) : Promise < KeySpec >  { 
@@ -112,7 +129,10 @@ export class KeyStore<Collector extends KeyCollector = KeyCollector> {
112129      for  ( let  key  of  keys )  { 
113130        if  ( key . kid  ==  kid )  { 
114131          if  ( ! key . matchesAlgorithm ( header . alg ) )  { 
115-             throw  new  jose . errors . JOSEAlgNotAllowed ( `Unexpected token algorithm ${ header . alg }  ) ; 
132+             throw  new  AuthorizationError ( ErrorCode . PSYNC_S2101 ,  `Unexpected token algorithm ${ header . alg }  ,  { 
133+               configurationDetails : `Key kid: ${ key . source . kid } ${ key . source . alg } ${ key . source . kty }  
134+               // Token details automatically populated elsewhere 
135+             } ) ; 
116136          } 
117137          return  key ; 
118138        } 
@@ -145,8 +165,13 @@ export class KeyStore<Collector extends KeyCollector = KeyCollector> {
145165        logger . error ( `Failed to refresh keys` ,  e ) ; 
146166      } ) ; 
147167
148-       throw  new  jose . errors . JOSEError ( 
149-         'Could not find an appropriate key in the keystore. The key is missing or no key matched the token KID' 
168+       throw  new  AuthorizationError ( 
169+         ErrorCode . PSYNC_S2101 , 
170+         'Could not find an appropriate key in the keystore. The key is missing or no key matched the token KID' , 
171+         { 
172+           configurationDetails : `Known kid values: ${ keys . map ( ( key )  =>  key . kid  ??  '*' ) . join ( ', ' ) }  
173+           // tokenDetails automatically populated later 
174+         } 
150175      ) ; 
151176    } 
152177  } 
0 commit comments