@nestjs/authentication 0.0.1
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/LICENSE +23 -0
- package/README.md +50 -0
- package/dist/account/email-verification.handler.d.ts +23 -0
- package/dist/account/email-verification.handler.js +17 -0
- package/dist/account/email-verification.service.d.ts +46 -0
- package/dist/account/email-verification.service.js +111 -0
- package/dist/account/email.util.d.ts +14 -0
- package/dist/account/email.util.js +28 -0
- package/dist/account/in-memory-email-token.store.d.ts +12 -0
- package/dist/account/in-memory-email-token.store.js +60 -0
- package/dist/account/password-reset.handler.d.ts +19 -0
- package/dist/account/password-reset.handler.js +9 -0
- package/dist/account/password-reset.service.d.ts +69 -0
- package/dist/account/password-reset.service.js +204 -0
- package/dist/authentication.constants.d.ts +13 -0
- package/dist/authentication.constants.js +13 -0
- package/dist/authentication.module-definition.d.ts +21 -0
- package/dist/authentication.module-definition.js +147 -0
- package/dist/authentication.module.d.ts +34 -0
- package/dist/authentication.module.js +139 -0
- package/dist/context/authentication-scope.service.d.ts +39 -0
- package/dist/context/authentication-scope.service.js +64 -0
- package/dist/context/authentication.context.d.ts +28 -0
- package/dist/context/authentication.context.js +58 -0
- package/dist/context/index.d.ts +2 -0
- package/dist/context/index.js +2 -0
- package/dist/decorators/authenticate.decorator.d.ts +10 -0
- package/dist/decorators/authenticate.decorator.js +18 -0
- package/dist/decorators/current-session.decorator.d.ts +2 -0
- package/dist/decorators/current-session.decorator.js +6 -0
- package/dist/decorators/current-user.decorator.d.ts +6 -0
- package/dist/decorators/current-user.decorator.js +11 -0
- package/dist/decorators/index.d.ts +4 -0
- package/dist/decorators/index.js +4 -0
- package/dist/decorators/public.decorator.d.ts +5 -0
- package/dist/decorators/public.decorator.js +6 -0
- package/dist/errors/authentication.error.d.ts +27 -0
- package/dist/errors/authentication.error.js +33 -0
- package/dist/errors/index.d.ts +7 -0
- package/dist/errors/index.js +7 -0
- package/dist/errors/jwt.error.d.ts +9 -0
- package/dist/errors/jwt.error.js +12 -0
- package/dist/errors/magic-link.error.d.ts +18 -0
- package/dist/errors/magic-link.error.js +21 -0
- package/dist/errors/mfa-already-enrolled.error.d.ts +14 -0
- package/dist/errors/mfa-already-enrolled.error.js +17 -0
- package/dist/errors/oidc.error.d.ts +15 -0
- package/dist/errors/oidc.error.js +13 -0
- package/dist/errors/refresh-token.error.d.ts +10 -0
- package/dist/errors/refresh-token.error.js +12 -0
- package/dist/errors/secret-decryption.error.d.ts +3 -0
- package/dist/errors/secret-decryption.error.js +6 -0
- package/dist/events/authentication-events.interface.d.ts +121 -0
- package/dist/events/authentication-events.interface.js +1 -0
- package/dist/events/authentication-events.service.d.ts +19 -0
- package/dist/events/authentication-events.service.js +39 -0
- package/dist/events/authentication.channels.d.ts +4 -0
- package/dist/events/authentication.channels.js +11 -0
- package/dist/events/index.d.ts +3 -0
- package/dist/events/index.js +3 -0
- package/dist/guards/authentication.guard.d.ts +57 -0
- package/dist/guards/authentication.guard.js +171 -0
- package/dist/guards/index.d.ts +1 -0
- package/dist/guards/index.js +1 -0
- package/dist/index.d.ts +52 -0
- package/dist/index.js +52 -0
- package/dist/interceptors/authentication-scope.interceptor.d.ts +20 -0
- package/dist/interceptors/authentication-scope.interceptor.js +48 -0
- package/dist/interceptors/index.d.ts +1 -0
- package/dist/interceptors/index.js +1 -0
- package/dist/interfaces/api-key.interface.d.ts +41 -0
- package/dist/interfaces/api-key.interface.js +1 -0
- package/dist/interfaces/authenticate-options.interface.d.ts +31 -0
- package/dist/interfaces/authenticate-options.interface.js +1 -0
- package/dist/interfaces/authentication-error-options.interface.d.ts +8 -0
- package/dist/interfaces/authentication-error-options.interface.js +1 -0
- package/dist/interfaces/authentication-module-options.interface.d.ts +58 -0
- package/dist/interfaces/authentication-module-options.interface.js +1 -0
- package/dist/interfaces/authentication-registry.interface.d.ts +41 -0
- package/dist/interfaces/authentication-registry.interface.js +1 -0
- package/dist/interfaces/authentication-result.interface.d.ts +25 -0
- package/dist/interfaces/authentication-result.interface.js +1 -0
- package/dist/interfaces/authentication-storage.interface.d.ts +29 -0
- package/dist/interfaces/authentication-storage.interface.js +1 -0
- package/dist/interfaces/duration.interface.d.ts +2 -0
- package/dist/interfaces/duration.interface.js +1 -0
- package/dist/interfaces/email-token-store.interface.d.ts +52 -0
- package/dist/interfaces/email-token-store.interface.js +1 -0
- package/dist/interfaces/email-verification.interface.d.ts +18 -0
- package/dist/interfaces/email-verification.interface.js +1 -0
- package/dist/interfaces/index.d.ts +27 -0
- package/dist/interfaces/index.js +27 -0
- package/dist/interfaces/jwt-options.interface.d.ts +68 -0
- package/dist/interfaces/jwt-options.interface.js +1 -0
- package/dist/interfaces/jwt.interface.d.ts +36 -0
- package/dist/interfaces/jwt.interface.js +1 -0
- package/dist/interfaces/magic-link-store.interface.d.ts +36 -0
- package/dist/interfaces/magic-link-store.interface.js +1 -0
- package/dist/interfaces/magic-link.interface.d.ts +55 -0
- package/dist/interfaces/magic-link.interface.js +1 -0
- package/dist/interfaces/mfa-options.interface.d.ts +44 -0
- package/dist/interfaces/mfa-options.interface.js +1 -0
- package/dist/interfaces/mfa-store.interface.d.ts +70 -0
- package/dist/interfaces/mfa-store.interface.js +1 -0
- package/dist/interfaces/oidc-provider.interface.d.ts +56 -0
- package/dist/interfaces/oidc-provider.interface.js +1 -0
- package/dist/interfaces/oidc-state-store.interface.d.ts +25 -0
- package/dist/interfaces/oidc-state-store.interface.js +1 -0
- package/dist/interfaces/oidc.interface.d.ts +84 -0
- package/dist/interfaces/oidc.interface.js +1 -0
- package/dist/interfaces/password-hasher-options.interface.d.ts +17 -0
- package/dist/interfaces/password-hasher-options.interface.js +1 -0
- package/dist/interfaces/password-reset.interface.d.ts +49 -0
- package/dist/interfaces/password-reset.interface.js +1 -0
- package/dist/interfaces/refresh-token-options.interface.d.ts +10 -0
- package/dist/interfaces/refresh-token-options.interface.js +1 -0
- package/dist/interfaces/refresh-token-store.interface.d.ts +61 -0
- package/dist/interfaces/refresh-token-store.interface.js +1 -0
- package/dist/interfaces/session-store.interface.d.ts +53 -0
- package/dist/interfaces/session-store.interface.js +1 -0
- package/dist/interfaces/session.interface.d.ts +50 -0
- package/dist/interfaces/session.interface.js +1 -0
- package/dist/interfaces/sign-in-options.interface.d.ts +9 -0
- package/dist/interfaces/sign-in-options.interface.js +1 -0
- package/dist/interfaces/token.interface.d.ts +30 -0
- package/dist/interfaces/token.interface.js +1 -0
- package/dist/jwt/amr.util.d.ts +9 -0
- package/dist/jwt/amr.util.js +11 -0
- package/dist/jwt/in-memory-refresh-token.store.d.ts +29 -0
- package/dist/jwt/in-memory-refresh-token.store.js +67 -0
- package/dist/jwt/jwks.client.d.ts +34 -0
- package/dist/jwt/jwks.client.js +154 -0
- package/dist/jwt/jws.util.d.ts +45 -0
- package/dist/jwt/jws.util.js +282 -0
- package/dist/jwt/jwt-bearer.provider.d.ts +48 -0
- package/dist/jwt/jwt-bearer.provider.js +126 -0
- package/dist/jwt/jwt-signer.service.d.ts +19 -0
- package/dist/jwt/jwt-signer.service.js +36 -0
- package/dist/jwt/jwt-verifier.service.d.ts +22 -0
- package/dist/jwt/jwt-verifier.service.js +109 -0
- package/dist/jwt/token.service.d.ts +81 -0
- package/dist/jwt/token.service.js +230 -0
- package/dist/magic-link/in-memory-magic-link.store.d.ts +12 -0
- package/dist/magic-link/in-memory-magic-link.store.js +45 -0
- package/dist/magic-link/magic-link.handler.d.ts +25 -0
- package/dist/magic-link/magic-link.handler.js +14 -0
- package/dist/magic-link/magic-link.service.d.ts +79 -0
- package/dist/magic-link/magic-link.service.js +215 -0
- package/dist/mfa/in-memory-mfa.store.d.ts +25 -0
- package/dist/mfa/in-memory-mfa.store.js +73 -0
- package/dist/mfa/mfa.service.d.ts +104 -0
- package/dist/mfa/mfa.service.js +346 -0
- package/dist/mfa/otp.util.d.ts +7 -0
- package/dist/mfa/otp.util.js +52 -0
- package/dist/mfa/secret-cipher.service.d.ts +28 -0
- package/dist/mfa/secret-cipher.service.js +106 -0
- package/dist/oidc/in-memory-oidc-state.store.d.ts +13 -0
- package/dist/oidc/in-memory-oidc-state.store.js +45 -0
- package/dist/oidc/oidc-account.resolver.d.ts +23 -0
- package/dist/oidc/oidc-account.resolver.js +16 -0
- package/dist/oidc/oidc.client.d.ts +44 -0
- package/dist/oidc/oidc.client.js +286 -0
- package/dist/oidc/oidc.presets.d.ts +24 -0
- package/dist/oidc/oidc.presets.js +53 -0
- package/dist/oidc/oidc.service.d.ts +88 -0
- package/dist/oidc/oidc.service.js +259 -0
- package/dist/providers/api-key.provider.d.ts +51 -0
- package/dist/providers/api-key.provider.js +93 -0
- package/dist/providers/authentication.provider.d.ts +40 -0
- package/dist/providers/authentication.provider.js +33 -0
- package/dist/providers/index.d.ts +2 -0
- package/dist/providers/index.js +2 -0
- package/dist/services/authentication-registry.service.d.ts +75 -0
- package/dist/services/authentication-registry.service.js +235 -0
- package/dist/services/index.d.ts +3 -0
- package/dist/services/index.js +3 -0
- package/dist/services/password-hasher.service.d.ts +47 -0
- package/dist/services/password-hasher.service.js +170 -0
- package/dist/services/ws-authenticator.service.d.ts +38 -0
- package/dist/services/ws-authenticator.service.js +92 -0
- package/dist/session/cookies.util.d.ts +34 -0
- package/dist/session/cookies.util.js +82 -0
- package/dist/session/in-memory-session.store.d.ts +33 -0
- package/dist/session/in-memory-session.store.js +60 -0
- package/dist/session/session-cookie.provider.d.ts +36 -0
- package/dist/session/session-cookie.provider.js +58 -0
- package/dist/session/session.service.d.ts +97 -0
- package/dist/session/session.service.js +208 -0
- package/dist/session/sign-in.service.d.ts +87 -0
- package/dist/session/sign-in.service.js +206 -0
- package/dist/storage/authentication.storage.d.ts +130 -0
- package/dist/storage/authentication.storage.js +350 -0
- package/dist/storage/index.d.ts +1 -0
- package/dist/storage/index.js +1 -0
- package/dist/testing/index.d.ts +39 -0
- package/dist/testing/index.js +531 -0
- package/dist/utils/api-key.util.d.ts +10 -0
- package/dist/utils/api-key.util.js +10 -0
- package/dist/utils/auth-state.util.d.ts +54 -0
- package/dist/utils/auth-state.util.js +209 -0
- package/dist/utils/bearer.util.d.ts +9 -0
- package/dist/utils/bearer.util.js +21 -0
- package/dist/utils/cross-origin.util.d.ts +29 -0
- package/dist/utils/cross-origin.util.js +90 -0
- package/dist/utils/crypto.util.d.ts +19 -0
- package/dist/utils/crypto.util.js +38 -0
- package/dist/utils/duration.util.d.ts +9 -0
- package/dist/utils/duration.util.js +44 -0
- package/dist/utils/index.d.ts +8 -0
- package/dist/utils/index.js +8 -0
- package/dist/utils/options.util.d.ts +20 -0
- package/dist/utils/options.util.js +44 -0
- package/dist/utils/transport-errors.util.d.ts +33 -0
- package/dist/utils/transport-errors.util.js +70 -0
- package/package.json +108 -0
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { createPublicKey } from 'node:crypto';
|
|
2
|
+
import { durationOr } from '../utils/duration.util.js';
|
|
3
|
+
import { JwtError } from '../errors/jwt.error.js';
|
|
4
|
+
import { ASYMMETRIC_ALGORITHMS } from './jws.util.js';
|
|
5
|
+
const LOOPBACK = new Set(['localhost', '127.0.0.1', '[::1]']);
|
|
6
|
+
function algorithmsFor(jwk) {
|
|
7
|
+
if (jwk.kty === 'RSA') {
|
|
8
|
+
return ['RS256'];
|
|
9
|
+
}
|
|
10
|
+
if (jwk.kty === 'EC' && jwk.crv === 'P-256') {
|
|
11
|
+
return ['ES256'];
|
|
12
|
+
}
|
|
13
|
+
if (jwk.kty === 'OKP' && jwk.crv === 'Ed25519') {
|
|
14
|
+
return ['EdDSA'];
|
|
15
|
+
}
|
|
16
|
+
return [];
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* A cached JSON Web Key Set, shared by bearer-token verification and OIDC
|
|
20
|
+
* ID tokens.
|
|
21
|
+
*
|
|
22
|
+
* - Keys are cached for `cacheTtl`; an unknown `kid` triggers a refetch
|
|
23
|
+
* (key rotation), at most once per `cooldown`.
|
|
24
|
+
* - Concurrent refreshes share one request.
|
|
25
|
+
* - A failed refresh keeps serving the previous keys. With no keys to
|
|
26
|
+
* serve, it is an outage (a plain `Error`, so a 500, never a 401 that
|
|
27
|
+
* would make clients drop valid tokens), retried at most once per
|
|
28
|
+
* `cooldown`.
|
|
29
|
+
* - Keys marked `use` ≠ `sig`, with `key_ops` lacking `verify`, of
|
|
30
|
+
* unsupported types, or whose `alg` disagrees with the token are ignored.
|
|
31
|
+
*/
|
|
32
|
+
export class JwksClient {
|
|
33
|
+
constructor(uri, options = {}) {
|
|
34
|
+
this.uri = uri;
|
|
35
|
+
this.keys = [];
|
|
36
|
+
this.fetchedAt = -Infinity;
|
|
37
|
+
this.attemptedAt = -Infinity;
|
|
38
|
+
/** Number of network fetches, for tests and metrics. */
|
|
39
|
+
this.fetchCount = 0;
|
|
40
|
+
let url;
|
|
41
|
+
try {
|
|
42
|
+
url = new URL(uri);
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
url = undefined;
|
|
46
|
+
}
|
|
47
|
+
// Whoever serves the key set decides which tokens verify: never over plain http, but on loopback.
|
|
48
|
+
if (!url || (url.protocol !== 'https:' && !(url.protocol === 'http:' && LOOPBACK.has(url.hostname)))) {
|
|
49
|
+
throw new TypeError(`JwksClient: the key set must be an https URL (http only on loopback), got ${JSON.stringify(uri)}.`);
|
|
50
|
+
}
|
|
51
|
+
this.options = {
|
|
52
|
+
...options,
|
|
53
|
+
cacheTtl: durationOr(options.cacheTtl, '10m'),
|
|
54
|
+
cooldown: durationOr(options.cooldown, '30s'),
|
|
55
|
+
timeout: durationOr(options.timeout, '5s'),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
async getKey(header) {
|
|
59
|
+
const alg = header.alg;
|
|
60
|
+
if (!ASYMMETRIC_ALGORITHMS.includes(alg)) {
|
|
61
|
+
throw new JwtError('unsupported algorithm');
|
|
62
|
+
}
|
|
63
|
+
if (this.now() - this.fetchedAt >= this.options.cacheTtl) {
|
|
64
|
+
await this.refresh(true);
|
|
65
|
+
}
|
|
66
|
+
let found = this.find(header.kid, alg);
|
|
67
|
+
if (!found && this.now() - this.attemptedAt >= this.options.cooldown) {
|
|
68
|
+
await this.refresh(false);
|
|
69
|
+
found = this.find(header.kid, alg);
|
|
70
|
+
}
|
|
71
|
+
if (!found) {
|
|
72
|
+
throw new JwtError('no matching key');
|
|
73
|
+
}
|
|
74
|
+
return found;
|
|
75
|
+
}
|
|
76
|
+
find(kid, alg) {
|
|
77
|
+
const candidates = this.keys.filter((k) => k.algorithms.includes(alg) && (!k.alg || k.alg === alg));
|
|
78
|
+
if (kid !== undefined) {
|
|
79
|
+
return candidates.find((k) => k.kid === kid)?.key;
|
|
80
|
+
}
|
|
81
|
+
// No kid: only unambiguous sets are usable.
|
|
82
|
+
return candidates.length === 1 ? candidates[0].key : undefined;
|
|
83
|
+
}
|
|
84
|
+
refresh(expired) {
|
|
85
|
+
this.inflight ??= this.load(expired).finally(() => (this.inflight = undefined));
|
|
86
|
+
return this.inflight;
|
|
87
|
+
}
|
|
88
|
+
async load(expired) {
|
|
89
|
+
// Nothing to serve and the last attempt failed recently: do not hammer a provider that is down.
|
|
90
|
+
if (this.failure && this.keys.length === 0 && this.now() - this.attemptedAt < this.options.cooldown) {
|
|
91
|
+
throw this.failure;
|
|
92
|
+
}
|
|
93
|
+
this.attemptedAt = this.now();
|
|
94
|
+
try {
|
|
95
|
+
this.fetchCount++;
|
|
96
|
+
const response = await (this.options.fetch ?? fetch)(this.uri, {
|
|
97
|
+
headers: { accept: 'application/json' },
|
|
98
|
+
signal: AbortSignal.timeout(this.options.timeout),
|
|
99
|
+
redirect: 'error',
|
|
100
|
+
});
|
|
101
|
+
if (!response.ok) {
|
|
102
|
+
throw new Error(`it answered ${response.status}`);
|
|
103
|
+
}
|
|
104
|
+
const body = (await response.json());
|
|
105
|
+
if (!Array.isArray(body.keys)) {
|
|
106
|
+
throw new Error('it has no keys array');
|
|
107
|
+
}
|
|
108
|
+
// Each entry on its own: one the client cannot read (null, `key_ops` that is no list) is skipped,
|
|
109
|
+
// and the set's other keys still load.
|
|
110
|
+
this.keys = body.keys.flatMap((entry) => {
|
|
111
|
+
try {
|
|
112
|
+
if (typeof entry !== 'object' || entry === null) {
|
|
113
|
+
return [];
|
|
114
|
+
}
|
|
115
|
+
const jwk = entry;
|
|
116
|
+
if (jwk.use !== undefined && jwk.use !== 'sig') {
|
|
117
|
+
return [];
|
|
118
|
+
}
|
|
119
|
+
if (jwk.key_ops !== undefined && !(Array.isArray(jwk.key_ops) && jwk.key_ops.includes('verify'))) {
|
|
120
|
+
return [];
|
|
121
|
+
}
|
|
122
|
+
const algorithms = algorithmsFor(jwk);
|
|
123
|
+
if (algorithms.length === 0) {
|
|
124
|
+
return [];
|
|
125
|
+
}
|
|
126
|
+
// Only the public members are read; a JWKS that leaks `d` still yields a public key.
|
|
127
|
+
const { d: _d, p: _p, q: _q, dp: _dp, dq: _dq, qi: _qi, ...publicJwk } = jwk;
|
|
128
|
+
const key = createPublicKey({ key: publicJwk, format: 'jwk' });
|
|
129
|
+
const kid = typeof jwk.kid === 'string' ? jwk.kid : undefined;
|
|
130
|
+
const alg = typeof jwk.alg === 'string' ? jwk.alg : undefined;
|
|
131
|
+
return [{ kid, alg, algorithms, key }];
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
return [];
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
this.fetchedAt = this.now();
|
|
138
|
+
this.failure = undefined;
|
|
139
|
+
}
|
|
140
|
+
catch (error) {
|
|
141
|
+
// Keep serving the last good set; fail only if there is none.
|
|
142
|
+
if (this.keys.length === 0) {
|
|
143
|
+
this.failure = new Error(`JWKS ${this.uri} is unavailable: ${error.message}`, { cause: error });
|
|
144
|
+
throw this.failure;
|
|
145
|
+
}
|
|
146
|
+
if (expired) {
|
|
147
|
+
this.fetchedAt = this.now() - this.options.cacheTtl + this.options.cooldown;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
now() {
|
|
152
|
+
return this.options.now?.() ?? Date.now();
|
|
153
|
+
}
|
|
154
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { KeyObject } from 'node:crypto';
|
|
2
|
+
import type { JwsAlgorithm, JwtClaims, ClaimRules } from '../interfaces/jwt.interface.js';
|
|
3
|
+
export declare const ASYMMETRIC_ALGORITHMS: readonly JwsAlgorithm[];
|
|
4
|
+
export interface JwsHeader {
|
|
5
|
+
alg: string;
|
|
6
|
+
kid?: string;
|
|
7
|
+
typ?: string;
|
|
8
|
+
crit?: unknown;
|
|
9
|
+
[key: string]: unknown;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* A key from configuration. PEM text (a string or a `Buffer`: SPKI, PKCS#1,
|
|
13
|
+
* PKCS#8, SEC1, or an X.509 certificate) is read as the key it encodes;
|
|
14
|
+
* only other text is an HMAC secret. Read as a secret, a PEM public key
|
|
15
|
+
* would let anyone who has it, which is everyone, sign HS256 tokens. So
|
|
16
|
+
* would a JWK, or a key in DER (bytes, or base64 of them), which are refused:
|
|
17
|
+
* import it with `createPublicKey({ key, format: 'jwk' })` (or `'der'`, or
|
|
18
|
+
* `createPrivateKey`) and pass the `KeyObject`.
|
|
19
|
+
*/
|
|
20
|
+
export declare function toKeyObject(key: KeyObject | string | Buffer): KeyObject;
|
|
21
|
+
/** The algorithm a key implies: HS256 for a secret, the key's own for asymmetric keys. */
|
|
22
|
+
export declare function algorithmFor(key: KeyObject): JwsAlgorithm | undefined;
|
|
23
|
+
/**
|
|
24
|
+
* Whether `key` is appropriate for `alg`. This is the algorithm/key binding
|
|
25
|
+
* that stops key-confusion attacks: an RSA public key can never be used as
|
|
26
|
+
* an HMAC secret, a P-384 key never verifies ES256, and so on.
|
|
27
|
+
*/
|
|
28
|
+
export declare function keyFits(alg: JwsAlgorithm, key: KeyObject): boolean;
|
|
29
|
+
/** Throws a `TypeError` (configuration, not a token) unless `key` can serve `alg`. */
|
|
30
|
+
export declare function assertKeyFits(alg: JwsAlgorithm, key: KeyObject, owner: string): void;
|
|
31
|
+
/** Throws a `TypeError` (configuration, not a token) unless `key` can sign `alg`. */
|
|
32
|
+
export declare function assertSigningKey(alg: JwsAlgorithm, key: KeyObject, owner: string): void;
|
|
33
|
+
export declare function signJws(header: JwsHeader & {
|
|
34
|
+
alg: JwsAlgorithm;
|
|
35
|
+
}, payload: object, key: KeyObject): string;
|
|
36
|
+
export interface DecodedJws {
|
|
37
|
+
header: JwsHeader;
|
|
38
|
+
payload: JwtClaims;
|
|
39
|
+
signingInput: Buffer;
|
|
40
|
+
signature: Buffer;
|
|
41
|
+
}
|
|
42
|
+
/** Splits and decodes without verifying. */
|
|
43
|
+
export declare function decodeJws(token: string): DecodedJws;
|
|
44
|
+
export declare function verifySignature(alg: JwsAlgorithm, key: KeyObject, decoded: DecodedJws): void;
|
|
45
|
+
export declare function validateClaims(claims: JwtClaims, rules: ClaimRules): void;
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import { KeyObject, X509Certificate, createHmac, createPrivateKey, createPublicKey, createSecretKey, sign as cryptoSign, verify as cryptoVerify, timingSafeEqual, } from 'node:crypto';
|
|
2
|
+
import { toMs } from '../utils/duration.util.js';
|
|
3
|
+
import { JwtError } from '../errors/jwt.error.js';
|
|
4
|
+
export const ASYMMETRIC_ALGORITHMS = ['RS256', 'ES256', 'EdDSA'];
|
|
5
|
+
const SEGMENT = /^[A-Za-z0-9_-]+$/;
|
|
6
|
+
const PEM = /-----BEGIN ([A-Z0-9 ]+)-----/g;
|
|
7
|
+
/**
|
|
8
|
+
* A key from configuration. PEM text (a string or a `Buffer`: SPKI, PKCS#1,
|
|
9
|
+
* PKCS#8, SEC1, or an X.509 certificate) is read as the key it encodes;
|
|
10
|
+
* only other text is an HMAC secret. Read as a secret, a PEM public key
|
|
11
|
+
* would let anyone who has it, which is everyone, sign HS256 tokens. So
|
|
12
|
+
* would a JWK, or a key in DER (bytes, or base64 of them), which are refused:
|
|
13
|
+
* import it with `createPublicKey({ key, format: 'jwk' })` (or `'der'`, or
|
|
14
|
+
* `createPrivateKey`) and pass the `KeyObject`.
|
|
15
|
+
*/
|
|
16
|
+
export function toKeyObject(key) {
|
|
17
|
+
if (key instanceof KeyObject) {
|
|
18
|
+
return key;
|
|
19
|
+
}
|
|
20
|
+
const text = typeof key === 'string' ? key : key.toString('latin1');
|
|
21
|
+
// Every block's label: `openssl ecparam -genkey` puts EC PARAMETERS before the private key, and
|
|
22
|
+
// a bundle may put a certificate first.
|
|
23
|
+
const labels = [...text.matchAll(PEM)].map((match) => match[1]);
|
|
24
|
+
if (labels.length === 0) {
|
|
25
|
+
if (isJwk(text)) {
|
|
26
|
+
throw new TypeError('The key is a JWK, which would be read as an HS256 secret. Import it and pass the KeyObject: ' +
|
|
27
|
+
"`createPublicKey({ key: JSON.parse(jwk), format: 'jwk' })`, or `createPrivateKey` for a private key.");
|
|
28
|
+
}
|
|
29
|
+
if (isEncodedKey(key)) {
|
|
30
|
+
throw new TypeError('The key is DER (binary, or base64 of it, as Keycloak shows `public_key`), which would be read as an ' +
|
|
31
|
+
"HS256 secret. Pass PEM text, or the KeyObject: `createPublicKey({ key, format: 'der', type: 'spki' })`.");
|
|
32
|
+
}
|
|
33
|
+
return createSecretKey(typeof key === 'string' ? Buffer.from(key) : key);
|
|
34
|
+
}
|
|
35
|
+
const label = labels.find((name) => name.includes('PRIVATE KEY')) ?? labels[0];
|
|
36
|
+
try {
|
|
37
|
+
return label.includes('PRIVATE KEY') ? createPrivateKey(key) : createPublicKey(key);
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
throw new TypeError(`Cannot read the PEM ${label.toLowerCase()}: ${error.message}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/** The algorithm a key implies: HS256 for a secret, the key's own for asymmetric keys. */
|
|
44
|
+
export function algorithmFor(key) {
|
|
45
|
+
if (key.type === 'secret') {
|
|
46
|
+
return 'HS256';
|
|
47
|
+
}
|
|
48
|
+
switch (key.asymmetricKeyType) {
|
|
49
|
+
case 'rsa':
|
|
50
|
+
return 'RS256';
|
|
51
|
+
case 'ec':
|
|
52
|
+
return 'ES256';
|
|
53
|
+
case 'ed25519':
|
|
54
|
+
return 'EdDSA';
|
|
55
|
+
default:
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Whether `key` is appropriate for `alg`. This is the algorithm/key binding
|
|
61
|
+
* that stops key-confusion attacks: an RSA public key can never be used as
|
|
62
|
+
* an HMAC secret, a P-384 key never verifies ES256, and so on.
|
|
63
|
+
*/
|
|
64
|
+
export function keyFits(alg, key) {
|
|
65
|
+
const details = key.asymmetricKeyDetails;
|
|
66
|
+
switch (alg) {
|
|
67
|
+
case 'HS256':
|
|
68
|
+
return key.type === 'secret' && (key.symmetricKeySize ?? 0) >= 32;
|
|
69
|
+
case 'RS256':
|
|
70
|
+
return key.asymmetricKeyType === 'rsa' && (details?.modulusLength ?? 0) >= 2048;
|
|
71
|
+
case 'ES256':
|
|
72
|
+
return key.asymmetricKeyType === 'ec' && details?.namedCurve === 'prime256v1';
|
|
73
|
+
case 'EdDSA':
|
|
74
|
+
return key.asymmetricKeyType === 'ed25519';
|
|
75
|
+
default:
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
const REQUIREMENTS = {
|
|
80
|
+
HS256: 'a secret of at least 32 bytes (`openssl rand -base64 32`)',
|
|
81
|
+
RS256: 'an RSA key of at least 2048 bits',
|
|
82
|
+
ES256: 'a P-256 key',
|
|
83
|
+
EdDSA: 'an Ed25519 key',
|
|
84
|
+
};
|
|
85
|
+
/** Throws a `TypeError` (configuration, not a token) unless `key` can serve `alg`. */
|
|
86
|
+
export function assertKeyFits(alg, key, owner) {
|
|
87
|
+
if (!keyFits(alg, key)) {
|
|
88
|
+
throw new TypeError(`${owner}: the key is not a valid ${alg} key; ${alg} needs ${REQUIREMENTS[alg]}.`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/** Throws a `TypeError` (configuration, not a token) unless `key` can sign `alg`. */
|
|
92
|
+
export function assertSigningKey(alg, key, owner) {
|
|
93
|
+
assertKeyFits(alg, key, owner);
|
|
94
|
+
if (alg !== 'HS256' && key.type !== 'private') {
|
|
95
|
+
throw new TypeError(`${owner}: ${alg} signing needs a private key.`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
export function signJws(header, payload, key) {
|
|
99
|
+
assertSigningKey(header.alg, key, 'JwtSigner');
|
|
100
|
+
const input = `${encode(header)}.${encode(payload)}`;
|
|
101
|
+
const data = Buffer.from(input);
|
|
102
|
+
let signature;
|
|
103
|
+
switch (header.alg) {
|
|
104
|
+
case 'HS256':
|
|
105
|
+
signature = createHmac('sha256', key).update(data).digest();
|
|
106
|
+
break;
|
|
107
|
+
case 'RS256':
|
|
108
|
+
signature = cryptoSign('sha256', data, key);
|
|
109
|
+
break;
|
|
110
|
+
case 'ES256':
|
|
111
|
+
signature = cryptoSign('sha256', data, { key, dsaEncoding: 'ieee-p1363' });
|
|
112
|
+
break;
|
|
113
|
+
case 'EdDSA':
|
|
114
|
+
signature = cryptoSign(null, data, key);
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
return `${input}.${signature.toString('base64url')}`;
|
|
118
|
+
}
|
|
119
|
+
/** Splits and decodes without verifying. */
|
|
120
|
+
export function decodeJws(token) {
|
|
121
|
+
const parts = token.split('.');
|
|
122
|
+
if (parts.length !== 3 || !parts.every((p) => SEGMENT.test(p))) {
|
|
123
|
+
throw new JwtError('malformed token');
|
|
124
|
+
}
|
|
125
|
+
const header = decodeJson(parts[0]);
|
|
126
|
+
if (typeof header.alg !== 'string') {
|
|
127
|
+
throw new JwtError('malformed token');
|
|
128
|
+
}
|
|
129
|
+
// One signature, one spelling: the unused low bits of the last character would otherwise give
|
|
130
|
+
// several token strings for one token, and a list or a cache keyed by the string would miss them.
|
|
131
|
+
const signature = Buffer.from(parts[2], 'base64url');
|
|
132
|
+
if (signature.toString('base64url') !== parts[2]) {
|
|
133
|
+
throw new JwtError('malformed token');
|
|
134
|
+
}
|
|
135
|
+
return {
|
|
136
|
+
header,
|
|
137
|
+
payload: decodeJson(parts[1]),
|
|
138
|
+
signingInput: Buffer.from(`${parts[0]}.${parts[1]}`),
|
|
139
|
+
signature,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
export function verifySignature(alg, key, decoded) {
|
|
143
|
+
const publicKey = key.type === 'private' ? createPublicKey(key) : key;
|
|
144
|
+
if (!keyFits(alg, publicKey)) {
|
|
145
|
+
throw new JwtError(`key is not a valid ${alg} key`);
|
|
146
|
+
}
|
|
147
|
+
const { signingInput, signature } = decoded;
|
|
148
|
+
let valid;
|
|
149
|
+
switch (alg) {
|
|
150
|
+
case 'HS256': {
|
|
151
|
+
const expected = createHmac('sha256', publicKey).update(signingInput).digest();
|
|
152
|
+
valid = signature.length === expected.length && timingSafeEqual(signature, expected);
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
155
|
+
case 'RS256':
|
|
156
|
+
valid = cryptoVerify('sha256', signingInput, publicKey, signature);
|
|
157
|
+
break;
|
|
158
|
+
case 'ES256':
|
|
159
|
+
// JWS uses the raw r||s form (RFC 7518 §3.4), not DER.
|
|
160
|
+
valid =
|
|
161
|
+
signature.length === 64 &&
|
|
162
|
+
cryptoVerify('sha256', signingInput, { key: publicKey, dsaEncoding: 'ieee-p1363' }, signature);
|
|
163
|
+
break;
|
|
164
|
+
case 'EdDSA':
|
|
165
|
+
valid = signature.length === 64 && cryptoVerify(null, signingInput, publicKey, signature);
|
|
166
|
+
break;
|
|
167
|
+
default:
|
|
168
|
+
valid = false;
|
|
169
|
+
}
|
|
170
|
+
if (!valid) {
|
|
171
|
+
throw new JwtError('invalid signature');
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
export function validateClaims(claims, rules) {
|
|
175
|
+
// JWT times are seconds (RFC 7519 §2, NumericDate).
|
|
176
|
+
const now = Math.floor((rules.now?.() ?? Date.now()) / 1000);
|
|
177
|
+
const leeway = toMs(rules.clockTolerance ?? 0) / 1000;
|
|
178
|
+
for (const name of rules.required ?? ['exp']) {
|
|
179
|
+
if (claims[name] === undefined) {
|
|
180
|
+
throw new JwtError(`missing ${name}`);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
for (const name of ['exp', 'nbf', 'iat']) {
|
|
184
|
+
if (claims[name] !== undefined && (typeof claims[name] !== 'number' || !Number.isFinite(claims[name]))) {
|
|
185
|
+
throw new JwtError(`invalid ${name}`);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
// What `JwtClaims` promises `validate()`.
|
|
189
|
+
for (const name of ['iss', 'sub', 'jti']) {
|
|
190
|
+
if (claims[name] !== undefined && typeof claims[name] !== 'string') {
|
|
191
|
+
throw new JwtError(`invalid ${name}`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
if (claims.exp !== undefined && now >= claims.exp + leeway) {
|
|
195
|
+
throw new JwtError('token expired');
|
|
196
|
+
}
|
|
197
|
+
if (claims.nbf !== undefined && now < claims.nbf - leeway) {
|
|
198
|
+
throw new JwtError('token not yet valid');
|
|
199
|
+
}
|
|
200
|
+
if (claims.iat !== undefined && claims.iat > now + leeway) {
|
|
201
|
+
throw new JwtError('token issued in the future');
|
|
202
|
+
}
|
|
203
|
+
if (rules.maxAge !== undefined) {
|
|
204
|
+
if (claims.iat === undefined) {
|
|
205
|
+
throw new JwtError('missing iat');
|
|
206
|
+
}
|
|
207
|
+
if (now - claims.iat > toMs(rules.maxAge) / 1000 + leeway) {
|
|
208
|
+
throw new JwtError('token too old');
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
if (rules.issuer !== undefined && claims.iss !== rules.issuer) {
|
|
212
|
+
throw new JwtError('unexpected issuer');
|
|
213
|
+
}
|
|
214
|
+
if (rules.audience !== undefined && rules.audience !== false) {
|
|
215
|
+
const accepted = [].concat(rules.audience);
|
|
216
|
+
const given = [].concat(claims.aud ?? []);
|
|
217
|
+
if (!given.some((aud) => typeof aud === 'string' && accepted.includes(aud))) {
|
|
218
|
+
throw new JwtError('unexpected audience');
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Whether the bytes are a key or a certificate in DER, given as bytes or as
|
|
224
|
+
* base64 text: never a secret (random bytes do not parse as DER), and what a
|
|
225
|
+
* key management service's `GetPublicKey` or Keycloak's `public_key` gives.
|
|
226
|
+
*/
|
|
227
|
+
function isEncodedKey(key) {
|
|
228
|
+
const candidates = [];
|
|
229
|
+
if (Buffer.isBuffer(key)) {
|
|
230
|
+
candidates.push(key);
|
|
231
|
+
}
|
|
232
|
+
const compact = (typeof key === 'string' ? key : key.toString('latin1')).replace(/\s+/g, '');
|
|
233
|
+
if (/^[A-Za-z0-9+/_-]{40,}={0,2}$/.test(compact)) {
|
|
234
|
+
candidates.push(Buffer.from(compact, 'base64'));
|
|
235
|
+
}
|
|
236
|
+
const readers = [
|
|
237
|
+
(der) => createPublicKey({ key: der, format: 'der', type: 'spki' }),
|
|
238
|
+
(der) => createPublicKey({ key: der, format: 'der', type: 'pkcs1' }),
|
|
239
|
+
(der) => createPrivateKey({ key: der, format: 'der', type: 'pkcs8' }),
|
|
240
|
+
(der) => createPrivateKey({ key: der, format: 'der', type: 'pkcs1' }),
|
|
241
|
+
(der) => createPrivateKey({ key: der, format: 'der', type: 'sec1' }),
|
|
242
|
+
(der) => new X509Certificate(der),
|
|
243
|
+
];
|
|
244
|
+
return candidates.some((der) => readers.some((read) => {
|
|
245
|
+
try {
|
|
246
|
+
read(der);
|
|
247
|
+
return true;
|
|
248
|
+
}
|
|
249
|
+
catch {
|
|
250
|
+
return false;
|
|
251
|
+
}
|
|
252
|
+
}));
|
|
253
|
+
}
|
|
254
|
+
/** A JSON Web Key (a JSON object with `kty`), as a JWKS or an environment variable holds one. */
|
|
255
|
+
function isJwk(text) {
|
|
256
|
+
if (!text.trimStart().startsWith('{')) {
|
|
257
|
+
return false;
|
|
258
|
+
}
|
|
259
|
+
try {
|
|
260
|
+
const value = JSON.parse(text);
|
|
261
|
+
return typeof value === 'object' && value !== null && typeof value.kty === 'string';
|
|
262
|
+
}
|
|
263
|
+
catch {
|
|
264
|
+
return false;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
function encode(value) {
|
|
268
|
+
return Buffer.from(JSON.stringify(value)).toString('base64url');
|
|
269
|
+
}
|
|
270
|
+
function decodeJson(segment) {
|
|
271
|
+
let value;
|
|
272
|
+
try {
|
|
273
|
+
value = JSON.parse(Buffer.from(segment, 'base64url').toString('utf8'));
|
|
274
|
+
}
|
|
275
|
+
catch {
|
|
276
|
+
throw new JwtError('malformed token');
|
|
277
|
+
}
|
|
278
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
279
|
+
throw new JwtError('malformed token');
|
|
280
|
+
}
|
|
281
|
+
return value;
|
|
282
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { type ExecutionContext } from '@nestjs/common';
|
|
2
|
+
import { AuthenticationProvider, PROVIDER_INIT, type ProviderInit } from '../providers/authentication.provider.js';
|
|
3
|
+
import type { AuthenticationResult, MfaState } from '../interfaces/authentication-result.interface.js';
|
|
4
|
+
import type { JwtClaims } from '../interfaces/jwt.interface.js';
|
|
5
|
+
import type { JwtBearerProviderOptions } from '../interfaces/jwt-options.interface.js';
|
|
6
|
+
/**
|
|
7
|
+
* Base class for `Authorization: Bearer <jwt>` authentication. Extend it,
|
|
8
|
+
* inject what `validate()` needs, and load the user:
|
|
9
|
+
*
|
|
10
|
+
* ```ts
|
|
11
|
+
* @Injectable()
|
|
12
|
+
* export class JwtAuth extends JwtBearerProvider<User> {
|
|
13
|
+
* constructor(private readonly users: UsersRepository) {
|
|
14
|
+
* super(); // verifies the tokens the module's `accessToken` option signs
|
|
15
|
+
* }
|
|
16
|
+
* validate(claims: JwtClaims) {
|
|
17
|
+
* return this.users.findById(claims.sub!);
|
|
18
|
+
* }
|
|
19
|
+
* }
|
|
20
|
+
* ```
|
|
21
|
+
*
|
|
22
|
+
* Without `key` or `jwks`, it verifies what the module's `accessToken`
|
|
23
|
+
* option issues: the same key, algorithm, issuer, audience and `typ`. For
|
|
24
|
+
* tokens from another issuer, pass them: `super({ jwks, issuer, audience })`
|
|
25
|
+
* (a `jwks` needs both `issuer` and `audience`).
|
|
26
|
+
*
|
|
27
|
+
* Over socket.io it also reads `handshake.auth.token`. A token shaped like
|
|
28
|
+
* an API key (`ApiKeyProvider`) is left to the other providers. `session`
|
|
29
|
+
* is the verified claim set; an `amr` claim containing `mfa`, `otp` or
|
|
30
|
+
* `hwk` marks the result as MFA-verified.
|
|
31
|
+
*/
|
|
32
|
+
export declare abstract class JwtBearerProvider<TUser> extends AuthenticationProvider<TUser, JwtClaims> {
|
|
33
|
+
private readonly options;
|
|
34
|
+
private moduleOptions?;
|
|
35
|
+
private verifier?;
|
|
36
|
+
private readonly realm;
|
|
37
|
+
constructor(options?: JwtBearerProviderOptions);
|
|
38
|
+
/** Maps verified claims to the user. `null` (a deleted user) rejects the token. */
|
|
39
|
+
protected abstract validate(claims: JwtClaims): TUser | null | undefined | Promise<TUser | null | undefined>;
|
|
40
|
+
authenticate(context: ExecutionContext): Promise<AuthenticationResult<TUser, JwtClaims> | null>;
|
|
41
|
+
challenge(): string;
|
|
42
|
+
protected mfaState(claims: JwtClaims): MfaState | undefined;
|
|
43
|
+
/** @internal */
|
|
44
|
+
[PROVIDER_INIT](resolve: Parameters<ProviderInit>[0]): void;
|
|
45
|
+
private getVerifier;
|
|
46
|
+
private extract;
|
|
47
|
+
private reject;
|
|
48
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
8
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
9
|
+
};
|
|
10
|
+
import { Inject, Optional } from '@nestjs/common';
|
|
11
|
+
import { AuthenticationProvider, PROVIDER_INIT } from '../providers/authentication.provider.js';
|
|
12
|
+
import { AUTHENTICATION_MODULE_OPTIONS } from '../authentication.constants.js';
|
|
13
|
+
import { AuthenticationError } from '../errors/authentication.error.js';
|
|
14
|
+
import { JwtError } from '../errors/jwt.error.js';
|
|
15
|
+
import { API_KEY_PATTERN } from '../utils/api-key.util.js';
|
|
16
|
+
import { bearerToken } from '../utils/bearer.util.js';
|
|
17
|
+
import { hasMfaAmr } from './amr.util.js';
|
|
18
|
+
import { JwtVerifier, verifierForSigner } from './jwt-verifier.service.js';
|
|
19
|
+
/**
|
|
20
|
+
* Base class for `Authorization: Bearer <jwt>` authentication. Extend it,
|
|
21
|
+
* inject what `validate()` needs, and load the user:
|
|
22
|
+
*
|
|
23
|
+
* ```ts
|
|
24
|
+
* @Injectable()
|
|
25
|
+
* export class JwtAuth extends JwtBearerProvider<User> {
|
|
26
|
+
* constructor(private readonly users: UsersRepository) {
|
|
27
|
+
* super(); // verifies the tokens the module's `accessToken` option signs
|
|
28
|
+
* }
|
|
29
|
+
* validate(claims: JwtClaims) {
|
|
30
|
+
* return this.users.findById(claims.sub!);
|
|
31
|
+
* }
|
|
32
|
+
* }
|
|
33
|
+
* ```
|
|
34
|
+
*
|
|
35
|
+
* Without `key` or `jwks`, it verifies what the module's `accessToken`
|
|
36
|
+
* option issues: the same key, algorithm, issuer, audience and `typ`. For
|
|
37
|
+
* tokens from another issuer, pass them: `super({ jwks, issuer, audience })`
|
|
38
|
+
* (a `jwks` needs both `issuer` and `audience`).
|
|
39
|
+
*
|
|
40
|
+
* Over socket.io it also reads `handshake.auth.token`. A token shaped like
|
|
41
|
+
* an API key (`ApiKeyProvider`) is left to the other providers. `session`
|
|
42
|
+
* is the verified claim set; an `amr` claim containing `mfa`, `otp` or
|
|
43
|
+
* `hwk` marks the result as MFA-verified.
|
|
44
|
+
*/
|
|
45
|
+
export class JwtBearerProvider extends AuthenticationProvider {
|
|
46
|
+
constructor(options = {}) {
|
|
47
|
+
super();
|
|
48
|
+
this.options = options;
|
|
49
|
+
this.realm = options.realm ?? 'api';
|
|
50
|
+
// `super({ key: process.env.PARTNER_KEY })` with the variable unset must not fall back to the
|
|
51
|
+
// module's `accessToken`: this provider would take the app's own tokens as its issuer's.
|
|
52
|
+
const named = ['key', 'jwks'].filter((name) => Object.hasOwn(options, name));
|
|
53
|
+
if (named.length > 0 && !options.key && !options.jwks) {
|
|
54
|
+
throw new TypeError(`${new.target.name}: \`${named.join('` and `')}\` ${named.length > 1 ? 'are' : 'is'} empty: is the environment ` +
|
|
55
|
+
"variable it reads set? Leave both out to verify the tokens the module's `accessToken` issues.");
|
|
56
|
+
}
|
|
57
|
+
if (options.key !== undefined || options.jwks !== undefined) {
|
|
58
|
+
this.verifier = new JwtVerifier(options);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
async authenticate(context) {
|
|
62
|
+
const token = this.extract(context);
|
|
63
|
+
if (token === undefined) {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
let claims;
|
|
67
|
+
try {
|
|
68
|
+
claims = await this.getVerifier().verify(token);
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
if (error instanceof JwtError) {
|
|
72
|
+
this.reject(error.message);
|
|
73
|
+
}
|
|
74
|
+
throw error;
|
|
75
|
+
}
|
|
76
|
+
const user = await this.validate(claims);
|
|
77
|
+
if (user === null || user === undefined) {
|
|
78
|
+
this.reject('unknown subject');
|
|
79
|
+
}
|
|
80
|
+
return { user, session: claims, mfa: this.mfaState(claims) };
|
|
81
|
+
}
|
|
82
|
+
challenge() {
|
|
83
|
+
return `Bearer realm="${this.realm}"`;
|
|
84
|
+
}
|
|
85
|
+
mfaState(claims) {
|
|
86
|
+
return hasMfaAmr(claims.amr) ? 'verified' : undefined;
|
|
87
|
+
}
|
|
88
|
+
/** @internal */
|
|
89
|
+
[PROVIDER_INIT](resolve) {
|
|
90
|
+
this.moduleOptions ??= resolve(AUTHENTICATION_MODULE_OPTIONS);
|
|
91
|
+
this.getVerifier();
|
|
92
|
+
}
|
|
93
|
+
getVerifier() {
|
|
94
|
+
if (this.verifier) {
|
|
95
|
+
return this.verifier;
|
|
96
|
+
}
|
|
97
|
+
const accessToken = this.moduleOptions?.accessToken;
|
|
98
|
+
if (!accessToken) {
|
|
99
|
+
throw new Error(`${this.constructor.name}: nothing to verify tokens with. Configure \`accessToken\` in the ` +
|
|
100
|
+
'AuthenticationModule options, or pass `key` or `jwks` to super().');
|
|
101
|
+
}
|
|
102
|
+
const { realm: _, ...rules } = this.options;
|
|
103
|
+
const defined = Object.fromEntries(Object.entries(rules).filter(([, value]) => value !== undefined));
|
|
104
|
+
this.verifier = verifierForSigner(accessToken, defined);
|
|
105
|
+
return this.verifier;
|
|
106
|
+
}
|
|
107
|
+
extract(context) {
|
|
108
|
+
const token = bearerToken(context, this.header(context, 'authorization'));
|
|
109
|
+
if (token === null) {
|
|
110
|
+
this.reject('malformed authorization header');
|
|
111
|
+
}
|
|
112
|
+
// An API key is `ApiKeyProvider`'s, which may run after this one.
|
|
113
|
+
return token === undefined || API_KEY_PATTERN.test(token) ? undefined : token;
|
|
114
|
+
}
|
|
115
|
+
reject(description) {
|
|
116
|
+
// RFC 6750 §3.1
|
|
117
|
+
throw new AuthenticationError(description, {
|
|
118
|
+
challenge: `Bearer realm="${this.realm}", error="invalid_token", error_description="${description}"`,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
__decorate([
|
|
123
|
+
Optional(),
|
|
124
|
+
Inject(AUTHENTICATION_MODULE_OPTIONS),
|
|
125
|
+
__metadata("design:type", Object)
|
|
126
|
+
], JwtBearerProvider.prototype, "moduleOptions", void 0);
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Duration } from '../interfaces/duration.interface.js';
|
|
2
|
+
import type { JwtClaims } from '../interfaces/jwt.interface.js';
|
|
3
|
+
import type { JwtSignerOptions } from '../interfaces/jwt-options.interface.js';
|
|
4
|
+
/**
|
|
5
|
+
* Issues signed JWTs. Adds `iat`, `exp`, `jti` and the configured
|
|
6
|
+
* `iss`/`aud`; callers pass `sub` and custom claims. The module's
|
|
7
|
+
* `accessToken` option configures the one `TokenService` uses; construct
|
|
8
|
+
* others directly.
|
|
9
|
+
*/
|
|
10
|
+
export declare class JwtSigner {
|
|
11
|
+
private readonly options;
|
|
12
|
+
private readonly key;
|
|
13
|
+
private readonly alg;
|
|
14
|
+
/** Throws a `TypeError` for a key that cannot sign, or an invalid `ttl`. */
|
|
15
|
+
constructor(options: JwtSignerOptions);
|
|
16
|
+
sign(claims: JwtClaims, { ttl }?: {
|
|
17
|
+
ttl?: Duration;
|
|
18
|
+
}): string;
|
|
19
|
+
}
|