@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,82 @@
1
+ /**
2
+ * Only the whitespace browsers strip around a cookie's name and value (RFC
3
+ * 6265 §5.2): space and tab. `String.prototype.trim()` would also strip
4
+ * U+00A0 and U+FEFF, so a sibling subdomain could plant `\xA0__Host-sid`,
5
+ * which browsers store as an unprefixed name, and have it read as `__Host-sid`.
6
+ */
7
+ const trimSpaces = (text) => text.replace(/^[ \t]+|[ \t]+$/g, '');
8
+ /**
9
+ * Reads one cookie from a raw `Cookie` header (RFC 6265 §5.4), so neither
10
+ * `cookie-parser` nor `@fastify/cookie` is needed. The first occurrence
11
+ * wins, as browsers send the most specific path first.
12
+ */
13
+ export function readCookie(header, name) {
14
+ if (!header) {
15
+ return undefined;
16
+ }
17
+ for (const pair of header.split(';')) {
18
+ const eq = pair.indexOf('=');
19
+ if (eq === -1) {
20
+ continue;
21
+ }
22
+ if (trimSpaces(pair.slice(0, eq)) !== name) {
23
+ continue;
24
+ }
25
+ let value = trimSpaces(pair.slice(eq + 1));
26
+ if (value.length >= 2 && value.startsWith('"') && value.endsWith('"')) {
27
+ value = value.slice(1, -1);
28
+ }
29
+ try {
30
+ return decodeURIComponent(value);
31
+ }
32
+ catch {
33
+ return value;
34
+ }
35
+ }
36
+ return undefined;
37
+ }
38
+ /**
39
+ * Whether browsers accept a `__Host-` cookie with these attributes: Secure,
40
+ * `Path=/` and no `Domain`. Such a cookie can only be set by the host
41
+ * itself, so a sibling subdomain cannot plant one (cookie tossing).
42
+ */
43
+ export function allowsHostPrefix(attributes = {}) {
44
+ return (attributes.secure ?? true) && (attributes.path ?? '/') === '/' && !attributes.domain;
45
+ }
46
+ /** `__Host-<name>` when the attributes allow it, `<name>` otherwise. */
47
+ export function defaultCookieName(name, attributes = {}) {
48
+ return allowsHostPrefix(attributes) ? `__Host-${name}` : name;
49
+ }
50
+ /**
51
+ * Fails at startup on a cookie browsers would silently drop: a prefixed name
52
+ * its attributes break, or `SameSite=None` without `Secure` (rejected by
53
+ * every browser: the cookie is never stored, so every sign-in looks like it
54
+ * worked and nothing stays signed in). `option` is the options object the
55
+ * message names (`session`).
56
+ */
57
+ export function assertCookieAttributes(name, attributes = {}, option) {
58
+ if (name.startsWith('__Host-') && !allowsHostPrefix(attributes)) {
59
+ throw new TypeError(`${option}.cookieName: browsers drop a __Host- cookie unless it is Secure, with Path=/ and no Domain.`);
60
+ }
61
+ if (name.startsWith('__Secure-') && !(attributes.secure ?? true)) {
62
+ throw new TypeError(`${option}.cookieName: browsers drop a __Secure- cookie unless it is Secure.`);
63
+ }
64
+ if (attributes.sameSite === 'none' && !(attributes.secure ?? true)) {
65
+ throw new TypeError(`${option}.cookie: browsers drop a SameSite=None cookie unless it is Secure. Keep \`secure\` on (the default), or ` +
66
+ "use sameSite 'lax' for plain-HTTP development.");
67
+ }
68
+ }
69
+ export function serializeCookie(name, value, attributes) {
70
+ const parts = [`${name}=${encodeURIComponent(value)}`, `Max-Age=${Math.max(0, Math.floor(attributes.maxAge))}`];
71
+ parts.push(`Path=${attributes.path ?? '/'}`);
72
+ if (attributes.domain) {
73
+ parts.push(`Domain=${attributes.domain}`);
74
+ }
75
+ parts.push('HttpOnly');
76
+ if (attributes.secure ?? true) {
77
+ parts.push('Secure');
78
+ }
79
+ const sameSite = attributes.sameSite ?? 'lax';
80
+ parts.push(`SameSite=${sameSite[0].toUpperCase()}${sameSite.slice(1)}`);
81
+ return parts.join('; ');
82
+ }
@@ -0,0 +1,33 @@
1
+ import type { SessionRecord, SessionStore } from '../interfaces/session-store.interface.js';
2
+ /**
3
+ * Process-local store: the default when no source is registered, and a
4
+ * test double. Sessions past their absolute expiry are dropped as new ones
5
+ * are written, so an app that runs for months does not grow without bound.
6
+ */
7
+ export declare class InMemorySessionStore implements SessionStore {
8
+ private readonly sessions;
9
+ private sweepAt;
10
+ getSession(id: string): Promise<{
11
+ id: string;
12
+ userId: string;
13
+ createdAt: Date;
14
+ expiresAt: Date;
15
+ lastActiveAt: Date;
16
+ mfa?: import("../index.js").MfaState;
17
+ metadata?: Record<string, unknown>;
18
+ } | undefined>;
19
+ createSession(record: SessionRecord): Promise<void>;
20
+ touchSession(id: string, lastActiveAt: Date): Promise<void>;
21
+ deleteSession(id: string): Promise<boolean>;
22
+ listUserSessions(userId: string): Promise<{
23
+ id: string;
24
+ userId: string;
25
+ createdAt: Date;
26
+ expiresAt: Date;
27
+ lastActiveAt: Date;
28
+ mfa?: import("../index.js").MfaState;
29
+ metadata?: Record<string, unknown>;
30
+ }[]>;
31
+ deleteUserSessions(userId: string): Promise<void>;
32
+ private sweep;
33
+ }
@@ -0,0 +1,60 @@
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. Sessions past their absolute expiry are dropped as new ones
11
+ * are written, so an app that runs for months does not grow without bound.
12
+ */
13
+ let InMemorySessionStore = class InMemorySessionStore {
14
+ constructor() {
15
+ this.sessions = new Map();
16
+ this.sweepAt = 1_024;
17
+ }
18
+ async getSession(id) {
19
+ const record = this.sessions.get(id);
20
+ return record && { ...record };
21
+ }
22
+ async createSession(record) {
23
+ this.sessions.set(record.id, { ...record });
24
+ // The new session's own clock, so tests with a fake one agree.
25
+ if (this.sessions.size >= this.sweepAt) {
26
+ this.sweep(record.lastActiveAt.getTime());
27
+ }
28
+ }
29
+ async touchSession(id, lastActiveAt) {
30
+ const record = this.sessions.get(id);
31
+ if (record && record.lastActiveAt < lastActiveAt) {
32
+ this.sessions.set(id, { ...record, lastActiveAt });
33
+ }
34
+ }
35
+ async deleteSession(id) {
36
+ return this.sessions.delete(id);
37
+ }
38
+ async listUserSessions(userId) {
39
+ return [...this.sessions.values()].filter((s) => s.userId === userId).map((s) => ({ ...s }));
40
+ }
41
+ async deleteUserSessions(userId) {
42
+ for (const [id, s] of this.sessions) {
43
+ if (s.userId === userId) {
44
+ this.sessions.delete(id);
45
+ }
46
+ }
47
+ }
48
+ sweep(now) {
49
+ for (const [id, s] of this.sessions) {
50
+ if (s.expiresAt.getTime() <= now) {
51
+ this.sessions.delete(id);
52
+ }
53
+ }
54
+ this.sweepAt = Math.max(1_024, this.sessions.size * 2);
55
+ }
56
+ };
57
+ InMemorySessionStore = __decorate([
58
+ Injectable()
59
+ ], InMemorySessionStore);
60
+ export { InMemorySessionStore };
@@ -0,0 +1,36 @@
1
+ import { type ExecutionContext } from '@nestjs/common';
2
+ import { AuthenticationProvider, PROVIDER_INIT, type ProviderInit } from '../providers/authentication.provider.js';
3
+ import type { AuthenticationResult } from '../interfaces/authentication-result.interface.js';
4
+ import type { SessionRecord } from '../interfaces/session-store.interface.js';
5
+ import { SessionService } from './session.service.js';
6
+ /**
7
+ * Base class for cookie-session authentication. Extend it and load the
8
+ * user; `SessionService` is property-injected, so the subclass constructor
9
+ * only lists its own dependencies:
10
+ *
11
+ * ```ts
12
+ * @Injectable()
13
+ * export class SessionAuth extends SessionCookieProvider<User> {
14
+ * constructor(private readonly users: UsersRepository) { super(); }
15
+ * validate(session: SessionRecord) {
16
+ * return this.users.findById(session.userId);
17
+ * }
18
+ * }
19
+ * ```
20
+ *
21
+ * A missing, stale or unknown cookie counts as "no credentials" rather than
22
+ * a 401: browsers keep sending them, and optional routes should keep
23
+ * working. So does the cookie on a cross-origin POST, PUT, PATCH or DELETE
24
+ * (CSRF defence on top of `SameSite=Lax`; see `session.trustedOrigins`).
25
+ * Works on socket.io handshakes and `ws` upgrade requests too; check
26
+ * `Origin` in the gateway against cross-site WebSocket hijacking, since
27
+ * browsers send cookies on cross-site upgrades.
28
+ */
29
+ export declare abstract class SessionCookieProvider<TUser> extends AuthenticationProvider<TUser, SessionRecord> {
30
+ protected readonly sessions: SessionService;
31
+ /** Loads the user for a live session; `null` for deleted or banned users, which ends the session. */
32
+ protected abstract validate(session: SessionRecord): TUser | null | undefined | Promise<TUser | null | undefined>;
33
+ authenticate(context: ExecutionContext): Promise<AuthenticationResult<TUser, SessionRecord> | null>;
34
+ /** @internal An instance created with `new` was not property-injected. */
35
+ [PROVIDER_INIT](resolve: Parameters<ProviderInit>[0]): void;
36
+ }
@@ -0,0 +1,58 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ var __metadata = (this && this.__metadata) || function (k, v) {
8
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
9
+ };
10
+ import { Inject } from '@nestjs/common';
11
+ import { requestOf } from '../utils/auth-state.util.js';
12
+ import { AuthenticationProvider, PROVIDER_INIT } from '../providers/authentication.provider.js';
13
+ import { SessionService } from './session.service.js';
14
+ /**
15
+ * Base class for cookie-session authentication. Extend it and load the
16
+ * user; `SessionService` is property-injected, so the subclass constructor
17
+ * only lists its own dependencies:
18
+ *
19
+ * ```ts
20
+ * @Injectable()
21
+ * export class SessionAuth extends SessionCookieProvider<User> {
22
+ * constructor(private readonly users: UsersRepository) { super(); }
23
+ * validate(session: SessionRecord) {
24
+ * return this.users.findById(session.userId);
25
+ * }
26
+ * }
27
+ * ```
28
+ *
29
+ * A missing, stale or unknown cookie counts as "no credentials" rather than
30
+ * a 401: browsers keep sending them, and optional routes should keep
31
+ * working. So does the cookie on a cross-origin POST, PUT, PATCH or DELETE
32
+ * (CSRF defence on top of `SameSite=Lax`; see `session.trustedOrigins`).
33
+ * Works on socket.io handshakes and `ws` upgrade requests too; check
34
+ * `Origin` in the gateway against cross-site WebSocket hijacking, since
35
+ * browsers send cookies on cross-site upgrades.
36
+ */
37
+ export class SessionCookieProvider extends AuthenticationProvider {
38
+ async authenticate(context) {
39
+ const session = await this.sessions.validate(this.sessions.tokenFrom(requestOf(context)));
40
+ if (!session) {
41
+ return null;
42
+ }
43
+ const user = await this.validate(session);
44
+ if (user === null || user === undefined) {
45
+ await this.sessions.discard(session.id);
46
+ return null;
47
+ }
48
+ return { user, session, mfa: session.mfa };
49
+ }
50
+ /** @internal An instance created with `new` was not property-injected. */
51
+ [PROVIDER_INIT](resolve) {
52
+ this.sessions ??= resolve(SessionService);
53
+ }
54
+ }
55
+ __decorate([
56
+ Inject(SessionService),
57
+ __metadata("design:type", SessionService)
58
+ ], SessionCookieProvider.prototype, "sessions", void 0);
@@ -0,0 +1,97 @@
1
+ import type { Duration } from '../interfaces/duration.interface.js';
2
+ import { AuthenticationEvents } from '../events/authentication-events.service.js';
3
+ import type { MfaState } from '../interfaces/authentication-result.interface.js';
4
+ import { AuthenticationStorage } from '../storage/authentication.storage.js';
5
+ import type { SessionRecord } from '../interfaces/session-store.interface.js';
6
+ import type { SessionRequest, SessionOptions, IssuedSession } from '../interfaces/session.interface.js';
7
+ type RequestHeaders = Record<string, string | string[] | undefined>;
8
+ /**
9
+ * Server-side sessions: opaque 256-bit tokens in an `HttpOnly` cookie,
10
+ * stored by their SHA-256.
11
+ *
12
+ * - Absolute expiry plus an idle (sliding) timeout.
13
+ * - `list()`, `revoke()` and `revokeAll()` for "your devices" pages and
14
+ * "sign out everywhere".
15
+ * - `create()`, `validate()` and `rotate()` are the low-level operations
16
+ * behind `SignInService`, which also reads and writes the cookie.
17
+ */
18
+ export declare class SessionService {
19
+ private readonly storage;
20
+ private readonly events;
21
+ private readonly options;
22
+ private readonly absoluteTtl;
23
+ private readonly pendingTtl;
24
+ private readonly idleTtl;
25
+ private readonly touchInterval;
26
+ private readonly cookieName;
27
+ private readonly trustedOrigins;
28
+ constructor(storage: AuthenticationStorage, options?: {
29
+ session?: SessionOptions;
30
+ mfa?: {
31
+ pendingTtl?: Duration;
32
+ };
33
+ }, events?: AuthenticationEvents);
34
+ /**
35
+ * Starts a session. `replacing` is the token of the session this client
36
+ * had before; it is deleted (session fixation defence). An `mfa: 'pending'`
37
+ * session expires after `mfa.pendingTtl` (default `'10m'`) instead of
38
+ * `absoluteTtl`: `rotate()` to `mfa: 'verified'` gives it the full
39
+ * lifetime, counted from its creation.
40
+ */
41
+ create(userId: string, { mfa, metadata, replacing }?: {
42
+ mfa?: MfaState;
43
+ metadata?: Record<string, unknown>;
44
+ replacing?: string;
45
+ }): Promise<IssuedSession>;
46
+ /** The live session for a token, sliding its idle timeout. */
47
+ validate(token: string | undefined): Promise<SessionRecord | null>;
48
+ /**
49
+ * Replaces the session id, keeping its user and absolute expiry. For
50
+ * privilege changes; `SignInService.rotateSession()` does it for the
51
+ * current browser and updates its cookie. A pending session rotated to
52
+ * `mfa: 'verified'` gets the absolute lifetime a verified sign-in has,
53
+ * `absoluteTtl` from its creation, in place of its `mfa.pendingTtl`.
54
+ * Resolves `null` when the session was gone by the time it was replaced:
55
+ * revoked (a sign-out everywhere, a password reset) or rotated by another
56
+ * request, which leaves no new session behind.
57
+ */
58
+ rotate(session: SessionRecord, changes?: Pick<Partial<SessionRecord>, 'mfa' | 'metadata'>): Promise<IssuedSession | null>;
59
+ /**
60
+ * Ends one of `userId`'s sessions. Resolves `false` when the id is unknown
61
+ * or belongs to another user, so a route can pass an id from the URL
62
+ * without checking ownership first.
63
+ */
64
+ revoke(sessionId: string, { userId }: {
65
+ userId: string;
66
+ }): Promise<boolean>;
67
+ /** Ends every session of a user, optionally keeping one (`except`: its id). */
68
+ revokeAll(userId: string, { except }?: {
69
+ except?: string;
70
+ }): Promise<void>;
71
+ /** Live sessions of a user: not past their absolute expiry or their idle timeout. */
72
+ list(userId: string): Promise<SessionRecord[]>;
73
+ /**
74
+ * @internal The session token a request carries. The cookie is ignored on
75
+ * unsafe requests (and WebSocket handshakes) from another origin: see
76
+ * {@link isCrossOriginWrite}.
77
+ */
78
+ tokenFrom(request: {
79
+ headers: RequestHeaders;
80
+ method?: string;
81
+ } | undefined): string | undefined;
82
+ /** @internal Whether `request` changes state from another origin than the app's own and `trustedOrigins`. */
83
+ isCrossOriginWrite(request: {
84
+ headers: RequestHeaders;
85
+ method?: string;
86
+ }): boolean;
87
+ /** @internal */
88
+ metadataFor(request: SessionRequest | undefined): Record<string, unknown> | undefined;
89
+ /** @internal */
90
+ clearCookie(): string;
91
+ /** @internal Deletes a session by id, for sessions whose user is gone. */
92
+ discard(sessionId: string): Promise<void>;
93
+ private isLive;
94
+ private issue;
95
+ private now;
96
+ }
97
+ export {};
@@ -0,0 +1,208 @@
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 { firstHeader } from '../utils/auth-state.util.js';
15
+ import { AUTHENTICATION_MODULE_OPTIONS } from '../authentication.constants.js';
16
+ import { isCrossOriginWrite, normalizeOrigin } from '../utils/cross-origin.util.js';
17
+ import { TOKEN_PATTERN, randomToken, sha256 } from '../utils/crypto.util.js';
18
+ import { durationOr, hasExpired } from '../utils/duration.util.js';
19
+ import { AuthenticationEvents } from '../events/authentication-events.service.js';
20
+ import { AuthenticationStorage } from '../storage/authentication.storage.js';
21
+ import { assertCookieAttributes, defaultCookieName, readCookie, serializeCookie } from './cookies.util.js';
22
+ /**
23
+ * Server-side sessions: opaque 256-bit tokens in an `HttpOnly` cookie,
24
+ * stored by their SHA-256.
25
+ *
26
+ * - Absolute expiry plus an idle (sliding) timeout.
27
+ * - `list()`, `revoke()` and `revokeAll()` for "your devices" pages and
28
+ * "sign out everywhere".
29
+ * - `create()`, `validate()` and `rotate()` are the low-level operations
30
+ * behind `SignInService`, which also reads and writes the cookie.
31
+ */
32
+ let SessionService = class SessionService {
33
+ constructor(storage, options, events = new AuthenticationEvents()) {
34
+ this.storage = storage;
35
+ this.events = events;
36
+ this.options = options?.session ?? {};
37
+ this.absoluteTtl = durationOr(this.options.absoluteTtl, '7d');
38
+ // A pending session never outlives a verified one would.
39
+ this.pendingTtl = Math.min(durationOr(options?.mfa?.pendingTtl, '10m'), this.absoluteTtl);
40
+ this.idleTtl = durationOr(this.options.idleTtl, '1d');
41
+ this.touchInterval = durationOr(this.options.touchInterval, '1m');
42
+ // Activity is recorded at most once per touchInterval: at or past idleTtl, a user active all
43
+ // along would be signed out as idle.
44
+ if (this.idleTtl > 0 && this.touchInterval >= this.idleTtl) {
45
+ throw new TypeError(`session.touchInterval (${this.touchInterval} ms) must be shorter than session.idleTtl (${this.idleTtl} ms): ` +
46
+ 'activity is recorded at most once per touchInterval, so active users would be signed out as idle.');
47
+ }
48
+ this.cookieName = this.options.cookieName ?? defaultCookieName('sid', this.options.cookie);
49
+ assertCookieAttributes(this.cookieName, this.options.cookie, 'session');
50
+ this.trustedOrigins = new Set((this.options.trustedOrigins ?? []).map((origin) => normalizeOrigin(origin, 'session.trustedOrigins')));
51
+ }
52
+ /**
53
+ * Starts a session. `replacing` is the token of the session this client
54
+ * had before; it is deleted (session fixation defence). An `mfa: 'pending'`
55
+ * session expires after `mfa.pendingTtl` (default `'10m'`) instead of
56
+ * `absoluteTtl`: `rotate()` to `mfa: 'verified'` gives it the full
57
+ * lifetime, counted from its creation.
58
+ */
59
+ async create(userId, { mfa, metadata, replacing } = {}) {
60
+ if (replacing && TOKEN_PATTERN.test(replacing)) {
61
+ await this.storage.sessions.deleteSession(sha256(replacing));
62
+ }
63
+ const now = this.now();
64
+ return this.issue({
65
+ userId,
66
+ createdAt: new Date(now),
67
+ expiresAt: new Date(now + (mfa === 'pending' ? this.pendingTtl : this.absoluteTtl)),
68
+ lastActiveAt: new Date(now),
69
+ ...(mfa && { mfa }),
70
+ ...(metadata && { metadata }),
71
+ });
72
+ }
73
+ /** The live session for a token, sliding its idle timeout. */
74
+ async validate(token) {
75
+ if (!token || !TOKEN_PATTERN.test(token)) {
76
+ return null;
77
+ }
78
+ const record = await this.storage.sessions.getSession(sha256(token));
79
+ if (!record) {
80
+ return null;
81
+ }
82
+ const now = this.now();
83
+ if (!this.isLive(record, now)) {
84
+ // Past its absolute expiry, nothing can revive it. Idle, it is only idle as this read saw
85
+ // it: another request (another instance's clock) may have touched it since, so it stays;
86
+ // the store prunes it once its absolute expiry passes.
87
+ if (hasExpired(record.expiresAt, now)) {
88
+ await this.storage.sessions.deleteSession(record.id);
89
+ }
90
+ return null;
91
+ }
92
+ if (now - record.lastActiveAt.getTime() >= this.touchInterval) {
93
+ record.lastActiveAt = new Date(now);
94
+ await this.storage.sessions.touchSession(record.id, record.lastActiveAt);
95
+ }
96
+ return record;
97
+ }
98
+ /**
99
+ * Replaces the session id, keeping its user and absolute expiry. For
100
+ * privilege changes; `SignInService.rotateSession()` does it for the
101
+ * current browser and updates its cookie. A pending session rotated to
102
+ * `mfa: 'verified'` gets the absolute lifetime a verified sign-in has,
103
+ * `absoluteTtl` from its creation, in place of its `mfa.pendingTtl`.
104
+ * Resolves `null` when the session was gone by the time it was replaced:
105
+ * revoked (a sign-out everywhere, a password reset) or rotated by another
106
+ * request, which leaves no new session behind.
107
+ */
108
+ async rotate(session, changes = {}) {
109
+ const { id: _, ...rest } = session;
110
+ const verifying = session.mfa === 'pending' && changes.mfa === 'verified';
111
+ // The new session first, then the old one, which must still be there to delete: if a revocation
112
+ // took it meanwhile (deleting the user's sessions, this new one included, or not yet), or another
113
+ // rotation did, this one lost, and its new session must not outlive the old.
114
+ const issued = await this.issue({
115
+ ...rest,
116
+ ...changes,
117
+ lastActiveAt: new Date(this.now()),
118
+ ...(verifying && { expiresAt: new Date(session.createdAt.getTime() + this.absoluteTtl) }),
119
+ });
120
+ if (!(await this.storage.sessions.deleteSession(session.id))) {
121
+ await this.storage.sessions.deleteSession(issued.session.id);
122
+ return null;
123
+ }
124
+ return issued;
125
+ }
126
+ /**
127
+ * Ends one of `userId`'s sessions. Resolves `false` when the id is unknown
128
+ * or belongs to another user, so a route can pass an id from the URL
129
+ * without checking ownership first.
130
+ */
131
+ async revoke(sessionId, { userId }) {
132
+ const record = typeof sessionId === 'string' ? await this.storage.sessions.getSession(sessionId) : undefined;
133
+ if (!record || record.userId !== userId) {
134
+ return false;
135
+ }
136
+ await this.storage.sessions.deleteSession(record.id);
137
+ this.events.emit({ type: 'sign-out', userId, sessionId: record.id });
138
+ return true;
139
+ }
140
+ /** Ends every session of a user, optionally keeping one (`except`: its id). */
141
+ async revokeAll(userId, { except } = {}) {
142
+ const store = this.storage.sessions;
143
+ if (!except) {
144
+ return store.deleteUserSessions(userId);
145
+ }
146
+ for (const session of await store.listUserSessions(userId)) {
147
+ if (session.id !== except) {
148
+ await store.deleteSession(session.id);
149
+ }
150
+ }
151
+ }
152
+ /** Live sessions of a user: not past their absolute expiry or their idle timeout. */
153
+ async list(userId) {
154
+ const now = this.now();
155
+ return (await this.storage.sessions.listUserSessions(userId)).filter((s) => this.isLive(s, now));
156
+ }
157
+ /**
158
+ * @internal The session token a request carries. The cookie is ignored on
159
+ * unsafe requests (and WebSocket handshakes) from another origin: see
160
+ * {@link isCrossOriginWrite}.
161
+ */
162
+ tokenFrom(request) {
163
+ if (!request || this.isCrossOriginWrite(request)) {
164
+ return undefined;
165
+ }
166
+ const token = readCookie(firstHeader(request.headers, 'cookie'), this.cookieName);
167
+ return token && TOKEN_PATTERN.test(token) ? token : undefined;
168
+ }
169
+ /** @internal Whether `request` changes state from another origin than the app's own and `trustedOrigins`. */
170
+ isCrossOriginWrite(request) {
171
+ return isCrossOriginWrite(request, this.trustedOrigins);
172
+ }
173
+ /** @internal */
174
+ metadataFor(request) {
175
+ return request && this.options.metadata ? this.options.metadata(request) : undefined;
176
+ }
177
+ /** @internal */
178
+ clearCookie() {
179
+ return serializeCookie(this.cookieName, '', { ...this.options.cookie, maxAge: 0 });
180
+ }
181
+ /** @internal Deletes a session by id, for sessions whose user is gone. */
182
+ async discard(sessionId) {
183
+ await this.storage.sessions.deleteSession(sessionId);
184
+ }
185
+ isLive(record, now) {
186
+ if (hasExpired(record.expiresAt, now)) {
187
+ return false;
188
+ }
189
+ return this.idleTtl === 0 || now < record.lastActiveAt.getTime() + this.idleTtl;
190
+ }
191
+ async issue(fields) {
192
+ const token = randomToken();
193
+ const session = { id: sha256(token), ...fields };
194
+ await this.storage.sessions.createSession(session);
195
+ const maxAge = Math.round((session.expiresAt.getTime() - this.now()) / 1000);
196
+ return { session, token, cookie: serializeCookie(this.cookieName, token, { ...this.options.cookie, maxAge }) };
197
+ }
198
+ now() {
199
+ return this.options.now?.() ?? Date.now();
200
+ }
201
+ };
202
+ SessionService = __decorate([
203
+ Injectable(),
204
+ __param(1, Optional()),
205
+ __param(1, Inject(AUTHENTICATION_MODULE_OPTIONS)),
206
+ __metadata("design:paramtypes", [AuthenticationStorage, Object, AuthenticationEvents])
207
+ ], SessionService);
208
+ export { SessionService };
@@ -0,0 +1,87 @@
1
+ import { HttpAdapterHost } from '@nestjs/core';
2
+ import { AuthenticationScope, type HttpExchange } from '../context/authentication-scope.service.js';
3
+ import { AuthenticationEvents } from '../events/authentication-events.service.js';
4
+ import { TokenService } from '../jwt/token.service.js';
5
+ import { MfaService } from '../mfa/mfa.service.js';
6
+ import type { IssuedSession } from '../interfaces/session.interface.js';
7
+ import type { SignInOptions } from '../interfaces/sign-in-options.interface.js';
8
+ import { SessionService } from './session.service.js';
9
+ /**
10
+ * The browser's side of signing in. Called from an HTTP handler (or a
11
+ * GraphQL resolver whose context has `req` and `res`), each method reads
12
+ * this browser's session cookie from the request and sets the new one on
13
+ * the response, on Express and Fastify alike. Elsewhere, nothing is read or
14
+ * set: send the returned `cookie` yourself.
15
+ *
16
+ * `signIn()` is the one place a first factor (password, magic link, OIDC)
17
+ * turns into a session:
18
+ * - a sign-in posted from another origin's page is refused with a 403
19
+ * (login CSRF: signing the victim's browser in to the attacker's
20
+ * account), by the same rule as `app.enableCsrfProtection()`;
21
+ * - the browser's previous session is deleted (session fixation defence);
22
+ * - users with a confirmed authenticator get an `mfa: 'pending'` session,
23
+ * which is not signed in anywhere until `completeMfa()` verifies a code.
24
+ */
25
+ export declare class SignInService {
26
+ private readonly sessions;
27
+ private readonly mfa;
28
+ private readonly tokens;
29
+ private readonly scope;
30
+ private readonly adapterHost?;
31
+ private readonly events;
32
+ constructor(sessions: SessionService, mfa: MfaService, tokens: TokenService, scope?: AuthenticationScope, adapterHost?: HttpAdapterHost | undefined, events?: AuthenticationEvents);
33
+ /**
34
+ * Signs the user in on this browser. Throws a `ForbiddenException` for a
35
+ * request that changes state from another origin than the app's own and
36
+ * `session.trustedOrigins`.
37
+ */
38
+ signIn(userId: string, { method, metadata }?: SignInOptions): Promise<IssuedSession>;
39
+ /**
40
+ * Completes the second factor of this browser's session with a TOTP code
41
+ * or a recovery code, and rotates the session to `mfa: 'verified'` under a
42
+ * new id. `null` when the browser has no session or the code is wrong. A
43
+ * request with neither counts nothing against the user's lockout, as
44
+ * `TokenService.issue()` without a `secondFactor` doesn't.
45
+ */
46
+ completeMfa({ code, recoveryCode }: {
47
+ code?: string;
48
+ recoveryCode?: string;
49
+ }): Promise<IssuedSession | null>;
50
+ /**
51
+ * Confirms the user's new authenticator with a code from it
52
+ * (`MfaService.confirm()`), and, when this browser holds a session of the
53
+ * user, rotates it to `mfa: 'verified'` under a new id: the code proved
54
+ * this browser has the authenticator, so the user isn't asked for another
55
+ * code until their next sign-in. Without such a session (a token client,
56
+ * another user's session), it only confirms. `false` when the code is
57
+ * wrong, and nothing changes.
58
+ */
59
+ confirmMfa(userId: string, code: string): Promise<boolean>;
60
+ /**
61
+ * Gives this browser's session a new id, keeping its user, expiry and
62
+ * second factor, and updates the cookie. Call it on privilege changes: a
63
+ * new password, a new role. `null` when the browser has no session, or
64
+ * lost it meanwhile (revoked, or rotated by another request).
65
+ */
66
+ rotateSession(): Promise<IssuedSession | null>;
67
+ /**
68
+ * Ends this browser's session, pending or not, and clears its cookie.
69
+ * `false` when the browser had no live session (a bearer-token client, an
70
+ * expired cookie).
71
+ */
72
+ signOut(): Promise<boolean>;
73
+ /**
74
+ * Ends every session and refresh-token family of the user, and clears
75
+ * this browser's cookie if it was one of them. API keys live in your own
76
+ * table: revoke those yourself.
77
+ */
78
+ signOutEverywhere(userId: string): Promise<void>;
79
+ /**
80
+ * @internal The 403 `signIn()` answers a request that changes state from
81
+ * another origin than the app's own and `session.trustedOrigins` with.
82
+ * What binds a sign-in to a browser (a magic link's `create()`) checks it
83
+ * too: another site's page must not start one in the victim's browser.
84
+ */
85
+ refuseCrossOrigin(request: HttpExchange['request'] | undefined): void;
86
+ private setCookie;
87
+ }