@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,259 @@
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 { BadGatewayException, BadRequestException, ForbiddenException, Inject, Injectable, NotFoundException, Optional, UnauthorizedException, } from '@nestjs/common';
14
+ import { HttpAdapterHost } from '@nestjs/core';
15
+ import { AuthenticationScope } from '../context/authentication-scope.service.js';
16
+ import { AuthenticationRegistry } from '../services/authentication-registry.service.js';
17
+ import { AuthenticationStorage } from '../storage/authentication.storage.js';
18
+ import { AUTHENTICATION_MODULE_OPTIONS } from '../authentication.constants.js';
19
+ import { safeEqual, safeRedirectPath, sha256 } from '../utils/crypto.util.js';
20
+ import { durationOr, hasExpired } from '../utils/duration.util.js';
21
+ import { requireOption } from '../utils/options.util.js';
22
+ import { defaultCookieName, readCookie, serializeCookie } from '../session/cookies.util.js';
23
+ import { SessionService } from '../session/session.service.js';
24
+ import { SignInService } from '../session/sign-in.service.js';
25
+ import { OidcError } from '../errors/oidc.error.js';
26
+ import { OidcClient } from './oidc.client.js';
27
+ const header = (headers, name) => {
28
+ const value = headers[name];
29
+ return Array.isArray(value) ? value[0] : value;
30
+ };
31
+ /**
32
+ * OpenID Connect sign-in, behind two routes of your own: a login route that
33
+ * redirects to the provider, and the callback route it sends the browser
34
+ * back to. Called from an HTTP handler, each method reads the request and
35
+ * sets its cookies on the response, on Express and Fastify alike:
36
+ *
37
+ * ```ts
38
+ * @Public()
39
+ * @Controller('auth/oidc')
40
+ * export class OidcController {
41
+ * constructor(private readonly oidc: OidcService) {}
42
+ *
43
+ * @Get(':provider/login')
44
+ * @Redirect()
45
+ * login(@Param('provider') provider: string, @Query('redirectTo') redirectTo?: string, @Query('link') link?: string) {
46
+ * return this.oidc.start(provider, { redirectTo, link: link === 'true' });
47
+ * }
48
+ *
49
+ * @Get(':provider/callback')
50
+ * @Redirect()
51
+ * callback(@Param('provider') provider: string, @Query() query: Record<string, string>) {
52
+ * return this.oidc.finish(provider, query);
53
+ * }
54
+ * }
55
+ * ```
56
+ *
57
+ * The transaction (PKCE verifier, nonce, return path) stays server-side,
58
+ * keyed by `state`; the browser gets, in an `HttpOnly` cookie, the secret
59
+ * whose SHA-256 is the `state`. The callback must present both, which binds
60
+ * the response to the browser that started the login: another browser
61
+ * cannot finish it (login CSRF), nor can whoever reads the callback URL in a
62
+ * log or a history. `SameSite=Lax` lets the cookie through the IdP's
63
+ * top-level redirect, and the `__Host-` prefix keeps sibling subdomains from
64
+ * planting one.
65
+ *
66
+ * `start(provider, { link: true })` begins a link flow for the signed-in
67
+ * user instead of a sign-in: the callback re-checks that the same session is
68
+ * still live, hands `{ linkTo }` to the resolver, and keeps the session as
69
+ * it is.
70
+ */
71
+ let OidcService = class OidcService {
72
+ constructor(storage, registry, signIn, sessions, options, scope = new AuthenticationScope(), adapterHost) {
73
+ this.storage = storage;
74
+ this.registry = registry;
75
+ this.signIn = signIn;
76
+ this.sessions = sessions;
77
+ this.scope = scope;
78
+ this.adapterHost = adapterHost;
79
+ this.clients = new Map();
80
+ this.options = options?.oidc;
81
+ if (this.options) {
82
+ assertCallbackUrl(this.options.callbackUrl);
83
+ assertProviderCredentials(this.options.providers);
84
+ }
85
+ this.transactionTtl = durationOr(this.options?.transactionTtl, '10m');
86
+ this.txCookieName = defaultCookieName('oidc_tx', this.options?.cookie);
87
+ }
88
+ client(name) {
89
+ const { options } = this.feature();
90
+ const config = Object.hasOwn(options.providers, name) ? options.providers[name] : undefined;
91
+ if (!config) {
92
+ throw new NotFoundException();
93
+ }
94
+ let client = this.clients.get(name);
95
+ if (!client) {
96
+ client = new OidcClient(name, config, options);
97
+ this.clients.set(name, client);
98
+ }
99
+ return client;
100
+ }
101
+ /**
102
+ * Starts a login: stores the transaction, sets its cookie, and returns
103
+ * the provider's authorization URL. 404 for a provider that isn't
104
+ * configured; 401 for a link flow without a signed-in session.
105
+ */
106
+ async start(provider, { redirectTo, link = false, request } = {}) {
107
+ const exchange = this.scope.exchange();
108
+ const req = request ?? exchange?.request ?? { headers: {} };
109
+ let linking;
110
+ if (link) {
111
+ // A link attaches whatever account the provider has signed in on this browser: never at the
112
+ // bidding of another site's page (a top-level navigation carries the Lax session cookie).
113
+ if (header(req.headers, 'sec-fetch-site') === 'cross-site') {
114
+ throw new ForbiddenException('Cross-site link refused');
115
+ }
116
+ const session = await this.currentSession(req);
117
+ if (!session) {
118
+ throw new UnauthorizedException('sign in to link an account');
119
+ }
120
+ linking = { userId: session.userId, sessionId: session.id };
121
+ }
122
+ const { url, transaction, binding } = await this.client(provider)
123
+ .authorizationRequest(this.callbackUrl(provider), safeRedirectPath(redirectTo), this.transactionTtl)
124
+ .catch(toHttp);
125
+ await this.storage.oidcStates.saveOidcState({ ...transaction, ...(linking && { link: linking }) });
126
+ return this.redirect(exchange?.response, url, [this.txCookie(binding, this.transactionTtl / 1000)]);
127
+ }
128
+ /**
129
+ * Completes a login on the callback route, and returns where to send the
130
+ * browser: the login's `redirectTo`, else `redirectAfterLogin`. A sign-in
131
+ * goes through `SignInService`, which sets the session cookie on the
132
+ * response; the transaction cookie is cleared.
133
+ */
134
+ async finish(provider, query, { request } = {}) {
135
+ const { options, resolver } = this.feature();
136
+ const exchange = this.scope.exchange();
137
+ const req = request ?? exchange?.request ?? { headers: {} };
138
+ // The cookie holds the secret whose SHA-256 is the state. The state travels in URLs, the
139
+ // secret never does: a callback URL read from a log is useless without this browser's cookie.
140
+ const state = query.state;
141
+ const bound = readCookie(header(req.headers, 'cookie'), this.txCookieName);
142
+ if (typeof state !== 'string' || !bound || !safeEqual(sha256(bound), state)) {
143
+ throw new BadRequestException('state mismatch');
144
+ }
145
+ const transaction = await this.storage.oidcStates.consumeOidcState(state);
146
+ if (!transaction || transaction.provider !== provider || hasExpired(transaction.expiresAt, this.now())) {
147
+ throw new BadRequestException('unknown or expired login');
148
+ }
149
+ const result = await this.client(provider).callback(query, transaction, this.callbackUrl(provider)).catch(toHttp);
150
+ const location = transaction.redirectTo ?? options.redirectAfterLogin ?? '/';
151
+ if (transaction.link) {
152
+ // The session that asked to link must still be the one on this browser.
153
+ const session = await this.currentSession(req);
154
+ if (session?.id !== transaction.link.sessionId) {
155
+ throw new UnauthorizedException('sign in to link an account');
156
+ }
157
+ const user = await resolver.resolveUser(result.profile, result.tokens, { linkTo: { id: session.userId } });
158
+ if (!user || user.id !== session.userId) {
159
+ throw new ForbiddenException('account not linked');
160
+ }
161
+ return this.redirect(exchange?.response, location, [this.txCookie('', 0)]);
162
+ }
163
+ const user = await resolver.resolveUser(result.profile, result.tokens, {});
164
+ if (!user) {
165
+ throw new ForbiddenException('account not allowed');
166
+ }
167
+ const issued = await this.signIn.signIn(user.id, { method: `oidc:${provider}` });
168
+ // `signIn()` set the session cookie on the response itself; the caller outside HTTP needs it too.
169
+ const { url } = this.redirect(exchange?.response, location, [this.txCookie('', 0)]);
170
+ return { url, cookies: [issued.cookie, this.txCookie('', 0)] };
171
+ }
172
+ /** Sets the cookies (and `no-store`) on the HTTP response, if there is one. */
173
+ redirect(response, url, cookies) {
174
+ const adapter = this.adapterHost?.httpAdapter;
175
+ if (response && adapter) {
176
+ for (const cookie of cookies) {
177
+ adapter.appendHeader(response, 'Set-Cookie', cookie);
178
+ }
179
+ adapter.setHeader(response, 'Cache-Control', 'no-store');
180
+ }
181
+ return { url, cookies };
182
+ }
183
+ /** The options and the resolver; the module refuses to start with one and not the other. */
184
+ feature() {
185
+ const resolver = this.registry.handler('oidc');
186
+ if (!this.options || !resolver) {
187
+ throw new Error('OidcService: OpenID Connect is not enabled. Configure `oidc` in the AuthenticationModule options, and register ' +
188
+ "an OidcAccountResolver: `registry.registerHandler('oidc', this)`.");
189
+ }
190
+ return { options: this.options, resolver };
191
+ }
192
+ /** A fully signed-in session (second factor done, if any) on this request. */
193
+ async currentSession(request) {
194
+ const session = await this.sessions.validate(this.sessions.tokenFrom(request));
195
+ return session && session.mfa !== 'pending' ? session : null;
196
+ }
197
+ callbackUrl(provider) {
198
+ return this.feature().options.callbackUrl.replaceAll(':provider', encodeURIComponent(provider));
199
+ }
200
+ txCookie(value, maxAge) {
201
+ return serializeCookie(this.txCookieName, value, { ...this.options?.cookie, sameSite: 'lax', maxAge });
202
+ }
203
+ now() {
204
+ return this.options?.now?.() ?? Date.now();
205
+ }
206
+ };
207
+ OidcService = __decorate([
208
+ Injectable(),
209
+ __param(4, Optional()),
210
+ __param(4, Inject(AUTHENTICATION_MODULE_OPTIONS)),
211
+ __param(6, Optional()),
212
+ __metadata("design:paramtypes", [AuthenticationStorage,
213
+ AuthenticationRegistry,
214
+ SignInService,
215
+ SessionService, Object, AuthenticationScope,
216
+ HttpAdapterHost])
217
+ ], OidcService);
218
+ export { OidcService };
219
+ /** `oidc.callbackUrl`, checked at startup: an absolute http(s) URL. */
220
+ function assertCallbackUrl(value) {
221
+ let url;
222
+ try {
223
+ url = typeof value === 'string' ? new URL(value.replaceAll(':provider', 'provider')) : undefined;
224
+ }
225
+ catch {
226
+ url = undefined;
227
+ }
228
+ if (!url || (url.protocol !== 'https:' && url.protocol !== 'http:')) {
229
+ throw new Error('OidcService: `oidc.callbackUrl` must be the absolute URL of your callback route, with `:provider` where the ' +
230
+ `provider's name goes, e.g. 'https://app.example.com/auth/oidc/:provider/callback'. Got ${JSON.stringify(value)}.`);
231
+ }
232
+ }
233
+ /**
234
+ * `oidc.providers.<name>.clientId`, and `clientSecret` unless the provider
235
+ * is a public client (`tokenEndpointAuthMethod: 'none'`), checked at
236
+ * startup: an unset environment variable would otherwise turn a
237
+ * confidential client into a public one, and fail at the token exchange.
238
+ */
239
+ function assertProviderCredentials(providers) {
240
+ requireOption(providers, 'oidc.providers', 'the providers users can sign in with, by name');
241
+ for (const [name, config] of Object.entries(providers)) {
242
+ requireOption(config?.clientId, `oidc.providers.${name}.clientId`, 'the client id the provider issued');
243
+ if (config.tokenEndpointAuthMethod !== 'none') {
244
+ requireOption(config.clientSecret, `oidc.providers.${name}.clientSecret`, "the client secret the provider issued; a public client sets `tokenEndpointAuthMethod: 'none'` instead");
245
+ }
246
+ }
247
+ }
248
+ /** An `OidcError` as the HTTP answer: 400 for a bad callback, 401 for a failed check, 502 for a provider that is down. */
249
+ function toHttp(error) {
250
+ if (!(error instanceof OidcError)) {
251
+ throw error;
252
+ }
253
+ const Exception = {
254
+ request: BadRequestException,
255
+ verification: UnauthorizedException,
256
+ unavailable: BadGatewayException,
257
+ }[error.kind];
258
+ throw new Exception(error.message);
259
+ }
@@ -0,0 +1,51 @@
1
+ import type { ExecutionContext } from '@nestjs/common';
2
+ import type { ApiKeyProviderOptions, ApiKeyRecord, ApiKeySession, GeneratedApiKey } from '../interfaces/api-key.interface.js';
3
+ import type { AuthenticationResult } from '../interfaces/authentication-result.interface.js';
4
+ import { AuthenticationProvider } from './authentication.provider.js';
5
+ /**
6
+ * Base class for API keys: `Authorization: Bearer <prefix>_<id>_<secret>`,
7
+ * for scripts and partner systems. Extend it and look a key up by its id in
8
+ * your own table:
9
+ *
10
+ * ```ts
11
+ * @Injectable()
12
+ * export class ApiKeyAuth extends ApiKeyProvider<User> {
13
+ * constructor(private readonly apiKeysRepository: ApiKeysRepository, registry: AuthenticationRegistry) {
14
+ * super({ prefix: 'cat' });
15
+ * registry.registerProvider(this, { order: 2 });
16
+ * }
17
+ * findKey(id: string) {
18
+ * return this.apiKeysRepository.findWithUser(id); // { hash, user, expiresAt } or null
19
+ * }
20
+ * }
21
+ * ```
22
+ *
23
+ * `generate()` makes a key: show the whole key once, and store its `id` and
24
+ * `hash`. Keys carry 256 random bits, so a SHA-256 of the key is all the
25
+ * store needs (a slow hash would only cost CPU on every request), and the
26
+ * hash is compared in constant time, with the same work for an unknown id.
27
+ *
28
+ * A key signs its user in on every route, but never counts as a second
29
+ * factor: `@Authenticate({ mfa: true })` refuses it. Keep keys to the routes
30
+ * meant for them with `@Authenticate({ providers })`. Permissions per key
31
+ * are authorization's job (`@nestjs/authorization`), with the key id from
32
+ * the session. Tokens without this provider's prefix are left to the other
33
+ * providers; a JWT never looks like a key, so `JwtBearerProvider` can run
34
+ * before or after this one.
35
+ */
36
+ export declare abstract class ApiKeyProvider<TUser> extends AuthenticationProvider<TUser, ApiKeySession> {
37
+ private readonly prefix;
38
+ private readonly realm;
39
+ private readonly now;
40
+ constructor({ prefix, realm, now }: ApiKeyProviderOptions);
41
+ /**
42
+ * The key with this id, as stored, or `null` for none: unknown, revoked,
43
+ * or its user is gone. The provider checks the hash and `expiresAt`.
44
+ */
45
+ protected abstract findKey(id: string): ApiKeyRecord<TUser> | null | undefined | Promise<ApiKeyRecord<TUser> | null | undefined>;
46
+ /** A new key with this provider's prefix. */
47
+ generate(): GeneratedApiKey;
48
+ authenticate(context: ExecutionContext): Promise<AuthenticationResult<TUser, ApiKeySession> | null>;
49
+ challenge(): string;
50
+ private reject;
51
+ }
@@ -0,0 +1,93 @@
1
+ import { randomBytes } from 'node:crypto';
2
+ import { AuthenticationError } from '../errors/authentication.error.js';
3
+ import { API_KEY_PATTERN, API_KEY_PREFIX_PATTERN } from '../utils/api-key.util.js';
4
+ import { bearerToken } from '../utils/bearer.util.js';
5
+ import { randomToken, safeEqual, sha256 } from '../utils/crypto.util.js';
6
+ import { hasExpired } from '../utils/duration.util.js';
7
+ import { AuthenticationProvider } from './authentication.provider.js';
8
+ /** Compared against when the id is unknown, so every key costs the same hashing. */
9
+ const NO_KEY = sha256('');
10
+ /**
11
+ * Base class for API keys: `Authorization: Bearer <prefix>_<id>_<secret>`,
12
+ * for scripts and partner systems. Extend it and look a key up by its id in
13
+ * your own table:
14
+ *
15
+ * ```ts
16
+ * @Injectable()
17
+ * export class ApiKeyAuth extends ApiKeyProvider<User> {
18
+ * constructor(private readonly apiKeysRepository: ApiKeysRepository, registry: AuthenticationRegistry) {
19
+ * super({ prefix: 'cat' });
20
+ * registry.registerProvider(this, { order: 2 });
21
+ * }
22
+ * findKey(id: string) {
23
+ * return this.apiKeysRepository.findWithUser(id); // { hash, user, expiresAt } or null
24
+ * }
25
+ * }
26
+ * ```
27
+ *
28
+ * `generate()` makes a key: show the whole key once, and store its `id` and
29
+ * `hash`. Keys carry 256 random bits, so a SHA-256 of the key is all the
30
+ * store needs (a slow hash would only cost CPU on every request), and the
31
+ * hash is compared in constant time, with the same work for an unknown id.
32
+ *
33
+ * A key signs its user in on every route, but never counts as a second
34
+ * factor: `@Authenticate({ mfa: true })` refuses it. Keep keys to the routes
35
+ * meant for them with `@Authenticate({ providers })`. Permissions per key
36
+ * are authorization's job (`@nestjs/authorization`), with the key id from
37
+ * the session. Tokens without this provider's prefix are left to the other
38
+ * providers; a JWT never looks like a key, so `JwtBearerProvider` can run
39
+ * before or after this one.
40
+ */
41
+ export class ApiKeyProvider extends AuthenticationProvider {
42
+ constructor({ prefix, realm = 'api', now = Date.now }) {
43
+ super();
44
+ if (typeof prefix !== 'string' || !API_KEY_PREFIX_PATTERN.test(prefix)) {
45
+ throw new TypeError(`${new.target.name}: \`prefix\` must be 1-16 lowercase letters and digits, starting with a letter ` +
46
+ `(e.g. 'cat' for cat_<id>_<secret>), got ${JSON.stringify(prefix)}.`);
47
+ }
48
+ this.prefix = prefix;
49
+ this.realm = realm;
50
+ this.now = now;
51
+ }
52
+ /** A new key with this provider's prefix. */
53
+ generate() {
54
+ const id = randomBytes(8).toString('hex');
55
+ const key = `${this.prefix}_${id}_${randomToken()}`;
56
+ return { key, id, hash: sha256(key) };
57
+ }
58
+ async authenticate(context) {
59
+ const token = bearerToken(context, this.header(context, 'authorization'));
60
+ if (token === null) {
61
+ this.reject('malformed authorization header');
62
+ }
63
+ if (token === undefined || !token.startsWith(`${this.prefix}_`)) {
64
+ return null;
65
+ }
66
+ const match = API_KEY_PATTERN.exec(token);
67
+ if (!match) {
68
+ this.reject('malformed api key');
69
+ }
70
+ const id = match[2];
71
+ const record = await this.findKey(id);
72
+ // Hash and compare whether or not the id exists.
73
+ const matches = safeEqual(sha256(token), typeof record?.hash === 'string' ? record.hash : NO_KEY);
74
+ if (!record || !matches || record.user === null || record.user === undefined) {
75
+ this.reject('invalid api key');
76
+ }
77
+ const expiresAt = record.expiresAt ?? undefined;
78
+ if (expiresAt && hasExpired(expiresAt, this.now())) {
79
+ this.reject('api key expired');
80
+ }
81
+ const session = { method: 'api-key', keyId: id, ...(expiresAt && { expiresAt }) };
82
+ return { user: record.user, session };
83
+ }
84
+ challenge() {
85
+ return `Bearer realm="${this.realm}"`;
86
+ }
87
+ reject(description) {
88
+ // RFC 6750 §3.1, as JwtBearerProvider answers
89
+ throw new AuthenticationError(description, {
90
+ challenge: `Bearer realm="${this.realm}", error="invalid_token", error_description="${description}"`,
91
+ });
92
+ }
93
+ }
@@ -0,0 +1,40 @@
1
+ import type { ExecutionContext } from '@nestjs/common';
2
+ import type { AuthenticatedSession, AuthenticatedUser, AuthenticationResult } from '../interfaces/authentication-result.interface.js';
3
+ /**
4
+ * A credential source: the contract every provider meets, and the base
5
+ * class of `SessionCookieProvider` and `JwtBearerProvider`.
6
+ *
7
+ * - Resolve to `null` when the request carries no credentials this provider
8
+ * understands; the guard tries the next provider.
9
+ * - Throw `AuthenticationError` when credentials are present but invalid;
10
+ * the guard answers 401, even on `@Authenticate({ optional: true })`
11
+ * routes.
12
+ *
13
+ * Subclasses are ordinary `@Injectable()` classes: they can live in any
14
+ * module and inject repositories, config, etc. The same class serves HTTP,
15
+ * GraphQL, WebSockets and RPC, because it receives the `ExecutionContext`.
16
+ */
17
+ export declare abstract class AuthenticationProvider<TUser = AuthenticatedUser, TSession = AuthenticatedSession> {
18
+ abstract authenticate(context: ExecutionContext): Promise<AuthenticationResult<TUser, TSession> | null> | AuthenticationResult<TUser, TSession> | null;
19
+ /**
20
+ * A request header, whatever the transport: the HTTP request, GraphQL's
21
+ * `context.req`, or the WebSocket handshake. `undefined` over RPC, whose
22
+ * metadata is transport specific. A repeated header gives its first value.
23
+ */
24
+ protected header(context: ExecutionContext, name: string): string | undefined;
25
+ }
26
+ export interface AuthenticationProvider<TUser = AuthenticatedUser, TSession = AuthenticatedSession> {
27
+ /** `WWW-Authenticate` challenge sent with a 401 when no provider found credentials. */
28
+ challenge?(context: ExecutionContext): string | undefined;
29
+ }
30
+ /** A provider class, possibly abstract (`SessionCookieProvider`), for `@Authenticate({ providers })`. */
31
+ export type ProviderClass = abstract new (...args: any[]) => AuthenticationProvider<any, any>;
32
+ /**
33
+ * @internal Called by the module once the provider is resolved, before the
34
+ * first request, so a misconfigured provider fails at startup. `resolve`
35
+ * looks up the module's providers, for instances created with `new` (which
36
+ * Nest did not property-inject).
37
+ */
38
+ export declare const PROVIDER_INIT: unique symbol;
39
+ /** @internal */
40
+ export type ProviderInit = (resolve: <T>(token: string | symbol | (abstract new (...args: any[]) => T)) => T) => void;
@@ -0,0 +1,33 @@
1
+ import { firstHeader, getRequestHeaders } from '../utils/auth-state.util.js';
2
+ /**
3
+ * A credential source: the contract every provider meets, and the base
4
+ * class of `SessionCookieProvider` and `JwtBearerProvider`.
5
+ *
6
+ * - Resolve to `null` when the request carries no credentials this provider
7
+ * understands; the guard tries the next provider.
8
+ * - Throw `AuthenticationError` when credentials are present but invalid;
9
+ * the guard answers 401, even on `@Authenticate({ optional: true })`
10
+ * routes.
11
+ *
12
+ * Subclasses are ordinary `@Injectable()` classes: they can live in any
13
+ * module and inject repositories, config, etc. The same class serves HTTP,
14
+ * GraphQL, WebSockets and RPC, because it receives the `ExecutionContext`.
15
+ */
16
+ // oxlint-disable-next-line typescript/no-unsafe-declaration-merging -- see the interface below
17
+ export class AuthenticationProvider {
18
+ /**
19
+ * A request header, whatever the transport: the HTTP request, GraphQL's
20
+ * `context.req`, or the WebSocket handshake. `undefined` over RPC, whose
21
+ * metadata is transport specific. A repeated header gives its first value.
22
+ */
23
+ header(context, name) {
24
+ return firstHeader(getRequestHeaders(context), name.toLowerCase());
25
+ }
26
+ }
27
+ /**
28
+ * @internal Called by the module once the provider is resolved, before the
29
+ * first request, so a misconfigured provider fails at startup. `resolve`
30
+ * looks up the module's providers, for instances created with `new` (which
31
+ * Nest did not property-inject).
32
+ */
33
+ export const PROVIDER_INIT = Symbol('nestjs.authentication.provider-init');
@@ -0,0 +1,2 @@
1
+ export * from './authentication.provider.js';
2
+ export * from './api-key.provider.js';
@@ -0,0 +1,2 @@
1
+ export * from './authentication.provider.js';
2
+ export * from './api-key.provider.js';
@@ -0,0 +1,75 @@
1
+ import { ModuleRef } from '@nestjs/core';
2
+ import type { AuthenticationHandlers, AuthenticationHandlerName, RegisterProviderOptions, RegisterHandlerOptions, CredentialProvider } from '../interfaces/authentication-registry.interface.js';
3
+ /** @internal Locks the registry. `AuthenticationModule.onModuleInit()` and the first read call it. */
4
+ export declare const LOCK_REGISTRY: unique symbol;
5
+ /**
6
+ * Where the app's credential providers and feature handlers register, from
7
+ * their own constructors, as stores do with `AuthenticationStorage`. They
8
+ * are ordinary providers of the app's modules, so they inject whatever
9
+ * those modules provide:
10
+ *
11
+ * ```ts
12
+ * @Injectable()
13
+ * export class SessionAuth extends SessionCookieProvider<User> {
14
+ * constructor(private readonly users: UsersRepository, registry: AuthenticationRegistry) {
15
+ * super();
16
+ * registry.registerProvider(this);
17
+ * }
18
+ * // ...
19
+ * }
20
+ *
21
+ * @Injectable()
22
+ * export class PasswordResetMailer extends PasswordResetHandler {
23
+ * constructor(private readonly users: UsersRepository, registry: AuthenticationRegistry) {
24
+ * super();
25
+ * registry.registerHandler('passwordReset', this);
26
+ * }
27
+ * // ...
28
+ * }
29
+ * ```
30
+ *
31
+ * The registry locks in `AuthenticationModule`'s `onModuleInit` (every
32
+ * provider constructor has run by then), or at the first read if that is
33
+ * earlier. At the lock it fails when a feature has its option without its
34
+ * handler, or the reverse, and it logs the chain and the handlers.
35
+ */
36
+ export declare class AuthenticationRegistry {
37
+ private readonly options?;
38
+ private readonly moduleRef?;
39
+ private static readonly logger;
40
+ private readonly registered;
41
+ private readonly handlers;
42
+ private chain;
43
+ private locked;
44
+ constructor(options?: Record<string, unknown> | undefined, moduleRef?: ModuleRef | undefined);
45
+ /**
46
+ * Adds a credential provider to the chain the guard runs, in ascending
47
+ * `order` (default `0`). Call it from the constructor of a singleton
48
+ * provider. Throws, registering nothing, for an object without
49
+ * `authenticate()`, an order another provider has (unless `replace` is
50
+ * set), a provider registered twice, and once the registry has locked.
51
+ */
52
+ registerProvider(provider: CredentialProvider, { order, replace }?: RegisterProviderOptions): void;
53
+ /**
54
+ * Registers the handler of a feature: `registerHandler('passwordReset', this)`.
55
+ * The feature also needs its module option. Throws, registering nothing,
56
+ * for an unknown feature, a handler missing one of its methods, a feature
57
+ * that already has one (unless `replace` is set), and once the registry
58
+ * has locked.
59
+ */
60
+ registerHandler<K extends AuthenticationHandlerName>(feature: K, handler: AuthenticationHandlers[K], { replace }?: RegisterHandlerOptions): void;
61
+ /** The credential providers, in the order the guard runs them. Reading them locks the registry. */
62
+ get providers(): readonly CredentialProvider<unknown, unknown>[];
63
+ /** The handler registered for a feature, or `undefined`. Reading it locks the registry. */
64
+ handler<K extends AuthenticationHandlerName>(feature: K): AuthenticationHandlers[K] | undefined;
65
+ /**
66
+ * @internal Checks every feature has both its option and its handler,
67
+ * orders the chain, lets the providers check their configuration, and
68
+ * logs what registered. `log: false` for registries built outside a
69
+ * module (tests, the contract suite).
70
+ */
71
+ [LOCK_REGISTRY]({ log }?: {
72
+ log?: boolean;
73
+ }): void;
74
+ private assertOpen;
75
+ }