1
- import { logger } from '@powersync/lib-services-framework' ;
1
+ import { logger , errors , AuthorizationError , ErrorCode } from '@powersync/lib-services-framework' ;
2
2
import * as jose from 'jose' ;
3
3
import secs from '../util/secs.js' ;
4
4
import { JwtPayload } from './JwtPayload.js' ;
5
5
import { KeyCollector } from './KeyCollector.js' ;
6
6
import { KeyOptions , KeySpec , SUPPORTED_ALGORITHMS } from './KeySpec.js' ;
7
+ import { mapAuthError } from './utils.js' ;
7
8
8
9
/**
9
10
* KeyStore to get keys and verify tokens.
@@ -49,7 +50,8 @@ export class KeyStore<Collector extends KeyCollector = KeyCollector> {
49
50
clockTolerance : 60 ,
50
51
// More specific algorithm checking is done when selecting the key to use.
51
52
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' ]
53
55
} ) ;
54
56
55
57
let audiences = options . defaultAudiences ;
@@ -60,16 +62,24 @@ export class KeyStore<Collector extends KeyCollector = KeyCollector> {
60
62
61
63
const tokenPayload = result . payload ;
62
64
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 ) ) {
65
71
aud = [ aud ] ;
66
72
}
67
73
if (
68
74
! aud . some ( ( a ) => {
69
75
return audiences . includes ( a ) ;
70
76
} )
71
77
) {
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
+ ) ;
73
83
}
74
84
75
85
const tokenDuration = tokenPayload . exp ! - tokenPayload . iat ! ;
@@ -78,29 +88,36 @@ export class KeyStore<Collector extends KeyCollector = KeyCollector> {
78
88
// is too far into the future.
79
89
const maxAge = keyOptions . maxLifetimeSeconds ?? secs ( options . maxAge ) ;
80
90
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
+ ) ;
82
95
}
83
96
84
97
const parameters = tokenPayload . parameters ;
85
98
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` ) ;
87
100
}
88
101
89
102
return tokenPayload as JwtPayload ;
90
103
}
91
104
92
105
private async verifyInternal ( token : string , options : jose . JWTVerifyOptions ) {
93
106
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
+ }
104
121
}
105
122
106
123
private async getCachedKey ( token : string , header : jose . JWTHeaderParameters ) : Promise < KeySpec > {
@@ -112,7 +129,10 @@ export class KeyStore<Collector extends KeyCollector = KeyCollector> {
112
129
for ( let key of keys ) {
113
130
if ( key . kid == kid ) {
114
131
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
+ } ) ;
116
136
}
117
137
return key ;
118
138
}
@@ -145,8 +165,13 @@ export class KeyStore<Collector extends KeyCollector = KeyCollector> {
145
165
logger . error ( `Failed to refresh keys` , e ) ;
146
166
} ) ;
147
167
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
+ }
150
175
) ;
151
176
}
152
177
}
0 commit comments