@@ -12,43 +12,15 @@ interface JWKMetadata {
1212const isJWKMetadata = ( value : any ) : value is JWKMetadata =>
1313 isNonNullObject ( value ) && ! ! value . keys && isArray ( value . keys ) ;
1414
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-
3615/**
3716 * Class to fetch public keys from a client certificates URL.
3817 */
3918export class UrlKeyFetcher implements KeyFetcher {
40- private readonly PUBLIC_KEY_CACHE_KEY = "google-public-jwks" ;
41-
4219 constructor (
43- private readonly clientCertUrl : string ,
20+ private readonly fetcher : Fetcher ,
21+ private readonly cacheKey : string ,
4422 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+ ) { }
5224
5325 /**
5426 * Fetches the public keys for the Google certs.
@@ -57,7 +29,7 @@ export class UrlKeyFetcher implements KeyFetcher {
5729 */
5830 public async fetchPublicKeys ( ) : Promise < Array < JsonWebKeyWithKid > > {
5931 const publicKeys = await this . cfKVNamespace . get < Array < JsonWebKeyWithKid > > (
60- this . PUBLIC_KEY_CACHE_KEY ,
32+ this . cacheKey ,
6133 "json"
6234 ) ;
6335 if ( publicKeys === null || typeof publicKeys !== "object" ) {
@@ -67,8 +39,7 @@ export class UrlKeyFetcher implements KeyFetcher {
6739 }
6840
6941 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 ( ) ;
7243 if ( ! resp . ok ) {
7344 let errorMessage = "Error fetching public keys for Google certs: " ;
7445 const text = await resp . text ( ) ;
@@ -78,48 +49,61 @@ export class UrlKeyFetcher implements KeyFetcher {
7849 const publicKeys = await resp . json ( ) ;
7950 if ( ! isJWKMetadata ( publicKeys ) ) {
8051 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 } `
8253 ) ;
8354 }
8455
8556 const cacheControlHeader = resp . headers . get ( "cache-control" ) ;
8657
8758 // 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 ,
9466 }
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+ ) ;
10468 }
10569
10670 return publicKeys . keys ;
10771 }
10872}
10973
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