@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.
Files changed (215) hide show
  1. package/LICENSE +23 -0
  2. package/README.md +50 -0
  3. package/dist/account/email-verification.handler.d.ts +23 -0
  4. package/dist/account/email-verification.handler.js +17 -0
  5. package/dist/account/email-verification.service.d.ts +46 -0
  6. package/dist/account/email-verification.service.js +111 -0
  7. package/dist/account/email.util.d.ts +14 -0
  8. package/dist/account/email.util.js +28 -0
  9. package/dist/account/in-memory-email-token.store.d.ts +12 -0
  10. package/dist/account/in-memory-email-token.store.js +60 -0
  11. package/dist/account/password-reset.handler.d.ts +19 -0
  12. package/dist/account/password-reset.handler.js +9 -0
  13. package/dist/account/password-reset.service.d.ts +69 -0
  14. package/dist/account/password-reset.service.js +204 -0
  15. package/dist/authentication.constants.d.ts +13 -0
  16. package/dist/authentication.constants.js +13 -0
  17. package/dist/authentication.module-definition.d.ts +21 -0
  18. package/dist/authentication.module-definition.js +147 -0
  19. package/dist/authentication.module.d.ts +34 -0
  20. package/dist/authentication.module.js +139 -0
  21. package/dist/context/authentication-scope.service.d.ts +39 -0
  22. package/dist/context/authentication-scope.service.js +64 -0
  23. package/dist/context/authentication.context.d.ts +28 -0
  24. package/dist/context/authentication.context.js +58 -0
  25. package/dist/context/index.d.ts +2 -0
  26. package/dist/context/index.js +2 -0
  27. package/dist/decorators/authenticate.decorator.d.ts +10 -0
  28. package/dist/decorators/authenticate.decorator.js +18 -0
  29. package/dist/decorators/current-session.decorator.d.ts +2 -0
  30. package/dist/decorators/current-session.decorator.js +6 -0
  31. package/dist/decorators/current-user.decorator.d.ts +6 -0
  32. package/dist/decorators/current-user.decorator.js +11 -0
  33. package/dist/decorators/index.d.ts +4 -0
  34. package/dist/decorators/index.js +4 -0
  35. package/dist/decorators/public.decorator.d.ts +5 -0
  36. package/dist/decorators/public.decorator.js +6 -0
  37. package/dist/errors/authentication.error.d.ts +27 -0
  38. package/dist/errors/authentication.error.js +33 -0
  39. package/dist/errors/index.d.ts +7 -0
  40. package/dist/errors/index.js +7 -0
  41. package/dist/errors/jwt.error.d.ts +9 -0
  42. package/dist/errors/jwt.error.js +12 -0
  43. package/dist/errors/magic-link.error.d.ts +18 -0
  44. package/dist/errors/magic-link.error.js +21 -0
  45. package/dist/errors/mfa-already-enrolled.error.d.ts +14 -0
  46. package/dist/errors/mfa-already-enrolled.error.js +17 -0
  47. package/dist/errors/oidc.error.d.ts +15 -0
  48. package/dist/errors/oidc.error.js +13 -0
  49. package/dist/errors/refresh-token.error.d.ts +10 -0
  50. package/dist/errors/refresh-token.error.js +12 -0
  51. package/dist/errors/secret-decryption.error.d.ts +3 -0
  52. package/dist/errors/secret-decryption.error.js +6 -0
  53. package/dist/events/authentication-events.interface.d.ts +121 -0
  54. package/dist/events/authentication-events.interface.js +1 -0
  55. package/dist/events/authentication-events.service.d.ts +19 -0
  56. package/dist/events/authentication-events.service.js +39 -0
  57. package/dist/events/authentication.channels.d.ts +4 -0
  58. package/dist/events/authentication.channels.js +11 -0
  59. package/dist/events/index.d.ts +3 -0
  60. package/dist/events/index.js +3 -0
  61. package/dist/guards/authentication.guard.d.ts +57 -0
  62. package/dist/guards/authentication.guard.js +171 -0
  63. package/dist/guards/index.d.ts +1 -0
  64. package/dist/guards/index.js +1 -0
  65. package/dist/index.d.ts +52 -0
  66. package/dist/index.js +52 -0
  67. package/dist/interceptors/authentication-scope.interceptor.d.ts +20 -0
  68. package/dist/interceptors/authentication-scope.interceptor.js +48 -0
  69. package/dist/interceptors/index.d.ts +1 -0
  70. package/dist/interceptors/index.js +1 -0
  71. package/dist/interfaces/api-key.interface.d.ts +41 -0
  72. package/dist/interfaces/api-key.interface.js +1 -0
  73. package/dist/interfaces/authenticate-options.interface.d.ts +31 -0
  74. package/dist/interfaces/authenticate-options.interface.js +1 -0
  75. package/dist/interfaces/authentication-error-options.interface.d.ts +8 -0
  76. package/dist/interfaces/authentication-error-options.interface.js +1 -0
  77. package/dist/interfaces/authentication-module-options.interface.d.ts +58 -0
  78. package/dist/interfaces/authentication-module-options.interface.js +1 -0
  79. package/dist/interfaces/authentication-registry.interface.d.ts +41 -0
  80. package/dist/interfaces/authentication-registry.interface.js +1 -0
  81. package/dist/interfaces/authentication-result.interface.d.ts +25 -0
  82. package/dist/interfaces/authentication-result.interface.js +1 -0
  83. package/dist/interfaces/authentication-storage.interface.d.ts +29 -0
  84. package/dist/interfaces/authentication-storage.interface.js +1 -0
  85. package/dist/interfaces/duration.interface.d.ts +2 -0
  86. package/dist/interfaces/duration.interface.js +1 -0
  87. package/dist/interfaces/email-token-store.interface.d.ts +52 -0
  88. package/dist/interfaces/email-token-store.interface.js +1 -0
  89. package/dist/interfaces/email-verification.interface.d.ts +18 -0
  90. package/dist/interfaces/email-verification.interface.js +1 -0
  91. package/dist/interfaces/index.d.ts +27 -0
  92. package/dist/interfaces/index.js +27 -0
  93. package/dist/interfaces/jwt-options.interface.d.ts +68 -0
  94. package/dist/interfaces/jwt-options.interface.js +1 -0
  95. package/dist/interfaces/jwt.interface.d.ts +36 -0
  96. package/dist/interfaces/jwt.interface.js +1 -0
  97. package/dist/interfaces/magic-link-store.interface.d.ts +36 -0
  98. package/dist/interfaces/magic-link-store.interface.js +1 -0
  99. package/dist/interfaces/magic-link.interface.d.ts +55 -0
  100. package/dist/interfaces/magic-link.interface.js +1 -0
  101. package/dist/interfaces/mfa-options.interface.d.ts +44 -0
  102. package/dist/interfaces/mfa-options.interface.js +1 -0
  103. package/dist/interfaces/mfa-store.interface.d.ts +70 -0
  104. package/dist/interfaces/mfa-store.interface.js +1 -0
  105. package/dist/interfaces/oidc-provider.interface.d.ts +56 -0
  106. package/dist/interfaces/oidc-provider.interface.js +1 -0
  107. package/dist/interfaces/oidc-state-store.interface.d.ts +25 -0
  108. package/dist/interfaces/oidc-state-store.interface.js +1 -0
  109. package/dist/interfaces/oidc.interface.d.ts +84 -0
  110. package/dist/interfaces/oidc.interface.js +1 -0
  111. package/dist/interfaces/password-hasher-options.interface.d.ts +17 -0
  112. package/dist/interfaces/password-hasher-options.interface.js +1 -0
  113. package/dist/interfaces/password-reset.interface.d.ts +49 -0
  114. package/dist/interfaces/password-reset.interface.js +1 -0
  115. package/dist/interfaces/refresh-token-options.interface.d.ts +10 -0
  116. package/dist/interfaces/refresh-token-options.interface.js +1 -0
  117. package/dist/interfaces/refresh-token-store.interface.d.ts +61 -0
  118. package/dist/interfaces/refresh-token-store.interface.js +1 -0
  119. package/dist/interfaces/session-store.interface.d.ts +53 -0
  120. package/dist/interfaces/session-store.interface.js +1 -0
  121. package/dist/interfaces/session.interface.d.ts +50 -0
  122. package/dist/interfaces/session.interface.js +1 -0
  123. package/dist/interfaces/sign-in-options.interface.d.ts +9 -0
  124. package/dist/interfaces/sign-in-options.interface.js +1 -0
  125. package/dist/interfaces/token.interface.d.ts +30 -0
  126. package/dist/interfaces/token.interface.js +1 -0
  127. package/dist/jwt/amr.util.d.ts +9 -0
  128. package/dist/jwt/amr.util.js +11 -0
  129. package/dist/jwt/in-memory-refresh-token.store.d.ts +29 -0
  130. package/dist/jwt/in-memory-refresh-token.store.js +67 -0
  131. package/dist/jwt/jwks.client.d.ts +34 -0
  132. package/dist/jwt/jwks.client.js +154 -0
  133. package/dist/jwt/jws.util.d.ts +45 -0
  134. package/dist/jwt/jws.util.js +282 -0
  135. package/dist/jwt/jwt-bearer.provider.d.ts +48 -0
  136. package/dist/jwt/jwt-bearer.provider.js +126 -0
  137. package/dist/jwt/jwt-signer.service.d.ts +19 -0
  138. package/dist/jwt/jwt-signer.service.js +36 -0
  139. package/dist/jwt/jwt-verifier.service.d.ts +22 -0
  140. package/dist/jwt/jwt-verifier.service.js +109 -0
  141. package/dist/jwt/token.service.d.ts +81 -0
  142. package/dist/jwt/token.service.js +230 -0
  143. package/dist/magic-link/in-memory-magic-link.store.d.ts +12 -0
  144. package/dist/magic-link/in-memory-magic-link.store.js +45 -0
  145. package/dist/magic-link/magic-link.handler.d.ts +25 -0
  146. package/dist/magic-link/magic-link.handler.js +14 -0
  147. package/dist/magic-link/magic-link.service.d.ts +79 -0
  148. package/dist/magic-link/magic-link.service.js +215 -0
  149. package/dist/mfa/in-memory-mfa.store.d.ts +25 -0
  150. package/dist/mfa/in-memory-mfa.store.js +73 -0
  151. package/dist/mfa/mfa.service.d.ts +104 -0
  152. package/dist/mfa/mfa.service.js +346 -0
  153. package/dist/mfa/otp.util.d.ts +7 -0
  154. package/dist/mfa/otp.util.js +52 -0
  155. package/dist/mfa/secret-cipher.service.d.ts +28 -0
  156. package/dist/mfa/secret-cipher.service.js +106 -0
  157. package/dist/oidc/in-memory-oidc-state.store.d.ts +13 -0
  158. package/dist/oidc/in-memory-oidc-state.store.js +45 -0
  159. package/dist/oidc/oidc-account.resolver.d.ts +23 -0
  160. package/dist/oidc/oidc-account.resolver.js +16 -0
  161. package/dist/oidc/oidc.client.d.ts +44 -0
  162. package/dist/oidc/oidc.client.js +286 -0
  163. package/dist/oidc/oidc.presets.d.ts +24 -0
  164. package/dist/oidc/oidc.presets.js +53 -0
  165. package/dist/oidc/oidc.service.d.ts +88 -0
  166. package/dist/oidc/oidc.service.js +259 -0
  167. package/dist/providers/api-key.provider.d.ts +51 -0
  168. package/dist/providers/api-key.provider.js +93 -0
  169. package/dist/providers/authentication.provider.d.ts +40 -0
  170. package/dist/providers/authentication.provider.js +33 -0
  171. package/dist/providers/index.d.ts +2 -0
  172. package/dist/providers/index.js +2 -0
  173. package/dist/services/authentication-registry.service.d.ts +75 -0
  174. package/dist/services/authentication-registry.service.js +235 -0
  175. package/dist/services/index.d.ts +3 -0
  176. package/dist/services/index.js +3 -0
  177. package/dist/services/password-hasher.service.d.ts +47 -0
  178. package/dist/services/password-hasher.service.js +170 -0
  179. package/dist/services/ws-authenticator.service.d.ts +38 -0
  180. package/dist/services/ws-authenticator.service.js +92 -0
  181. package/dist/session/cookies.util.d.ts +34 -0
  182. package/dist/session/cookies.util.js +82 -0
  183. package/dist/session/in-memory-session.store.d.ts +33 -0
  184. package/dist/session/in-memory-session.store.js +60 -0
  185. package/dist/session/session-cookie.provider.d.ts +36 -0
  186. package/dist/session/session-cookie.provider.js +58 -0
  187. package/dist/session/session.service.d.ts +97 -0
  188. package/dist/session/session.service.js +208 -0
  189. package/dist/session/sign-in.service.d.ts +87 -0
  190. package/dist/session/sign-in.service.js +206 -0
  191. package/dist/storage/authentication.storage.d.ts +130 -0
  192. package/dist/storage/authentication.storage.js +350 -0
  193. package/dist/storage/index.d.ts +1 -0
  194. package/dist/storage/index.js +1 -0
  195. package/dist/testing/index.d.ts +39 -0
  196. package/dist/testing/index.js +531 -0
  197. package/dist/utils/api-key.util.d.ts +10 -0
  198. package/dist/utils/api-key.util.js +10 -0
  199. package/dist/utils/auth-state.util.d.ts +54 -0
  200. package/dist/utils/auth-state.util.js +209 -0
  201. package/dist/utils/bearer.util.d.ts +9 -0
  202. package/dist/utils/bearer.util.js +21 -0
  203. package/dist/utils/cross-origin.util.d.ts +29 -0
  204. package/dist/utils/cross-origin.util.js +90 -0
  205. package/dist/utils/crypto.util.d.ts +19 -0
  206. package/dist/utils/crypto.util.js +38 -0
  207. package/dist/utils/duration.util.d.ts +9 -0
  208. package/dist/utils/duration.util.js +44 -0
  209. package/dist/utils/index.d.ts +8 -0
  210. package/dist/utils/index.js +8 -0
  211. package/dist/utils/options.util.d.ts +20 -0
  212. package/dist/utils/options.util.js +44 -0
  213. package/dist/utils/transport-errors.util.d.ts +33 -0
  214. package/dist/utils/transport-errors.util.js +70 -0
  215. package/package.json +108 -0
@@ -0,0 +1,36 @@
1
+ import { randomToken } from '../utils/crypto.util.js';
2
+ import { durationOr, toMs } from '../utils/duration.util.js';
3
+ import { algorithmFor, assertSigningKey, signJws, toKeyObject } from './jws.util.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 class JwtSigner {
11
+ /** Throws a `TypeError` for a key that cannot sign, or an invalid `ttl`. */
12
+ constructor(options) {
13
+ this.options = options;
14
+ this.key = toKeyObject(options.key);
15
+ const alg = options.alg ?? algorithmFor(this.key);
16
+ if (!alg) {
17
+ throw new TypeError(`JwtSigner: ${this.key.asymmetricKeyType} keys are not supported.`);
18
+ }
19
+ assertSigningKey(alg, this.key, 'JwtSigner');
20
+ this.alg = alg;
21
+ toMs(options.ttl ?? 0); // an invalid duration fails here, not at the first sign()
22
+ }
23
+ sign(claims, { ttl } = {}) {
24
+ const iat = Math.floor((this.options.now?.() ?? Date.now()) / 1000);
25
+ const lifetime = Math.floor(durationOr(ttl ?? this.options.ttl, '15m') / 1000);
26
+ const payload = {
27
+ ...(this.options.issuer !== undefined && { iss: this.options.issuer }),
28
+ ...(this.options.audience !== undefined && { aud: this.options.audience }),
29
+ jti: randomToken(16),
30
+ ...claims,
31
+ iat,
32
+ exp: iat + lifetime,
33
+ };
34
+ return signJws({ alg: this.alg, typ: this.options.type ?? 'JWT', ...(this.options.kid && { kid: this.options.kid }) }, payload, this.key);
35
+ }
36
+ }
@@ -0,0 +1,22 @@
1
+ import type { JwsAlgorithm, JwtClaims } from '../interfaces/jwt.interface.js';
2
+ import type { JwtSignerOptions } from '../interfaces/jwt-options.interface.js';
3
+ import type { JwtVerifierOptions } from '../interfaces/jwt-options.interface.js';
4
+ /**
5
+ * Verifies compact JWS tokens and their registered claims. `verify()`
6
+ * throws {@link JwtError}; a configuration that could not verify anything
7
+ * throws a `TypeError` in the constructor.
8
+ */
9
+ export declare class JwtVerifier {
10
+ private readonly options;
11
+ private readonly key?;
12
+ private readonly jwks?;
13
+ readonly algorithms: JwsAlgorithm[];
14
+ constructor(options: JwtVerifierOptions);
15
+ verify(token: string): Promise<JwtClaims>;
16
+ }
17
+ /**
18
+ * @internal A verifier for what a `JwtSigner` with these options issues:
19
+ * the same secret, or the public half of its private key, with its
20
+ * algorithm, issuer, audience and `typ`. `overrides` add rules (`maxAge`).
21
+ */
22
+ export declare function verifierForSigner(signer: JwtSignerOptions, overrides?: Omit<JwtVerifierOptions, 'key' | 'jwks'>): JwtVerifier;
@@ -0,0 +1,109 @@
1
+ import { createPublicKey } from 'node:crypto';
2
+ import { toMs } from '../utils/duration.util.js';
3
+ import { JwtError } from '../errors/jwt.error.js';
4
+ import { JwksClient } from './jwks.client.js';
5
+ import { algorithmFor, assertKeyFits, decodeJws, toKeyObject, validateClaims, verifySignature } from './jws.util.js';
6
+ /**
7
+ * Verifies compact JWS tokens and their registered claims. `verify()`
8
+ * throws {@link JwtError}; a configuration that could not verify anything
9
+ * throws a `TypeError` in the constructor.
10
+ */
11
+ export class JwtVerifier {
12
+ constructor(options) {
13
+ this.options = options;
14
+ if (!!options.key === !!options.jwks) {
15
+ throw new TypeError('JwtVerifier: pass exactly one of `key` or `jwks`.');
16
+ }
17
+ let given;
18
+ if (options.key) {
19
+ given = toKeyObject(options.key);
20
+ // A private key verifies with its public half, derived once.
21
+ this.key = given.type === 'private' ? createPublicKey(given) : given;
22
+ }
23
+ if (options.jwks) {
24
+ this.jwks = typeof options.jwks === 'string' ? new JwksClient(options.jwks) : options.jwks;
25
+ }
26
+ if (this.key) {
27
+ const implied = algorithmFor(this.key);
28
+ if (!implied) {
29
+ throw new TypeError(`JwtVerifier: ${this.key.asymmetricKeyType} keys are not supported.`);
30
+ }
31
+ this.algorithms = options.algorithms ?? [implied];
32
+ for (const alg of this.algorithms) {
33
+ assertKeyFits(alg, this.key, 'JwtVerifier');
34
+ }
35
+ // A public key is someone else's, an identity provider's that signs every client's tokens with
36
+ // it, as with a JWKS. A secret or a private key is the app's own (`verifierForSigner()`).
37
+ if (given?.type === 'public') {
38
+ requireIssuerAndAudience(options, 'public `key`');
39
+ }
40
+ }
41
+ else {
42
+ this.algorithms = options.algorithms ?? ['RS256', 'ES256'];
43
+ if (this.algorithms.includes('HS256')) {
44
+ throw new TypeError('JwtVerifier: HS256 cannot be combined with a JWKS.');
45
+ }
46
+ requireIssuerAndAudience(options, '`jwks`');
47
+ }
48
+ // Invalid durations fail here, not at the first request.
49
+ toMs(options.clockTolerance ?? 0);
50
+ toMs(options.maxAge ?? 0);
51
+ }
52
+ async verify(token) {
53
+ const decoded = decodeJws(token);
54
+ const { header } = decoded;
55
+ const alg = header.alg;
56
+ if (!this.algorithms.includes(alg)) {
57
+ throw new JwtError('unsupported algorithm');
58
+ }
59
+ // RFC 7515 §4.1.11: extensions we do not understand must be rejected.
60
+ if (header.crit !== undefined) {
61
+ throw new JwtError('unsupported critical header');
62
+ }
63
+ if (this.options.type !== undefined && (typeof header.typ !== 'string' || mediaType(header.typ) !== mediaType(this.options.type))) {
64
+ throw new JwtError('unexpected token type');
65
+ }
66
+ const key = this.key ?? (await this.jwks.getKey(header));
67
+ verifySignature(alg, key, decoded);
68
+ validateClaims(decoded.payload, this.options);
69
+ return decoded.payload;
70
+ }
71
+ }
72
+ /** Throws unless the tokens of a key the app does not own are pinned to their issuer and to the app. */
73
+ function requireIssuerAndAudience(options, what) {
74
+ if (typeof options.issuer === 'string' && options.audience !== undefined) {
75
+ return;
76
+ }
77
+ throw new TypeError(`JwtVerifier: a ${what} verifier needs \`issuer\` and \`audience\`. An identity provider signs tokens for ` +
78
+ 'all of its clients with the same keys, so without them tokens issued to other applications verify ' +
79
+ 'too. For tokens that carry no audience, pass `audience: false` and check the claim that names your ' +
80
+ 'client in `validate()`.');
81
+ }
82
+ /**
83
+ * A `typ` as RFC 7515 §4.1.9 compares it: without regard to case, and with
84
+ * `application/` implied when it has no `/`, so `at+jwt` and
85
+ * `application/at+jwt` are one type (RFC 9068 §4 accepts both).
86
+ */
87
+ function mediaType(typ) {
88
+ const lower = typ.toLowerCase();
89
+ return lower.includes('/') ? lower : `application/${lower}`;
90
+ }
91
+ /**
92
+ * @internal A verifier for what a `JwtSigner` with these options issues:
93
+ * the same secret, or the public half of its private key, with its
94
+ * algorithm, issuer, audience and `typ`. `overrides` add rules (`maxAge`).
95
+ */
96
+ export function verifierForSigner(signer, overrides = {}) {
97
+ const key = toKeyObject(signer.key);
98
+ const alg = signer.alg ?? algorithmFor(key);
99
+ return new JwtVerifier({
100
+ issuer: signer.issuer,
101
+ audience: signer.audience,
102
+ type: signer.type,
103
+ now: signer.now,
104
+ ...(alg && { algorithms: [alg] }),
105
+ ...overrides,
106
+ // The signer's own key (a secret or a private key): its tokens are the app's.
107
+ key,
108
+ });
109
+ }
@@ -0,0 +1,81 @@
1
+ import { AuthenticationStorage } from '../storage/authentication.storage.js';
2
+ import { AuthenticationEvents } from '../events/authentication-events.service.js';
3
+ import { MfaService } from '../mfa/mfa.service.js';
4
+ import type { JwtSignerOptions } from '../interfaces/jwt-options.interface.js';
5
+ import type { RefreshTokenOptions } from '../interfaces/refresh-token-options.interface.js';
6
+ import type { IssueTokensOptions, TokenPair } from '../interfaces/token.interface.js';
7
+ /**
8
+ * Tokens for clients that do not keep cookies (mobile apps, CLIs): a
9
+ * short-lived JWT access token, and an opaque refresh token that renews it.
10
+ *
11
+ * - `issue()` is to token clients what `SignInService.signIn()` is to
12
+ * browsers: a user with a confirmed authenticator gets no tokens without
13
+ * a second factor.
14
+ * - Access tokens are signed with the module's `accessToken` options, the
15
+ * same ones `JwtBearerProvider` verifies with by default.
16
+ * - Refresh tokens are single use and stored hashed. Each rotation stays in
17
+ * the family of the original sign-in, which has an absolute lifetime.
18
+ * - Presenting a refresh token that was already rotated, including two
19
+ * concurrent presentations (`markRefreshTokenUsed()` is a compare-and-set), means two
20
+ * parties hold it: the whole family is revoked, and both have to sign in
21
+ * again (OAuth 2.0 Security BCP §4.14.2).
22
+ */
23
+ export declare class TokenService {
24
+ private readonly storage;
25
+ private readonly events;
26
+ private readonly mfa;
27
+ private static readonly logger;
28
+ /** The `amr` warning is a misconfiguration: once per process says enough. */
29
+ private static warnedAmr;
30
+ private readonly signer?;
31
+ private readonly expiresIn;
32
+ private readonly ttl;
33
+ private readonly absoluteTtl;
34
+ private readonly clock?;
35
+ constructor(storage: AuthenticationStorage, options?: {
36
+ accessToken?: JwtSignerOptions;
37
+ refreshToken?: RefreshTokenOptions;
38
+ }, events?: AuthenticationEvents, mfa?: MfaService);
39
+ /**
40
+ * At sign-in: signs an access token and starts a refresh-token family.
41
+ *
42
+ * A user with a confirmed authenticator must send a second factor
43
+ * (`secondFactor`). Without one, `issue()` throws an
44
+ * `AuthenticationError` whose body says `mfa_required`, and counts
45
+ * nothing against the user's lockout; with a wrong one, `Invalid code`.
46
+ * A verified second factor adds `mfa` to the `amr` claim, which
47
+ * `@Authenticate({ mfa: true })` accepts; `claims.amr` cannot (see
48
+ * {@link IssueTokensOptions.claims}).
49
+ */
50
+ issue(userId: string, { claims, method, secondFactor }?: IssueTokensOptions): Promise<TokenPair>;
51
+ /**
52
+ * Spends a refresh token: returns its successor, and an access token with
53
+ * the family's claims. Throws {@link RefreshTokenError} (`invalid`,
54
+ * `expired`, `reused`), which a route answers with 401.
55
+ */
56
+ refresh(refreshToken: string): Promise<TokenPair>;
57
+ /** Two parties hold the token: the family is revoked, and the theft reported. */
58
+ private reused;
59
+ /**
60
+ * Signs out the client holding `refreshToken` (one device): revokes its
61
+ * family. Spent tokens of the family work too. Resolves `false` for
62
+ * unknown or malformed tokens, which a route should not reveal (RFC 7009
63
+ * §2.2).
64
+ */
65
+ revoke(refreshToken: string): Promise<boolean>;
66
+ /** Signs the user out of every token client. Issued access tokens live until they expire. */
67
+ revokeAll(userId: string): Promise<void>;
68
+ /** `true` when a second factor was required and verified; throws when one is required and missing or wrong. */
69
+ private checkSecondFactor;
70
+ private pair;
71
+ private requireSigner;
72
+ private create;
73
+ /**
74
+ * The app's claims with `amr` deduplicated and stripped of the values that
75
+ * mean a second factor: `JwtBearerProvider` would take them as verified,
76
+ * and only `issue()` knows whether a code was. An `amr` left empty is
77
+ * dropped. The first token issued with such values logs a warning.
78
+ */
79
+ private ownClaims;
80
+ private now;
81
+ }
@@ -0,0 +1,230 @@
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
+ var __param = (this && this.__param) || function (paramIndex, decorator) {
11
+ return function (target, key) { decorator(target, key, paramIndex); }
12
+ };
13
+ var TokenService_1;
14
+ import { Inject, Injectable, Logger, Optional } from '@nestjs/common';
15
+ import { AUTHENTICATION_MODULE_OPTIONS } from '../authentication.constants.js';
16
+ import { TOKEN_PATTERN, randomToken, sha256 } from '../utils/crypto.util.js';
17
+ import { durationOr, hasExpired } from '../utils/duration.util.js';
18
+ import { AuthenticationStorage } from '../storage/authentication.storage.js';
19
+ import { AuthenticationError } from '../errors/authentication.error.js';
20
+ import { AuthenticationEvents } from '../events/authentication-events.service.js';
21
+ import { requireOption } from '../utils/options.util.js';
22
+ import { MfaService } from '../mfa/mfa.service.js';
23
+ import { RefreshTokenError } from '../errors/refresh-token.error.js';
24
+ import { MFA_AMR_VALUES } from './amr.util.js';
25
+ import { JwtSigner } from './jwt-signer.service.js';
26
+ /**
27
+ * Tokens for clients that do not keep cookies (mobile apps, CLIs): a
28
+ * short-lived JWT access token, and an opaque refresh token that renews it.
29
+ *
30
+ * - `issue()` is to token clients what `SignInService.signIn()` is to
31
+ * browsers: a user with a confirmed authenticator gets no tokens without
32
+ * a second factor.
33
+ * - Access tokens are signed with the module's `accessToken` options, the
34
+ * same ones `JwtBearerProvider` verifies with by default.
35
+ * - Refresh tokens are single use and stored hashed. Each rotation stays in
36
+ * the family of the original sign-in, which has an absolute lifetime.
37
+ * - Presenting a refresh token that was already rotated, including two
38
+ * concurrent presentations (`markRefreshTokenUsed()` is a compare-and-set), means two
39
+ * parties hold it: the whole family is revoked, and both have to sign in
40
+ * again (OAuth 2.0 Security BCP §4.14.2).
41
+ */
42
+ let TokenService = TokenService_1 = class TokenService {
43
+ constructor(storage, options, events = new AuthenticationEvents(), mfa = new MfaService(storage)) {
44
+ this.storage = storage;
45
+ this.events = events;
46
+ this.mfa = mfa;
47
+ const refresh = options?.refreshToken ?? {};
48
+ this.ttl = durationOr(refresh.ttl, '30d');
49
+ this.absoluteTtl = durationOr(refresh.absoluteTtl, '90d');
50
+ this.clock = refresh.now;
51
+ // A bad key fails at startup, not at the first sign-in.
52
+ if (options?.accessToken) {
53
+ requireOption(options.accessToken.key, 'accessToken.key', 'a secret of at least 32 bytes or a KeyObject');
54
+ try {
55
+ this.signer = new JwtSigner(options.accessToken);
56
+ }
57
+ catch (error) {
58
+ throw new TypeError(`AuthenticationModule: \`accessToken\`: ${error.message.replace(/^JwtSigner: /, '')}`);
59
+ }
60
+ }
61
+ this.expiresIn = Math.floor(durationOr(options?.accessToken?.ttl, '15m') / 1000);
62
+ }
63
+ /**
64
+ * At sign-in: signs an access token and starts a refresh-token family.
65
+ *
66
+ * A user with a confirmed authenticator must send a second factor
67
+ * (`secondFactor`). Without one, `issue()` throws an
68
+ * `AuthenticationError` whose body says `mfa_required`, and counts
69
+ * nothing against the user's lockout; with a wrong one, `Invalid code`.
70
+ * A verified second factor adds `mfa` to the `amr` claim, which
71
+ * `@Authenticate({ mfa: true })` accepts; `claims.amr` cannot (see
72
+ * {@link IssueTokensOptions.claims}).
73
+ */
74
+ async issue(userId, { claims, method, secondFactor } = {}) {
75
+ const signer = this.requireSigner();
76
+ const verified = await this.checkSecondFactor(userId, secondFactor);
77
+ const own = this.ownClaims(claims);
78
+ const familyClaims = verified ? { ...own, amr: [...(own?.amr ?? []), 'mfa'] } : own;
79
+ const familyExpiresAt = new Date(this.now() + this.absoluteTtl);
80
+ const { token, record } = await this.create(userId, randomToken(16), familyExpiresAt, familyClaims);
81
+ this.events.emit({
82
+ type: 'sign-in',
83
+ userId,
84
+ tokenFamilyId: record.familyId,
85
+ ...(method && { method }),
86
+ ...(verified && { mfa: 'verified' }),
87
+ });
88
+ return this.pair(signer, token, record);
89
+ }
90
+ /**
91
+ * Spends a refresh token: returns its successor, and an access token with
92
+ * the family's claims. Throws {@link RefreshTokenError} (`invalid`,
93
+ * `expired`, `reused`), which a route answers with 401.
94
+ */
95
+ async refresh(refreshToken) {
96
+ const signer = this.requireSigner();
97
+ if (typeof refreshToken !== 'string' || !TOKEN_PATTERN.test(refreshToken)) {
98
+ throw new RefreshTokenError('invalid');
99
+ }
100
+ const store = this.storage.refreshTokens;
101
+ const record = await store.getRefreshToken(sha256(refreshToken));
102
+ if (!record || (await store.isRefreshTokenFamilyRevoked(record.familyId))) {
103
+ throw new RefreshTokenError('invalid');
104
+ }
105
+ const now = new Date(this.now());
106
+ const expired = hasExpired(record.expiresAt, now.getTime()) || hasExpired(record.familyExpiresAt, now.getTime());
107
+ // An unspent token that expired is refused as such, and not spent: presenting it
108
+ // again is a client retrying, not theft. A spent one stays a reuse, expired or not.
109
+ if (expired && !record.usedAt) {
110
+ throw new RefreshTokenError('expired');
111
+ }
112
+ if (record.usedAt) {
113
+ throw await this.reused(record);
114
+ }
115
+ // The successor is saved before this token is spent: a store that fails in between leaves it
116
+ // unspent, so the client's retry is a retry, not a theft that signs the user out. If another
117
+ // request spent it first, revoking the family revokes this successor too.
118
+ const next = await this.create(record.userId, record.familyId, record.familyExpiresAt, record.claims);
119
+ if (!(await store.markRefreshTokenUsed(record.id, now))) {
120
+ throw await this.reused(record);
121
+ }
122
+ return this.pair(signer, next.token, next.record);
123
+ }
124
+ /** Two parties hold the token: the family is revoked, and the theft reported. */
125
+ async reused(record) {
126
+ await this.storage.refreshTokens.revokeRefreshTokenFamily(record.familyId);
127
+ this.events.emit({ type: 'refresh-token-reused', userId: record.userId, tokenFamilyId: record.familyId });
128
+ return new RefreshTokenError('reused');
129
+ }
130
+ /**
131
+ * Signs out the client holding `refreshToken` (one device): revokes its
132
+ * family. Spent tokens of the family work too. Resolves `false` for
133
+ * unknown or malformed tokens, which a route should not reveal (RFC 7009
134
+ * §2.2).
135
+ */
136
+ async revoke(refreshToken) {
137
+ if (typeof refreshToken !== 'string' || !TOKEN_PATTERN.test(refreshToken)) {
138
+ return false;
139
+ }
140
+ const store = this.storage.refreshTokens;
141
+ const record = await store.getRefreshToken(sha256(refreshToken));
142
+ if (!record) {
143
+ return false;
144
+ }
145
+ await store.revokeRefreshTokenFamily(record.familyId);
146
+ this.events.emit({ type: 'sign-out', userId: record.userId, tokenFamilyId: record.familyId });
147
+ return true;
148
+ }
149
+ /** Signs the user out of every token client. Issued access tokens live until they expire. */
150
+ revokeAll(userId) {
151
+ return this.storage.refreshTokens.revokeUserRefreshTokens(userId);
152
+ }
153
+ /** `true` when a second factor was required and verified; throws when one is required and missing or wrong. */
154
+ async checkSecondFactor(userId, secondFactor) {
155
+ if (!(await this.mfa.isEnrolled(userId))) {
156
+ return false;
157
+ }
158
+ const { code, recoveryCode } = secondFactor ?? {};
159
+ if (!code && !recoveryCode) {
160
+ throw new AuthenticationError('Second factor required', { code: 'mfa_required' });
161
+ }
162
+ const verified = recoveryCode
163
+ ? await this.mfa.verifyRecoveryCode(userId, recoveryCode)
164
+ : await this.mfa.verifyTotp(userId, code);
165
+ if (!verified) {
166
+ throw new AuthenticationError('Invalid code', { code: 'mfa_required' });
167
+ }
168
+ return true;
169
+ }
170
+ pair(signer, refreshToken, record) {
171
+ const accessToken = signer.sign({ ...record.claims, sub: record.userId });
172
+ return { accessToken, refreshToken, expiresIn: this.expiresIn };
173
+ }
174
+ requireSigner() {
175
+ if (!this.signer) {
176
+ throw new Error('TokenService: configure `accessToken` (the signing key) in the AuthenticationModule options');
177
+ }
178
+ return this.signer;
179
+ }
180
+ async create(userId, familyId, familyExpiresAt, claims) {
181
+ const token = randomToken();
182
+ const now = this.now();
183
+ const record = {
184
+ id: sha256(token),
185
+ familyId,
186
+ userId,
187
+ createdAt: new Date(now),
188
+ expiresAt: new Date(Math.min(now + this.ttl, familyExpiresAt.getTime())),
189
+ familyExpiresAt,
190
+ ...(claims && { claims }),
191
+ };
192
+ await this.storage.refreshTokens.saveRefreshToken(record);
193
+ return { token, record };
194
+ }
195
+ /**
196
+ * The app's claims with `amr` deduplicated and stripped of the values that
197
+ * mean a second factor: `JwtBearerProvider` would take them as verified,
198
+ * and only `issue()` knows whether a code was. An `amr` left empty is
199
+ * dropped. The first token issued with such values logs a warning.
200
+ */
201
+ ownClaims(claims) {
202
+ if (!claims || !Array.isArray(claims.amr)) {
203
+ return claims;
204
+ }
205
+ const { amr, ...rest } = claims;
206
+ const unique = [...new Set(amr)];
207
+ const stripped = unique.filter((method) => MFA_AMR_VALUES.includes(method));
208
+ if (stripped.length && !TokenService_1.warnedAmr) {
209
+ TokenService_1.warnedAmr = true;
210
+ TokenService_1.logger.warn(`issue() dropped ${stripped.map((m) => `'${m}'`).join(', ')} from \`claims.amr\`: those values mean a verified second ` +
211
+ 'factor, which only the service records (`mfa`, once a code was verified). Remove them from the claims passed to issue().');
212
+ }
213
+ const kept = unique.filter((method) => !stripped.includes(method));
214
+ return kept.length ? { ...rest, amr: kept } : rest;
215
+ }
216
+ now() {
217
+ return this.clock?.() ?? Date.now();
218
+ }
219
+ };
220
+ TokenService.logger = new Logger('TokenService');
221
+ /** The `amr` warning is a misconfiguration: once per process says enough. */
222
+ TokenService.warnedAmr = false;
223
+ TokenService = TokenService_1 = __decorate([
224
+ Injectable(),
225
+ __param(1, Optional()),
226
+ __param(1, Inject(AUTHENTICATION_MODULE_OPTIONS)),
227
+ __metadata("design:paramtypes", [AuthenticationStorage, Object, AuthenticationEvents,
228
+ MfaService])
229
+ ], TokenService);
230
+ export { TokenService };
@@ -0,0 +1,12 @@
1
+ import type { MagicLinkRecord, MagicLinkStore } from '../interfaces/magic-link-store.interface.js';
2
+ /**
3
+ * Process-local store: the default when no source is registered, and a
4
+ * test double. Drops expired links as new ones are saved, and holds at most
5
+ * 10,000, dropping the oldest: anyone can request links, so the store must
6
+ * not grow with them.
7
+ */
8
+ export declare class InMemoryMagicLinkStore implements MagicLinkStore {
9
+ private readonly links;
10
+ saveMagicLink(record: MagicLinkRecord): Promise<void>;
11
+ consumeMagicLink(id: string): Promise<MagicLinkRecord | undefined>;
12
+ }
@@ -0,0 +1,45 @@
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
+ import { Injectable } from '@nestjs/common';
8
+ const MAX_PENDING = 10_000;
9
+ /**
10
+ * Process-local store: the default when no source is registered, and a
11
+ * test double. Drops expired links as new ones are saved, and holds at most
12
+ * 10,000, dropping the oldest: anyone can request links, so the store must
13
+ * not grow with them.
14
+ */
15
+ let InMemoryMagicLinkStore = class InMemoryMagicLinkStore {
16
+ constructor() {
17
+ this.links = new Map();
18
+ }
19
+ async saveMagicLink(record) {
20
+ const now = record.createdAt.getTime();
21
+ // Insertion order is expiry order (one lifetime per service): stop at the first live entry.
22
+ for (const [id, link] of this.links) {
23
+ if (link.expiresAt.getTime() > now) {
24
+ break;
25
+ }
26
+ this.links.delete(id);
27
+ }
28
+ this.links.set(record.id, { ...record });
29
+ for (const id of this.links.keys()) {
30
+ if (this.links.size <= MAX_PENDING) {
31
+ break;
32
+ }
33
+ this.links.delete(id); // insertion order: the oldest first
34
+ }
35
+ }
36
+ async consumeMagicLink(id) {
37
+ const record = this.links.get(id);
38
+ this.links.delete(id);
39
+ return record;
40
+ }
41
+ };
42
+ InMemoryMagicLinkStore = __decorate([
43
+ Injectable()
44
+ ], InMemoryMagicLinkStore);
45
+ export { InMemoryMagicLinkStore };
@@ -0,0 +1,25 @@
1
+ import type { MagicLink, MagicLinkAccount } from '../interfaces/magic-link.interface.js';
2
+ /**
3
+ * Implemented by the app: delivery (the module ships no mailer) and the
4
+ * email-to-user mapping (look up, or create for passwordless sign-up).
5
+ * An ordinary provider of one of your modules, which registers itself from
6
+ * its constructor: `registry.registerHandler('magicLink', this)`.
7
+ *
8
+ * The link proves the address, not that the account behind it was created
9
+ * by the same person: if accounts can be registered without verifying the
10
+ * email (password sign-up), refuse links to accounts whose address was
11
+ * never verified, or someone who registered the address first keeps access
12
+ * (account pre-hijacking).
13
+ */
14
+ export declare abstract class MagicLinkHandler {
15
+ abstract send(link: MagicLink): void | Promise<void>;
16
+ /**
17
+ * The account for an address (trimmed, lowercased, NFC), with the
18
+ * address it has stored, or `null` to refuse. The link signs in only an
19
+ * account whose stored `email` is the link's address, compared trimmed
20
+ * and lowercased: a lookup that ignores accents or dots (MySQL's default
21
+ * collation does) would otherwise sign whoever reads `victim@exämple.com`
22
+ * in to `victim@example.com`'s account.
23
+ */
24
+ abstract resolveUser(email: string): MagicLinkAccount | null | Promise<MagicLinkAccount | null>;
25
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Implemented by the app: delivery (the module ships no mailer) and the
3
+ * email-to-user mapping (look up, or create for passwordless sign-up).
4
+ * An ordinary provider of one of your modules, which registers itself from
5
+ * its constructor: `registry.registerHandler('magicLink', this)`.
6
+ *
7
+ * The link proves the address, not that the account behind it was created
8
+ * by the same person: if accounts can be registered without verifying the
9
+ * email (password sign-up), refuse links to accounts whose address was
10
+ * never verified, or someone who registered the address first keeps access
11
+ * (account pre-hijacking).
12
+ */
13
+ export class MagicLinkHandler {
14
+ }
@@ -0,0 +1,79 @@
1
+ import { HttpAdapterHost } from '@nestjs/core';
2
+ import { AuthenticationScope } from '../context/authentication-scope.service.js';
3
+ import { AuthenticationRegistry } from '../services/authentication-registry.service.js';
4
+ import { AuthenticationStorage } from '../storage/authentication.storage.js';
5
+ import { AuthenticationEvents } from '../events/authentication-events.service.js';
6
+ import type { IssuedSession } from '../interfaces/session.interface.js';
7
+ import { SignInService } from '../session/sign-in.service.js';
8
+ import type { MagicLinkOptions, MagicLinkRequest, CreatedMagicLink } from '../interfaces/magic-link.interface.js';
9
+ /**
10
+ * Passwordless sign-in links: 256-bit single-use tokens, stored hashed,
11
+ * short-lived, and bound to the browser that requested them.
12
+ *
13
+ * Point the link at a page that POSTs the token back: mail scanners and
14
+ * link previews follow GET links, and would burn (or use) the token.
15
+ *
16
+ * Like `OidcService`, the service binds each link to the browser that
17
+ * requested it: `create()` refuses a request from another origin (another
18
+ * site's page must not start a link here), and sets an `HttpOnly`,
19
+ * `SameSite=Lax` cookie, `__Host-magic_link_tx`, holding a secret that no
20
+ * link carries; the store keys the link by the token and that secret
21
+ * together, so `consume()` finds it only with the cookie. The `__Host-`
22
+ * prefix keeps sibling subdomains from planting one. So the link's page
23
+ * must be on the same site as the API, as the session cookie requires
24
+ * anyway, and a link forwarded to another browser or device, or read from
25
+ * a log, is refused there while it stays usable where it was requested.
26
+ * See `magicLink.bindToBrowser`.
27
+ */
28
+ export declare class MagicLinkService {
29
+ private readonly storage;
30
+ private readonly registry;
31
+ private readonly signIn;
32
+ private readonly scope;
33
+ private readonly adapterHost?;
34
+ private readonly events;
35
+ private readonly options?;
36
+ private readonly ttl;
37
+ private readonly bindToBrowser;
38
+ private readonly txCookieName;
39
+ constructor(storage: AuthenticationStorage, registry: AuthenticationRegistry, signIn: SignInService, options?: {
40
+ magicLink?: MagicLinkOptions;
41
+ }, scope?: AuthenticationScope, adapterHost?: HttpAdapterHost | undefined, events?: AuthenticationEvents);
42
+ /**
43
+ * Creates a link and hands it to `MagicLinkHandler.send()`. The token is
44
+ * not returned. Called from an HTTP handler, it sets the transaction
45
+ * cookie on the response (see `bindToBrowser`); the returned `cookie` is
46
+ * that `Set-Cookie` value, for callers outside HTTP. A request from another
47
+ * origin than the app's own and `session.trustedOrigins` gets a
48
+ * `ForbiddenException`, as `SignInService.signIn()` does. An `email` no
49
+ * mail should go to (missing, blank, without `@`, over 254 characters, or
50
+ * with whitespace or control characters) creates and sends nothing, and
51
+ * gets the same answer, like `PasswordResetService.request()`.
52
+ */
53
+ create(email: string, { redirectTo }?: {
54
+ redirectTo?: string;
55
+ }): Promise<CreatedMagicLink>;
56
+ /**
57
+ * Burns the token, asks `MagicLinkHandler.resolveUser()` for the user, and
58
+ * signs them in on this browser through `SignInService`, which sets the
59
+ * cookie. `null` for unknown, used or expired tokens and for addresses
60
+ * the handler refuses, one answer for all of them. A link this browser
61
+ * did not request (see `bindToBrowser`) throws {@link MagicLinkError}
62
+ * instead, before anything is looked up or burned: the module answers it
63
+ * with a 401 that tells the customer where to open the link. Each
64
+ * refusal is a `magic-link-refused` event with its reason. Outside an
65
+ * HTTP handler, pass the `request` that carries the cookie.
66
+ */
67
+ consume(token: string, { request }?: {
68
+ request?: MagicLinkRequest;
69
+ }): Promise<(IssuedSession & {
70
+ redirectTo?: string;
71
+ }) | null>;
72
+ /** The options and the handler; the module refuses to start with one and not the other. */
73
+ private feature;
74
+ /** The generic answer: which refusal it was is told only by the event. */
75
+ private refuse;
76
+ private txCookie;
77
+ private setCookie;
78
+ private now;
79
+ }