-
Notifications
You must be signed in to change notification settings - Fork 11
Expand file tree
/
Copy pathjwk-fetcher.ts
More file actions
133 lines (117 loc) · 3.71 KB
/
Copy pathjwk-fetcher.ts
File metadata and controls
133 lines (117 loc) · 3.71 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
import type { KeyStorer } from './key-store';
import { isNonNullObject, isObject, isURL } from './validator';
import { jwkFromX509 } from './x509';
export interface KeyFetcher {
fetchPublicKeys(): Promise<Array<JsonWebKeyWithKid>>;
}
interface JWKMetadata {
keys: Array<JsonWebKeyWithKid>;
}
export const isJWKMetadata = (value: any): value is JWKMetadata => {
if (!isNonNullObject(value) || !value.keys) {
return false;
}
const keys = value.keys;
if (!Array.isArray(keys)) {
return false;
}
const filtered = keys.filter(
(key): key is JsonWebKeyWithKid => isObject(key) && !!key.kid && typeof key.kid === 'string'
);
return keys.length === filtered.length;
};
export const isX509Certificates = (value: any): value is Record<string, string> => {
if (!isNonNullObject(value)) {
return false;
}
const values = Object.values(value);
if (values.length === 0) {
return false;
}
for (const v of values) {
if (typeof v !== 'string' || v === '') {
return false;
}
}
return true;
};
/**
* Class to fetch public keys from a client certificates URL.
*/
export class UrlKeyFetcher implements KeyFetcher {
constructor(
private readonly fetcher: Fetcher,
private readonly keyStorer: KeyStorer
) {}
/**
* Fetches the public keys for the Google certs.
*
* @returns A promise fulfilled with public keys for the Google certs.
*/
public async fetchPublicKeys(): Promise<Array<JsonWebKeyWithKid>> {
const publicKeys = await this.keyStorer.get<Array<JsonWebKeyWithKid>>();
if (publicKeys === null || typeof publicKeys !== 'object') {
return await this.refresh();
}
return publicKeys;
}
private async refresh(): Promise<Array<JsonWebKeyWithKid>> {
const resp = await this.fetcher.fetch();
if (!resp.ok) {
const errorMessage = 'Error fetching public keys for Google certs: ';
const text = await resp.text();
throw new Error(errorMessage + text);
}
const json = await resp.json();
const publicKeys = await this.retrievePublicKeys(json);
const cacheControlHeader = resp.headers.get('cache-control');
// store the public keys cache in the KV store.
const maxAge = parseMaxAge(cacheControlHeader);
if (!isNaN(maxAge) && maxAge > 0) {
await this.keyStorer.put(JSON.stringify(publicKeys), maxAge);
}
return publicKeys;
}
private async retrievePublicKeys(json: unknown): Promise<Array<JsonWebKeyWithKid>> {
if (isX509Certificates(json)) {
const jwks: JsonWebKeyWithKid[] = [];
for (const [kid, x509] of Object.entries(json)) {
jwks.push(await jwkFromX509(kid, x509));
}
return jwks;
}
if (!isJWKMetadata(json)) {
throw new Error(`The public keys are not an object or null: "${json}`);
}
return json.keys;
}
}
// parseMaxAge parses Cache-Control header and returns max-age value as number.
// returns NaN when Cache-Control header is none or max-age is not found, the value is invalid.
export const parseMaxAge = (cacheControlHeader: string | null): number => {
if (cacheControlHeader === null) {
return NaN;
}
const parts = cacheControlHeader.split(',');
for (const part of parts) {
const subParts = part.trim().split('=');
if (subParts[0] !== 'max-age') {
continue;
}
return Number(subParts[1]); // maxAge is a seconds value.
}
return NaN;
};
export interface Fetcher {
fetch(): Promise<Response>;
}
export class HTTPFetcher implements Fetcher {
constructor(private readonly clientCertUrl: string) {
if (!isURL(clientCertUrl)) {
throw new Error('The provided public client certificate URL is not a valid URL.');
}
}
public fetch(): Promise<Response> {
return fetch(this.clientCertUrl);
}
}