@fluojs/jwt 1.0.2 → 1.1.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/README.ko.md +19 -3
- package/README.md +19 -3
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/module.js +1 -1
- package/dist/refresh/refresh-token.d.ts +8 -1
- package/dist/refresh/refresh-token.d.ts.map +1 -1
- package/dist/refresh/refresh-token.js +15 -3
- package/dist/signing/jwks.d.ts +1 -0
- package/dist/signing/jwks.d.ts.map +1 -1
- package/dist/signing/jwks.js +36 -16
- package/dist/signing/verifier.d.ts +6 -1
- package/dist/signing/verifier.d.ts.map +1 -1
- package/dist/signing/verifier.js +7 -0
- package/package.json +4 -4
package/README.ko.md
CHANGED
|
@@ -150,7 +150,7 @@ const verifier = new DefaultJwtVerifier({
|
|
|
150
150
|
|
|
151
151
|
`jwksRequestTimeoutMs`의 기본값은 `5_000`이며, 예산을 넘기면 진행 중인 JWKS fetch를 abort합니다.
|
|
152
152
|
|
|
153
|
-
JWKS key는 `jwksCacheTtl` 밀리초 동안 cache되며 기본값은 `600_000`입니다. in-memory cache는 `jwksCacheMaxEntries`로 제한되고 기본값은 `100`입니다. lookup 전 만료된 entry를 정리하고, 제한을 넘으면 가장 오래 보관된 key를 제거합니다. 수동 shutdown
|
|
153
|
+
JWKS key는 `jwksCacheTtl` 밀리초 동안 cache되며 기본값은 `600_000`입니다. in-memory cache는 `jwksCacheMaxEntries`로 제한되고 기본값은 `100`입니다. lookup 전 만료된 entry를 정리하고, 제한을 넘으면 가장 오래 보관된 key를 제거합니다. `JwtModule`은 관리 중인 `DefaultJwtVerifier` shutdown hook을 호출하므로 module teardown 중 보관 중인 remote key material이 정리됩니다. 수동으로 생성한 verifier나 client는 수동 shutdown 또는 identity-provider 재설정 시 여전히 `JwksClient.dispose()` / `DefaultJwtVerifier.dispose()`를 호출해야 합니다. 이 dispose method들은 보관 중인 JWKS key material을 정리하고 진행 중인 JWKS fetch를 abort합니다. `jwksCacheTtl`을 `0`으로 설정하면 bounded fetch timeout은 유지하면서 key 보관만 비활성화합니다.
|
|
154
154
|
|
|
155
155
|
`JwtService.verify(token, options)`는 호출 단위의 알고리즘/클레임 정책 재정의(`issuer`, `audience`, `clockSkewSeconds`, `maxAge`, `requireExp`)를 적용하더라도, 내부 JWKS client나 정적 key-resolution cache를 다시 만들지 않습니다. 호출 단위 검증은 `jwksUri`, `keys[]`, `publicKey`, `secret`, `secretOrKeyProvider` 같은 구성된 key source 자체를 교체하지는 않습니다.
|
|
156
156
|
|
|
@@ -160,7 +160,9 @@ JWKS key는 `jwksCacheTtl` 밀리초 동안 cache되며 기본값은 `600_000`
|
|
|
160
160
|
|
|
161
161
|
### 리프레시 토큰
|
|
162
162
|
|
|
163
|
-
`RefreshTokenService`는 전용 HMAC refresh-token 경로를 사용합니다. `refreshToken.secret`은 access-token 서명 키와 별도로 설정하세요. Rotation은 `RefreshTokenStore.rotate(...)`를 사용해 현재 토큰을 소비 처리하고 대체 토큰을 같은 durable store 작업 안에서 저장할 수 있습니다. 따라서 성공한 rotation은 저장된 후속 토큰 없이 기존 토큰만 소비하지 않습니다. 기존 atomic `consume(...)` hook만 구현한 store도 계속
|
|
163
|
+
`RefreshTokenService`는 전용 HMAC refresh-token 경로를 사용합니다. `refreshToken.secret`은 access-token 서명 키와 별도로 설정하세요. Rotation은 `RefreshTokenStore.rotate(...)`를 사용해 현재 토큰을 소비 처리하고 대체 토큰을 같은 durable store 작업 안에서 저장할 수 있습니다. 따라서 성공한 rotation은 저장된 후속 토큰 없이 기존 토큰만 소비하지 않습니다. 기존 atomic `consume(...)` hook만 구현한 store도 계속 지원합니다. Consume이 성공하면 service가 `save(...)`로 대체 토큰을 저장하지만, 두 쓰기를 원자적으로 만드는 경로는 store 소유의 `rotate(...)`뿐입니다.
|
|
164
|
+
|
|
165
|
+
재사용을 감지하면 optional `revokeByFamily(family)` capability를 구현한 store는 침해된 token family만 revoke합니다. 기존 store는 source-compatible 상태를 유지합니다. `revokeByFamily(...)`가 없으면 `RefreshTokenService`는 보수적으로 `revokeBySubject(subject)`로 fallback하며, 이 경우 해당 subject의 독립적인 refresh-token family도 함께 revoke됩니다. 다른 family가 침해된 뒤에도 별도 device 또는 session family를 유지해야 하는 production store는 `revokeByFamily(...)`를 구현하세요.
|
|
164
166
|
|
|
165
167
|
## 설정 가드레일
|
|
166
168
|
|
|
@@ -170,8 +172,18 @@ JWT 서명과 검증에는 `algorithms`에 지원되는 알고리즘이 하나
|
|
|
170
172
|
|
|
171
173
|
검증은 잘못된 시간 정책에 대해 fail closed로 동작합니다. 검증에 참여하는 `exp`, `nbf`, `iat` 클레임은 유한한 JWT NumericDate 숫자여야 하며, `clockSkewSeconds`도 음수가 아닌 유한 숫자여야 합니다. 유한하지 않은 값은 expiration, not-before, age check를 늘리는 대신 거부됩니다. verifier 시간이 `exp` NumericDate에 도달하면 토큰은 만료된 것으로 처리되며, 양수 clock skew가 경계를 덮지 않는 한 equality도 만료로 간주합니다.
|
|
172
174
|
|
|
175
|
+
### Node 런타임 경계
|
|
176
|
+
|
|
173
177
|
루트 `@fluojs/jwt` import surface는 runtime-specific 인증 경로를 선택하기 전에도 안전하게 로드할 수 있습니다. Node.js `node:crypto` primitive는 서명, 검증, JWKS key parsing, refresh-token id 생성이 실제로 실행될 때만 lazy load됩니다. 이 방식은 기존 public export를 유지하면서 module import 시점의 Node-specific crypto 작업을 피합니다.
|
|
174
178
|
|
|
179
|
+
Lazy loading은 import-time 안전성 속성일 뿐입니다. 서명이나 검증을 runtime 간 이식 가능하게 만들지는 **않습니다**. 서명, 검증, JWKS key parsing, refresh-token id 생성 경로가 실행되면 해당 경로는 Node.js 호환 `node:crypto` 구현(`createHmac`, `createSign`, `createVerify`, `createPublicKey`, `timingSafeEqual`, `randomUUID`)을 필요로 합니다. Bun은 Node 호환성 레이어로 이를 만족하지만, Deno와 Cloudflare Workers는 이러한 연산에 호환되는 `node:crypto` surface를 제공하지 않으므로 지원되는 JWT 서명/검증 runtime이 아닙니다. `@fluojs/jwt`를 Node-runtime auth 패키지로 취급하세요. import-time 로딩은 lazy로 유지하되, 호환되는 `node:crypto` polyfill 없이 non-Node runtime에서 토큰을 서명하거나 검증할 수 있다고 가정하지 마세요.
|
|
180
|
+
|
|
181
|
+
### `decode()` trust boundary
|
|
182
|
+
|
|
183
|
+
`JwtService.decode(token)`는 서명, `alg`, `exp`, `nbf`, `iss`, `aud` 또는 기타 클레임을 검증하지 않고 JWT payload segment를 읽습니다. 반환된 객체는 **검증되지 않은 입력(unverified input)**이며, 권한 결정(authorization decisions), 신원 확인(identity resolution), 또는 접근을 허가하는 모든 코드 경로에 사용해서는 안 됩니다. 먼저 `JwtService.verify(token, options)`(또는 `DefaultJwtVerifier.verifyAccessToken(token)`)를 호출하고, 검증이 반환하는 정규화된 `JwtPrincipal`에서 신원을 읽으세요.
|
|
184
|
+
|
|
185
|
+
`decode()`는 진단(diagnostics) 및 비권위적 검사(non-authoritative inspection)에만 사용됩니다. 예를 들어 로깅을 위해 토큰 메타데이터를 읽거나 `verify()` 호출 전에 검증 키를 선택할 때 사용할 수 있습니다. `decode()` 출력에서 읽은 모든 클레임 값 — `sub`, `roles`, `scopes`, `iss`, `aud`, `exp` 포함 — 은 `verify()`가 성공하기 전까지 공격자가 제어한 값으로 취급해야 합니다. `decode()` 출력을 기준으로 요청을 허가하거나 거부하는 분기를 만들지 말고, 검증되지 않은 클레임을 검증된 것처럼 downstream 코드에 노출하지 마세요.
|
|
186
|
+
|
|
175
187
|
## 공개 API 개요
|
|
176
188
|
|
|
177
189
|
### 주요 클래스
|
|
@@ -187,13 +199,17 @@ JWT 서명과 검증에는 `algorithms`에 지원되는 알고리즘이 하나
|
|
|
187
199
|
- `JwtVerifierOptions`: 알고리즘, 키, 검증 정책 설정을 위한 타입입니다.
|
|
188
200
|
- `SignOptions`, `VerifyOptions`: 호출 단위 서명 및 검증 재정의 타입입니다.
|
|
189
201
|
- `JwtClaims`, `JwtSigner`, `JwtVerifier`, `JwtKeyEntry`, `JwtAlgorithm`: 공개 서명 및 검증 계약입니다.
|
|
190
|
-
- `RefreshTokenOptions`, `RefreshTokenStore`, `RefreshTokenRecord`, `RefreshTokenConsumeInput`, `RefreshTokenRotateInput`, `RefreshTokenConsumeResult`: refresh-token 저장, rotation, replay detection 계약입니다.
|
|
202
|
+
- `RefreshTokenOptions`, `RefreshTokenStore`, `RefreshTokenRecord`, `RefreshTokenConsumeInput`, `RefreshTokenRotateInput`, `RefreshTokenConsumeResult`: refresh-token 저장, rotation, family-scoped revocation, replay detection 계약입니다. `RefreshTokenStore.revokeByFamily(...)`는 subject-revocation store와의 호환성을 위해 optional입니다.
|
|
203
|
+
- `JwtPlatformStatusSnapshot`, `JwtStatusAdapterInput`: platform diagnostic helper와 함께 export되는 status snapshot 및 adapter input 타입입니다.
|
|
191
204
|
|
|
192
205
|
### 에러와 diagnostics
|
|
193
206
|
- `JwtVerificationError`, `JwtInvalidTokenError`, `JwtExpiredTokenError`, `JwtConfigurationError`: 타입이 지정된 JWT 실패입니다.
|
|
194
207
|
- `createJwtPlatformStatusSnapshot(...)`, `createJwtPlatformDiagnosticIssues(...)`: status 및 diagnostic helper입니다.
|
|
195
208
|
- `JWT_OPTIONS`, `HMAC_HASH`, `ASYMMETRIC_HASH`: 모듈과 검증 레이어에서 사용하는 export token/constant입니다.
|
|
196
209
|
|
|
210
|
+
### Deprecated compatibility helper
|
|
211
|
+
- `normalizeRefreshTokenOptions(...)`: 기존 caller의 root import 호환성만을 위해 유지됩니다. package normalization 내부 helper를 직접 호출하기보다 `JwtModule.forRoot(...)` / `JwtModule.forRootAsync(...)`와 `RefreshTokenService`를 사용하세요.
|
|
212
|
+
|
|
197
213
|
## 관련 패키지
|
|
198
214
|
|
|
199
215
|
- `@fluojs/passport`: 이 코어 패키지를 사용하여 가드와 전략을 실행하는 인증 계층입니다.
|
package/README.md
CHANGED
|
@@ -150,7 +150,7 @@ const verifier = new DefaultJwtVerifier({
|
|
|
150
150
|
|
|
151
151
|
`jwksRequestTimeoutMs` defaults to `5_000` and aborts the outbound JWKS fetch once that budget is exceeded.
|
|
152
152
|
|
|
153
|
-
JWKS keys are cached for `jwksCacheTtl` milliseconds (`600_000` by default) and the in-memory cache is bounded by `jwksCacheMaxEntries` (`100` by default). Expired entries are pruned before lookups, the oldest retained key is evicted when the bound is exceeded, and `JwksClient.dispose()` / `DefaultJwtVerifier.dispose()`
|
|
153
|
+
JWKS keys are cached for `jwksCacheTtl` milliseconds (`600_000` by default) and the in-memory cache is bounded by `jwksCacheMaxEntries` (`100` by default). Expired entries are pruned before lookups, the oldest retained key is evicted when the bound is exceeded, and `JwtModule` calls the managed `DefaultJwtVerifier` shutdown hook so retained remote key material is cleared during module teardown. Manually constructed verifiers or clients should still call `JwksClient.dispose()` / `DefaultJwtVerifier.dispose()` during manual shutdown or identity-provider reconfiguration. These dispose methods clear retained JWKS key material and abort active JWKS fetches. A `jwksCacheTtl` of `0` disables key retention while still using bounded fetch timeouts.
|
|
154
154
|
|
|
155
155
|
`JwtService.verify(token, options)` applies per-call algorithm and claim-policy overrides (`issuer`, `audience`, `clockSkewSeconds`, `maxAge`, `requireExp`) without rebuilding the underlying JWKS client or static key-resolution cache. Per-call verification does not replace configured key sources such as `jwksUri`, `keys[]`, `publicKey`, `secret`, or `secretOrKeyProvider`.
|
|
156
156
|
|
|
@@ -160,7 +160,9 @@ For multi-tenant systems, prefer putting a tenant-specific `kid` in issued token
|
|
|
160
160
|
|
|
161
161
|
### Refresh tokens
|
|
162
162
|
|
|
163
|
-
`RefreshTokenService` uses a dedicated HMAC refresh-token path. Configure `refreshToken.secret` separately from access-token signing keys. Rotation can use `RefreshTokenStore.rotate(...)` to atomically mark the current token as consumed and persist the replacement token in the same durable store operation, so a successful rotation never consumes the old token without a stored successor. Stores that only implement the older atomic `consume(...)` hook remain supported,
|
|
163
|
+
`RefreshTokenService` uses a dedicated HMAC refresh-token path. Configure `refreshToken.secret` separately from access-token signing keys. Rotation can use `RefreshTokenStore.rotate(...)` to atomically mark the current token as consumed and persist the replacement token in the same durable store operation, so a successful rotation never consumes the old token without a stored successor. Stores that only implement the older atomic `consume(...)` hook remain supported: after a successful consume, the service saves the replacement through `save(...)`, but only store-owned `rotate(...)` makes those two writes atomic.
|
|
164
|
+
|
|
165
|
+
When reuse is detected, stores that implement the optional `revokeByFamily(family)` capability revoke only the compromised token family. Existing stores remain source-compatible: if `revokeByFamily(...)` is absent, `RefreshTokenService` conservatively falls back to `revokeBySubject(subject)`, which also revokes the subject's independent refresh-token families. Implement `revokeByFamily(...)` in production stores when separate device or session families must remain active after another family is compromised.
|
|
164
166
|
|
|
165
167
|
## Configuration Guardrails
|
|
166
168
|
|
|
@@ -170,8 +172,18 @@ Access-token TTL must also be a positive finite number. When `accessTokenTtlSeco
|
|
|
170
172
|
|
|
171
173
|
Verification fails closed on malformed time policy. `exp`, `nbf`, and `iat` claims that participate in verification must be finite JWT NumericDate numbers, and `clockSkewSeconds` must be a non-negative finite number. Non-finite values are rejected instead of extending expiration, not-before, or age checks. A token is expired when verifier time reaches its `exp` NumericDate; equality is treated as expired unless positive clock skew still covers the boundary.
|
|
172
174
|
|
|
175
|
+
### Node runtime boundary
|
|
176
|
+
|
|
173
177
|
The root `@fluojs/jwt` import surface is safe to load before selecting a runtime-specific authentication path: Node.js `node:crypto` primitives are loaded lazily only when signing, verification, JWKS key parsing, or refresh-token id generation actually executes. This preserves the existing public exports while avoiding Node-specific crypto work at module import time.
|
|
174
178
|
|
|
179
|
+
Lazy loading is an import-time safety property only. It does **not** make signing or verification portable across runtimes. Once a signing, verification, JWKS key-parsing, or refresh-token id-generation path executes, that path requires a Node.js-compatible `node:crypto` implementation (`createHmac`, `createSign`, `createVerify`, `createPublicKey`, `timingSafeEqual`, `randomUUID`). Bun satisfies this through its Node compatibility layer; Deno and Cloudflare Workers do not provide a compatible `node:crypto` surface for these operations and are not supported JWT signing/verification runtimes. Treat `@fluojs/jwt` as a Node-runtime auth package: keep import-time loading lazy, but do not assume the package can sign or verify tokens on non-Node runtimes without a compatible `node:crypto` polyfill.
|
|
180
|
+
|
|
181
|
+
### `decode()` trust boundary
|
|
182
|
+
|
|
183
|
+
`JwtService.decode(token)` reads the JWT payload segment without verifying the signature, `alg`, `exp`, `nbf`, `iss`, `aud`, or any other claim. The returned object is **unverified input** and must never be used for authorization decisions, identity resolution, or any code path that grants access. Use `JwtService.verify(token, options)` (or `DefaultJwtVerifier.verifyAccessToken(token)`) first, and read identity from the normalized `JwtPrincipal` that verification returns.
|
|
184
|
+
|
|
185
|
+
`decode()` exists for diagnostics and non-authoritative inspection only, such as reading token metadata for logging or selecting a verification key before calling `verify()`. Any claim value read from `decode()` — including `sub`, `roles`, `scopes`, `iss`, `aud`, and `exp` — must be treated as attacker-controlled until `verify()` succeeds. Never branch on `decode()` output to allow or deny a request, and never expose decoded claims to downstream code as if they were verified.
|
|
186
|
+
|
|
175
187
|
## Public API Overview
|
|
176
188
|
|
|
177
189
|
### Core Classes
|
|
@@ -187,13 +199,17 @@ The root `@fluojs/jwt` import surface is safe to load before selecting a runtime
|
|
|
187
199
|
- `JwtVerifierOptions`: Configuration for algorithms, keys, and validation policy.
|
|
188
200
|
- `SignOptions` and `VerifyOptions`: Per-call signing and verification overrides.
|
|
189
201
|
- `JwtClaims`, `JwtSigner`, `JwtVerifier`, `JwtKeyEntry`, `JwtAlgorithm`: Public signing and verification contracts.
|
|
190
|
-
- `RefreshTokenOptions`, `RefreshTokenStore`, `RefreshTokenRecord`, `RefreshTokenConsumeInput`, `RefreshTokenRotateInput`, and `RefreshTokenConsumeResult`: Refresh-token storage, rotation, and replay-detection contracts.
|
|
202
|
+
- `RefreshTokenOptions`, `RefreshTokenStore`, `RefreshTokenRecord`, `RefreshTokenConsumeInput`, `RefreshTokenRotateInput`, and `RefreshTokenConsumeResult`: Refresh-token storage, rotation, family-scoped revocation, and replay-detection contracts. `RefreshTokenStore.revokeByFamily(...)` is optional for compatibility with subject-revocation stores.
|
|
203
|
+
- `JwtPlatformStatusSnapshot` and `JwtStatusAdapterInput`: Status snapshot and adapter input types exported with the platform diagnostic helpers.
|
|
191
204
|
|
|
192
205
|
### Errors and diagnostics
|
|
193
206
|
- `JwtVerificationError`, `JwtInvalidTokenError`, `JwtExpiredTokenError`, `JwtConfigurationError`: Typed JWT failures.
|
|
194
207
|
- `createJwtPlatformStatusSnapshot(...)` and `createJwtPlatformDiagnosticIssues(...)`: Status and diagnostic helpers.
|
|
195
208
|
- `JWT_OPTIONS`, `HMAC_HASH`, `ASYMMETRIC_HASH`: Exported tokens/constants used by the module and verification layer.
|
|
196
209
|
|
|
210
|
+
### Deprecated compatibility helpers
|
|
211
|
+
- `normalizeRefreshTokenOptions(...)`: Retained only for root-import compatibility with existing callers. Prefer `JwtModule.forRoot(...)` / `JwtModule.forRootAsync(...)` plus `RefreshTokenService` instead of calling package normalization internals.
|
|
212
|
+
|
|
197
213
|
## Related Packages
|
|
198
214
|
|
|
199
215
|
- `@fluojs/passport`: The auth execution layer that uses this core for guards and strategies.
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
export * from './errors.js';
|
|
2
2
|
export * from './signing/jwks.js';
|
|
3
3
|
export * from './module.js';
|
|
4
|
-
export
|
|
4
|
+
export { RefreshTokenService, normalizeRefreshTokenOptions, } from './refresh/refresh-token.js';
|
|
5
|
+
export type { RefreshTokenConsumeInput, RefreshTokenConsumeResult, RefreshTokenOptions, RefreshTokenRecord, RefreshTokenRotateInput, RefreshTokenStore, } from './refresh/refresh-token.js';
|
|
5
6
|
export * from './service.js';
|
|
6
7
|
export * from './signing/signer.js';
|
|
7
8
|
export * from './status.js';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC;AAC5B,cAAc,mBAAmB,CAAC;AAClC,cAAc,aAAa,CAAC;AAC5B,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC;AAC5B,cAAc,mBAAmB,CAAC;AAClC,cAAc,aAAa,CAAC;AAC5B,OAAO,EACL,mBAAmB,EACnB,4BAA4B,GAC7B,MAAM,4BAA4B,CAAC;AACpC,YAAY,EACV,wBAAwB,EACxB,yBAAyB,EACzB,mBAAmB,EACnB,kBAAkB,EAClB,uBAAuB,EACvB,iBAAiB,GAClB,MAAM,4BAA4B,CAAC;AACpC,cAAc,cAAc,CAAC;AAC7B,cAAc,qBAAqB,CAAC;AACpC,cAAc,aAAa,CAAC;AAC5B,cAAc,YAAY,CAAC;AAC3B,cAAc,uBAAuB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export * from './errors.js';
|
|
2
2
|
export * from './signing/jwks.js';
|
|
3
3
|
export * from './module.js';
|
|
4
|
-
export
|
|
4
|
+
export { RefreshTokenService, normalizeRefreshTokenOptions } from './refresh/refresh-token.js';
|
|
5
5
|
export * from './service.js';
|
|
6
6
|
export * from './signing/signer.js';
|
|
7
7
|
export * from './status.js';
|
package/dist/module.js
CHANGED
|
@@ -64,7 +64,7 @@ export class JwtModule {
|
|
|
64
64
|
provide: JWT_OPTIONS,
|
|
65
65
|
scope: 'singleton',
|
|
66
66
|
useValue: options
|
|
67
|
-
},
|
|
67
|
+
}, true, true, options.refreshToken ? 'singleton' : 'transient', false, options.global ?? false);
|
|
68
68
|
}
|
|
69
69
|
static forRootAsync(options) {
|
|
70
70
|
return this.createModule({
|
|
@@ -8,6 +8,7 @@ export interface RefreshTokenStore {
|
|
|
8
8
|
find(tokenId: string): Promise<RefreshTokenRecord | undefined>;
|
|
9
9
|
revoke(tokenId: string): Promise<void>;
|
|
10
10
|
revokeBySubject(subject: string): Promise<void>;
|
|
11
|
+
revokeByFamily?(family: string): Promise<void>;
|
|
11
12
|
consume?(input: RefreshTokenConsumeInput): Promise<RefreshTokenConsumeResult>;
|
|
12
13
|
rotate?(input: RefreshTokenRotateInput): Promise<RefreshTokenConsumeResult>;
|
|
13
14
|
}
|
|
@@ -52,7 +53,12 @@ export interface RefreshTokenOptions {
|
|
|
52
53
|
store: RefreshTokenStore;
|
|
53
54
|
}
|
|
54
55
|
/**
|
|
55
|
-
* Normalize refresh token options.
|
|
56
|
+
* Normalize refresh token options for legacy root-import callers.
|
|
57
|
+
*
|
|
58
|
+
* @deprecated This helper is retained only for compatibility with existing
|
|
59
|
+
* root `@fluojs/jwt` imports. Prefer configuring refresh tokens through
|
|
60
|
+
* `JwtModule.forRoot(...)` / `JwtModule.forRootAsync(...)` and resolving
|
|
61
|
+
* `RefreshTokenService` instead of calling package normalization internals.
|
|
56
62
|
*
|
|
57
63
|
* @param options The options.
|
|
58
64
|
* @returns The normalize refresh token options result.
|
|
@@ -75,6 +81,7 @@ export declare class RefreshTokenService {
|
|
|
75
81
|
revokeAllForSubject(subject: string): Promise<void>;
|
|
76
82
|
private issueRefreshTokenWithFamily;
|
|
77
83
|
private consumeRefreshToken;
|
|
84
|
+
private revokeCompromisedFamily;
|
|
78
85
|
private createRefreshTokenWithFamily;
|
|
79
86
|
private verifyRefreshClaims;
|
|
80
87
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"refresh-token.d.ts","sourceRoot":"","sources":["../../src/refresh/refresh-token.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;
|
|
1
|
+
{"version":3,"file":"refresh-token.d.ts","sourceRoot":"","sources":["../../src/refresh/refresh-token.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAC7D,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AAGjE;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC,IAAI,CAAC,KAAK,EAAE,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/C,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,kBAAkB,GAAG,SAAS,CAAC,CAAC;IAC/D,MAAM,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACvC,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAChD,cAAc,CAAC,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/C,OAAO,CAAC,CAAC,KAAK,EAAE,wBAAwB,GAAG,OAAO,CAAC,yBAAyB,CAAC,CAAC;IAC9E,MAAM,CAAC,CAAC,KAAK,EAAE,uBAAuB,GAAG,OAAO,CAAC,yBAAyB,CAAC,CAAC;CAC7E;AAED;;GAEG;AACH,MAAM,WAAW,wBAAwB;IACvC,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,IAAI,CAAC;CACX;AAED;;GAEG;AACH,MAAM,WAAW,uBAAwB,SAAQ,wBAAwB;IACvE,WAAW,EAAE,kBAAkB,CAAC;CACjC;AAED;;GAEG;AACH,MAAM,MAAM,yBAAyB,GAAG,UAAU,GAAG,cAAc,GAAG,SAAS,GAAG,WAAW,GAAG,UAAU,GAAG,SAAS,CAAC;AAEvH;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,IAAI,CAAC;IAChB,IAAI,EAAE,OAAO,CAAC;IACd,SAAS,EAAE,IAAI,CAAC;CACjB;AAED;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,MAAM,CAAC;IACf,gBAAgB,EAAE,MAAM,CAAC;IACzB,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,QAAQ,EAAE,OAAO,CAAC;IAClB,KAAK,EAAE,iBAAiB,CAAC;CAC1B;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,4BAA4B,CAAC,OAAO,EAAE,mBAAmB,GAAG,SAAS,GAAG,mBAAmB,CA6B1G;AAQD;;GAEG;AACH,qBAAa,mBAAmB;IAK5B,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,QAAQ;IAL3B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAsB;gBAG5C,OAAO,EAAE,mBAAmB,EACX,MAAM,EAAE,gBAAgB,EACxB,QAAQ,EAAE,kBAAkB;IAKzC,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAOnD,kBAAkB,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,WAAW,EAAE,MAAM,CAAC;QAAC,YAAY,EAAE,MAAM,CAAA;KAAE,CAAC;IA0EhG,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAIlD,mBAAmB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;YAI3C,2BAA2B;YAQ3B,mBAAmB;YAUnB,uBAAuB;YASvB,4BAA4B;YA+B5B,mBAAmB;CA4BlC"}
|
|
@@ -25,7 +25,12 @@ import { JwtConfigurationError, JwtExpiredTokenError, JwtInvalidTokenError } fro
|
|
|
25
25
|
*/
|
|
26
26
|
|
|
27
27
|
/**
|
|
28
|
-
* Normalize refresh token options.
|
|
28
|
+
* Normalize refresh token options for legacy root-import callers.
|
|
29
|
+
*
|
|
30
|
+
* @deprecated This helper is retained only for compatibility with existing
|
|
31
|
+
* root `@fluojs/jwt` imports. Prefer configuring refresh tokens through
|
|
32
|
+
* `JwtModule.forRoot(...)` / `JwtModule.forRootAsync(...)` and resolving
|
|
33
|
+
* `RefreshTokenService` instead of calling package normalization internals.
|
|
29
34
|
*
|
|
30
35
|
* @param options The options.
|
|
31
36
|
* @returns The normalize refresh token options result.
|
|
@@ -99,7 +104,7 @@ export class RefreshTokenService {
|
|
|
99
104
|
};
|
|
100
105
|
}
|
|
101
106
|
if (consumeResult === 'already_used') {
|
|
102
|
-
await this.
|
|
107
|
+
await this.revokeCompromisedFamily(claims.sub, claims.family);
|
|
103
108
|
throw new JwtInvalidTokenError('Refresh token reuse detected.');
|
|
104
109
|
}
|
|
105
110
|
if (consumeResult === 'expired') {
|
|
@@ -121,7 +126,7 @@ export class RefreshTokenService {
|
|
|
121
126
|
throw new JwtExpiredTokenError('Refresh token has expired.');
|
|
122
127
|
}
|
|
123
128
|
if (record.used) {
|
|
124
|
-
await this.
|
|
129
|
+
await this.revokeCompromisedFamily(record.subject, record.family);
|
|
125
130
|
throw new JwtInvalidTokenError('Refresh token reuse detected.');
|
|
126
131
|
}
|
|
127
132
|
const accessToken = await this.signer.signAccessToken({
|
|
@@ -152,6 +157,13 @@ export class RefreshTokenService {
|
|
|
152
157
|
}
|
|
153
158
|
return this.options.store.consume(input);
|
|
154
159
|
}
|
|
160
|
+
async revokeCompromisedFamily(subject, family) {
|
|
161
|
+
if (this.options.store.revokeByFamily) {
|
|
162
|
+
await this.options.store.revokeByFamily(family);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
await this.options.store.revokeBySubject(subject);
|
|
166
|
+
}
|
|
155
167
|
async createRefreshTokenWithFamily(subject, family) {
|
|
156
168
|
const now = Math.floor(Date.now() / 1000);
|
|
157
169
|
const {
|
package/dist/signing/jwks.d.ts
CHANGED
|
@@ -8,6 +8,7 @@ export declare class JwksClient {
|
|
|
8
8
|
private readonly requestTimeoutMs;
|
|
9
9
|
private readonly cacheMaxEntries;
|
|
10
10
|
private readonly cache;
|
|
11
|
+
private readonly activeFetchControllers;
|
|
11
12
|
private lifecycleGeneration;
|
|
12
13
|
constructor(uri: string, cacheTtl?: number, requestTimeoutMs?: number, cacheMaxEntries?: number);
|
|
13
14
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"jwks.d.ts","sourceRoot":"","sources":["../../src/signing/jwks.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAiC7C;;GAEG;AACH,qBAAa,UAAU;
|
|
1
|
+
{"version":3,"file":"jwks.d.ts","sourceRoot":"","sources":["../../src/signing/jwks.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAiC7C;;GAEG;AACH,qBAAa,UAAU;IAMnB,OAAO,CAAC,QAAQ,CAAC,GAAG;IACpB,OAAO,CAAC,QAAQ,CAAC,QAAQ;IACzB,OAAO,CAAC,QAAQ,CAAC,gBAAgB;IACjC,OAAO,CAAC,QAAQ,CAAC,eAAe;IARlC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAA4D;IAClF,OAAO,CAAC,QAAQ,CAAC,sBAAsB,CAA8B;IACrE,OAAO,CAAC,mBAAmB,CAAK;gBAGb,GAAG,EAAE,MAAM,EACX,QAAQ,GAAE,MAAgB,EAC1B,gBAAgB,GAAE,MAAc,EAChC,eAAe,GAAE,MAAuC;IAO3E;;;;;OAKG;IACH,OAAO,IAAI,IAAI;IAWf,OAAO,CAAC,YAAY;IAId,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,CAAC;IA2CpD,OAAO,CAAC,wBAAwB;IAQhC,OAAO,CAAC,uBAAuB;YAYjB,SAAS;CAwDxB"}
|
package/dist/signing/jwks.js
CHANGED
|
@@ -21,6 +21,7 @@ function assertPositiveInteger(value, label) {
|
|
|
21
21
|
*/
|
|
22
22
|
export class JwksClient {
|
|
23
23
|
cache = new Map();
|
|
24
|
+
activeFetchControllers = new Set();
|
|
24
25
|
lifecycleGeneration = 0;
|
|
25
26
|
constructor(uri, cacheTtl = 600_000, requestTimeoutMs = 5_000, cacheMaxEntries = DEFAULT_JWKS_CACHE_MAX_ENTRIES) {
|
|
26
27
|
this.uri = uri;
|
|
@@ -41,6 +42,10 @@ export class JwksClient {
|
|
|
41
42
|
dispose() {
|
|
42
43
|
this.lifecycleGeneration += 1;
|
|
43
44
|
this.cache.clear();
|
|
45
|
+
for (const controller of this.activeFetchControllers) {
|
|
46
|
+
controller.abort();
|
|
47
|
+
}
|
|
48
|
+
this.activeFetchControllers.clear();
|
|
44
49
|
}
|
|
45
50
|
isAbortError(error) {
|
|
46
51
|
return error instanceof Error && error.name === 'AbortError';
|
|
@@ -99,36 +104,51 @@ export class JwksClient {
|
|
|
99
104
|
}
|
|
100
105
|
}
|
|
101
106
|
async fetchKeys() {
|
|
102
|
-
|
|
107
|
+
const fetchGeneration = this.lifecycleGeneration;
|
|
103
108
|
const controller = new AbortController();
|
|
109
|
+
this.activeFetchControllers.add(controller);
|
|
110
|
+
let timedOut = false;
|
|
104
111
|
const timeout = setTimeout(() => {
|
|
112
|
+
timedOut = true;
|
|
105
113
|
controller.abort();
|
|
106
114
|
}, this.requestTimeoutMs);
|
|
107
115
|
timeout.unref?.();
|
|
108
116
|
try {
|
|
109
|
-
response = await fetch(this.uri, {
|
|
117
|
+
const response = await fetch(this.uri, {
|
|
110
118
|
signal: controller.signal
|
|
111
119
|
});
|
|
120
|
+
if (!response.ok) {
|
|
121
|
+
throw new JwtConfigurationError(`JWKS endpoint returned HTTP ${response.status}.`);
|
|
122
|
+
}
|
|
123
|
+
let body;
|
|
124
|
+
try {
|
|
125
|
+
body = await response.json();
|
|
126
|
+
} catch (error) {
|
|
127
|
+
if (this.isAbortError(error)) {
|
|
128
|
+
throw error;
|
|
129
|
+
}
|
|
130
|
+
throw new JwtConfigurationError('JWKS endpoint did not return valid JSON.');
|
|
131
|
+
}
|
|
132
|
+
if (!Array.isArray(body.keys)) {
|
|
133
|
+
throw new JwtConfigurationError('JWKS endpoint did not return a keys array.');
|
|
134
|
+
}
|
|
135
|
+
return body.keys;
|
|
112
136
|
} catch (error) {
|
|
137
|
+
if (error instanceof JwtConfigurationError) {
|
|
138
|
+
throw error;
|
|
139
|
+
}
|
|
113
140
|
if (this.isAbortError(error)) {
|
|
114
|
-
|
|
141
|
+
if (timedOut) {
|
|
142
|
+
throw new JwtConfigurationError(`JWKS fetch timed out after ${String(this.requestTimeoutMs)}ms.`);
|
|
143
|
+
}
|
|
144
|
+
if (fetchGeneration !== this.lifecycleGeneration) {
|
|
145
|
+
throw new JwtConfigurationError('JWKS client was disposed while fetching keys.');
|
|
146
|
+
}
|
|
115
147
|
}
|
|
116
148
|
throw new JwtConfigurationError(`Failed to fetch JWKS from "${this.uri}".`);
|
|
117
149
|
} finally {
|
|
118
150
|
clearTimeout(timeout);
|
|
151
|
+
this.activeFetchControllers.delete(controller);
|
|
119
152
|
}
|
|
120
|
-
if (!response.ok) {
|
|
121
|
-
throw new JwtConfigurationError(`JWKS endpoint returned HTTP ${response.status}.`);
|
|
122
|
-
}
|
|
123
|
-
let body;
|
|
124
|
-
try {
|
|
125
|
-
body = await response.json();
|
|
126
|
-
} catch {
|
|
127
|
-
throw new JwtConfigurationError('JWKS endpoint did not return valid JSON.');
|
|
128
|
-
}
|
|
129
|
-
if (!Array.isArray(body.keys)) {
|
|
130
|
-
throw new JwtConfigurationError('JWKS endpoint did not return a keys array.');
|
|
131
|
-
}
|
|
132
|
-
return body.keys;
|
|
133
153
|
}
|
|
134
154
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { OnModuleDestroy } from '@fluojs/runtime';
|
|
1
2
|
import type { JwtAlgorithm, JwtPrincipal, JwtVerifierOptions } from '../types.js';
|
|
2
3
|
/**
|
|
3
4
|
* Provides the resolved JWT verifier options through dependency injection.
|
|
@@ -15,7 +16,7 @@ type AccessTokenVerificationOverrides = Pick<JwtVerifierOptions, 'algorithms' |
|
|
|
15
16
|
/**
|
|
16
17
|
* Verifies JWT access and refresh tokens against the configured key sources.
|
|
17
18
|
*/
|
|
18
|
-
export declare class DefaultJwtVerifier {
|
|
19
|
+
export declare class DefaultJwtVerifier implements OnModuleDestroy {
|
|
19
20
|
private readonly options;
|
|
20
21
|
private readonly jwksClient;
|
|
21
22
|
private readonly keyResolutionState;
|
|
@@ -31,6 +32,10 @@ export declare class DefaultJwtVerifier {
|
|
|
31
32
|
* owned by the verifier and are therefore left untouched.
|
|
32
33
|
*/
|
|
33
34
|
dispose(): void;
|
|
35
|
+
/**
|
|
36
|
+
* Releases verifier-owned remote JWKS cache entries during module teardown.
|
|
37
|
+
*/
|
|
38
|
+
onModuleDestroy(): void;
|
|
34
39
|
/**
|
|
35
40
|
* Verifies a JWT access token with per-call claim-policy overrides while reusing configured key sources.
|
|
36
41
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"verifier.d.ts","sourceRoot":"","sources":["../../src/signing/verifier.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"verifier.d.ts","sourceRoot":"","sources":["../../src/signing/verifier.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAIvD,OAAO,KAAK,EAAE,YAAY,EAA0B,YAAY,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAG1G;;GAEG;AACH,eAAO,MAAM,WAAW,eAAiC,CAAC;AAE1D;;GAEG;AACH,eAAO,MAAM,SAAS,EAAE,OAAO,CAAC,MAAM,CAAC,YAAY,EAAE,MAAM,CAAC,CAI3D,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,eAAe,EAAE,OAAO,CAAC,MAAM,CAAC,YAAY,EAAE,MAAM,CAAC,CAOjE,CAAC;AAmDF,KAAK,gCAAgC,GAAG,IAAI,CAC1C,kBAAkB,EAClB,YAAY,GAAG,UAAU,GAAG,kBAAkB,GAAG,QAAQ,GAAG,QAAQ,GAAG,YAAY,CACpF,CAAC;AAiMF;;GAEG;AACH,qBACa,kBAAmB,YAAW,eAAe;IAM5C,OAAO,CAAC,QAAQ,CAAC,OAAO;IALpC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAyB;IACpD,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAAqB;IACxD,OAAO,CAAC,QAAQ,CAAC,yBAAyB,CAAqB;IAC/D,OAAO,CAAC,QAAQ,CAAC,0BAA0B,CAAiC;gBAE/C,OAAO,EAAE,kBAAkB;IAalD,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC;IAI7D;;;;;;OAMG;IACH,OAAO,IAAI,IAAI;IAIf;;OAEG;IACH,eAAe,IAAI,IAAI;IAIvB;;;;;;;;;;OAUG;IACG,8BAA8B,CAClC,KAAK,EAAE,MAAM,EACb,SAAS,EAAE,OAAO,CAAC,gCAAgC,CAAC,GACnD,OAAO,CAAC,YAAY,CAAC;IAqBlB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC;IAQ9D,OAAO,CAAC,gCAAgC;YAsB1B,WAAW;IA+BzB,OAAO,CAAC,kBAAkB;YAUZ,oBAAoB;YAgBpB,wBAAwB;YAsBxB,8BAA8B;YAsB9B,kBAAkB;IAWhC,OAAO,CAAC,mBAAmB;IAwB3B,OAAO,CAAC,oBAAoB;IA2B5B,OAAO,CAAC,yBAAyB;YAiBnB,oBAAoB;CAOnC"}
|
package/dist/signing/verifier.js
CHANGED
|
@@ -232,6 +232,13 @@ class DefaultJwtVerifier {
|
|
|
232
232
|
this.jwksClient?.dispose();
|
|
233
233
|
}
|
|
234
234
|
|
|
235
|
+
/**
|
|
236
|
+
* Releases verifier-owned remote JWKS cache entries during module teardown.
|
|
237
|
+
*/
|
|
238
|
+
onModuleDestroy() {
|
|
239
|
+
this.dispose();
|
|
240
|
+
}
|
|
241
|
+
|
|
235
242
|
/**
|
|
236
243
|
* Verifies a JWT access token with per-call claim-policy overrides while reusing configured key sources.
|
|
237
244
|
*
|
package/package.json
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"signing",
|
|
10
10
|
"verification"
|
|
11
11
|
],
|
|
12
|
-
"version": "1.0
|
|
12
|
+
"version": "1.1.0",
|
|
13
13
|
"private": false,
|
|
14
14
|
"license": "MIT",
|
|
15
15
|
"repository": {
|
|
@@ -36,9 +36,9 @@
|
|
|
36
36
|
"dist"
|
|
37
37
|
],
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"@fluojs/core": "^1.0
|
|
40
|
-
"@fluojs/di": "^
|
|
41
|
-
"@fluojs/runtime": "^
|
|
39
|
+
"@fluojs/core": "^1.1.0",
|
|
40
|
+
"@fluojs/di": "^2.0.0",
|
|
41
|
+
"@fluojs/runtime": "^2.0.1"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"vitest": "^3.2.4"
|