@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,209 @@
1
+ import { scopedResult } from '../context/authentication-scope.service.js';
2
+ const AUTH_STATE = Symbol.for('nestjs.authentication.state');
3
+ /** The `session` this package last mirrored on a carrier, to tell it from another package's. */
4
+ const MIRRORED_SESSION = Symbol.for('nestjs.authentication.mirrored-session');
5
+ /**
6
+ * Where a ws client, or GraphQL's `context.req`, carries {@link userOf}, for other packages to call
7
+ * without importing this one (`@nestjs/authorization` does). A registry symbol, so every copy of
8
+ * either package agrees.
9
+ */
10
+ const USER_OF = Symbol.for('nestjs.authentication.userOf');
11
+ /**
12
+ * Per-call state. For HTTP and RPC the carrier below is already per call, and
13
+ * a GraphQL operation keeps its own on its context ({@link stateCarrierOf}). A
14
+ * WebSocket carrier is the socket, which outlives the message and is shared
15
+ * by concurrent messages, so ws calls are also keyed by their message payload
16
+ * (the same object reaches guards, interceptors and param decorators) or, for
17
+ * primitive payloads, by the args array (shared by guards and interceptors).
18
+ */
19
+ const perCall = new WeakMap();
20
+ function callKeys(context) {
21
+ if (context.getType() !== 'ws') {
22
+ return [];
23
+ }
24
+ const data = context.switchToWs().getData();
25
+ const keys = [context.getArgs()];
26
+ if (typeof data === 'object' && data !== null) {
27
+ keys.push(data);
28
+ }
29
+ return keys;
30
+ }
31
+ /**
32
+ * The object that carries the result for one call, mirrored as `user` /
33
+ * `session` where Passport-era code and `@nestjs/authorization` look:
34
+ * the request (http), the client socket (ws), the transport context (rpc),
35
+ * `context.req` (graphql; over graphql-ws, the socket's upgrade request, which
36
+ * every operation of the socket shares).
37
+ */
38
+ function carrierOf(context) {
39
+ switch (context.getType()) {
40
+ case 'http':
41
+ return context.switchToHttp().getRequest();
42
+ case 'ws':
43
+ return context.switchToWs().getClient();
44
+ case 'rpc': {
45
+ const ctx = context.switchToRpc().getContext();
46
+ return typeof ctx === 'object' && ctx !== null ? ctx : undefined;
47
+ }
48
+ case 'graphql':
49
+ return context.getArgByIndex(2)?.req;
50
+ default:
51
+ return undefined;
52
+ }
53
+ }
54
+ /**
55
+ * Records what the handler sees (`@CurrentUser()`, `AuthContext`,
56
+ * `request.user`). `perCallOnly` skips the carrier mirror; used for ws
57
+ * `@Public()` messages, so they do not clear `client.user` under a
58
+ * concurrent message on the same socket.
59
+ */
60
+ export function setAuthState(context, result, { perCallOnly = false } = {}) {
61
+ for (const key of callKeys(context)) {
62
+ perCall.set(key, result);
63
+ }
64
+ const carrier = carrierOf(context);
65
+ const type = context.getType();
66
+ if (carrier && (type === 'ws' || type === 'graphql')) {
67
+ // A function of the call, not a user: safe on a carrier that other calls share.
68
+ carrier[USER_OF] = userOf;
69
+ }
70
+ if (perCallOnly) {
71
+ return;
72
+ }
73
+ const own = stateCarrierOf(context);
74
+ if (own) {
75
+ own[AUTH_STATE] = result;
76
+ }
77
+ if (!carrier) {
78
+ return;
79
+ }
80
+ carrier.user = result?.user ?? null;
81
+ // `request.session` is often another package's (express-session, @fastify/session): mirrored only
82
+ // where it is free, or still holds what this package put there.
83
+ if (!('session' in carrier) || carrier.session === carrier[MIRRORED_SESSION]) {
84
+ carrier.session = result?.session ?? null;
85
+ carrier[MIRRORED_SESSION] = carrier.session;
86
+ }
87
+ }
88
+ /**
89
+ * `undefined` when authentication has not run for this call, `null` when it
90
+ * ran and the caller is anonymous.
91
+ */
92
+ export function getAuthState(context) {
93
+ for (const key of callKeys(context)) {
94
+ if (perCall.has(key)) {
95
+ return perCall.get(key);
96
+ }
97
+ }
98
+ const own = stateCarrierOf(context);
99
+ return own && AUTH_STATE in own ? own[AUTH_STATE] : undefined;
100
+ }
101
+ /**
102
+ * @internal This call's result: the scope the interceptor opened for it, else what the guard
103
+ * recorded on the carrier. A ws message whose payload is a primitive shares nothing else with its
104
+ * guard but the socket, which outlives it, and would otherwise read another message's user.
105
+ */
106
+ export function resultOf(context) {
107
+ const scoped = scopedResult(context);
108
+ return scoped !== undefined ? scoped : getAuthState(context);
109
+ }
110
+ /**
111
+ * The user of one ws message or GraphQL operation, for other packages: they call
112
+ * `carrier[USER_OF](context)` on a ws client, or on GraphQL's `context.req`, that this package
113
+ * has recorded a result on. Both outlive the call: the client is the connection, and over
114
+ * graphql-ws `context.req` is the socket's upgrade request. Their `user` is whatever the last
115
+ * authenticated call left, and a `@Public()` call leaves it alone. This answers what
116
+ * `@CurrentUser()` gets instead: the call's user, `null` when the call is anonymous (a `@Public()`
117
+ * one is), and `undefined` when nothing was recorded, for the message or its connection. A GraphQL
118
+ * operation records its own result, so one that recorded nothing authenticated nothing: `null`,
119
+ * never the `user` another operation left on the `req` they share.
120
+ */
121
+ function userOf(context) {
122
+ const result = resultOf(context);
123
+ if (result === undefined) {
124
+ return context.getType() === 'graphql' ? null : undefined;
125
+ }
126
+ return result?.user ?? null;
127
+ }
128
+ const RAW = Symbol.for('nestjs.authentication.raw');
129
+ const rawPerCall = new WeakMap();
130
+ /**
131
+ * The providers' unfiltered answer, cached so a guard applied twice (global
132
+ * plus `@UseGuards`, or GraphQL root and field resolvers sharing one
133
+ * operation's context) authenticates once. Per message for ws, and per
134
+ * operation for GraphQL: over graphql-ws one upgrade request serves every
135
+ * operation of the socket, and each must see a revocation.
136
+ */
137
+ export function setRawResult(context, result) {
138
+ if (context.getType() === 'ws') {
139
+ for (const key of callKeys(context)) {
140
+ rawPerCall.set(key, result);
141
+ }
142
+ return;
143
+ }
144
+ const own = stateCarrierOf(context);
145
+ if (own) {
146
+ own[RAW] = result;
147
+ }
148
+ }
149
+ export function getRawResult(context) {
150
+ if (context.getType() === 'ws') {
151
+ for (const key of callKeys(context)) {
152
+ if (rawPerCall.has(key)) {
153
+ return rawPerCall.get(key);
154
+ }
155
+ }
156
+ return undefined;
157
+ }
158
+ const own = stateCarrierOf(context);
159
+ return own && RAW in own ? own[RAW] : undefined;
160
+ }
161
+ /**
162
+ * Where a call's own state is kept, the providers' raw answer and the result
163
+ * recorded for the handler: the GraphQL operation's context, else the call's
164
+ * carrier. The context is built for each operation; its `req` is not, over
165
+ * graphql-ws.
166
+ */
167
+ function stateCarrierOf(context) {
168
+ if (context.getType() === 'graphql') {
169
+ const operation = context.getArgByIndex(2);
170
+ return typeof operation === 'object' && operation !== null ? operation : undefined;
171
+ }
172
+ return carrierOf(context);
173
+ }
174
+ /**
175
+ * Request headers for the transports that have them:
176
+ * - `http`: `request.headers`
177
+ * - `ws`: socket.io's `client.handshake.headers`, else `client.request.headers`
178
+ * (`WsAuthenticator.authenticateConnection()` stores the upgrade request
179
+ * there for the `ws` library)
180
+ * - `graphql`: `context.req.headers`
181
+ * - `rpc`: `undefined`; metadata is transport specific
182
+ */
183
+ export function getRequestHeaders(context) {
184
+ return requestOf(context)?.headers;
185
+ }
186
+ /**
187
+ * The request behind a call, as far as authentication needs it: headers,
188
+ * and the method for HTTP and GraphQL (a WebSocket handshake has none, so
189
+ * it counts as a safe request). `undefined` for RPC.
190
+ */
191
+ export function requestOf(context) {
192
+ switch (context.getType()) {
193
+ case 'http':
194
+ return context.switchToHttp().getRequest();
195
+ case 'ws': {
196
+ const client = context.switchToWs().getClient();
197
+ const headers = client?.handshake?.headers ?? client?.request?.headers;
198
+ return headers ? { headers } : undefined;
199
+ }
200
+ case 'graphql':
201
+ return context.getArgByIndex(2)?.req;
202
+ default:
203
+ return undefined;
204
+ }
205
+ }
206
+ export function firstHeader(headers, name) {
207
+ const value = headers?.[name];
208
+ return Array.isArray(value) ? value[0] : value;
209
+ }
@@ -0,0 +1,9 @@
1
+ import type { ExecutionContext } from '@nestjs/common';
2
+ /**
3
+ * The token a bearer provider reads: the `Authorization: Bearer` header's,
4
+ * else, over socket.io, `handshake.auth.token`. `undefined` when there is
5
+ * none, or the header names another scheme (another provider's).
6
+ * `null` for a Bearer header without exactly one token (RFC 6750 §3.1
7
+ * `invalid_request`, answered as `invalid_token` like any bad credential).
8
+ */
9
+ export declare function bearerToken(context: ExecutionContext, authorization: string | undefined): string | null | undefined;
@@ -0,0 +1,21 @@
1
+ /**
2
+ * The token a bearer provider reads: the `Authorization: Bearer` header's,
3
+ * else, over socket.io, `handshake.auth.token`. `undefined` when there is
4
+ * none, or the header names another scheme (another provider's).
5
+ * `null` for a Bearer header without exactly one token (RFC 6750 §3.1
6
+ * `invalid_request`, answered as `invalid_token` like any bad credential).
7
+ */
8
+ export function bearerToken(context, authorization) {
9
+ if (authorization) {
10
+ const [scheme, token, ...rest] = authorization.trim().split(/\s+/);
11
+ if (scheme.toLowerCase() !== 'bearer') {
12
+ return undefined;
13
+ }
14
+ return token && !rest.length ? token : null;
15
+ }
16
+ if (context.getType() === 'ws') {
17
+ const token = context.switchToWs().getClient()?.handshake?.auth?.token;
18
+ return typeof token === 'string' ? token : undefined;
19
+ }
20
+ return undefined;
21
+ }
@@ -0,0 +1,29 @@
1
+ type Headers = Record<string, string | string[] | undefined>;
2
+ /**
3
+ * Whether a request would change state from another origin. It is the
4
+ * decision `app.enableCsrfProtection()` makes from Nest 12.1 on (after Go's
5
+ * `net/http.CrossOriginProtection`), so the two never disagree:
6
+ *
7
+ * 1. `GET`, `HEAD` and `OPTIONS` never are.
8
+ * 2. With `Sec-Fetch-Site`, anything but `same-origin` and `none` is.
9
+ * 3. Without it, a request with no `Origin` is not: it comes from a
10
+ * non-browser client.
11
+ * 4. Otherwise `Origin` must name the request's host (`:authority` on
12
+ * HTTP/2, else `Host`), ignoring case and the scheme's default port.
13
+ *
14
+ * A request whose `Origin` is one of `trusted` never is. A WebSocket
15
+ * handshake (no method here, or a GET with `Upgrade: websocket`) opens a
16
+ * channel that carries writes, so steps 2 to 4 apply to it (cross-site
17
+ * WebSocket hijacking).
18
+ */
19
+ export declare function isCrossOriginWrite(request: {
20
+ headers: Headers;
21
+ method?: string;
22
+ }, trusted: ReadonlySet<string>): boolean;
23
+ /**
24
+ * A trusted origin as browsers serialize `Origin`: lower-case scheme and
25
+ * host, no default port. Fails at startup, naming `option`, for anything
26
+ * that is not `scheme://host[:port]`.
27
+ */
28
+ export declare function normalizeOrigin(origin: unknown, option: string): string;
29
+ export {};
@@ -0,0 +1,90 @@
1
+ const SAFE_METHODS = new Set(['GET', 'HEAD', 'OPTIONS']);
2
+ const DEFAULT_PORTS = { 'http:': '80', 'https:': '443' };
3
+ /**
4
+ * Whether a request would change state from another origin. It is the
5
+ * decision `app.enableCsrfProtection()` makes from Nest 12.1 on (after Go's
6
+ * `net/http.CrossOriginProtection`), so the two never disagree:
7
+ *
8
+ * 1. `GET`, `HEAD` and `OPTIONS` never are.
9
+ * 2. With `Sec-Fetch-Site`, anything but `same-origin` and `none` is.
10
+ * 3. Without it, a request with no `Origin` is not: it comes from a
11
+ * non-browser client.
12
+ * 4. Otherwise `Origin` must name the request's host (`:authority` on
13
+ * HTTP/2, else `Host`), ignoring case and the scheme's default port.
14
+ *
15
+ * A request whose `Origin` is one of `trusted` never is. A WebSocket
16
+ * handshake (no method here, or a GET with `Upgrade: websocket`) opens a
17
+ * channel that carries writes, so steps 2 to 4 apply to it (cross-site
18
+ * WebSocket hijacking).
19
+ */
20
+ export function isCrossOriginWrite(request, trusted) {
21
+ // A GET that upgrades to a WebSocket (graphql-ws hands resolvers its upgrade request) opens a
22
+ // channel that carries writes: a handshake, not a safe request.
23
+ const upgrade = header(request.headers, 'upgrade')?.toLowerCase() === 'websocket';
24
+ if (request.method !== undefined && SAFE_METHODS.has(request.method) && !upgrade) {
25
+ return false;
26
+ }
27
+ const origin = header(request.headers, 'origin');
28
+ const site = header(request.headers, 'sec-fetch-site');
29
+ if (site !== undefined) {
30
+ if (site === 'same-origin' || site === 'none') {
31
+ return false;
32
+ }
33
+ return !(origin !== undefined && trusted.has(origin));
34
+ }
35
+ if (origin === undefined) {
36
+ return false;
37
+ }
38
+ const host = header(request.headers, ':authority') ?? header(request.headers, 'host');
39
+ if (host !== undefined && originMatchesHost(origin, host)) {
40
+ return false;
41
+ }
42
+ return !trusted.has(origin);
43
+ }
44
+ /**
45
+ * A trusted origin as browsers serialize `Origin`: lower-case scheme and
46
+ * host, no default port. Fails at startup, naming `option`, for anything
47
+ * that is not `scheme://host[:port]`.
48
+ */
49
+ export function normalizeOrigin(origin, option) {
50
+ const fail = () => {
51
+ throw new TypeError(`${option}: ${JSON.stringify(origin)} is not an origin. Write it as scheme://host[:port], without a path.`);
52
+ };
53
+ if (typeof origin !== 'string' || !/^[a-z][a-z0-9+.-]*:\/\/[^/?#\\*]+$/i.test(origin)) {
54
+ return fail();
55
+ }
56
+ let url;
57
+ try {
58
+ url = new URL(origin);
59
+ }
60
+ catch {
61
+ return fail();
62
+ }
63
+ if (!url.host || url.username || url.password) {
64
+ fail();
65
+ }
66
+ return `${url.protocol}//${url.host}`;
67
+ }
68
+ function originMatchesHost(origin, host) {
69
+ let url;
70
+ try {
71
+ url = new URL(origin); // `null` (sandboxed frames) and non-URLs never match
72
+ }
73
+ catch {
74
+ return false;
75
+ }
76
+ if (!url.host) {
77
+ return false;
78
+ }
79
+ const authority = host.toLowerCase();
80
+ if (url.host === authority) {
81
+ return true;
82
+ }
83
+ const port = DEFAULT_PORTS[url.protocol];
84
+ return !url.port && port !== undefined && `${url.hostname}:${port}` === authority;
85
+ }
86
+ /** Repeated headers are joined, so conflicting values never equal `same-origin` or a trusted origin. */
87
+ function header(headers, name) {
88
+ const value = headers[name];
89
+ return Array.isArray(value) ? value.join(', ') : value;
90
+ }
@@ -0,0 +1,19 @@
1
+ /** 256 random bits, base64url (43 chars). */
2
+ export declare function randomToken(bytes?: number): string;
3
+ /** Pattern of a {@link randomToken} of 32 bytes. */
4
+ export declare const TOKEN_PATTERN: RegExp;
5
+ /**
6
+ * SHA-256, base64url. Used as the storage key for high-entropy bearer
7
+ * secrets (session ids, magic links, refresh tokens, recovery codes), so a
8
+ * leaked store cannot be replayed. A slow hash is unnecessary at 256 bits.
9
+ */
10
+ export declare function sha256(value: string): string;
11
+ /** Constant-time string comparison (length is not secret). */
12
+ export declare function safeEqual(a: string, b: string): boolean;
13
+ /**
14
+ * Accepts only same-origin relative paths (`/x`, not `//evil`, `/\evil`,
15
+ * or absolute URLs), to prevent open redirects, written only with the
16
+ * printable ASCII a `Location` header carries (anything else
17
+ * percent-encoded, as browsers send it), and at most 2,048 characters.
18
+ */
19
+ export declare function safeRedirectPath(value: unknown): string | undefined;
@@ -0,0 +1,38 @@
1
+ import { createHash, randomBytes, timingSafeEqual } from 'node:crypto';
2
+ /** 256 random bits, base64url (43 chars). */
3
+ export function randomToken(bytes = 32) {
4
+ return randomBytes(bytes).toString('base64url');
5
+ }
6
+ /** Pattern of a {@link randomToken} of 32 bytes. */
7
+ export const TOKEN_PATTERN = /^[A-Za-z0-9_-]{43}$/;
8
+ /**
9
+ * SHA-256, base64url. Used as the storage key for high-entropy bearer
10
+ * secrets (session ids, magic links, refresh tokens, recovery codes), so a
11
+ * leaked store cannot be replayed. A slow hash is unnecessary at 256 bits.
12
+ */
13
+ export function sha256(value) {
14
+ return createHash('sha256').update(value).digest('base64url');
15
+ }
16
+ /** Constant-time string comparison (length is not secret). */
17
+ export function safeEqual(a, b) {
18
+ const left = Buffer.from(a);
19
+ const right = Buffer.from(b);
20
+ return left.length === right.length && timingSafeEqual(left, right);
21
+ }
22
+ /** The longest return path kept: a login or a link stores it until used, and anyone can start one. */
23
+ const MAX_REDIRECT_LENGTH = 2_048;
24
+ /**
25
+ * Accepts only same-origin relative paths (`/x`, not `//evil`, `/\evil`,
26
+ * or absolute URLs), to prevent open redirects, written only with the
27
+ * printable ASCII a `Location` header carries (anything else
28
+ * percent-encoded, as browsers send it), and at most 2,048 characters.
29
+ */
30
+ export function safeRedirectPath(value) {
31
+ if (typeof value !== 'string' || !value.startsWith('/') || value.length > MAX_REDIRECT_LENGTH) {
32
+ return undefined;
33
+ }
34
+ if (value.startsWith('//') || value.includes('\\') || !/^[\x21-\x7e]+$/.test(value)) {
35
+ return undefined;
36
+ }
37
+ return value;
38
+ }
@@ -0,0 +1,9 @@
1
+ import type { Duration } from '../interfaces/duration.interface.js';
2
+ export declare function toMs(duration: Duration): number;
3
+ export declare function durationOr(value: Duration | undefined, fallback: Duration): number;
4
+ /**
5
+ * Whether `expiresAt` has passed at `now` (epoch milliseconds). Fails closed: a date that is not
6
+ * one (`Invalid Date`, from a store that lost or misnamed the column) has passed, where
7
+ * `now >= NaN`, which is always false, would keep a session, token or link alive forever.
8
+ */
9
+ export declare function hasExpired(expiresAt: Date, now: number): boolean;
@@ -0,0 +1,44 @@
1
+ const UNITS = {
2
+ ms: 1,
3
+ s: 1_000,
4
+ m: 60_000,
5
+ h: 3_600_000,
6
+ d: 86_400_000,
7
+ w: 604_800_000,
8
+ };
9
+ /**
10
+ * The longest duration: 100 years. Longer, a lifetime added to the clock leaves the range of
11
+ * `Date` (an Invalid Date), and every token or session made with it would be born expired.
12
+ */
13
+ const MAX_DURATION = 100 * 365.25 * 86_400_000;
14
+ export function toMs(duration) {
15
+ let ms;
16
+ if (typeof duration === 'number') {
17
+ if (!Number.isFinite(duration) || duration < 0) {
18
+ throw new TypeError(`Invalid duration ${duration}. Use a non-negative number of milliseconds.`);
19
+ }
20
+ ms = duration;
21
+ }
22
+ else {
23
+ const match = /^(\d+(?:\.\d+)?)(ms|s|m|h|d|w)$/.exec(duration);
24
+ if (!match) {
25
+ throw new TypeError(`Invalid duration "${duration}". Use milliseconds or a string such as "15m" or "3d".`);
26
+ }
27
+ ms = Math.round(Number(match[1]) * UNITS[match[2]]);
28
+ }
29
+ if (ms > MAX_DURATION) {
30
+ throw new TypeError(`Invalid duration ${typeof duration === 'string' ? `"${duration}"` : duration}: longer than 100 years.`);
31
+ }
32
+ return ms;
33
+ }
34
+ export function durationOr(value, fallback) {
35
+ return toMs(value ?? fallback);
36
+ }
37
+ /**
38
+ * Whether `expiresAt` has passed at `now` (epoch milliseconds). Fails closed: a date that is not
39
+ * one (`Invalid Date`, from a store that lost or misnamed the column) has passed, where
40
+ * `now >= NaN`, which is always false, would keep a session, token or link alive forever.
41
+ */
42
+ export function hasExpired(expiresAt, now) {
43
+ return !(now < expiresAt.getTime());
44
+ }
@@ -0,0 +1,8 @@
1
+ export * from './api-key.util.js';
2
+ export * from './auth-state.util.js';
3
+ export * from './bearer.util.js';
4
+ export * from './cross-origin.util.js';
5
+ export * from './crypto.util.js';
6
+ export * from './duration.util.js';
7
+ export * from './options.util.js';
8
+ export * from './transport-errors.util.js';
@@ -0,0 +1,8 @@
1
+ export * from './api-key.util.js';
2
+ export * from './auth-state.util.js';
3
+ export * from './bearer.util.js';
4
+ export * from './cross-origin.util.js';
5
+ export * from './crypto.util.js';
6
+ export * from './duration.util.js';
7
+ export * from './options.util.js';
8
+ export * from './transport-errors.util.js';
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Startup checks for values that must be there: a `process.env` variable
3
+ * that isn't set reaches the options as `undefined` (or as
4
+ * `"undefined/verify-email"` inside a template string), and would otherwise
5
+ * fail at the first request, or in a constructor with a message about
6
+ * `toString`. Each error names the option.
7
+ */
8
+ /** `value` must be set and not empty. `what` describes it: `a secret of at least 32 bytes or a KeyObject`. */
9
+ export declare function requireOption(value: unknown, option: string, what: string): void;
10
+ /**
11
+ * `value`, when set, must be an integer from `min` to `max`. `Number(process.env.X)` of a variable
12
+ * that isn't set is `NaN`, and a comparison with `NaN` is always false: a limit set that way would
13
+ * silently stop limiting.
14
+ */
15
+ export declare function requireIntegerOption(value: unknown, option: string, { min, max }: {
16
+ min: number;
17
+ max?: number;
18
+ }): void;
19
+ /** `value` must be an absolute http(s) URL. `what` describes it: `the page that receives the link`. */
20
+ export declare function requireUrlOption(value: unknown, option: string, what: string): void;
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Startup checks for values that must be there: a `process.env` variable
3
+ * that isn't set reaches the options as `undefined` (or as
4
+ * `"undefined/verify-email"` inside a template string), and would otherwise
5
+ * fail at the first request, or in a constructor with a message about
6
+ * `toString`. Each error names the option.
7
+ */
8
+ /** `value` must be set and not empty. `what` describes it: `a secret of at least 32 bytes or a KeyObject`. */
9
+ export function requireOption(value, option, what) {
10
+ if (value !== undefined && value !== null && value !== '') {
11
+ return;
12
+ }
13
+ throw new TypeError(`AuthenticationModule: \`${option}\` is required (${what}), but it is ${value === '' ? 'empty' : 'undefined'}: ` +
14
+ 'is the environment variable it reads set?');
15
+ }
16
+ /**
17
+ * `value`, when set, must be an integer from `min` to `max`. `Number(process.env.X)` of a variable
18
+ * that isn't set is `NaN`, and a comparison with `NaN` is always false: a limit set that way would
19
+ * silently stop limiting.
20
+ */
21
+ export function requireIntegerOption(value, option, { min, max = Number.MAX_SAFE_INTEGER }) {
22
+ if (value === undefined || (Number.isSafeInteger(value) && value >= min && value <= max)) {
23
+ return;
24
+ }
25
+ const range = max === Number.MAX_SAFE_INTEGER ? `of at least ${min}` : `from ${min} to ${max}`;
26
+ throw new TypeError(`AuthenticationModule: \`${option}\` must be an integer ${range}, but it is ` +
27
+ `${typeof value === 'string' ? JSON.stringify(value) : String(value)}` +
28
+ (Number.isNaN(value) ? ': is the environment variable it reads set?' : '.'));
29
+ }
30
+ /** `value` must be an absolute http(s) URL. `what` describes it: `the page that receives the link`. */
31
+ export function requireUrlOption(value, option, what) {
32
+ requireOption(value, option, what);
33
+ let url;
34
+ try {
35
+ url = typeof value === 'string' ? new URL(value) : undefined;
36
+ }
37
+ catch {
38
+ url = undefined;
39
+ }
40
+ if (!url || (url.protocol !== 'https:' && url.protocol !== 'http:')) {
41
+ throw new TypeError(`AuthenticationModule: \`${option}\` must be an absolute http(s) URL (${what}). Got ${JSON.stringify(value)}` +
42
+ (typeof value === 'string' && value.startsWith('undefined') ? ': is the environment variable it reads set?' : '.'));
43
+ }
44
+ }
@@ -0,0 +1,33 @@
1
+ import { type ExecutionContext } from '@nestjs/common';
2
+ import type { HttpAdapterHost } from '@nestjs/core';
3
+ export interface Refusal {
4
+ /** Default 401. 403 for a user the route refuses (an unverified address), 409 for a conflict (`MfaAlreadyEnrolledError`). */
5
+ status?: 401 | 403 | 409;
6
+ /** Default `Unauthorized`. */
7
+ message?: string;
8
+ /** Machine-readable reason (`mfa_required`), in the body's `error` field. */
9
+ code?: string;
10
+ /** The error behind the refusal, kept as the exception's `cause` for logs. */
11
+ cause?: unknown;
12
+ }
13
+ /**
14
+ * The one place this package's refusals become responses, for the guard and
15
+ * for `AuthenticationError`s thrown by handlers: the error each transport's
16
+ * exception filter understands, for the refusal's status.
17
+ *
18
+ * - HTTP and GraphQL: Nest's own `UnauthorizedException` (or
19
+ * `ForbiddenException` for a 403, `ConflictException` for a 409), so the body is Nest's own:
20
+ * `{"message":"Unauthorized","statusCode":401}`, or with a message,
21
+ * `{"message":"Refresh token reused","error":"Unauthorized","statusCode":401}`.
22
+ * A `code` replaces `error`. Over HTTP, `challenge` becomes the
23
+ * `WWW-Authenticate` header of a 401.
24
+ * - ws and rpc: a `WsException` / `RpcException` carrying
25
+ * `{ statusCode, message }` (plus `status: 'error'` for ws, and `error`
26
+ * with a `code`). Those filters report `HttpException`s as "Internal
27
+ * server error", hence their own classes, loaded only when the packages
28
+ * are installed.
29
+ */
30
+ export declare function refusal(context: ExecutionContext, { status, message, code, cause }?: Refusal, { challenge, adapterHost }?: {
31
+ challenge?: string;
32
+ adapterHost?: HttpAdapterHost;
33
+ }): Promise<Error>;
@@ -0,0 +1,70 @@
1
+ import { ConflictException, ForbiddenException, UnauthorizedException } from '@nestjs/common';
2
+ const loaded = new Map();
3
+ /** Lazily loads an optional peer's exception class, once. */
4
+ function load(pkg, name) {
5
+ let pending = loaded.get(pkg);
6
+ if (!pending) {
7
+ pending = import(pkg).then((mod) => mod[name], () => undefined);
8
+ loaded.set(pkg, pending);
9
+ }
10
+ return pending;
11
+ }
12
+ const DEFAULT_MESSAGE = 'Unauthorized';
13
+ /**
14
+ * The one place this package's refusals become responses, for the guard and
15
+ * for `AuthenticationError`s thrown by handlers: the error each transport's
16
+ * exception filter understands, for the refusal's status.
17
+ *
18
+ * - HTTP and GraphQL: Nest's own `UnauthorizedException` (or
19
+ * `ForbiddenException` for a 403, `ConflictException` for a 409), so the body is Nest's own:
20
+ * `{"message":"Unauthorized","statusCode":401}`, or with a message,
21
+ * `{"message":"Refresh token reused","error":"Unauthorized","statusCode":401}`.
22
+ * A `code` replaces `error`. Over HTTP, `challenge` becomes the
23
+ * `WWW-Authenticate` header of a 401.
24
+ * - ws and rpc: a `WsException` / `RpcException` carrying
25
+ * `{ statusCode, message }` (plus `status: 'error'` for ws, and `error`
26
+ * with a `code`). Those filters report `HttpException`s as "Internal
27
+ * server error", hence their own classes, loaded only when the packages
28
+ * are installed.
29
+ */
30
+ export async function refusal(context, { status = 401, message = DEFAULT_MESSAGE, code, cause } = {}, { challenge, adapterHost } = {}) {
31
+ const payload = { statusCode: status, ...(code && { error: code }), message };
32
+ switch (context.getType()) {
33
+ case 'ws': {
34
+ const WsException = await load('@nestjs/websockets', 'WsException');
35
+ if (WsException) {
36
+ return withCause(new WsException({ status: 'error', ...payload }), cause);
37
+ }
38
+ break;
39
+ }
40
+ case 'rpc': {
41
+ const RpcException = await load('@nestjs/microservices', 'RpcException');
42
+ if (RpcException) {
43
+ return withCause(new RpcException(payload), cause);
44
+ }
45
+ break;
46
+ }
47
+ case 'http':
48
+ if (challenge && status === 401) {
49
+ adapterHost?.httpAdapter?.setHeader(context.switchToHttp().getResponse(), 'WWW-Authenticate', challenge);
50
+ }
51
+ break;
52
+ }
53
+ const options = { cause, ...(code && { description: code }) };
54
+ if (status === 409) {
55
+ return new ConflictException(message, options);
56
+ }
57
+ if (status === 403) {
58
+ return new ForbiddenException(message, options);
59
+ }
60
+ // The default message gives the body of a bare `new UnauthorizedException()`.
61
+ return message === DEFAULT_MESSAGE && !code
62
+ ? new UnauthorizedException(undefined, options)
63
+ : new UnauthorizedException(message, options);
64
+ }
65
+ function withCause(error, cause) {
66
+ if (cause !== undefined) {
67
+ error.cause = cause;
68
+ }
69
+ return error;
70
+ }