@fabrika/jwks 0.3.2 → 0.4.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 +2 -2
- package/src/verifier.ts +21 -10
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fabrika/jwks",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
29
|
"zod": "^3.25.76",
|
|
30
|
-
"@fabrika/auth-core": "^0.
|
|
30
|
+
"@fabrika/auth-core": "^0.4.0",
|
|
31
31
|
"jose": "^5.9.0"
|
|
32
32
|
}
|
|
33
33
|
}
|
package/src/verifier.ts
CHANGED
|
@@ -7,7 +7,14 @@
|
|
|
7
7
|
* Collapsing them would let a JWKS outage read as a forged token.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
type AccessTokenClaims,
|
|
12
|
+
type Jwks,
|
|
13
|
+
matchesTokenAudience,
|
|
14
|
+
parseAccessClaims,
|
|
15
|
+
RESOURCE_ACCESS_TOKEN_TYP,
|
|
16
|
+
type TokenAudienceProfile,
|
|
17
|
+
} from '@fabrika/auth-core'
|
|
11
18
|
import { createLocalJWKSet, errors as joseErrors, jwtVerify } from 'jose'
|
|
12
19
|
|
|
13
20
|
export type VerifyResult =
|
|
@@ -84,16 +91,18 @@ export class TokenVerifier {
|
|
|
84
91
|
}
|
|
85
92
|
|
|
86
93
|
/**
|
|
87
|
-
* Verify `token` for `audience` and narrow it to access claims.
|
|
88
|
-
*
|
|
89
|
-
*
|
|
94
|
+
* Verify `token` for `audience` and narrow it to access claims. A bare string is an app id
|
|
95
|
+
* (`{ kind: 'app', app }`). An unknown `kid` forces one JWKS refetch (a key rotated in), then a
|
|
96
|
+
* retry — but at most once per cooldown, because otherwise any caller can turn every request into
|
|
97
|
+
* an IAM round trip just by inventing a `kid`.
|
|
90
98
|
*/
|
|
91
|
-
async verify(token: string, audience: string): Promise<VerifyResult> {
|
|
99
|
+
async verify(token: string, audience: string | TokenAudienceProfile): Promise<VerifyResult> {
|
|
100
|
+
const profile: TokenAudienceProfile = typeof audience === 'string' ? { kind: 'app', app: audience } : audience
|
|
92
101
|
let payload: unknown
|
|
93
102
|
try {
|
|
94
|
-
payload = await this.verifyWith(token,
|
|
103
|
+
payload = await this.verifyWith(token, profile, false)
|
|
95
104
|
if (payload === NO_KEY && this.mayForceRefetch()) {
|
|
96
|
-
payload = await this.verifyWith(token,
|
|
105
|
+
payload = await this.verifyWith(token, profile, true)
|
|
97
106
|
}
|
|
98
107
|
} catch (err) {
|
|
99
108
|
if (err instanceof JwksUnavailableError) {
|
|
@@ -105,7 +114,7 @@ export class TokenVerifier {
|
|
|
105
114
|
return { ok: false, reason: 'invalid' }
|
|
106
115
|
}
|
|
107
116
|
const claims = parseAccessClaims(payload)
|
|
108
|
-
return claims === null ? { ok: false, reason: 'invalid' } : { ok: true, claims }
|
|
117
|
+
return claims === null || !matchesTokenAudience(claims, profile) ? { ok: false, reason: 'invalid' } : { ok: true, claims }
|
|
109
118
|
}
|
|
110
119
|
|
|
111
120
|
/**
|
|
@@ -123,10 +132,12 @@ export class TokenVerifier {
|
|
|
123
132
|
}
|
|
124
133
|
|
|
125
134
|
/** Returns the payload, `null` on a genuine verification failure, or `NO_KEY` on a kid miss. */
|
|
126
|
-
private async verifyWith(token: string,
|
|
135
|
+
private async verifyWith(token: string, profile: TokenAudienceProfile, force: boolean): Promise<unknown> {
|
|
127
136
|
const keys = await this.keySet(force)
|
|
137
|
+
const audience = profile.kind === 'app' ? profile.app : profile.resource
|
|
128
138
|
try {
|
|
129
|
-
const { payload } = await jwtVerify(token, keys, { issuer: this.issuer, audience })
|
|
139
|
+
const { payload, protectedHeader } = await jwtVerify(token, keys, { issuer: this.issuer, audience })
|
|
140
|
+
if (profile.kind === 'resource' && protectedHeader.typ !== RESOURCE_ACCESS_TOKEN_TYP) return null
|
|
130
141
|
return payload
|
|
131
142
|
} catch (err) {
|
|
132
143
|
return err instanceof joseErrors.JWKSNoMatchingKey ? NO_KEY : null
|