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 } seconds, got ${ tokenDuration } ` ) ;
91+ throw new AuthorizationError (
92+ ErrorCode . PSYNC_S2104 ,
93+ `Token must expire in a maximum of ${ maxAge } seconds, got ${ tokenDuration } s`
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 } , alg: ${ key . source . alg } , kty: ${ 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