@@ -12,43 +12,15 @@ interface JWKMetadata {
12
12
const isJWKMetadata = ( value : any ) : value is JWKMetadata =>
13
13
isNonNullObject ( value ) && ! ! value . keys && isArray ( value . keys ) ;
14
14
15
- // {
16
- // "keys": [
17
- // {
18
- // "kid": "f90fb1ae048a548fb681ad6092b0b869ea467ac6",
19
- // "e": "AQAB",
20
- // "kty": "RSA",
21
- // "n": "v1DLA89xpRpQ2bA2Ku__34z98eISnT1coBgA3QNjitmpM-4rf1pPNH6MKxOOj4ZxvzSeGlOjB7XiQwX3lQJ-ZDeSvS45fWIKrDW33AyFn-Z4VFJLVRb7j4sqLa6xsTj5rkbJBDwwGbGXOo37o5Ewfn0S52GFDjl2ALKexIgu7cUKKHsykr_h6D6RdhwpHvjG_H5Omq9mY7wDxLTvtYyrpN3wONAf4uMsJn9GDgMsAu7UkhDSICX5jmhVUDvYJA3FKokFyjG7PdetNnh00prL_CtH1Bs8f06sWwQKQMTDUrKEyEHuc2bzWNfGXRrc-c_gRNWP9k7vzOTcAIFSWlA7Fw",
22
- // "alg": "RS256",
23
- // "use": "sig"
24
- // },
25
- // {
26
- // "use": "sig",
27
- // "kid": "9897cf9459e254ff1c67a4eb6efea52f21a9ba14",
28
- // "n": "ylSiwcLD0KXrnzo4QlVFdVjx3OL5x0qYOgkcdLgiBxABUq9Y7AuIwABlCKVYcMCscUnooQvEATShnLdbqu0lLOaTiK1JxblIGonZrOB8-MlXn7-RnEmQNuMbvNK7QdwTrz3uzbqB64Z70DoC0qLVPT5v9ivzNfulh6UEuNVvFupC2zbrP84oxzRmpgcF0lxpiZf4qfCC2aKU8wDCqP14-PqHLI54nfm9QBLJLz4uS00OqdwWITSjX3nlBVcDqvCbJi3_V-eoBP42prVTreILWHw0SqP6FGt2lFPWeMnGinlRLAdwaEStrPzclvAupR5vEs3-m0UCOUt0rZOZBtTNkw",
29
- // "e": "AQAB",
30
- // "kty": "RSA",
31
- // "alg": "RS256"
32
- // }
33
- // ]
34
- // }
35
-
36
15
/**
37
16
* Class to fetch public keys from a client certificates URL.
38
17
*/
39
18
export class UrlKeyFetcher implements KeyFetcher {
40
- private readonly PUBLIC_KEY_CACHE_KEY = "google-public-jwks" ;
41
-
42
19
constructor (
43
- private readonly clientCertUrl : string ,
20
+ private readonly fetcher : Fetcher ,
21
+ private readonly cacheKey : string ,
44
22
private readonly cfKVNamespace : KVNamespace
45
- ) {
46
- if ( ! isURL ( clientCertUrl ) ) {
47
- throw new Error (
48
- "The provided public client certificate URL is not a valid URL."
49
- ) ;
50
- }
51
- }
23
+ ) { }
52
24
53
25
/**
54
26
* Fetches the public keys for the Google certs.
@@ -57,7 +29,7 @@ export class UrlKeyFetcher implements KeyFetcher {
57
29
*/
58
30
public async fetchPublicKeys ( ) : Promise < Array < JsonWebKeyWithKid > > {
59
31
const publicKeys = await this . cfKVNamespace . get < Array < JsonWebKeyWithKid > > (
60
- this . PUBLIC_KEY_CACHE_KEY ,
32
+ this . cacheKey ,
61
33
"json"
62
34
) ;
63
35
if ( publicKeys === null || typeof publicKeys !== "object" ) {
@@ -67,8 +39,7 @@ export class UrlKeyFetcher implements KeyFetcher {
67
39
}
68
40
69
41
private async refresh ( ) : Promise < Array < JsonWebKeyWithKid > > {
70
- // TODO(codehex): add retry
71
- const resp = await fetch ( this . clientCertUrl ) ;
42
+ const resp = await this . fetcher . fetch ( ) ;
72
43
if ( ! resp . ok ) {
73
44
let errorMessage = "Error fetching public keys for Google certs: " ;
74
45
const text = await resp . text ( ) ;
@@ -78,48 +49,61 @@ export class UrlKeyFetcher implements KeyFetcher {
78
49
const publicKeys = await resp . json ( ) ;
79
50
if ( ! isJWKMetadata ( publicKeys ) ) {
80
51
throw new Error (
81
- `The public keys are not an object or null: ' ${ publicKeys } ' `
52
+ `The public keys are not an object or null: " ${ publicKeys } `
82
53
) ;
83
54
}
84
55
85
56
const cacheControlHeader = resp . headers . get ( "cache-control" ) ;
86
57
87
58
// store the public keys cache in the KV store.
88
- if ( cacheControlHeader !== null ) {
89
- const parts = cacheControlHeader . split ( "," ) ;
90
- for ( const part of parts ) {
91
- const subParts = part . trim ( ) . split ( "=" ) ;
92
- if ( subParts [ 0 ] !== "max-age" ) {
93
- continue ;
59
+ const maxAge = parseMaxAge ( cacheControlHeader )
60
+ if ( ! isNaN ( maxAge ) ) {
61
+ this . cfKVNamespace . put (
62
+ this . cacheKey ,
63
+ JSON . stringify ( publicKeys . keys ) ,
64
+ {
65
+ expirationTtl : maxAge ,
94
66
}
95
- const maxAge : number = + subParts [ 1 ] ; // maxAge is a seconds value.
96
- this . cfKVNamespace . put (
97
- this . PUBLIC_KEY_CACHE_KEY ,
98
- JSON . stringify ( publicKeys . keys ) ,
99
- {
100
- expirationTtl : maxAge ,
101
- }
102
- ) ;
103
- }
67
+ ) ;
104
68
}
105
69
106
70
return publicKeys . keys ;
107
71
}
108
72
}
109
73
110
- // This is an example of a response header that fetches public keys from a "clientCertUrl".
111
- // HTTP/2 200
112
- // < vary: X-Origin
113
- // < vary: Referer
114
- // < vary: Origin,Accept-Encoding
115
- // < server: scaffolding on HTTPServer2
116
- // < x-xss-protection: 0
117
- // < x-frame-options: SAMEORIGIN
118
- // < x-content-type-options: nosniff
119
- // < date: Sun, 26 Jun 2022 03:33:09 GMT
120
- // < expires: Sun, 26 Jun 2022 08:44:20 GMT
121
- // < cache-control: public, max-age=18671, must-revalidate, no-transform
122
- // < content-type: application/json; charset=UTF-8
123
- // < age: 32
124
- // < alt-svc: h3=":443"; ma=2592000,h3-29=":443"; ma=2592000,h3-Q050=":443"; ma=2592000,h3-Q046=":443"; ma=2592000,h3-Q043=":443"; ma=2592000,quic=":443"; ma=2592000; v="46,43"
125
- // < accept-ranges: none
74
+ // parseMaxAge parses Cache-Control header and returns max-age value as number.
75
+ // returns NaN when Cache-Control header is none or max-age is not found, the value is invalid.
76
+ export const parseMaxAge = ( cacheControlHeader : string | null ) : number => {
77
+ if ( cacheControlHeader === null ) {
78
+ return NaN
79
+ }
80
+ const parts = cacheControlHeader . split ( "," ) ;
81
+ for ( const part of parts ) {
82
+ const subParts = part . trim ( ) . split ( "=" ) ;
83
+ if ( subParts [ 0 ] !== "max-age" ) {
84
+ continue ;
85
+ }
86
+ return Number ( subParts [ 1 ] ) ; // maxAge is a seconds value.
87
+ }
88
+ return NaN
89
+ }
90
+
91
+ export interface Fetcher {
92
+ fetch ( ) : Promise < Response >
93
+ }
94
+
95
+ export class HTTPFetcher implements Fetcher {
96
+ constructor (
97
+ private readonly clientCertUrl : string ,
98
+ ) {
99
+ if ( ! isURL ( clientCertUrl ) ) {
100
+ throw new Error (
101
+ "The provided public client certificate URL is not a valid URL."
102
+ ) ;
103
+ }
104
+ }
105
+
106
+ public fetch ( ) : Promise < Response > {
107
+ return fetch ( this . clientCertUrl )
108
+ }
109
+ }
0 commit comments