@fluojs/jwt 1.1.0 → 2.0.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 +65 -28
- package/README.md +65 -28
- package/dist/module.d.ts +9 -0
- package/dist/module.d.ts.map +1 -1
- package/dist/module.js +15 -0
- package/dist/refresh/refresh-token-claims.d.ts +22 -0
- package/dist/refresh/refresh-token-claims.d.ts.map +1 -0
- package/dist/refresh/refresh-token-claims.js +37 -0
- package/dist/refresh/refresh-token.d.ts +17 -6
- package/dist/refresh/refresh-token.d.ts.map +1 -1
- package/dist/refresh/refresh-token.js +27 -24
- package/dist/signing/algorithm-policy.d.ts +4 -0
- package/dist/signing/algorithm-policy.d.ts.map +1 -0
- package/dist/signing/algorithm-policy.js +13 -0
- package/dist/signing/key-entries.d.ts +8 -0
- package/dist/signing/key-entries.d.ts.map +1 -0
- package/dist/signing/key-entries.js +18 -0
- package/dist/signing/signer.d.ts.map +1 -1
- package/dist/signing/signer.js +13 -8
- package/dist/signing/verifier.d.ts +9 -4
- package/dist/signing/verifier.d.ts.map +1 -1
- package/dist/signing/verifier.js +28 -20
- package/dist/status.d.ts +49 -5
- package/dist/status.d.ts.map +1 -1
- package/dist/status.js +20 -0
- package/package.json +6 -6
package/README.ko.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
<p><a href="./README.md"><kbd>English</kbd></a> <strong><kbd>한국어</kbd></strong></p>
|
|
4
4
|
|
|
5
|
+
Node.js 지원 범위는 `>=24.0.0 <27`입니다. 업그레이드 절차는 [Node.js 지원 및 마이그레이션](../../docs/reference/node-support.ko.md)을 참조하세요.
|
|
6
|
+
|
|
5
7
|
HTTP에 독립적인 JWT 토큰 코어로, 액세스 토큰의 서명 및 검증을 담당하며 검증된 결과를 정규화된 `JwtPrincipal` 객체로 변환합니다.
|
|
6
8
|
|
|
7
9
|
## 목차
|
|
@@ -61,38 +63,47 @@ JWT 설정이 다른 provider에서 와야 한다면, `JwtModule.forRootAsync(..
|
|
|
61
63
|
|
|
62
64
|
`forRootAsync(...)`는 `inject`에 나열된 provider에서 module-level `JwtVerifierOptions` 객체 하나를 resolve합니다. 이 factory는 요청별 상태를 받지 않습니다. 테넌트별 secret이나 identity provider가 필요한 경우 tenant lookup은 애플리케이션의 auth layer에 두고, token verification 중에는 `kid` 같은 token metadata와 미리 구성한 `keys[]`, `jwksUri`, 또는 `secretOrKeyProvider`를 사용해 검증 material을 선택하세요.
|
|
63
65
|
|
|
64
|
-
|
|
65
|
-
import { Module, type Token } from '@fluojs/core';
|
|
66
|
-
import { JwtModule } from '@fluojs/jwt';
|
|
66
|
+
지원되는 계약은 `JwtModule.forRootAsync({ inject, useFactory, global? })`입니다. `JwtModule.forRootAsync(...)`의 `inject`에 지정한 의존성은 JWT options provider가 resolve되기 전에 application module graph에 먼저 등록해야 하며, `useFactory`는 최종 `JwtVerifierOptions`를 반환합니다. 최상위 `global?`은 반환된 module의 가시성을 제어하며, `useFactory`가 반환하는 최종 `JwtVerifierOptions`와는 별개입니다. NestJS dynamic-module `imports`, `useClass`, `useExisting`은 지원되는 typed configuration의 일부가 아니며 dynamic-module 의미도 없습니다. 추가 JavaScript object property는 runtime에서 읽지 않을 뿐 validate하거나 reject하지 않습니다. `JwtModule.forRootAsync(...)`의 의존성은 global로 visible한 module export 또는 `JwtRuntimeModule`이 resolve할 수 있는 application graph의 bootstrap runtime provider에서 와야 합니다. ordinary sibling 또는 parent module의 export만으로는 충분하지 않으며, `AuthModule.providers`에만 local인 provider는 JWT options provider에서 보이지 않습니다. `JwtModule.forRootAsync(...)`는 암묵적 module 또는 provider discovery를 지원하지 않습니다.
|
|
67
67
|
|
|
68
|
-
|
|
68
|
+
```typescript
|
|
69
|
+
import { Module } from '@fluojs/core';
|
|
70
|
+
import { ConfigModule, ConfigService } from '@fluojs/config';
|
|
71
|
+
import { JwtModule, type JwtVerifierOptions } from '@fluojs/jwt';
|
|
69
72
|
|
|
70
73
|
@Module({
|
|
71
74
|
imports: [
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
useFactory: async (settings) => ({
|
|
75
|
-
accessTokenTtlSeconds: 900,
|
|
76
|
-
algorithms: ['HS256'],
|
|
77
|
-
audience: 'my-app',
|
|
78
|
-
issuer: settings.issuer,
|
|
79
|
-
secret: settings.secret,
|
|
80
|
-
}),
|
|
75
|
+
ConfigModule.forRoot({
|
|
76
|
+
processEnv: { JWT_SECRET: process.env.JWT_SECRET },
|
|
81
77
|
}),
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
78
|
+
JwtModule.forRootAsync({
|
|
79
|
+
inject: [ConfigService],
|
|
80
|
+
useFactory: async (...deps: unknown[]): Promise<JwtVerifierOptions> => {
|
|
81
|
+
const [config] = deps;
|
|
82
|
+
if (!(config instanceof ConfigService)) {
|
|
83
|
+
throw new TypeError('ConfigService dependency is required');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const secret = config.snapshot()['JWT_SECRET'];
|
|
87
|
+
if (typeof secret !== 'string') {
|
|
88
|
+
throw new TypeError('JWT_SECRET must be a string');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
accessTokenTtlSeconds: 900,
|
|
93
|
+
algorithms: ['HS256'],
|
|
94
|
+
audience: 'my-app',
|
|
95
|
+
issuer: 'my-api',
|
|
96
|
+
secret,
|
|
97
|
+
};
|
|
89
98
|
},
|
|
90
|
-
},
|
|
99
|
+
}),
|
|
91
100
|
],
|
|
92
101
|
})
|
|
93
102
|
export class AuthModule {}
|
|
94
103
|
```
|
|
95
104
|
|
|
105
|
+
여기서는 `ConfigModule.forRoot(...)`가 기본으로 `ConfigService`를 global export하므로 `JwtRuntimeModule` options provider가 이를 resolve할 수 있습니다. `AuthModule.providers`에만 선언한 provider는 그 imported module에서 보이지 않습니다.
|
|
106
|
+
|
|
96
107
|
### 토큰 서명 및 검증
|
|
97
108
|
|
|
98
109
|
`DefaultJwtSigner`를 주입받아 토큰을 발행하고, `DefaultJwtVerifier`를 통해 검증합니다.
|
|
@@ -152,21 +163,46 @@ const verifier = new DefaultJwtVerifier({
|
|
|
152
163
|
|
|
153
164
|
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
165
|
|
|
155
|
-
`
|
|
166
|
+
`DefaultJwtVerifier.verifyAccessTokenWithOverrides(token, options)`는 호출 단위의 알고리즘/클레임 정책 재정의(`algorithms`, `issuer`, `audience`, `clockSkewSeconds`, `maxAge`, `requireExp`)를 적용하더라도, 내부 JWKS client나 정적 key-resolution cache를 다시 만들지 않습니다. 호출 단위 검증은 `jwksUri`, `keys[]`, `publicKey`, `secret`, `secretOrKeyProvider` 같은 구성된 key source 자체를 교체하지는 않습니다.
|
|
156
167
|
|
|
157
|
-
호환되는 키가 여러 개 설정되어 있으면 `kid`가 검증 키를 구분합니다. 호환되는 정적 키가 하나뿐이면 `kid` 없이도 토큰을 검증할 수 있고, JWKS 기반 검증은 원격 key set과 cache policy를 따릅니다.
|
|
168
|
+
호환되는 키가 여러 개 설정되어 있으면 `kid`가 검증 키를 구분합니다. `keys[]`의 모든 entry는 비어 있지 않고 고유한 `kid`를 가져야 합니다. `DefaultJwtSigner`와 `DefaultJwtVerifier`는 key rotation 중 서명과 검증이 서로 다른 키를 선택하지 않도록 construction 시점에 빈 값 또는 중복 값을 `JwtConfigurationError`로 거부합니다. 호환되는 정적 키가 하나뿐이면 `kid` 없이도 토큰을 검증할 수 있고, JWKS 기반 검증은 원격 key set과 cache policy를 따릅니다.
|
|
158
169
|
|
|
159
170
|
멀티테넌트 시스템에서는 발행된 토큰 header에 tenant-specific `kid`를 넣고 호환되는 key source를 미리 구성하는 방식을 권장합니다. `secretOrKeyProvider`는 decoded token header만 인자로 받으므로, request header, route param, 기타 request-context tenant hint는 JWT verifier 호출 전에 애플리케이션 수준 strategy/guard 코드에서 처리해야 합니다.
|
|
160
171
|
|
|
161
172
|
### 리프레시 토큰
|
|
162
173
|
|
|
163
|
-
`RefreshTokenService`는 전용 HMAC refresh-token 경로를 사용합니다. `refreshToken.secret`은 access-token 서명 키와 별도로 설정하세요.
|
|
174
|
+
`RefreshTokenService`는 전용 HMAC refresh-token 경로를 사용합니다. `refreshToken.secret`은 access-token 서명 키와 별도로 설정하세요. access-token `algorithms`가 비대칭 알고리즘만 허용할 때는 `refreshToken.algorithms`에 `['HS256']` 같은 명시적인 HMAC allowlist를 설정하세요. 그렇지 않으면 기존 구성과의 호환성을 위해 top-level HMAC algorithms에서 refresh policy를 파생합니다. 이 policy는 refresh-token 서명과 검증에만 적용되므로 access-token 검증 범위를 넓히지 않습니다.
|
|
175
|
+
|
|
176
|
+
```typescript
|
|
177
|
+
const options = {
|
|
178
|
+
algorithms: ['RS256'],
|
|
179
|
+
privateKey: '...private PEM...',
|
|
180
|
+
publicKey: '...public PEM...',
|
|
181
|
+
refreshToken: {
|
|
182
|
+
algorithms: ['HS256'],
|
|
183
|
+
secret: 'refresh-secret',
|
|
184
|
+
expiresInSeconds: 300,
|
|
185
|
+
rotation: false,
|
|
186
|
+
store,
|
|
187
|
+
},
|
|
188
|
+
};
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
Rotation은 `RefreshTokenStore.rotate(...)`를 사용해 현재 토큰을 소비 처리하고 대체 토큰을 같은 durable store 작업 안에서 저장할 수 있습니다. 따라서 성공한 rotation은 저장된 후속 토큰 없이 기존 토큰만 소비하지 않습니다. 기존 atomic `consume(...)` hook만 구현한 store도 계속 지원합니다. Consume이 성공하면 service가 `save(...)`로 대체 토큰을 저장하지만, 두 쓰기를 원자적으로 만드는 경로는 store 소유의 `rotate(...)`뿐입니다.
|
|
164
192
|
|
|
165
193
|
재사용을 감지하면 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(...)`를 구현하세요.
|
|
166
194
|
|
|
195
|
+
단일 세션 로그아웃에서는 caller가 제시한 compact token을 `revokePresentedRefreshToken(...)`에 전달하세요.
|
|
196
|
+
|
|
197
|
+
```typescript
|
|
198
|
+
await refreshTokens.revokePresentedRefreshToken(refreshToken);
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
이 method는 일치하는 record를 revoke하기 전에 signature, expiry, `type`, `jti`, `family`, `sub` claim을 검증합니다. `revokeRefreshToken(tokenId)`는 신뢰할 수 있는 record ID를 이미 가진 caller를 위한 API로 유지됩니다. raw compact token을 이 ID 기반 method에 전달하지 마세요.
|
|
202
|
+
|
|
167
203
|
## 설정 가드레일
|
|
168
204
|
|
|
169
|
-
JWT 서명과 검증에는 `algorithms`에 지원되는 알고리즘이 하나 이상 필요합니다. 기본 signer는 `HS256`, `HS384`, `HS512`, `RS256`, `RS384`, `RS512`, `ES256`, `ES384`, `ES512`를 지원하며, 빈 알고리즘 목록은 모호한 토큰을 발행하거나 수락하지 않도록 즉시 실패합니다.
|
|
205
|
+
JWT 서명과 access-token 검증에는 `algorithms`에 지원되는 알고리즘이 하나 이상 필요합니다. Refresh-token 서명과 검증은 `refreshToken.algorithms`가 설정되어 있으면 이를 사용하고, 설정되지 않으면 backward compatibility를 위해 top-level HMAC algorithms를 사용합니다. 기본 signer는 `HS256`, `HS384`, `HS512`, `RS256`, `RS384`, `RS512`, `ES256`, `ES384`, `ES512`를 지원하며, 빈 알고리즘 목록은 모호한 토큰을 발행하거나 수락하지 않도록 즉시 실패합니다.
|
|
170
206
|
|
|
171
207
|
액세스 토큰 TTL도 양의 유한 숫자여야 합니다. `accessTokenTtlSeconds`를 생략하면 `DefaultJwtSigner`는 문서화된 기본값인 `3600`초를 사용합니다. 소수 초는 JWT NumericDate `exp` 클레임에 그대로 보존됩니다. `0`, 음수 또는 유한하지 않은 값이 제공되면 토큰을 발행하기 전에 `JwtConfigurationError`로 실패합니다.
|
|
172
208
|
|
|
@@ -180,7 +216,7 @@ Lazy loading은 import-time 안전성 속성일 뿐입니다. 서명이나 검
|
|
|
180
216
|
|
|
181
217
|
### `decode()` trust boundary
|
|
182
218
|
|
|
183
|
-
`JwtService.decode(token)`는 서명, `alg`, `exp`, `nbf`, `iss`, `aud` 또는 기타 클레임을 검증하지 않고 JWT payload segment를 읽습니다. 반환된 객체는 **검증되지 않은 입력(unverified input)**이며, 권한 결정(authorization decisions), 신원 확인(identity resolution), 또는 접근을 허가하는 모든 코드 경로에 사용해서는 안 됩니다.
|
|
219
|
+
`JwtService.decode(token)`는 서명, `alg`, `exp`, `nbf`, `iss`, `aud` 또는 기타 클레임을 검증하지 않고 JWT payload segment를 읽습니다. 반환된 객체는 **검증되지 않은 입력(unverified input)**이며, 권한 결정(authorization decisions), 신원 확인(identity resolution), 또는 접근을 허가하는 모든 코드 경로에 사용해서는 안 됩니다. 검증된 클레임은 `JwtService.verify(token, options)`로 얻으세요. 정규화된 `JwtPrincipal`이 필요하면 호출 단위 재정의 없이 `DefaultJwtVerifier.verifyAccessToken(token)`을 사용하고, 호출 단위 `algorithms`, `audience`, `issuer`, `clockSkewSeconds`, `maxAge`, `requireExp`를 보존해야 하면 `DefaultJwtVerifier.verifyAccessTokenWithOverrides(token, options)`을 사용하세요.
|
|
184
220
|
|
|
185
221
|
`decode()`는 진단(diagnostics) 및 비권위적 검사(non-authoritative inspection)에만 사용됩니다. 예를 들어 로깅을 위해 토큰 메타데이터를 읽거나 `verify()` 호출 전에 검증 키를 선택할 때 사용할 수 있습니다. `decode()` 출력에서 읽은 모든 클레임 값 — `sub`, `roles`, `scopes`, `iss`, `aud`, `exp` 포함 — 은 `verify()`가 성공하기 전까지 공격자가 제어한 값으로 취급해야 합니다. `decode()` 출력을 기준으로 요청을 허가하거나 거부하는 분기를 만들지 말고, 검증되지 않은 클레임을 검증된 것처럼 downstream 코드에 노출하지 마세요.
|
|
186
222
|
|
|
@@ -192,7 +228,7 @@ Lazy loading은 import-time 안전성 속성일 뿐입니다. 서명이나 검
|
|
|
192
228
|
- `DefaultJwtVerifier`: 토큰 검증 및 정규화를 담당하는 클래스입니다.
|
|
193
229
|
- `JwtService`: 서명과 검증 기능을 결합한 편의용 파사드(facade)입니다.
|
|
194
230
|
- `JwksClient`: 제한된 요청 시간 안에서 원격 JWKS 키를 가져오고 캐싱합니다.
|
|
195
|
-
- `RefreshTokenService`: `refreshToken` 옵션이 구성된 경우 refresh token을 발행, 회전, 폐기합니다.
|
|
231
|
+
- `RefreshTokenService`: `refreshToken` 옵션이 구성된 경우 refresh token을 발행, 회전, 폐기합니다. `revokePresentedRefreshToken(...)`은 compact refresh token을 검증한 뒤 record를 revoke하며, `revokeRefreshToken(tokenId)`는 신뢰된 ID를 받는 대안입니다.
|
|
196
232
|
|
|
197
233
|
### 타입
|
|
198
234
|
- `JwtPrincipal`: 정규화된 사용자 식별 객체 (`subject`, `roles`, `scopes`, `claims`).
|
|
@@ -205,10 +241,11 @@ Lazy loading은 import-time 안전성 속성일 뿐입니다. 서명이나 검
|
|
|
205
241
|
### 에러와 diagnostics
|
|
206
242
|
- `JwtVerificationError`, `JwtInvalidTokenError`, `JwtExpiredTokenError`, `JwtConfigurationError`: 타입이 지정된 JWT 실패입니다.
|
|
207
243
|
- `createJwtPlatformStatusSnapshot(...)`, `createJwtPlatformDiagnosticIssues(...)`: status 및 diagnostic helper입니다.
|
|
208
|
-
- `JWT_OPTIONS`, `HMAC_HASH`, `ASYMMETRIC_HASH`: 모듈과 검증 레이어에서 사용하는 export token/constant입니다.
|
|
244
|
+
- `JWT_OPTIONS`, `HMAC_HASH`, `ASYMMETRIC_HASH`: 모듈과 검증 레이어에서 사용하는 export token/constant입니다. `HMAC_HASH`와 `ASYMMETRIC_HASH`는 readonly lookup 값이므로 변경하지 마세요.
|
|
209
245
|
|
|
210
246
|
### Deprecated compatibility helper
|
|
211
247
|
- `normalizeRefreshTokenOptions(...)`: 기존 caller의 root import 호환성만을 위해 유지됩니다. package normalization 내부 helper를 직접 호출하기보다 `JwtModule.forRoot(...)` / `JwtModule.forRootAsync(...)`와 `RefreshTokenService`를 사용하세요.
|
|
248
|
+
- `createJwtCoreProviders(...)`: 기존 direct module composition caller의 root import 호환성만을 위해 유지됩니다. registration이 published module surface와 정렬되도록 `JwtModule.forRoot(...)` / `JwtModule.forRootAsync(...)`를 사용하세요.
|
|
212
249
|
|
|
213
250
|
## 관련 패키지
|
|
214
251
|
|
package/README.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
<p><strong><kbd>English</kbd></strong> <a href="./README.ko.md"><kbd>한국어</kbd></a></p>
|
|
4
4
|
|
|
5
|
+
Node.js support is `>=24.0.0 <27`. See [Node.js support and migration](../../docs/reference/node-support.md) before upgrading.
|
|
6
|
+
|
|
5
7
|
HTTP-agnostic JWT token core that handles signing access tokens and verifying them into a normalized `JwtPrincipal`.
|
|
6
8
|
|
|
7
9
|
## Table of Contents
|
|
@@ -61,38 +63,47 @@ Async registration exports the same JWT provider surface as the synchronous path
|
|
|
61
63
|
|
|
62
64
|
`forRootAsync(...)` resolves one module-level `JwtVerifierOptions` object from the providers listed in `inject`. It does not receive per-request state. For tenant-specific secrets or identity providers, keep tenant lookup in your application auth layer and use token metadata such as `kid` with configured `keys[]`, `jwksUri`, or `secretOrKeyProvider` to select verification material during token verification.
|
|
63
65
|
|
|
64
|
-
|
|
65
|
-
import { Module, type Token } from '@fluojs/core';
|
|
66
|
-
import { JwtModule } from '@fluojs/jwt';
|
|
66
|
+
The supported contract is `JwtModule.forRootAsync({ inject, useFactory, global? })`: dependencies named by `inject` must already be registered in the application module graph before the JWT options provider resolves, and `useFactory` returns the final `JwtVerifierOptions`. The top-level `global?` controls returned module visibility and is distinct from the final `JwtVerifierOptions` returned by `useFactory`. NestJS dynamic-module `imports`, `useClass`, and `useExisting` are not part of the supported typed configuration and have no dynamic-module semantics; extra JavaScript object properties are unread at runtime, not validated or rejected. For `JwtModule.forRootAsync(...)`, dependencies must come from a globally visible module export or bootstrap runtime providers in the application graph that `JwtRuntimeModule` can resolve. An ordinary sibling or parent module export alone, and a provider local only to `AuthModule.providers`, are not visible to the JWT options provider. `JwtModule.forRootAsync(...)` performs no implicit module or provider discovery.
|
|
67
67
|
|
|
68
|
-
|
|
68
|
+
```typescript
|
|
69
|
+
import { Module } from '@fluojs/core';
|
|
70
|
+
import { ConfigModule, ConfigService } from '@fluojs/config';
|
|
71
|
+
import { JwtModule, type JwtVerifierOptions } from '@fluojs/jwt';
|
|
69
72
|
|
|
70
73
|
@Module({
|
|
71
74
|
imports: [
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
useFactory: async (settings) => ({
|
|
75
|
-
accessTokenTtlSeconds: 900,
|
|
76
|
-
algorithms: ['HS256'],
|
|
77
|
-
audience: 'my-app',
|
|
78
|
-
issuer: settings.issuer,
|
|
79
|
-
secret: settings.secret,
|
|
80
|
-
}),
|
|
75
|
+
ConfigModule.forRoot({
|
|
76
|
+
processEnv: { JWT_SECRET: process.env.JWT_SECRET },
|
|
81
77
|
}),
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
78
|
+
JwtModule.forRootAsync({
|
|
79
|
+
inject: [ConfigService],
|
|
80
|
+
useFactory: async (...deps: unknown[]): Promise<JwtVerifierOptions> => {
|
|
81
|
+
const [config] = deps;
|
|
82
|
+
if (!(config instanceof ConfigService)) {
|
|
83
|
+
throw new TypeError('ConfigService dependency is required');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const secret = config.snapshot()['JWT_SECRET'];
|
|
87
|
+
if (typeof secret !== 'string') {
|
|
88
|
+
throw new TypeError('JWT_SECRET must be a string');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
accessTokenTtlSeconds: 900,
|
|
93
|
+
algorithms: ['HS256'],
|
|
94
|
+
audience: 'my-app',
|
|
95
|
+
issuer: 'my-api',
|
|
96
|
+
secret,
|
|
97
|
+
};
|
|
89
98
|
},
|
|
90
|
-
},
|
|
99
|
+
}),
|
|
91
100
|
],
|
|
92
101
|
})
|
|
93
102
|
export class AuthModule {}
|
|
94
103
|
```
|
|
95
104
|
|
|
105
|
+
Here, `ConfigModule.forRoot(...)` exports `ConfigService` globally by default, so the `JwtRuntimeModule` options provider can resolve it. A provider declared only in `AuthModule.providers` is not visible to that imported module.
|
|
106
|
+
|
|
96
107
|
### Sign and Verify Tokens
|
|
97
108
|
|
|
98
109
|
Inject `DefaultJwtSigner` to issue tokens and `DefaultJwtVerifier` to validate them.
|
|
@@ -152,21 +163,46 @@ const verifier = new DefaultJwtVerifier({
|
|
|
152
163
|
|
|
153
164
|
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
165
|
|
|
155
|
-
`
|
|
166
|
+
`DefaultJwtVerifier.verifyAccessTokenWithOverrides(token, options)` applies per-call algorithm and claim-policy overrides (`algorithms`, `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
167
|
|
|
157
|
-
When multiple compatible keys are configured, `kid` disambiguates the verification key. A single compatible static key can verify tokens without `kid`; JWKS-backed verification relies on the remote key set and its cache policy.
|
|
168
|
+
When multiple compatible keys are configured, `kid` disambiguates the verification key. Every `keys[]` entry must have a non-empty, unique `kid`; `DefaultJwtSigner` and `DefaultJwtVerifier` reject empty or duplicate values with `JwtConfigurationError` during construction so key rotation cannot select different keys for signing and verification. A single compatible static key can verify tokens without `kid`; JWKS-backed verification relies on the remote key set and its cache policy.
|
|
158
169
|
|
|
159
170
|
For multi-tenant systems, prefer putting a tenant-specific `kid` in issued token headers and configuring compatible key sources up front. `secretOrKeyProvider` is called with the decoded token header only, so request headers, route params, or other request-context tenant hints must be handled by application-level strategy/guard code before calling the JWT verifier.
|
|
160
171
|
|
|
161
172
|
### Refresh tokens
|
|
162
173
|
|
|
163
|
-
`RefreshTokenService` uses a dedicated HMAC refresh-token path. Configure `refreshToken.secret` separately from access-token signing keys.
|
|
174
|
+
`RefreshTokenService` uses a dedicated HMAC refresh-token path. Configure `refreshToken.secret` separately from access-token signing keys. Set `refreshToken.algorithms` to an explicit HMAC allowlist (for example, `['HS256']`) when the access-token `algorithms` list is asymmetric-only; otherwise, existing configurations continue to derive the refresh policy from the top-level HMAC algorithms. This policy applies only to refresh-token signing and verification, so it does not widen access-token verification.
|
|
175
|
+
|
|
176
|
+
```typescript
|
|
177
|
+
const options = {
|
|
178
|
+
algorithms: ['RS256'],
|
|
179
|
+
privateKey: '...private PEM...',
|
|
180
|
+
publicKey: '...public PEM...',
|
|
181
|
+
refreshToken: {
|
|
182
|
+
algorithms: ['HS256'],
|
|
183
|
+
secret: 'refresh-secret',
|
|
184
|
+
expiresInSeconds: 300,
|
|
185
|
+
rotation: false,
|
|
186
|
+
store,
|
|
187
|
+
},
|
|
188
|
+
};
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
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
192
|
|
|
165
193
|
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.
|
|
166
194
|
|
|
195
|
+
For single-session logout, pass the compact token that the caller presented to `revokePresentedRefreshToken(...)`:
|
|
196
|
+
|
|
197
|
+
```typescript
|
|
198
|
+
await refreshTokens.revokePresentedRefreshToken(refreshToken);
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
This verifies the signature, expiry, `type`, `jti`, `family`, and `sub` claims before revoking the matching record. `revokeRefreshToken(tokenId)` remains available for callers that already hold a trusted record ID; do not pass a raw compact token to that ID-based method.
|
|
202
|
+
|
|
167
203
|
## Configuration Guardrails
|
|
168
204
|
|
|
169
|
-
JWT signing and verification require at least one supported algorithm in `algorithms`. The built-in signer supports `HS256`, `HS384`, `HS512`, `RS256`, `RS384`, `RS512`, `ES256`, `ES384`, and `ES512`; configuration with an empty algorithm list fails fast instead of issuing or accepting ambiguous tokens.
|
|
205
|
+
JWT signing and access-token verification require at least one supported algorithm in `algorithms`. Refresh-token signing and verification use `refreshToken.algorithms` when configured, or the top-level HMAC algorithms for backward compatibility. The built-in signer supports `HS256`, `HS384`, `HS512`, `RS256`, `RS384`, `RS512`, `ES256`, `ES384`, and `ES512`; configuration with an empty algorithm list fails fast instead of issuing or accepting ambiguous tokens.
|
|
170
206
|
|
|
171
207
|
Access-token TTL must also be a positive finite number. When `accessTokenTtlSeconds` is omitted, `DefaultJwtSigner` uses the documented `3600` second default. Fractional seconds are preserved in the JWT NumericDate `exp` claim; when the option is provided as `0`, a negative number, or a non-finite value, signing fails with `JwtConfigurationError` before a token is issued.
|
|
172
208
|
|
|
@@ -180,7 +216,7 @@ Lazy loading is an import-time safety property only. It does **not** make signin
|
|
|
180
216
|
|
|
181
217
|
### `decode()` trust boundary
|
|
182
218
|
|
|
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)`
|
|
219
|
+
`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)` to obtain verified claims. To obtain a normalized `JwtPrincipal`, use `DefaultJwtVerifier.verifyAccessToken(token)` without per-call overrides, or `DefaultJwtVerifier.verifyAccessTokenWithOverrides(token, options)` when preserving per-call `algorithms`, `audience`, `issuer`, `clockSkewSeconds`, `maxAge`, or `requireExp`.
|
|
184
220
|
|
|
185
221
|
`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
222
|
|
|
@@ -192,7 +228,7 @@ Lazy loading is an import-time safety property only. It does **not** make signin
|
|
|
192
228
|
- `DefaultJwtVerifier`: Handles token validation and normalization.
|
|
193
229
|
- `JwtService`: A convenience facade combining signing and verification.
|
|
194
230
|
- `JwksClient`: Fetches and caches remote JWKS keys with bounded request timeouts.
|
|
195
|
-
- `RefreshTokenService`: Issues, rotates, and revokes refresh tokens when `refreshToken` options are configured.
|
|
231
|
+
- `RefreshTokenService`: Issues, rotates, and revokes refresh tokens when `refreshToken` options are configured. `revokePresentedRefreshToken(...)` verifies a compact refresh token before revoking its record; `revokeRefreshToken(tokenId)` is the trusted-ID alternative.
|
|
196
232
|
|
|
197
233
|
### Types
|
|
198
234
|
- `JwtPrincipal`: The normalized identity object (`subject`, `roles`, `scopes`, `claims`).
|
|
@@ -205,10 +241,11 @@ Lazy loading is an import-time safety property only. It does **not** make signin
|
|
|
205
241
|
### Errors and diagnostics
|
|
206
242
|
- `JwtVerificationError`, `JwtInvalidTokenError`, `JwtExpiredTokenError`, `JwtConfigurationError`: Typed JWT failures.
|
|
207
243
|
- `createJwtPlatformStatusSnapshot(...)` and `createJwtPlatformDiagnosticIssues(...)`: Status and diagnostic helpers.
|
|
208
|
-
- `JWT_OPTIONS`, `HMAC_HASH`, `ASYMMETRIC_HASH`: Exported tokens/constants used by the module and verification layer.
|
|
244
|
+
- `JWT_OPTIONS`, `HMAC_HASH`, `ASYMMETRIC_HASH`: Exported tokens/constants used by the module and verification layer. `HMAC_HASH` and `ASYMMETRIC_HASH` are readonly lookup values; do not mutate them.
|
|
209
245
|
|
|
210
246
|
### Deprecated compatibility helpers
|
|
211
247
|
- `normalizeRefreshTokenOptions(...)`: Retained only for root-import compatibility with existing callers. Prefer `JwtModule.forRoot(...)` / `JwtModule.forRootAsync(...)` plus `RefreshTokenService` instead of calling package normalization internals.
|
|
248
|
+
- `createJwtCoreProviders(...)`: Retained only for root-import compatibility with existing direct module composition callers. Prefer `JwtModule.forRoot(...)` / `JwtModule.forRootAsync(...)` so registration stays aligned with the published module surface.
|
|
212
249
|
|
|
213
250
|
## Related Packages
|
|
214
251
|
|
package/dist/module.d.ts
CHANGED
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
import { type AsyncModuleOptions, type Constructor } from '@fluojs/core';
|
|
2
|
+
import type { Provider } from '@fluojs/di';
|
|
2
3
|
import type { JwtVerifierOptions } from './types.js';
|
|
3
4
|
type ModuleType = Constructor;
|
|
5
|
+
/**
|
|
6
|
+
* Creates the core JWT providers for advanced direct module composition.
|
|
7
|
+
*
|
|
8
|
+
* @deprecated Prefer {@link JwtModule.forRoot} or {@link JwtModule.forRootAsync} so JWT registration stays aligned with the published module surface.
|
|
9
|
+
* @param options JWT verification and signing options used for provider registration.
|
|
10
|
+
* @returns Providers for the JWT verifier, signer, facade, and optional refresh-token service.
|
|
11
|
+
*/
|
|
12
|
+
export declare function createJwtCoreProviders(options: JwtVerifierOptions): Provider[];
|
|
4
13
|
/**
|
|
5
14
|
* Registers JWT services and optional refresh-token support for an application module.
|
|
6
15
|
*/
|
package/dist/module.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"module.d.ts","sourceRoot":"","sources":["../src/module.ts"],"names":[],"mappings":"AAAA,OAAO,EAAU,KAAK,kBAAkB,EAAE,KAAK,WAAW,
|
|
1
|
+
{"version":3,"file":"module.d.ts","sourceRoot":"","sources":["../src/module.ts"],"names":[],"mappings":"AAAA,OAAO,EAAU,KAAK,kBAAkB,EAAE,KAAK,WAAW,EAA0C,MAAM,cAAc,CAAC;AAEzH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAK3C,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAIrD,KAAK,UAAU,GAAG,WAAW,CAAC;AAyE9B;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,kBAAkB,GAAG,QAAQ,EAAE,CAM9E;AAED;;GAEG;AACH,qBAAa,SAAS;IACpB,MAAM,CAAC,OAAO,CAAC,OAAO,EAAE,kBAAkB,GAAG,UAAU;IAQvD,MAAM,CAAC,YAAY,CAAC,OAAO,EAAE,kBAAkB,CAAC,kBAAkB,CAAC,GAAG;QAAE,MAAM,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,UAAU;IASvG,OAAO,CAAC,MAAM,CAAC,YAAY;CAkB5B"}
|
package/dist/module.js
CHANGED
|
@@ -55,6 +55,21 @@ function createJwtModuleProviders(optionsProvider, includeRefreshTokenService, r
|
|
|
55
55
|
return providers;
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
/**
|
|
59
|
+
* Creates the core JWT providers for advanced direct module composition.
|
|
60
|
+
*
|
|
61
|
+
* @deprecated Prefer {@link JwtModule.forRoot} or {@link JwtModule.forRootAsync} so JWT registration stays aligned with the published module surface.
|
|
62
|
+
* @param options JWT verification and signing options used for provider registration.
|
|
63
|
+
* @returns Providers for the JWT verifier, signer, facade, and optional refresh-token service.
|
|
64
|
+
*/
|
|
65
|
+
export function createJwtCoreProviders(options) {
|
|
66
|
+
return createJwtModuleProviders({
|
|
67
|
+
provide: JWT_OPTIONS,
|
|
68
|
+
scope: 'singleton',
|
|
69
|
+
useValue: options
|
|
70
|
+
}, Boolean(options.refreshToken), 'singleton');
|
|
71
|
+
}
|
|
72
|
+
|
|
58
73
|
/**
|
|
59
74
|
* Registers JWT services and optional refresh-token support for an application module.
|
|
60
75
|
*/
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { DefaultJwtVerifier } from '../signing/verifier.js';
|
|
2
|
+
import type { JwtClaims } from '../types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Describes the claims required for a verified refresh token.
|
|
5
|
+
*/
|
|
6
|
+
export interface RefreshTokenClaims extends JwtClaims {
|
|
7
|
+
family: string;
|
|
8
|
+
jti: string;
|
|
9
|
+
type: 'refresh';
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Verifies a compact refresh token and returns its required claims.
|
|
13
|
+
*
|
|
14
|
+
* @param verifier Configured verifier for the refresh-token policy.
|
|
15
|
+
* @param token Compact refresh token to verify.
|
|
16
|
+
* @returns Verified refresh-token claims with non-empty required identifiers.
|
|
17
|
+
* @throws {JwtInvalidTokenError} When the token is not a refresh token or lacks required claims.
|
|
18
|
+
*/
|
|
19
|
+
export declare function verifyRefreshTokenClaims(verifier: DefaultJwtVerifier, token: string): Promise<RefreshTokenClaims & {
|
|
20
|
+
sub: string;
|
|
21
|
+
}>;
|
|
22
|
+
//# sourceMappingURL=refresh-token-claims.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"refresh-token-claims.d.ts","sourceRoot":"","sources":["../../src/refresh/refresh-token-claims.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AACjE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAE7C;;GAEG;AACH,MAAM,WAAW,kBAAmB,SAAQ,SAAS;IACnD,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,SAAS,CAAC;CACjB;AAED;;;;;;;GAOG;AACH,wBAAsB,wBAAwB,CAC5C,QAAQ,EAAE,kBAAkB,EAC5B,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC,kBAAkB,GAAG;IAAE,GAAG,EAAE,MAAM,CAAA;CAAE,CAAC,CA2B/C"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { JwtInvalidTokenError } from '../errors.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Describes the claims required for a verified refresh token.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Verifies a compact refresh token and returns its required claims.
|
|
9
|
+
*
|
|
10
|
+
* @param verifier Configured verifier for the refresh-token policy.
|
|
11
|
+
* @param token Compact refresh token to verify.
|
|
12
|
+
* @returns Verified refresh-token claims with non-empty required identifiers.
|
|
13
|
+
* @throws {JwtInvalidTokenError} When the token is not a refresh token or lacks required claims.
|
|
14
|
+
*/
|
|
15
|
+
export async function verifyRefreshTokenClaims(verifier, token) {
|
|
16
|
+
const principal = await verifier.verifyRefreshToken(token);
|
|
17
|
+
const claims = principal.claims;
|
|
18
|
+
if (claims.type !== 'refresh') {
|
|
19
|
+
throw new JwtInvalidTokenError('JWT is not a refresh token.');
|
|
20
|
+
}
|
|
21
|
+
if (typeof claims.jti !== 'string' || claims.jti.length === 0) {
|
|
22
|
+
throw new JwtInvalidTokenError('Refresh token is missing jti.');
|
|
23
|
+
}
|
|
24
|
+
if (typeof claims.family !== 'string' || claims.family.length === 0) {
|
|
25
|
+
throw new JwtInvalidTokenError('Refresh token is missing family.');
|
|
26
|
+
}
|
|
27
|
+
if (typeof claims.sub !== 'string' || claims.sub.length === 0) {
|
|
28
|
+
throw new JwtInvalidTokenError('Refresh token is missing sub.');
|
|
29
|
+
}
|
|
30
|
+
return {
|
|
31
|
+
...claims,
|
|
32
|
+
family: claims.family,
|
|
33
|
+
jti: claims.jti,
|
|
34
|
+
sub: claims.sub,
|
|
35
|
+
type: 'refresh'
|
|
36
|
+
};
|
|
37
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { DefaultJwtSigner } from '../signing/signer.js';
|
|
2
2
|
import type { DefaultJwtVerifier } from '../signing/verifier.js';
|
|
3
|
+
import type { JwtAlgorithm } from '../types.js';
|
|
3
4
|
/**
|
|
4
5
|
* Describes the refresh token store contract.
|
|
5
6
|
*/
|
|
@@ -46,11 +47,13 @@ export interface RefreshTokenRecord {
|
|
|
46
47
|
* Describes the refresh token options contract.
|
|
47
48
|
*/
|
|
48
49
|
export interface RefreshTokenOptions {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
50
|
+
/** HMAC algorithms allowed for refresh-token signing and verification. Defaults to HMAC algorithms from the top-level policy. */
|
|
51
|
+
readonly algorithms?: readonly Extract<JwtAlgorithm, 'HS256' | 'HS384' | 'HS512'>[];
|
|
52
|
+
readonly secret: string;
|
|
53
|
+
readonly expiresInSeconds: number;
|
|
54
|
+
readonly verifyMaxAgeSeconds?: number;
|
|
55
|
+
readonly rotation: boolean;
|
|
56
|
+
readonly store: RefreshTokenStore;
|
|
54
57
|
}
|
|
55
58
|
/**
|
|
56
59
|
* Normalize refresh token options for legacy root-import callers.
|
|
@@ -78,11 +81,19 @@ export declare class RefreshTokenService {
|
|
|
78
81
|
refreshToken: string;
|
|
79
82
|
}>;
|
|
80
83
|
revokeRefreshToken(tokenId: string): Promise<void>;
|
|
84
|
+
/**
|
|
85
|
+
* Revokes the record identified by a verified presented refresh token.
|
|
86
|
+
*
|
|
87
|
+
* @param token Compact refresh token to verify before its record is revoked.
|
|
88
|
+
* @returns A promise that resolves after the verified refresh-token record is revoked.
|
|
89
|
+
* @throws {JwtInvalidTokenError} When the token is malformed or lacks required refresh claims.
|
|
90
|
+
* @throws {JwtExpiredTokenError} When the refresh token has expired.
|
|
91
|
+
*/
|
|
92
|
+
revokePresentedRefreshToken(token: string): Promise<void>;
|
|
81
93
|
revokeAllForSubject(subject: string): Promise<void>;
|
|
82
94
|
private issueRefreshTokenWithFamily;
|
|
83
95
|
private consumeRefreshToken;
|
|
84
96
|
private revokeCompromisedFamily;
|
|
85
97
|
private createRefreshTokenWithFamily;
|
|
86
|
-
private verifyRefreshClaims;
|
|
87
98
|
}
|
|
88
99
|
//# sourceMappingURL=refresh-token.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"refresh-token.d.ts","sourceRoot":"","sources":["../../src/refresh/refresh-token.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"refresh-token.d.ts","sourceRoot":"","sources":["../../src/refresh/refresh-token.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAC7D,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AACjE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAGhD;;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,iIAAiI;IACjI,QAAQ,CAAC,UAAU,CAAC,EAAE,SAAS,OAAO,CAAC,YAAY,EAAE,OAAO,GAAG,OAAO,GAAG,OAAO,CAAC,EAAE,CAAC;IACpF,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;IAClC,QAAQ,CAAC,mBAAmB,CAAC,EAAE,MAAM,CAAC;IACtC,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,KAAK,EAAE,iBAAiB,CAAC;CACnC;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,4BAA4B,CAAC,OAAO,EAAE,mBAAmB,GAAG,SAAS,GAAG,mBAAmB,CA2C1G;AAED;;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;IAIxD;;;;;;;OAOG;IACG,2BAA2B,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAMzD,mBAAmB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;YAI3C,2BAA2B;YAQ3B,mBAAmB;YAUnB,uBAAuB;YASvB,4BAA4B;CA8B3C"}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { JwtConfigurationError, JwtExpiredTokenError, JwtInvalidTokenError } from '../errors.js';
|
|
2
|
+
import { SUPPORTED_HMAC_HASH } from '../signing/algorithm-policy.js';
|
|
3
|
+
import { verifyRefreshTokenClaims } from './refresh-token-claims.js';
|
|
2
4
|
|
|
3
5
|
/**
|
|
4
6
|
* Describes the refresh token store contract.
|
|
@@ -39,6 +41,16 @@ export function normalizeRefreshTokenOptions(options) {
|
|
|
39
41
|
if (!options) {
|
|
40
42
|
throw new JwtConfigurationError('JWT refresh token options are not configured.');
|
|
41
43
|
}
|
|
44
|
+
if (options.algorithms !== undefined) {
|
|
45
|
+
if (!Array.isArray(options.algorithms) || options.algorithms.length === 0) {
|
|
46
|
+
throw new JwtConfigurationError('JWT refresh token algorithms must contain at least one HMAC algorithm.');
|
|
47
|
+
}
|
|
48
|
+
for (const algorithm of options.algorithms) {
|
|
49
|
+
if (typeof algorithm !== 'string' || !Object.hasOwn(SUPPORTED_HMAC_HASH, algorithm)) {
|
|
50
|
+
throw new JwtConfigurationError(`JWT refresh token received unsupported algorithm "${String(algorithm)}"; only HS256, HS384, and HS512 are allowed.`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
42
54
|
if (typeof options.secret !== 'string' || options.secret.length === 0) {
|
|
43
55
|
throw new JwtConfigurationError('JWT refresh token secret must be a non-empty string.');
|
|
44
56
|
}
|
|
@@ -55,6 +67,7 @@ export function normalizeRefreshTokenOptions(options) {
|
|
|
55
67
|
...options
|
|
56
68
|
};
|
|
57
69
|
}
|
|
70
|
+
|
|
58
71
|
/**
|
|
59
72
|
* Represents the refresh token service.
|
|
60
73
|
*/
|
|
@@ -73,7 +86,7 @@ export class RefreshTokenService {
|
|
|
73
86
|
return this.issueRefreshTokenWithFamily(subject, family);
|
|
74
87
|
}
|
|
75
88
|
async rotateRefreshToken(currentToken) {
|
|
76
|
-
const claims = await this.
|
|
89
|
+
const claims = await verifyRefreshTokenClaims(this.verifier, currentToken);
|
|
77
90
|
if (this.options.rotation) {
|
|
78
91
|
if (!this.options.store.rotate && !this.options.store.consume) {
|
|
79
92
|
throw new JwtConfigurationError('Refresh token rotation requires an atomic store.rotate() or store.consume() implementation.');
|
|
@@ -140,6 +153,19 @@ export class RefreshTokenService {
|
|
|
140
153
|
async revokeRefreshToken(tokenId) {
|
|
141
154
|
await this.options.store.revoke(tokenId);
|
|
142
155
|
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Revokes the record identified by a verified presented refresh token.
|
|
159
|
+
*
|
|
160
|
+
* @param token Compact refresh token to verify before its record is revoked.
|
|
161
|
+
* @returns A promise that resolves after the verified refresh-token record is revoked.
|
|
162
|
+
* @throws {JwtInvalidTokenError} When the token is malformed or lacks required refresh claims.
|
|
163
|
+
* @throws {JwtExpiredTokenError} When the refresh token has expired.
|
|
164
|
+
*/
|
|
165
|
+
async revokePresentedRefreshToken(token) {
|
|
166
|
+
const claims = await verifyRefreshTokenClaims(this.verifier, token);
|
|
167
|
+
await this.options.store.revoke(claims.jti);
|
|
168
|
+
}
|
|
143
169
|
async revokeAllForSubject(subject) {
|
|
144
170
|
await this.options.store.revokeBySubject(subject);
|
|
145
171
|
}
|
|
@@ -193,27 +219,4 @@ export class RefreshTokenService {
|
|
|
193
219
|
token
|
|
194
220
|
};
|
|
195
221
|
}
|
|
196
|
-
async verifyRefreshClaims(token) {
|
|
197
|
-
const principal = await this.verifier.verifyRefreshToken(token);
|
|
198
|
-
const claims = principal.claims;
|
|
199
|
-
if (claims.type !== 'refresh') {
|
|
200
|
-
throw new JwtInvalidTokenError('JWT is not a refresh token.');
|
|
201
|
-
}
|
|
202
|
-
if (typeof claims.jti !== 'string' || claims.jti.length === 0) {
|
|
203
|
-
throw new JwtInvalidTokenError('Refresh token is missing jti.');
|
|
204
|
-
}
|
|
205
|
-
if (typeof claims.family !== 'string' || claims.family.length === 0) {
|
|
206
|
-
throw new JwtInvalidTokenError('Refresh token is missing family.');
|
|
207
|
-
}
|
|
208
|
-
if (typeof claims.sub !== 'string' || claims.sub.length === 0) {
|
|
209
|
-
throw new JwtInvalidTokenError('Refresh token is missing sub.');
|
|
210
|
-
}
|
|
211
|
-
return {
|
|
212
|
-
...claims,
|
|
213
|
-
family: claims.family,
|
|
214
|
-
jti: claims.jti,
|
|
215
|
-
sub: claims.sub,
|
|
216
|
-
type: 'refresh'
|
|
217
|
-
};
|
|
218
|
-
}
|
|
219
222
|
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { JwtAlgorithm } from '../types.js';
|
|
2
|
+
export declare const SUPPORTED_HMAC_HASH: Readonly<Partial<Record<JwtAlgorithm, string>>>;
|
|
3
|
+
export declare const SUPPORTED_ASYMMETRIC_HASH: Readonly<Partial<Record<JwtAlgorithm, string>>>;
|
|
4
|
+
//# sourceMappingURL=algorithm-policy.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"algorithm-policy.d.ts","sourceRoot":"","sources":["../../src/signing/algorithm-policy.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEhD,eAAO,MAAM,mBAAmB,EAAE,QAAQ,CAAC,OAAO,CAAC,MAAM,CAAC,YAAY,EAAE,MAAM,CAAC,CAAC,CAI9E,CAAC;AAEH,eAAO,MAAM,yBAAyB,EAAE,QAAQ,CAAC,OAAO,CAAC,MAAM,CAAC,YAAY,EAAE,MAAM,CAAC,CAAC,CAOpF,CAAC"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export const SUPPORTED_HMAC_HASH = Object.freeze({
|
|
2
|
+
HS256: 'sha256',
|
|
3
|
+
HS384: 'sha384',
|
|
4
|
+
HS512: 'sha512'
|
|
5
|
+
});
|
|
6
|
+
export const SUPPORTED_ASYMMETRIC_HASH = Object.freeze({
|
|
7
|
+
RS256: 'sha256',
|
|
8
|
+
RS384: 'sha384',
|
|
9
|
+
RS512: 'sha512',
|
|
10
|
+
ES256: 'sha256',
|
|
11
|
+
ES384: 'sha384',
|
|
12
|
+
ES512: 'sha512'
|
|
13
|
+
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { JwtKeyEntry } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Rejects static key entries whose IDs cannot unambiguously select one key.
|
|
4
|
+
*
|
|
5
|
+
* @param keys Static JWT key entries to validate.
|
|
6
|
+
*/
|
|
7
|
+
export declare function assertJwtKeyEntries(keys: JwtKeyEntry[] | undefined): void;
|
|
8
|
+
//# sourceMappingURL=key-entries.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"key-entries.d.ts","sourceRoot":"","sources":["../../src/signing/key-entries.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE/C;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,WAAW,EAAE,GAAG,SAAS,GAAG,IAAI,CAczE"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { JwtConfigurationError } from '../errors.js';
|
|
2
|
+
/**
|
|
3
|
+
* Rejects static key entries whose IDs cannot unambiguously select one key.
|
|
4
|
+
*
|
|
5
|
+
* @param keys Static JWT key entries to validate.
|
|
6
|
+
*/
|
|
7
|
+
export function assertJwtKeyEntries(keys) {
|
|
8
|
+
if (!Array.isArray(keys)) {
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
const keyIds = new Set();
|
|
12
|
+
for (const entry of keys) {
|
|
13
|
+
if (typeof entry.kid !== 'string' || entry.kid.length === 0 || keyIds.has(entry.kid)) {
|
|
14
|
+
throw new JwtConfigurationError('JWT key entries require non-empty unique kid values.');
|
|
15
|
+
}
|
|
16
|
+
keyIds.add(entry.kid);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"signer.d.ts","sourceRoot":"","sources":["../../src/signing/signer.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAgB,SAAS,EAAe,kBAAkB,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"signer.d.ts","sourceRoot":"","sources":["../../src/signing/signer.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAgB,SAAS,EAAe,kBAAkB,EAAE,MAAM,aAAa,CAAC;AA4D5F;;GAEG;AACH,qBACa,gBAAgB;IAGf,OAAO,CAAC,QAAQ,CAAC,OAAO;IAFpC,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAiB;gBAEtB,OAAO,EAAE,kBAAkB;IAYlD,eAAe,CAAC,MAAM,EAAE,SAAS,GAAG,OAAO,CAAC,MAAM,CAAC;IAInD,gBAAgB,CAAC,MAAM,EAAE,SAAS,GAAG,OAAO,CAAC,MAAM,CAAC;IAK1D,OAAO,CAAC,4BAA4B;YAYtB,SAAS;CAoFxB"}
|
package/dist/signing/signer.js
CHANGED
|
@@ -7,7 +7,9 @@ function _checkInRHS(e) { if (Object(e) !== e) throw TypeError("right-hand side
|
|
|
7
7
|
import { Inject } from '@fluojs/core';
|
|
8
8
|
import { JwtConfigurationError } from '../errors.js';
|
|
9
9
|
import { normalizeRefreshTokenOptions } from '../refresh/refresh-token.js';
|
|
10
|
-
import {
|
|
10
|
+
import { SUPPORTED_ASYMMETRIC_HASH, SUPPORTED_HMAC_HASH } from './algorithm-policy.js';
|
|
11
|
+
import { assertJwtKeyEntries } from './key-entries.js';
|
|
12
|
+
import { JWT_OPTIONS } from './verifier.js';
|
|
11
13
|
function encodeBase64Url(value) {
|
|
12
14
|
return Buffer.from(value).toString('base64').replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_');
|
|
13
15
|
}
|
|
@@ -16,7 +18,7 @@ function resolveSigningKeyEntry(options, algorithm) {
|
|
|
16
18
|
if (!Array.isArray(keys) || keys.length === 0) {
|
|
17
19
|
return undefined;
|
|
18
20
|
}
|
|
19
|
-
if (hasOwnAlgorithmMapping(
|
|
21
|
+
if (hasOwnAlgorithmMapping(SUPPORTED_HMAC_HASH, algorithm)) {
|
|
20
22
|
return keys.find(entry => typeof entry.secret === 'string' && entry.secret.length > 0);
|
|
21
23
|
}
|
|
22
24
|
return keys.find(entry => entry.privateKey !== undefined);
|
|
@@ -25,7 +27,7 @@ function hasOwnAlgorithmMapping(mappings, algorithm) {
|
|
|
25
27
|
return typeof algorithm === 'string' && Object.hasOwn(mappings, algorithm);
|
|
26
28
|
}
|
|
27
29
|
function isSupportedSigningAlgorithm(algorithm) {
|
|
28
|
-
return hasOwnAlgorithmMapping(
|
|
30
|
+
return hasOwnAlgorithmMapping(SUPPORTED_HMAC_HASH, algorithm) || hasOwnAlgorithmMapping(SUPPORTED_ASYMMETRIC_HASH, algorithm);
|
|
29
31
|
}
|
|
30
32
|
function assertSigningAlgorithms(algorithms) {
|
|
31
33
|
if (!Array.isArray(algorithms) || algorithms.length === 0) {
|
|
@@ -57,7 +59,10 @@ class DefaultJwtSigner {
|
|
|
57
59
|
constructor(options) {
|
|
58
60
|
this.options = options;
|
|
59
61
|
assertSigningAlgorithms(options.algorithms);
|
|
60
|
-
|
|
62
|
+
assertJwtKeyEntries(options.keys);
|
|
63
|
+
const refreshToken = this.options.refreshToken ? normalizeRefreshTokenOptions(this.options.refreshToken) : undefined;
|
|
64
|
+
const configuredRefreshAlgorithms = refreshToken?.algorithms;
|
|
65
|
+
this.refreshAlgorithms = (configuredRefreshAlgorithms ?? this.options.algorithms).filter(algorithm => hasOwnAlgorithmMapping(SUPPORTED_HMAC_HASH, algorithm));
|
|
61
66
|
}
|
|
62
67
|
async signAccessToken(claims) {
|
|
63
68
|
return this.signToken(claims, this.options, false);
|
|
@@ -79,7 +84,7 @@ class DefaultJwtSigner {
|
|
|
79
84
|
async signToken(claims, options, hmacOnly) {
|
|
80
85
|
const algorithm = options.algorithms.find(alg => {
|
|
81
86
|
if (hmacOnly) {
|
|
82
|
-
return hasOwnAlgorithmMapping(
|
|
87
|
+
return hasOwnAlgorithmMapping(SUPPORTED_HMAC_HASH, alg);
|
|
83
88
|
}
|
|
84
89
|
return isSupportedSigningAlgorithm(alg);
|
|
85
90
|
});
|
|
@@ -89,7 +94,7 @@ class DefaultJwtSigner {
|
|
|
89
94
|
}
|
|
90
95
|
throw new JwtConfigurationError('JWT signer requires at least one supported algorithm (HS256/HS384/HS512/RS256/RS384/RS512/ES256/ES384/ES512) in the allowed algorithms list.');
|
|
91
96
|
}
|
|
92
|
-
const isAsymmetric = hasOwnAlgorithmMapping(
|
|
97
|
+
const isAsymmetric = hasOwnAlgorithmMapping(SUPPORTED_ASYMMETRIC_HASH, algorithm);
|
|
93
98
|
const now = Math.floor(Date.now() / 1000);
|
|
94
99
|
const ttl = resolveAccessTokenTtlSeconds(options);
|
|
95
100
|
const payload = {
|
|
@@ -116,7 +121,7 @@ class DefaultJwtSigner {
|
|
|
116
121
|
if (!privateKey) {
|
|
117
122
|
throw new JwtConfigurationError('JWT private key is not configured.');
|
|
118
123
|
}
|
|
119
|
-
const hash =
|
|
124
|
+
const hash = SUPPORTED_ASYMMETRIC_HASH[algorithm];
|
|
120
125
|
if (!hash) {
|
|
121
126
|
throw new JwtConfigurationError(`No hash mapping for asymmetric algorithm "${algorithm}".`);
|
|
122
127
|
}
|
|
@@ -135,7 +140,7 @@ class DefaultJwtSigner {
|
|
|
135
140
|
if (!secret) {
|
|
136
141
|
throw new JwtConfigurationError('JWT secret is not configured.');
|
|
137
142
|
}
|
|
138
|
-
const hash =
|
|
143
|
+
const hash = SUPPORTED_HMAC_HASH[algorithm];
|
|
139
144
|
if (!hash) {
|
|
140
145
|
throw new JwtConfigurationError(`No hash mapping for HMAC algorithm "${algorithm}".`);
|
|
141
146
|
}
|
|
@@ -1,22 +1,27 @@
|
|
|
1
|
-
import type { OnModuleDestroy } from '@fluojs/runtime';
|
|
2
1
|
import type { JwtAlgorithm, JwtPrincipal, JwtVerifierOptions } from '../types.js';
|
|
3
2
|
/**
|
|
4
3
|
* Provides the resolved JWT verifier options through dependency injection.
|
|
5
4
|
*/
|
|
6
5
|
export declare const JWT_OPTIONS: unique symbol;
|
|
6
|
+
/**
|
|
7
|
+
* Describes the lifecycle hook called when a JWT module is destroyed.
|
|
8
|
+
*/
|
|
9
|
+
export interface JwtModuleDestroyLifecycle {
|
|
10
|
+
onModuleDestroy(): void | Promise<void>;
|
|
11
|
+
}
|
|
7
12
|
/**
|
|
8
13
|
* Maps supported HMAC JWT algorithms to their Node.js hash names.
|
|
9
14
|
*/
|
|
10
|
-
export declare const HMAC_HASH: Partial<Record<JwtAlgorithm, string
|
|
15
|
+
export declare const HMAC_HASH: Readonly<Partial<Record<JwtAlgorithm, string>>>;
|
|
11
16
|
/**
|
|
12
17
|
* Maps supported asymmetric JWT algorithms to their Node.js hash names.
|
|
13
18
|
*/
|
|
14
|
-
export declare const ASYMMETRIC_HASH: Partial<Record<JwtAlgorithm, string
|
|
19
|
+
export declare const ASYMMETRIC_HASH: Readonly<Partial<Record<JwtAlgorithm, string>>>;
|
|
15
20
|
type AccessTokenVerificationOverrides = Pick<JwtVerifierOptions, 'algorithms' | 'audience' | 'clockSkewSeconds' | 'issuer' | 'maxAge' | 'requireExp'>;
|
|
16
21
|
/**
|
|
17
22
|
* Verifies JWT access and refresh tokens against the configured key sources.
|
|
18
23
|
*/
|
|
19
|
-
export declare class DefaultJwtVerifier implements
|
|
24
|
+
export declare class DefaultJwtVerifier implements JwtModuleDestroyLifecycle {
|
|
20
25
|
private readonly options;
|
|
21
26
|
private readonly jwksClient;
|
|
22
27
|
private readonly keyResolutionState;
|
|
@@ -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":"AAMA,OAAO,KAAK,EAAE,YAAY,EAA0B,YAAY,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAK1G;;GAEG;AACH,eAAO,MAAM,WAAW,eAAiC,CAAC;AAE1D;;GAEG;AACH,MAAM,WAAW,yBAAyB;IACxC,eAAe,IAAI,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACzC;AAED;;GAEG;AACH,eAAO,MAAM,SAAS,EAAE,QAAQ,CAAC,OAAO,CAAC,MAAM,CAAC,YAAY,EAAE,MAAM,CAAC,CAAC,CAA6C,CAAC;AAEpH;;GAEG;AACH,eAAO,MAAM,eAAe,EAAE,QAAQ,CAAC,OAAO,CAAC,MAAM,CAAC,YAAY,EAAE,MAAM,CAAC,CAAC,CAAmD,CAAC;AAmDhI,KAAK,gCAAgC,GAAG,IAAI,CAC1C,kBAAkB,EAClB,YAAY,GAAG,UAAU,GAAG,kBAAkB,GAAG,QAAQ,GAAG,QAAQ,GAAG,YAAY,CACpF,CAAC;AA2MF;;GAEG;AACH,qBACa,kBAAmB,YAAW,yBAAyB;IAMtD,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;IAclD,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;YAyB1B,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
|
@@ -7,38 +7,37 @@ function _checkInRHS(e) { if (Object(e) !== e) throw TypeError("right-hand side
|
|
|
7
7
|
import { Inject } from '@fluojs/core';
|
|
8
8
|
import { JwtConfigurationError, JwtExpiredTokenError, JwtInvalidTokenError } from '../errors.js';
|
|
9
9
|
import { normalizeRefreshTokenOptions } from '../refresh/refresh-token.js';
|
|
10
|
+
import { SUPPORTED_ASYMMETRIC_HASH, SUPPORTED_HMAC_HASH } from './algorithm-policy.js';
|
|
10
11
|
import { JwksClient } from './jwks.js';
|
|
12
|
+
import { assertJwtKeyEntries } from './key-entries.js';
|
|
11
13
|
|
|
12
14
|
/**
|
|
13
15
|
* Provides the resolved JWT verifier options through dependency injection.
|
|
14
16
|
*/
|
|
15
17
|
export const JWT_OPTIONS = Symbol.for('fluo.jwt.options');
|
|
16
18
|
|
|
19
|
+
/**
|
|
20
|
+
* Describes the lifecycle hook called when a JWT module is destroyed.
|
|
21
|
+
*/
|
|
22
|
+
|
|
17
23
|
/**
|
|
18
24
|
* Maps supported HMAC JWT algorithms to their Node.js hash names.
|
|
19
25
|
*/
|
|
20
|
-
export const HMAC_HASH = {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
HS512: 'sha512'
|
|
24
|
-
};
|
|
26
|
+
export const HMAC_HASH = Object.freeze({
|
|
27
|
+
...SUPPORTED_HMAC_HASH
|
|
28
|
+
});
|
|
25
29
|
|
|
26
30
|
/**
|
|
27
31
|
* Maps supported asymmetric JWT algorithms to their Node.js hash names.
|
|
28
32
|
*/
|
|
29
|
-
export const ASYMMETRIC_HASH = {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
RS512: 'sha512',
|
|
33
|
-
ES256: 'sha256',
|
|
34
|
-
ES384: 'sha384',
|
|
35
|
-
ES512: 'sha512'
|
|
36
|
-
};
|
|
33
|
+
export const ASYMMETRIC_HASH = Object.freeze({
|
|
34
|
+
...SUPPORTED_ASYMMETRIC_HASH
|
|
35
|
+
});
|
|
37
36
|
function hasOwnAlgorithmMapping(mappings, alg) {
|
|
38
37
|
return typeof alg === 'string' && Object.hasOwn(mappings, alg);
|
|
39
38
|
}
|
|
40
39
|
function isSupportedAlgorithm(alg) {
|
|
41
|
-
return hasOwnAlgorithmMapping(
|
|
40
|
+
return hasOwnAlgorithmMapping(SUPPORTED_HMAC_HASH, alg) || hasOwnAlgorithmMapping(SUPPORTED_ASYMMETRIC_HASH, alg);
|
|
42
41
|
}
|
|
43
42
|
function assertJwtAlgorithms(algorithms, context) {
|
|
44
43
|
if (!Array.isArray(algorithms) || algorithms.length === 0) {
|
|
@@ -130,7 +129,7 @@ function resolveStaticPublicKey(options, keyState, kid) {
|
|
|
130
129
|
return keyState.defaultPublicKey ?? options.publicKey;
|
|
131
130
|
}
|
|
132
131
|
async function verifyHmacSignature(algorithm, secret, signingInput, signatureSegment) {
|
|
133
|
-
const hash =
|
|
132
|
+
const hash = SUPPORTED_HMAC_HASH[algorithm];
|
|
134
133
|
if (!hash) {
|
|
135
134
|
throw new JwtInvalidTokenError();
|
|
136
135
|
}
|
|
@@ -146,7 +145,7 @@ async function verifyHmacSignature(algorithm, secret, signingInput, signatureSeg
|
|
|
146
145
|
}
|
|
147
146
|
}
|
|
148
147
|
async function verifyAsymmetricSignature(algorithm, publicKey, signingInput, signatureSegment) {
|
|
149
|
-
const hash =
|
|
148
|
+
const hash = SUPPORTED_ASYMMETRIC_HASH[algorithm];
|
|
150
149
|
if (!hash) {
|
|
151
150
|
throw new JwtInvalidTokenError();
|
|
152
151
|
}
|
|
@@ -171,8 +170,15 @@ function decodeBase64Url(value) {
|
|
|
171
170
|
}
|
|
172
171
|
function parseJwtPart(value) {
|
|
173
172
|
try {
|
|
174
|
-
|
|
175
|
-
|
|
173
|
+
const parsed = JSON.parse(decodeBase64Url(value).toString('utf8'));
|
|
174
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
|
175
|
+
throw new JwtInvalidTokenError('JWT header and payload must be JSON objects.');
|
|
176
|
+
}
|
|
177
|
+
return parsed;
|
|
178
|
+
} catch (error) {
|
|
179
|
+
if (error instanceof JwtInvalidTokenError) {
|
|
180
|
+
throw error;
|
|
181
|
+
}
|
|
176
182
|
throw new JwtInvalidTokenError();
|
|
177
183
|
}
|
|
178
184
|
}
|
|
@@ -212,6 +218,7 @@ class DefaultJwtVerifier {
|
|
|
212
218
|
constructor(options) {
|
|
213
219
|
this.options = options;
|
|
214
220
|
assertJwtAlgorithms(options.algorithms, 'JWT verifier');
|
|
221
|
+
assertJwtKeyEntries(options.keys);
|
|
215
222
|
this.jwksClient = options.jwksUri ? new JwksClient(options.jwksUri, options.jwksCacheTtl, options.jwksRequestTimeoutMs, options.jwksCacheMaxEntries) : undefined;
|
|
216
223
|
this.keyResolutionState = createKeyResolutionState(options.keys);
|
|
217
224
|
this.refreshVerificationOptions = options.refreshToken ? this.createRefreshVerificationOptions(normalizeRefreshTokenOptions(options.refreshToken)) : undefined;
|
|
@@ -270,7 +277,8 @@ class DefaultJwtVerifier {
|
|
|
270
277
|
return this.verifyToken(token, this.refreshVerificationOptions, this.refreshKeyResolutionState, undefined);
|
|
271
278
|
}
|
|
272
279
|
createRefreshVerificationOptions(refreshToken) {
|
|
273
|
-
const
|
|
280
|
+
const configuredRefreshAlgorithms = refreshToken.algorithms;
|
|
281
|
+
const algorithms = (configuredRefreshAlgorithms ?? this.options.algorithms).filter(algorithm => hasOwnAlgorithmMapping(SUPPORTED_HMAC_HASH, algorithm));
|
|
274
282
|
if (algorithms.length === 0) {
|
|
275
283
|
throw new JwtConfigurationError('JWT refresh token verifier requires at least one HMAC algorithm (HS256/HS384/HS512) in the allowed algorithms list.');
|
|
276
284
|
}
|
|
@@ -308,7 +316,7 @@ class DefaultJwtVerifier {
|
|
|
308
316
|
return segments;
|
|
309
317
|
}
|
|
310
318
|
async verifyTokenSignature(header, signingInput, signatureSegment, options, keyResolutionState, jwksClient) {
|
|
311
|
-
if (hasOwnAlgorithmMapping(
|
|
319
|
+
if (hasOwnAlgorithmMapping(SUPPORTED_HMAC_HASH, header.alg)) {
|
|
312
320
|
await this.verifyHmacTokenSignature(header, signingInput, signatureSegment, options, keyResolutionState);
|
|
313
321
|
return;
|
|
314
322
|
}
|
package/dist/status.d.ts
CHANGED
|
@@ -1,12 +1,56 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Describes the readiness state exposed by JWT.
|
|
3
|
+
*/
|
|
4
|
+
export interface JwtPlatformReadinessReport {
|
|
5
|
+
critical: boolean;
|
|
6
|
+
reason?: string;
|
|
7
|
+
status: 'ready' | 'not-ready' | 'degraded';
|
|
8
|
+
checks?: JwtPlatformCheckResult[];
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Describes the health state exposed by JWT.
|
|
12
|
+
*/
|
|
13
|
+
export interface JwtPlatformHealthReport {
|
|
14
|
+
reason?: string;
|
|
15
|
+
status: 'healthy' | 'unhealthy' | 'degraded';
|
|
16
|
+
checks?: JwtPlatformCheckResult[];
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Describes one named readiness or health probe result exposed by JWT.
|
|
20
|
+
*/
|
|
21
|
+
export interface JwtPlatformCheckResult {
|
|
22
|
+
name: string;
|
|
23
|
+
status: 'pass' | 'fail' | 'degraded';
|
|
24
|
+
message?: string;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Describes ownership of JWT-managed resources.
|
|
28
|
+
*/
|
|
29
|
+
export interface JwtPlatformOwnership {
|
|
30
|
+
externallyManaged: boolean;
|
|
31
|
+
ownsResources: boolean;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Describes a JWT diagnostic issue.
|
|
35
|
+
*/
|
|
36
|
+
export interface JwtPlatformDiagnosticIssue {
|
|
37
|
+
cause?: string;
|
|
38
|
+
code: string;
|
|
39
|
+
componentId: string;
|
|
40
|
+
dependsOn?: string[];
|
|
41
|
+
docsUrl?: string;
|
|
42
|
+
fixHint?: string;
|
|
43
|
+
message: string;
|
|
44
|
+
severity: 'error' | 'warning' | 'info';
|
|
45
|
+
}
|
|
2
46
|
/**
|
|
3
47
|
* Describes the jwt platform status snapshot contract.
|
|
4
48
|
*/
|
|
5
49
|
export interface JwtPlatformStatusSnapshot {
|
|
6
|
-
readiness: PlatformReadinessReport;
|
|
7
|
-
health: PlatformHealthReport;
|
|
8
|
-
ownership: PlatformSnapshot['ownership'];
|
|
9
50
|
details: Record<string, unknown>;
|
|
51
|
+
health: JwtPlatformHealthReport;
|
|
52
|
+
ownership: JwtPlatformOwnership;
|
|
53
|
+
readiness: JwtPlatformReadinessReport;
|
|
10
54
|
}
|
|
11
55
|
/**
|
|
12
56
|
* Describes the jwt status adapter input contract.
|
|
@@ -33,5 +77,5 @@ export declare function createJwtPlatformStatusSnapshot(input: JwtStatusAdapterI
|
|
|
33
77
|
* @param input The input.
|
|
34
78
|
* @returns The create jwt platform diagnostic issues result.
|
|
35
79
|
*/
|
|
36
|
-
export declare function createJwtPlatformDiagnosticIssues(input: JwtStatusAdapterInput):
|
|
80
|
+
export declare function createJwtPlatformDiagnosticIssues(input: JwtStatusAdapterInput): JwtPlatformDiagnosticIssue[];
|
|
37
81
|
//# sourceMappingURL=status.d.ts.map
|
package/dist/status.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"status.d.ts","sourceRoot":"","sources":["../src/status.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"status.d.ts","sourceRoot":"","sources":["../src/status.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,WAAW,0BAA0B;IACzC,QAAQ,EAAE,OAAO,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,OAAO,GAAG,WAAW,GAAG,UAAU,CAAC;IAC3C,MAAM,CAAC,EAAE,sBAAsB,EAAE,CAAC;CACnC;AAED;;GAEG;AACH,MAAM,WAAW,uBAAuB;IACtC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,SAAS,GAAG,WAAW,GAAG,UAAU,CAAC;IAC7C,MAAM,CAAC,EAAE,sBAAsB,EAAE,CAAC;CACnC;AAED;;GAEG;AACH,MAAM,WAAW,sBAAsB;IACrC,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,UAAU,CAAC;IACrC,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACnC,iBAAiB,EAAE,OAAO,CAAC;IAC3B,aAAa,EAAE,OAAO,CAAC;CACxB;AAED;;GAEG;AACH,MAAM,WAAW,0BAA0B;IACzC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,OAAO,GAAG,SAAS,GAAG,MAAM,CAAC;CACxC;AAED;;GAEG;AACH,MAAM,WAAW,yBAAyB;IACxC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACjC,MAAM,EAAE,uBAAuB,CAAC;IAChC,SAAS,EAAE,oBAAoB,CAAC;IAChC,SAAS,EAAE,0BAA0B,CAAC;CACvC;AAED;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACpC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B,sBAAsB,CAAC,EAAE,OAAO,CAAC;IACjC,uBAAuB,CAAC,EAAE,MAAM,CAAC;IACjC,wBAAwB,CAAC,EAAE,MAAM,CAAC;IAClC,gBAAgB,CAAC,EAAE,eAAe,GAAG,UAAU,GAAG,MAAM,GAAG,cAAc,CAAC;CAC3E;AAwCD;;;;;GAKG;AACH,wBAAgB,+BAA+B,CAAC,KAAK,EAAE,qBAAqB,GAAG,yBAAyB,CA4CvG;AAED;;;;;GAKG;AACH,wBAAgB,iCAAiC,CAAC,KAAK,EAAE,qBAAqB,GAAG,0BAA0B,EAAE,CAqB5G"}
|
package/dist/status.js
CHANGED
|
@@ -1,3 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Describes the readiness state exposed by JWT.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Describes the health state exposed by JWT.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Describes one named readiness or health probe result exposed by JWT.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Describes ownership of JWT-managed resources.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Describes a JWT diagnostic issue.
|
|
19
|
+
*/
|
|
20
|
+
|
|
1
21
|
/**
|
|
2
22
|
* Describes the jwt platform status snapshot contract.
|
|
3
23
|
*/
|
package/package.json
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"signing",
|
|
10
10
|
"verification"
|
|
11
11
|
],
|
|
12
|
-
"version": "
|
|
12
|
+
"version": "2.0.0",
|
|
13
13
|
"private": false,
|
|
14
14
|
"license": "MIT",
|
|
15
15
|
"repository": {
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"directory": "packages/jwt"
|
|
19
19
|
},
|
|
20
20
|
"engines": {
|
|
21
|
-
"node": ">=
|
|
21
|
+
"node": ">=24.0.0 <27"
|
|
22
22
|
},
|
|
23
23
|
"publishConfig": {
|
|
24
24
|
"access": "public"
|
|
@@ -36,12 +36,12 @@
|
|
|
36
36
|
"dist"
|
|
37
37
|
],
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"@fluojs/core": "^
|
|
40
|
-
"@fluojs/di": "^
|
|
41
|
-
"@fluojs/runtime": "^2.0.1"
|
|
39
|
+
"@fluojs/core": "^2.0.0",
|
|
40
|
+
"@fluojs/di": "^3.0.0"
|
|
42
41
|
},
|
|
43
42
|
"devDependencies": {
|
|
44
|
-
"vitest": "^
|
|
43
|
+
"vitest": "^4.1.11",
|
|
44
|
+
"@fluojs/runtime": "^3.0.0"
|
|
45
45
|
},
|
|
46
46
|
"scripts": {
|
|
47
47
|
"prebuild": "node ../../tooling/scripts/clean-dist.mjs",
|