@fabrika/auth 0.0.26 → 0.2.0
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/package.json +3 -2
- package/src/verify.ts +25 -83
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fabrika/auth",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -26,7 +26,8 @@
|
|
|
26
26
|
"test": "bun test"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@fabrika/auth-core": "^0.0
|
|
29
|
+
"@fabrika/auth-core": "^0.2.0",
|
|
30
|
+
"@fabrika/jwks": "^0.2.0",
|
|
30
31
|
"jose": "^5.9.0"
|
|
31
32
|
}
|
|
32
33
|
}
|
package/src/verify.ts
CHANGED
|
@@ -7,102 +7,44 @@
|
|
|
7
7
|
* re-verified here against IAM's published JWKS — signature, `iss`, `aud`, `exp` — before it can
|
|
8
8
|
* become an `AuthContext`.
|
|
9
9
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* calls its `middleware(env)` factory on every request, so an SDK object built there is per-request
|
|
16
|
-
* while the binding lives for the isolate/process. An unknown `kid` (a key rotated in) forces exactly
|
|
17
|
-
* one refetch, and a stale verifier is never reused after a fetch failure — the previous key set may
|
|
18
|
-
* have been rotated out precisely because it was compromised.
|
|
10
|
+
* The verification itself is `@fabrika/jwks`, shared byte-for-byte with the proxy's enforcement path.
|
|
11
|
+
* What this file owns is the SDK's construction shape: the audience is fixed at construction, and the
|
|
12
|
+
* key set plus the unknown-`kid` cooldown are cached PER BINDING in a module-level WeakMap rather than
|
|
13
|
+
* per instance — `@fabrika/app` calls its `middleware(env)` factory on every request, so an SDK object
|
|
14
|
+
* built there is per-request while the binding lives for the isolate/process.
|
|
19
15
|
*/
|
|
20
16
|
|
|
21
|
-
import
|
|
22
|
-
import {
|
|
23
|
-
|
|
24
|
-
export type VerifyResult =
|
|
25
|
-
| { ok: true; claims: AccessTokenClaims }
|
|
26
|
-
/** Signature, issuer, audience, expiry or claim-shape failure — the token is not trustworthy. */
|
|
27
|
-
| { ok: false; reason: 'invalid' }
|
|
28
|
-
/** The key set could not be obtained, so nothing could be checked. Deny, but say why. */
|
|
29
|
-
| { ok: false; reason: 'unavailable' }
|
|
17
|
+
import type { IamRpc } from '@fabrika/auth-core'
|
|
18
|
+
import { createJwksCache, type JwksCache, TokenVerifier as JwksVerifier, type VerifyResult } from '@fabrika/jwks'
|
|
30
19
|
|
|
31
|
-
|
|
32
|
-
const JWKS_TTL_SECONDS = 600
|
|
20
|
+
export type { VerifyResult }
|
|
33
21
|
|
|
34
|
-
/**
|
|
35
|
-
const
|
|
22
|
+
/** Key set + cooldown cached per binding, so they survive the per-request SDK objects built over them. */
|
|
23
|
+
const jwksCache = new WeakMap<IamRpc, JwksCache>()
|
|
36
24
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
super('the IAM key set could not be fetched')
|
|
43
|
-
this.name = 'JwksUnavailableError'
|
|
25
|
+
function cacheFor(binding: IamRpc): JwksCache {
|
|
26
|
+
let cache = jwksCache.get(binding)
|
|
27
|
+
if (cache === undefined) {
|
|
28
|
+
cache = createJwksCache()
|
|
29
|
+
jwksCache.set(binding, cache)
|
|
44
30
|
}
|
|
31
|
+
return cache
|
|
45
32
|
}
|
|
46
33
|
|
|
47
|
-
/** JWKS verifier cached per binding, so it survives the per-request SDK objects built over it. */
|
|
48
|
-
const jwksCache = new WeakMap<IamRpc, { verifier: JwksVerifier; expiresAt: number }>()
|
|
49
|
-
|
|
50
34
|
/** Verifies tokens for ONE (binding, issuer, audience) triple. Cheap to construct; the cache is shared. */
|
|
51
35
|
export class TokenVerifier {
|
|
36
|
+
private readonly inner: JwksVerifier
|
|
37
|
+
|
|
52
38
|
constructor(
|
|
53
|
-
|
|
54
|
-
|
|
39
|
+
binding: IamRpc,
|
|
40
|
+
issuer: string,
|
|
55
41
|
private readonly audience: string,
|
|
56
|
-
|
|
57
|
-
) {
|
|
58
|
-
|
|
59
|
-
async verify(token: string): Promise<VerifyResult> {
|
|
60
|
-
let payload: unknown
|
|
61
|
-
try {
|
|
62
|
-
payload = await this.verifyWith(token, false)
|
|
63
|
-
if (payload === NO_KEY) {
|
|
64
|
-
// Unknown kid — a key was rotated in. Refetch the JWKS once and retry.
|
|
65
|
-
payload = await this.verifyWith(token, true)
|
|
66
|
-
}
|
|
67
|
-
} catch (err) {
|
|
68
|
-
if (err instanceof JwksUnavailableError) {
|
|
69
|
-
return { ok: false, reason: 'unavailable' }
|
|
70
|
-
}
|
|
71
|
-
throw err
|
|
72
|
-
}
|
|
73
|
-
if (payload === NO_KEY || payload === null) {
|
|
74
|
-
return { ok: false, reason: 'invalid' }
|
|
75
|
-
}
|
|
76
|
-
const claims = parseAccessClaims(payload)
|
|
77
|
-
return claims === null ? { ok: false, reason: 'invalid' } : { ok: true, claims }
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/** Returns the payload, `null` on a genuine verification failure, or `NO_KEY` on a kid miss. */
|
|
81
|
-
private async verifyWith(token: string, force: boolean): Promise<unknown> {
|
|
82
|
-
const jwks = await this.jwks(force)
|
|
83
|
-
try {
|
|
84
|
-
const { payload } = await jwtVerify(token, jwks, { issuer: this.issuer, audience: this.audience })
|
|
85
|
-
return payload
|
|
86
|
-
} catch (err) {
|
|
87
|
-
return err instanceof joseErrors.JWKSNoMatchingKey ? NO_KEY : null
|
|
88
|
-
}
|
|
42
|
+
now: () => number = () => Math.floor(Date.now() / 1000),
|
|
43
|
+
) {
|
|
44
|
+
this.inner = new JwksVerifier({ getJwks: () => binding.getJwks(), issuer, now, cache: cacheFor(binding) })
|
|
89
45
|
}
|
|
90
46
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
const cached = jwksCache.get(this.binding)
|
|
94
|
-
if (!force && cached !== undefined && cached.expiresAt > now) {
|
|
95
|
-
return cached.verifier
|
|
96
|
-
}
|
|
97
|
-
let keys: Jwks
|
|
98
|
-
try {
|
|
99
|
-
keys = await this.binding.getJwks()
|
|
100
|
-
} catch {
|
|
101
|
-
// Never re-wrap the cause: it can stringify a request URL or a transport credential.
|
|
102
|
-
throw new JwksUnavailableError()
|
|
103
|
-
}
|
|
104
|
-
const verifier = createLocalJWKSet(keys)
|
|
105
|
-
jwksCache.set(this.binding, { verifier, expiresAt: now + JWKS_TTL_SECONDS })
|
|
106
|
-
return verifier
|
|
47
|
+
verify(token: string): Promise<VerifyResult> {
|
|
48
|
+
return this.inner.verify(token, this.audience)
|
|
107
49
|
}
|
|
108
50
|
}
|