11import  {  AuthorizationError ,  ErrorCode  }  from  '@powersync/lib-services-framework' ; 
22import  *  as  jose  from  'jose' ; 
33
4- export  function  mapJoseError ( error : jose . errors . JOSEError ) : AuthorizationError  { 
5-   // TODO: improved message for exp issues, etc 
4+ export  function  mapJoseError ( error : jose . errors . JOSEError ,   token :  string ) : AuthorizationError  { 
5+   const   tokenDetails   =   tokenDebugDetails ( token ) ; 
66  if  ( error . code  ===  jose . errors . JWSInvalid . code  ||  error . code  ===  jose . errors . JWTInvalid . code )  { 
7-     throw  new  AuthorizationError ( ErrorCode . PSYNC_S2101 ,  'Token is not a well-formed JWT. Check the token format.' ,  { 
8-       details : error . message 
7+     return  new  AuthorizationError ( ErrorCode . PSYNC_S2101 ,  'Token is not a well-formed JWT. Check the token format.' ,  { 
8+       tokenDetails, 
9+       cause : error 
910    } ) ; 
1011  }  else  if  ( error . code  ===  jose . errors . JWTClaimValidationFailed . code )  { 
11-     // Example : missing required "sub" claim 
12+     // Jose message : missing required "sub" claim 
1213    const  claim  =  ( error  as  jose . errors . JWTClaimValidationFailed ) . claim ; 
13-     throw  new  AuthorizationError ( 
14+     return  new  AuthorizationError ( 
1415      ErrorCode . PSYNC_S2101 , 
1516      `JWT payload is missing a required claim ${ JSON . stringify ( claim ) }  , 
1617      { 
17-         cause : error 
18+         cause : error , 
19+         tokenDetails
1820      } 
1921    ) ; 
22+   }  else  if  ( error . code  ==  jose . errors . JWTExpired . code )  { 
23+     // Jose message: "exp" claim timestamp check failed 
24+     return  new  AuthorizationError ( ErrorCode . PSYNC_S2103 ,  `JWT has expired` ,  { 
25+       cause : error , 
26+       tokenDetails
27+     } ) ; 
2028  } 
2129  return  new  AuthorizationError ( ErrorCode . PSYNC_S2101 ,  error . message ,  {  cause : error  } ) ; 
2230} 
2331
24- export  function  mapAuthError ( error : any ) : AuthorizationError  { 
32+ export  function  mapAuthError ( error : any ,   token :  string ) : AuthorizationError  { 
2533  if  ( error  instanceof  AuthorizationError )  { 
34+     error . tokenDetails  ??=  tokenDebugDetails ( token ) ; 
2635    return  error ; 
2736  }  else  if  ( error  instanceof  jose . errors . JOSEError )  { 
28-     return  mapJoseError ( error ) ; 
37+     return  mapJoseError ( error ,   token ) ; 
2938  } 
30-   return  new  AuthorizationError ( ErrorCode . PSYNC_S2101 ,  error . message ,  {  cause : error  } ) ; 
39+   return  new  AuthorizationError ( ErrorCode . PSYNC_S2101 ,  error . message ,  { 
40+     cause : error , 
41+     tokenDetails : tokenDebugDetails ( token ) 
42+   } ) ; 
3143} 
3244
3345export  function  mapJoseConfigError ( error : jose . errors . JOSEError ) : AuthorizationError  { 
@@ -42,3 +54,49 @@ export function mapAuthConfigError(error: any): AuthorizationError {
4254  } 
4355  return  new  AuthorizationError ( ErrorCode . PSYNC_S2201 ,  error . message  ??  'Auth configuration error' ,  {  cause : error  } ) ; 
4456} 
57+ 
58+ /** 
59+  * Decode token for debugging purposes. 
60+  * 
61+  * We use this to add details to our logs. We don't log the entire token, since it may for example 
62+  * a password incorrectly used as a token. 
63+  */ 
64+ function  tokenDebugDetails ( token : string ) : string  { 
65+   try  { 
66+     // For valid tokens, we return the header and payload 
67+     const  header  =  jose . decodeProtectedHeader ( token ) ; 
68+     const  payload  =  jose . decodeJwt ( token ) ; 
69+     return  `<header: ${ JSON . stringify ( header ) } ${ JSON . stringify ( payload ) }  ; 
70+   }  catch  ( e )  { 
71+     // Token fails to parse. Return some details. 
72+     return  invalidTokenDetails ( token ) ; 
73+   } 
74+ } 
75+ 
76+ function  invalidTokenDetails ( token : string ) : string  { 
77+   const  parts  =  token . split ( '.' ) ; 
78+   if  ( parts . length  !==  3 )  { 
79+     return  `<token with ${ parts . length } ${ token . length }  ; 
80+   } 
81+ 
82+   const  [ headerB64 ,  payloadB64 ,  signatureB64 ]  =  parts ; 
83+ 
84+   try  { 
85+     JSON . parse ( Buffer . from ( headerB64 ,  'base64url' ) . toString ( 'utf8' ) ) ; 
86+   }  catch  ( e )  { 
87+     return  `<token with unparsable header>` ; 
88+   } 
89+ 
90+   try  { 
91+     JSON . parse ( Buffer . from ( payloadB64 ,  'base64url' ) . toString ( 'utf8' ) ) ; 
92+   }  catch  ( e )  { 
93+     return  `<token with unparsable payload>` ; 
94+   } 
95+   try  { 
96+     Buffer . from ( signatureB64 ,  'base64url' ) ; 
97+   }  catch  ( e )  { 
98+     return  `<token with unparsable signature>` ; 
99+   } 
100+ 
101+   return  `<invalid JWT, length=${ token . length }  ; 
102+ } 
0 commit comments