@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.
Files changed (2) hide show
  1. package/package.json +2 -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.2",
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.3.2",
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 { type AccessTokenClaims, type Jwks, parseAccessClaims } from '@fabrika/auth-core'
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. An unknown `kid` forces one JWKS
88
- * refetch (a key rotated in), then a retry — but at most once per cooldown, because otherwise any
89
- * caller can turn every request into an IAM round trip just by inventing a `kid`.
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, audience, false)
103
+ payload = await this.verifyWith(token, profile, false)
95
104
  if (payload === NO_KEY && this.mayForceRefetch()) {
96
- payload = await this.verifyWith(token, audience, true)
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, audience: string, force: boolean): Promise<unknown> {
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