7
7
import * as jsonwebtoken from "jsonwebtoken" ;
8
8
import { Config } from "../config" ;
9
9
import { inject , injectable } from "inversify" ;
10
- import { log } from "@gitpod/gitpod-protocol/lib/util/logging" ;
11
10
12
11
const algorithm : jsonwebtoken . Algorithm = "RS512" ;
13
12
@@ -21,43 +20,38 @@ export class AuthJWT {
21
20
expiresIn,
22
21
issuer : this . config . hostUrl . toStringWoRootSlash ( ) ,
23
22
subject,
23
+ keyid : this . config . auth . pki . signing . id ,
24
24
} ;
25
25
26
26
return sign ( payload , this . config . auth . pki . signing . privateKey , opts ) ;
27
27
}
28
28
29
29
async verify ( encoded : string ) : Promise < jsonwebtoken . JwtPayload > {
30
- // When we verify an encoded token, we verify it using all available public keys
31
- // That is, we check the following:
32
- // * The current signing public key
33
- // * All other validating public keys, in order
34
- //
35
- // We do this to allow for key-rotation. But tokens already issued would fail to validate
36
- // if the signing key was changed. To accept older sessions, which are still valid
37
- // we need to check for older keys also.
38
- const validatingPublicKeys = this . config . auth . pki . validating . map ( ( keypair ) => keypair . publicKey ) ;
39
- const publicKeys = [
40
- this . config . auth . pki . signing . publicKey , // signing key is checked first
41
- ...validatingPublicKeys ,
42
- ] ;
30
+ const keypairs = [ this . config . auth . pki . signing , ...this . config . auth . pki . validating ] ;
31
+ const publicKeysByID = keypairs . reduce < { [ id : string ] : string } > ( ( byID , keypair ) => {
32
+ byID [ keypair . id ] = keypair . publicKey ;
33
+ return byID ;
34
+ } , { } ) ;
43
35
44
- let lastErr ;
45
- for ( let publicKey of publicKeys ) {
46
- try {
47
- const decoded = await verify ( encoded , publicKey , {
48
- algorithms : [ algorithm ] ,
49
- } ) ;
50
- return decoded ;
51
- } catch ( err ) {
52
- log . debug ( `Failed to verify JWT token using public key.` , err ) ;
53
- lastErr = err ;
54
- }
36
+ // When we verify an encoded token, we first read the Key ID from the token header (without verifying the signature)
37
+ // and then lookup the appropriate key from out collection of available keys.
38
+ const decodedWithoutVerification = decodeWithoutVerification ( encoded ) ;
39
+ const keyID = decodedWithoutVerification . header . kid ;
40
+
41
+ if ( ! keyID ) {
42
+ throw new Error ( "JWT token does not contain kid (key id) property." ) ;
55
43
}
56
44
57
- log . error ( `Failed to verify JWT using any available public key.` , lastErr , {
58
- publicKeyCount : publicKeys . length ,
45
+ if ( ! publicKeysByID [ keyID ] ) {
46
+ throw new Error ( "JWT was signed with an unknown key id" ) ;
47
+ }
48
+
49
+ // Given we know which keyid this token was presumably signed with, let's verify the token using the public key.
50
+ const verified = await verify ( encoded , publicKeysByID [ keyID ] , {
51
+ algorithms : [ algorithm ] ,
59
52
} ) ;
60
- throw lastErr ;
53
+
54
+ return verified ;
61
55
}
62
56
}
63
57
@@ -90,3 +84,15 @@ export async function verify(
90
84
} ) ;
91
85
} ) ;
92
86
}
87
+
88
+ export function decodeWithoutVerification ( encoded : string , options ?: jsonwebtoken . DecodeOptions ) : jsonwebtoken . Jwt {
89
+ const decoded = jsonwebtoken . decode ( encoded , { ...options , complete : true } ) ;
90
+ if ( ! decoded ) {
91
+ throw new Error ( "Failed to decode JWT" ) ;
92
+ }
93
+ if ( typeof decoded === "string" ) {
94
+ throw new Error ( "Decoded JWT was a string, JSON payload required." ) ;
95
+ }
96
+
97
+ return decoded ;
98
+ }
0 commit comments