@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,215 @@
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
+ import { Inject, Injectable, Optional } from '@nestjs/common';
14
+ import { HttpAdapterHost } from '@nestjs/core';
15
+ import { firstHeader } from '../utils/auth-state.util.js';
16
+ import { AuthenticationScope } from '../context/authentication-scope.service.js';
17
+ import { AuthenticationRegistry } from '../services/authentication-registry.service.js';
18
+ import { AuthenticationStorage } from '../storage/authentication.storage.js';
19
+ import { AUTHENTICATION_MODULE_OPTIONS } from '../authentication.constants.js';
20
+ import { TOKEN_PATTERN, randomToken, safeEqual, safeRedirectPath, sha256 } from '../utils/crypto.util.js';
21
+ import { durationOr, hasExpired } from '../utils/duration.util.js';
22
+ import { AuthenticationEvents } from '../events/authentication-events.service.js';
23
+ import { requireUrlOption } from '../utils/options.util.js';
24
+ import { defaultCookieName, readCookie, serializeCookie } from '../session/cookies.util.js';
25
+ import { SignInService } from '../session/sign-in.service.js';
26
+ import { MagicLinkError } from '../errors/magic-link.error.js';
27
+ import { isMailableEmail, normalizeEmail } from '../account/email.util.js';
28
+ /**
29
+ * What the store keys a link by: the SHA-256 of its token, and of the secret
30
+ * that binds it to a browser when it is bound (`bindToBrowser`), so the link
31
+ * alone finds nothing.
32
+ */
33
+ function linkId(token, binding) {
34
+ return sha256(binding === undefined ? token : `${token}.${binding}`);
35
+ }
36
+ /** Which link a browser's cookie is for: a public hint, computable from the link. */
37
+ function hintOf(token) {
38
+ return sha256(`nestjs-authentication magic-link hint ${token}`).slice(0, 12);
39
+ }
40
+ /**
41
+ * Passwordless sign-in links: 256-bit single-use tokens, stored hashed,
42
+ * short-lived, and bound to the browser that requested them.
43
+ *
44
+ * Point the link at a page that POSTs the token back: mail scanners and
45
+ * link previews follow GET links, and would burn (or use) the token.
46
+ *
47
+ * Like `OidcService`, the service binds each link to the browser that
48
+ * requested it: `create()` refuses a request from another origin (another
49
+ * site's page must not start a link here), and sets an `HttpOnly`,
50
+ * `SameSite=Lax` cookie, `__Host-magic_link_tx`, holding a secret that no
51
+ * link carries; the store keys the link by the token and that secret
52
+ * together, so `consume()` finds it only with the cookie. The `__Host-`
53
+ * prefix keeps sibling subdomains from planting one. So the link's page
54
+ * must be on the same site as the API, as the session cookie requires
55
+ * anyway, and a link forwarded to another browser or device, or read from
56
+ * a log, is refused there while it stays usable where it was requested.
57
+ * See `magicLink.bindToBrowser`.
58
+ */
59
+ let MagicLinkService = class MagicLinkService {
60
+ constructor(storage, registry, signIn, options, scope = new AuthenticationScope(), adapterHost, events = new AuthenticationEvents()) {
61
+ this.storage = storage;
62
+ this.registry = registry;
63
+ this.signIn = signIn;
64
+ this.scope = scope;
65
+ this.adapterHost = adapterHost;
66
+ this.events = events;
67
+ this.options = options?.magicLink;
68
+ if (this.options) {
69
+ requireUrlOption(this.options.url, 'magicLink.url', 'the page that receives the link');
70
+ }
71
+ this.ttl = durationOr(this.options?.ttl, '15m');
72
+ this.bindToBrowser = this.options?.bindToBrowser !== false;
73
+ this.txCookieName = defaultCookieName('magic_link_tx', this.options?.cookie);
74
+ }
75
+ /**
76
+ * Creates a link and hands it to `MagicLinkHandler.send()`. The token is
77
+ * not returned. Called from an HTTP handler, it sets the transaction
78
+ * cookie on the response (see `bindToBrowser`); the returned `cookie` is
79
+ * that `Set-Cookie` value, for callers outside HTTP. A request from another
80
+ * origin than the app's own and `session.trustedOrigins` gets a
81
+ * `ForbiddenException`, as `SignInService.signIn()` does. An `email` no
82
+ * mail should go to (missing, blank, without `@`, over 254 characters, or
83
+ * with whitespace or control characters) creates and sends nothing, and
84
+ * gets the same answer, like `PasswordResetService.request()`.
85
+ */
86
+ async create(email, { redirectTo } = {}) {
87
+ const { options, handler } = this.feature();
88
+ // The cookie binds the link to this browser: another site's page must not get one set here for
89
+ // an address its author reads (login CSRF), whatever the request asks for.
90
+ this.signIn.refuseCrossOrigin(this.scope.exchange()?.request);
91
+ const now = this.now();
92
+ const expiresAt = new Date(now + this.ttl);
93
+ const normalized = typeof email === 'string' ? normalizeEmail(email) : '';
94
+ if (!isMailableEmail(normalized)) {
95
+ return { expiresAt };
96
+ }
97
+ const token = randomToken();
98
+ const binding = this.bindToBrowser ? randomToken() : undefined;
99
+ const safe = safeRedirectPath(redirectTo);
100
+ await this.storage.magicLinks.saveMagicLink({
101
+ id: linkId(token, binding),
102
+ email: normalized,
103
+ createdAt: new Date(now),
104
+ expiresAt,
105
+ ...(safe && { redirectTo: safe }),
106
+ });
107
+ let cookie;
108
+ if (binding) {
109
+ cookie = this.txCookie(`${hintOf(token)}.${binding}`, this.ttl / 1000);
110
+ this.setCookie(cookie);
111
+ }
112
+ const url = new URL(options.url);
113
+ url.searchParams.set('token', token);
114
+ await handler.send({ email: normalized, url: url.toString(), expiresAt });
115
+ return { expiresAt, ...(cookie && { cookie }) };
116
+ }
117
+ /**
118
+ * Burns the token, asks `MagicLinkHandler.resolveUser()` for the user, and
119
+ * signs them in on this browser through `SignInService`, which sets the
120
+ * cookie. `null` for unknown, used or expired tokens and for addresses
121
+ * the handler refuses, one answer for all of them. A link this browser
122
+ * did not request (see `bindToBrowser`) throws {@link MagicLinkError}
123
+ * instead, before anything is looked up or burned: the module answers it
124
+ * with a 401 that tells the customer where to open the link. Each
125
+ * refusal is a `magic-link-refused` event with its reason. Outside an
126
+ * HTTP handler, pass the `request` that carries the cookie.
127
+ */
128
+ async consume(token, { request } = {}) {
129
+ const { handler } = this.feature();
130
+ if (typeof token !== 'string' || !TOKEN_PATTERN.test(token)) {
131
+ return this.refuse('unknown');
132
+ }
133
+ let binding;
134
+ if (this.bindToBrowser) {
135
+ const req = request ?? this.scope.exchange()?.request;
136
+ const [hint, secret] = (readCookie(firstHeader(req?.headers, 'cookie'), this.txCookieName) ?? '').split('.');
137
+ // The hint names the link this browser requested; anyone holding the link can compute it, so it
138
+ // only tells a link opened elsewhere from a bad one. The secret, which no link carries, is what
139
+ // the link is stored under: a leaked or forwarded link finds nothing without it. On a refusal
140
+ // here the cookie stays (it may be for a link this browser did request), and nothing is looked
141
+ // up: a link was presented, not proved.
142
+ if (!hint || !secret || !TOKEN_PATTERN.test(secret) || !safeEqual(hint, hintOf(token))) {
143
+ this.events.emit({ type: 'magic-link-refused', reason: 'not-this-browser' });
144
+ throw new MagicLinkError();
145
+ }
146
+ binding = secret;
147
+ }
148
+ const record = await this.storage.magicLinks.consumeMagicLink(linkId(token, binding));
149
+ // This browser's link is settled either way: the cookie goes.
150
+ this.setCookie(this.txCookie('', 0));
151
+ if (!record) {
152
+ return this.refuse('unknown');
153
+ }
154
+ if (hasExpired(record.expiresAt, this.now())) {
155
+ return this.refuse('expired', record.email);
156
+ }
157
+ const user = await handler.resolveUser(record.email);
158
+ if (!user) {
159
+ return this.refuse('refused', record.email);
160
+ }
161
+ // The link proved the address it was sent to, and signs in only the account whose address that
162
+ // is: a lookup that folds accents or ignores dots would otherwise hand `victim@exämple.com`'s
163
+ // mailbox the account of `victim@example.com`.
164
+ if (typeof user.email !== 'string') {
165
+ throw new TypeError('MagicLinkHandler.resolveUser() must return the account’s stored `email` with its `id`: the link signs in ' +
166
+ 'only the account whose address it was sent to.');
167
+ }
168
+ if (normalizeEmail(user.email) !== record.email) {
169
+ return this.refuse('refused', record.email);
170
+ }
171
+ const issued = await this.signIn.signIn(user.id, { method: 'magic-link' });
172
+ return { ...issued, redirectTo: record.redirectTo };
173
+ }
174
+ /** The options and the handler; the module refuses to start with one and not the other. */
175
+ feature() {
176
+ const handler = this.registry.handler('magicLink');
177
+ if (!this.options || !handler) {
178
+ throw new Error('MagicLinkService: magic links are not enabled. Configure `magicLink` in the AuthenticationModule options, and ' +
179
+ "register a MagicLinkHandler: `registry.registerHandler('magicLink', this)`.");
180
+ }
181
+ return { options: this.options, handler };
182
+ }
183
+ /** The generic answer: which refusal it was is told only by the event. */
184
+ refuse(reason, email) {
185
+ this.events.emit({ type: 'magic-link-refused', reason, ...(email && { email }) });
186
+ return null;
187
+ }
188
+ txCookie(value, maxAge) {
189
+ return serializeCookie(this.txCookieName, value, { ...this.options?.cookie, sameSite: 'lax', maxAge });
190
+ }
191
+ setCookie(cookie) {
192
+ if (!this.bindToBrowser) {
193
+ return;
194
+ }
195
+ const response = this.scope.exchange()?.response;
196
+ if (response) {
197
+ this.adapterHost?.httpAdapter?.appendHeader(response, 'Set-Cookie', cookie);
198
+ }
199
+ }
200
+ now() {
201
+ return this.options?.now?.() ?? Date.now();
202
+ }
203
+ };
204
+ MagicLinkService = __decorate([
205
+ Injectable(),
206
+ __param(3, Optional()),
207
+ __param(3, Inject(AUTHENTICATION_MODULE_OPTIONS)),
208
+ __param(5, Optional()),
209
+ __metadata("design:paramtypes", [AuthenticationStorage,
210
+ AuthenticationRegistry,
211
+ SignInService, Object, AuthenticationScope,
212
+ HttpAdapterHost,
213
+ AuthenticationEvents])
214
+ ], MagicLinkService);
215
+ export { MagicLinkService };
@@ -0,0 +1,25 @@
1
+ import type { TotpRecord, MfaStore } from '../interfaces/mfa-store.interface.js';
2
+ /**
3
+ * Process-local store: the default when no source is registered, and a
4
+ * test double. Atomic because it is synchronous.
5
+ */
6
+ export declare class InMemoryMfaStore implements MfaStore {
7
+ private readonly totp;
8
+ private readonly codes;
9
+ private readonly failuresAt;
10
+ getTotp(userId: string): Promise<{
11
+ secret: string;
12
+ confirmed: boolean;
13
+ pendingSecret?: string;
14
+ lastUsedStep?: number;
15
+ } | undefined>;
16
+ saveTotp(userId: string, record: TotpRecord | null): Promise<void>;
17
+ claimTotpStep(userId: string, step: number): Promise<boolean>;
18
+ saveRecoveryCodes(userId: string, hashes: string[]): Promise<void>;
19
+ consumeRecoveryCode(userId: string, hash: string): Promise<boolean>;
20
+ countRecoveryCodes(userId: string): Promise<number>;
21
+ recordMfaFailure(userId: string, windowMs: number, now: number): Promise<number>;
22
+ countMfaFailures(userId: string, windowMs: number, now: number): Promise<number>;
23
+ clearMfaFailures(userId: string): Promise<void>;
24
+ private recent;
25
+ }
@@ -0,0 +1,73 @@
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
+ /**
9
+ * Process-local store: the default when no source is registered, and a
10
+ * test double. Atomic because it is synchronous.
11
+ */
12
+ let InMemoryMfaStore = class InMemoryMfaStore {
13
+ constructor() {
14
+ this.totp = new Map();
15
+ this.codes = new Map();
16
+ this.failuresAt = new Map();
17
+ }
18
+ async getTotp(userId) {
19
+ const record = this.totp.get(userId);
20
+ return record && { ...record };
21
+ }
22
+ async saveTotp(userId, record) {
23
+ if (!record) {
24
+ this.totp.delete(userId);
25
+ return;
26
+ }
27
+ const claimed = this.totp.get(userId)?.lastUsedStep;
28
+ const lastUsedStep = Math.max(claimed ?? -Infinity, record.lastUsedStep ?? -Infinity);
29
+ const { lastUsedStep: _, ...rest } = record;
30
+ this.totp.set(userId, { ...rest, ...(Number.isFinite(lastUsedStep) && { lastUsedStep }) });
31
+ }
32
+ async claimTotpStep(userId, step) {
33
+ const record = this.totp.get(userId);
34
+ if (!record || (record.lastUsedStep !== undefined && step <= record.lastUsedStep)) {
35
+ return false;
36
+ }
37
+ record.lastUsedStep = step;
38
+ return true;
39
+ }
40
+ async saveRecoveryCodes(userId, hashes) {
41
+ if (hashes.length === 0) {
42
+ this.codes.delete(userId);
43
+ }
44
+ else {
45
+ this.codes.set(userId, new Set(hashes));
46
+ }
47
+ }
48
+ async consumeRecoveryCode(userId, hash) {
49
+ return this.codes.get(userId)?.delete(hash) ?? false;
50
+ }
51
+ async countRecoveryCodes(userId) {
52
+ return this.codes.get(userId)?.size ?? 0;
53
+ }
54
+ async recordMfaFailure(userId, windowMs, now) {
55
+ const recent = this.recent(userId, windowMs, now);
56
+ recent.push(now);
57
+ this.failuresAt.set(userId, recent);
58
+ return recent.length;
59
+ }
60
+ async countMfaFailures(userId, windowMs, now) {
61
+ return this.recent(userId, windowMs, now).length;
62
+ }
63
+ async clearMfaFailures(userId) {
64
+ this.failuresAt.delete(userId);
65
+ }
66
+ recent(userId, windowMs, now) {
67
+ return (this.failuresAt.get(userId) ?? []).filter((t) => t > now - windowMs);
68
+ }
69
+ };
70
+ InMemoryMfaStore = __decorate([
71
+ Injectable()
72
+ ], InMemoryMfaStore);
73
+ export { InMemoryMfaStore };
@@ -0,0 +1,104 @@
1
+ import { AuthenticationStorage } from '../storage/authentication.storage.js';
2
+ import { AuthenticationEvents } from '../events/authentication-events.service.js';
3
+ import type { MfaOptions } from '../interfaces/mfa-options.interface.js';
4
+ /**
5
+ * TOTP second factor (RFC 6238 over RFC 4226 HOTP, SHA-1, 6 digits, 30 s:
6
+ * the parameters every authenticator app supports) plus recovery codes.
7
+ *
8
+ * - `enroll()` stores an unconfirmed 160-bit secret and returns the
9
+ * `otpauth://` URI for the QR code; `confirm()` activates it once a
10
+ * code verifies. It refuses to touch a confirmed authenticator
11
+ * ({@link MfaAlreadyEnrolledError}): replacing one takes
12
+ * `{ replace: true }`, which stages the new secret while the current one
13
+ * keeps working until `confirm()`. Call that only from a route that
14
+ * requires a verified second factor (`@Authenticate({ mfa: true })`), or
15
+ * remove the authenticator with `disable()` first.
16
+ * - Secrets are encrypted before they reach the store (AES-256-GCM, bound to
17
+ * the user id) and decrypted only to compute codes. A secret encrypted with
18
+ * an older key is re-encrypted with the current one after its next
19
+ * successful verification, or by `reencrypt(userId)`.
20
+ * - Each time step is accepted once per user (replay protection), across
21
+ * the ±`window` range.
22
+ * - After `maxAttempts` failures in `lockoutWindow` every check fails;
23
+ * 6 digits are otherwise brute-forceable. Each attempt is counted before
24
+ * it is checked, so a burst of parallel guesses cannot outrun the limit.
25
+ * This is a per-account control on the verifier, not request rate
26
+ * limiting.
27
+ * - Recovery codes are shown once, and stored under a slow hash salted
28
+ * with the user id (~49 bits of entropy each, plus the attempt limit).
29
+ */
30
+ export declare class MfaService {
31
+ private readonly storage;
32
+ private readonly events;
33
+ private static readonly logger;
34
+ private readonly options?;
35
+ private readonly cipher?;
36
+ private readonly lockoutWindow;
37
+ constructor(storage: AuthenticationStorage, options?: {
38
+ mfa?: MfaOptions;
39
+ }, events?: AuthenticationEvents);
40
+ isEnrolled(userId: string): Promise<boolean>;
41
+ /**
42
+ * Starts an enrollment. Throws {@link MfaAlreadyEnrolledError} when the user
43
+ * has a confirmed authenticator, unless `replace` is set: then the new
44
+ * secret is staged and the current authenticator stays active until
45
+ * `confirm()` succeeds with a code from the new one.
46
+ */
47
+ enroll(userId: string, accountName: string, { replace }?: {
48
+ replace?: boolean;
49
+ }): Promise<{
50
+ secret: string;
51
+ uri: string;
52
+ }>;
53
+ /**
54
+ * Activates a pending enrollment (or a staged replacement, which then
55
+ * takes the place of the current authenticator).
56
+ */
57
+ confirm(userId: string, code: string): Promise<boolean>;
58
+ verifyTotp(userId: string, code: string): Promise<boolean>;
59
+ /**
60
+ * Re-encrypts the stored secret with the current key if it was sealed
61
+ * with an older one (or is legacy plaintext under `migratePlaintext`).
62
+ * For key-rotation jobs; returns whether anything was rewritten.
63
+ */
64
+ reencrypt(userId: string): Promise<boolean>;
65
+ /**
66
+ * Returns fresh plaintext codes (show once); replaces any previous batch.
67
+ * Codes are a second factor: for a user with a confirmed authenticator,
68
+ * call it right after `confirm()` succeeded, or from a route that requires
69
+ * a verified second factor (`@Authenticate({ mfa: true })`). Otherwise an
70
+ * API key, or a token issued before the user enrolled, could mint codes
71
+ * that finish a sign-in with the password alone.
72
+ */
73
+ generateRecoveryCodes(userId: string): Promise<string[]>;
74
+ verifyRecoveryCode(userId: string, code: string): Promise<boolean>;
75
+ remainingRecoveryCodes(userId: string): Promise<number>;
76
+ /**
77
+ * Removes the authenticator and the recovery codes. Call it only from a
78
+ * route that requires a verified second factor
79
+ * (`@Authenticate({ mfa: true })`): an API key, or a token issued before
80
+ * the user enrolled, is no second factor.
81
+ */
82
+ disable(userId: string): Promise<void>;
83
+ keyUri(secret: string, accountName: string): string;
84
+ /** `opened` is undefined when the secret could not be decrypted: fail closed, not counted against the user. */
85
+ private check;
86
+ /**
87
+ * Counts an attempt before it is checked, and returns its number within
88
+ * the window, or `undefined` when it must be refused unchecked. Counting
89
+ * first is what stops a burst of parallel guesses: each one gets its own
90
+ * number from the store, and only the first `maxAttempts` are checked.
91
+ * An attempt made while already locked out is not counted, so the lockout
92
+ * ends `lockoutWindow` after it began.
93
+ */
94
+ private begin;
95
+ /** The attempt was already counted by {@link begin}; this reports it. */
96
+ private failed;
97
+ private seal;
98
+ private open;
99
+ private matchingStep;
100
+ /** Read at each call, never in the constructor: the registry locks once the module starts. */
101
+ private get store();
102
+ private get maxAttempts();
103
+ private now;
104
+ }