@columenlabs/lumen-auth 0.1.232 → 0.1.234
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/dist/access.d.ts +16 -1
- package/dist/access.js +54 -14
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -19,7 +19,8 @@ Pin it to the same exact version as the other Lumen packages.
|
|
|
19
19
|
```ts
|
|
20
20
|
import { AccessVerifier, accessToken } from '@columenlabs/lumen-auth';
|
|
21
21
|
|
|
22
|
-
// One per isolate: the team's signing keys are fetched once and kept an hour.
|
|
22
|
+
// One per isolate: the team's signing keys are fetched once and kept an hour. A key fetch gives up after
|
|
23
|
+
// 3 s (ACCESS_CERTS_MS), only one runs at a time, and keys already held keep verifying while it retries.
|
|
23
24
|
const access = new AccessVerifier({
|
|
24
25
|
teamDomain: 'myteam.cloudflareaccess.com',
|
|
25
26
|
aud: [env.ACCESS_AUD], // one application's AUD tag, or several (one Worker, several hosts)
|
package/dist/access.d.ts
CHANGED
|
@@ -10,23 +10,38 @@ export interface AccessIdentity {
|
|
|
10
10
|
/** Service-token client ID (`common_name`) when an Access service token, not a person, signed in. */
|
|
11
11
|
serviceToken?: string;
|
|
12
12
|
}
|
|
13
|
-
type FetchFn = (input: string) => Promise<Response>;
|
|
14
13
|
/** The header Access puts its signed token in. */
|
|
15
14
|
export declare const ACCESS_HEADER = "cf-access-jwt-assertion";
|
|
16
15
|
/** A request's Access token, if it has one. */
|
|
17
16
|
export declare const accessToken: (request: Request) => string | undefined;
|
|
17
|
+
type FetchFn = (input: string, init?: RequestInit) => Promise<Response>;
|
|
18
|
+
/**
|
|
19
|
+
* How long a fetch of the signing keys may take (ms). A key fetch sits in front of every request, so
|
|
20
|
+
* one that hangs (an Access or network incident) must never hold them: past this it fails, and the
|
|
21
|
+
* keys already held keep verifying.
|
|
22
|
+
*/
|
|
23
|
+
export declare const ACCESS_CERTS_MS = 3000;
|
|
18
24
|
/**
|
|
19
25
|
* Verifies Access tokens against the team's signing keys (fetched from
|
|
20
26
|
* `https://<teamDomain>/cdn-cgi/access/certs`, kept an hour, fetched again for an unknown key id):
|
|
21
27
|
* RS256 only, the audience one of `aud`, the issuer the team, not expired and not before its time.
|
|
22
28
|
* Keep one per isolate so the keys are fetched once.
|
|
29
|
+
*
|
|
30
|
+
* The key fetch never holds a request for long: it gives up after `ACCESS_CERTS_MS`, only one runs at
|
|
31
|
+
* a time (every request waiting shares it), and when the hour is up the keys already held keep
|
|
32
|
+
* verifying while new ones load in the background. Only a token signed by a key never seen waits for
|
|
33
|
+
* a fetch, and fails (null) if that fetch fails.
|
|
23
34
|
*/
|
|
24
35
|
export declare class AccessVerifier {
|
|
25
36
|
private readonly cfg;
|
|
26
37
|
private readonly fetchImpl;
|
|
27
38
|
private readonly now;
|
|
28
39
|
private keys;
|
|
40
|
+
private loading;
|
|
41
|
+
private tried;
|
|
29
42
|
constructor(cfg: AccessConfig, fetchImpl?: FetchFn, now?: () => number);
|
|
43
|
+
/** Fetches the keys once for everyone waiting; bounded by ACCESS_CERTS_MS. */
|
|
44
|
+
private load;
|
|
30
45
|
private keyFor;
|
|
31
46
|
/** Returns the identity, or null when the token is missing/invalid. */
|
|
32
47
|
verify(token: string | undefined): Promise<AccessIdentity | null>;
|
package/dist/access.js
CHANGED
|
@@ -18,37 +18,77 @@ const decodeJson = (s) => JSON.parse(new TextDecoder().decode(b64urlToBytes(s)))
|
|
|
18
18
|
export const ACCESS_HEADER = 'cf-access-jwt-assertion';
|
|
19
19
|
/** A request's Access token, if it has one. */
|
|
20
20
|
export const accessToken = (request) => request.headers.get(ACCESS_HEADER) ?? undefined;
|
|
21
|
+
/** How long the signing keys are trusted before they are fetched again (ms). */
|
|
22
|
+
const KEYS_MS = 60 * 60_000;
|
|
23
|
+
/**
|
|
24
|
+
* How long a fetch of the signing keys may take (ms). A key fetch sits in front of every request, so
|
|
25
|
+
* one that hangs (an Access or network incident) must never hold them: past this it fails, and the
|
|
26
|
+
* keys already held keep verifying.
|
|
27
|
+
*/
|
|
28
|
+
export const ACCESS_CERTS_MS = 3_000;
|
|
29
|
+
/** A token naming a key id we do not hold refetches at most this often (ms), so bad tokens cannot flood the endpoint. */
|
|
30
|
+
const UNKNOWN_KID_MS = 10_000;
|
|
21
31
|
/**
|
|
22
32
|
* Verifies Access tokens against the team's signing keys (fetched from
|
|
23
33
|
* `https://<teamDomain>/cdn-cgi/access/certs`, kept an hour, fetched again for an unknown key id):
|
|
24
34
|
* RS256 only, the audience one of `aud`, the issuer the team, not expired and not before its time.
|
|
25
35
|
* Keep one per isolate so the keys are fetched once.
|
|
36
|
+
*
|
|
37
|
+
* The key fetch never holds a request for long: it gives up after `ACCESS_CERTS_MS`, only one runs at
|
|
38
|
+
* a time (every request waiting shares it), and when the hour is up the keys already held keep
|
|
39
|
+
* verifying while new ones load in the background. Only a token signed by a key never seen waits for
|
|
40
|
+
* a fetch, and fails (null) if that fetch fails.
|
|
26
41
|
*/
|
|
27
42
|
export class AccessVerifier {
|
|
28
43
|
cfg;
|
|
29
44
|
fetchImpl;
|
|
30
45
|
now;
|
|
31
46
|
keys;
|
|
32
|
-
|
|
47
|
+
loading;
|
|
48
|
+
tried = 0;
|
|
49
|
+
constructor(cfg, fetchImpl = (u, init) => fetch(u, init), now = Date.now) {
|
|
33
50
|
this.cfg = cfg;
|
|
34
51
|
this.fetchImpl = fetchImpl;
|
|
35
52
|
this.now = now;
|
|
36
53
|
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
54
|
+
/** Fetches the keys once for everyone waiting; bounded by ACCESS_CERTS_MS. */
|
|
55
|
+
load() {
|
|
56
|
+
this.tried = this.now();
|
|
57
|
+
this.loading ??= (async () => {
|
|
58
|
+
try {
|
|
59
|
+
const res = await this.fetchImpl(`https://${this.cfg.teamDomain}/cdn-cgi/access/certs`, {
|
|
60
|
+
signal: AbortSignal.timeout(ACCESS_CERTS_MS),
|
|
61
|
+
});
|
|
62
|
+
if (!res.ok)
|
|
63
|
+
throw new Error(`access certs ${res.status}`);
|
|
64
|
+
const { keys } = (await res.json());
|
|
65
|
+
const map = new Map();
|
|
66
|
+
for (const k of keys) {
|
|
67
|
+
map.set(k.kid, await crypto.subtle.importKey('jwk',
|
|
68
|
+
// JWK from Access's certs endpoint; the lib's JsonWebKey type isn't in scope here.
|
|
69
|
+
k, { name: 'RSASSA-PKCS1-v1_5', hash: 'SHA-256' }, false, ['verify']));
|
|
70
|
+
}
|
|
71
|
+
this.keys = { at: this.now(), keys: map };
|
|
49
72
|
}
|
|
50
|
-
|
|
73
|
+
finally {
|
|
74
|
+
this.loading = undefined;
|
|
75
|
+
}
|
|
76
|
+
})();
|
|
77
|
+
return this.loading;
|
|
78
|
+
}
|
|
79
|
+
async keyFor(kid) {
|
|
80
|
+
const held = this.keys?.keys.get(kid);
|
|
81
|
+
if (held) {
|
|
82
|
+
// Past the hour: keep verifying with the key held while fresh ones load (a failed refresh is
|
|
83
|
+
// tried again on a later request).
|
|
84
|
+
if (this.keys && this.now() - this.keys.at >= KEYS_MS && !this.loading)
|
|
85
|
+
this.load().catch(() => { });
|
|
86
|
+
return held;
|
|
51
87
|
}
|
|
88
|
+
// A key never seen: wait for a fetch (shared, bounded), unless one just failed to name it.
|
|
89
|
+
if (this.keys && !this.loading && this.now() - this.tried < UNKNOWN_KID_MS)
|
|
90
|
+
return undefined;
|
|
91
|
+
await this.load();
|
|
52
92
|
return this.keys?.keys.get(kid);
|
|
53
93
|
}
|
|
54
94
|
/** Returns the identity, or null when the token is missing/invalid. */
|
package/package.json
CHANGED