@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.
- package/LICENSE +23 -0
- package/README.md +50 -0
- package/dist/account/email-verification.handler.d.ts +23 -0
- package/dist/account/email-verification.handler.js +17 -0
- package/dist/account/email-verification.service.d.ts +46 -0
- package/dist/account/email-verification.service.js +111 -0
- package/dist/account/email.util.d.ts +14 -0
- package/dist/account/email.util.js +28 -0
- package/dist/account/in-memory-email-token.store.d.ts +12 -0
- package/dist/account/in-memory-email-token.store.js +60 -0
- package/dist/account/password-reset.handler.d.ts +19 -0
- package/dist/account/password-reset.handler.js +9 -0
- package/dist/account/password-reset.service.d.ts +69 -0
- package/dist/account/password-reset.service.js +204 -0
- package/dist/authentication.constants.d.ts +13 -0
- package/dist/authentication.constants.js +13 -0
- package/dist/authentication.module-definition.d.ts +21 -0
- package/dist/authentication.module-definition.js +147 -0
- package/dist/authentication.module.d.ts +34 -0
- package/dist/authentication.module.js +139 -0
- package/dist/context/authentication-scope.service.d.ts +39 -0
- package/dist/context/authentication-scope.service.js +64 -0
- package/dist/context/authentication.context.d.ts +28 -0
- package/dist/context/authentication.context.js +58 -0
- package/dist/context/index.d.ts +2 -0
- package/dist/context/index.js +2 -0
- package/dist/decorators/authenticate.decorator.d.ts +10 -0
- package/dist/decorators/authenticate.decorator.js +18 -0
- package/dist/decorators/current-session.decorator.d.ts +2 -0
- package/dist/decorators/current-session.decorator.js +6 -0
- package/dist/decorators/current-user.decorator.d.ts +6 -0
- package/dist/decorators/current-user.decorator.js +11 -0
- package/dist/decorators/index.d.ts +4 -0
- package/dist/decorators/index.js +4 -0
- package/dist/decorators/public.decorator.d.ts +5 -0
- package/dist/decorators/public.decorator.js +6 -0
- package/dist/errors/authentication.error.d.ts +27 -0
- package/dist/errors/authentication.error.js +33 -0
- package/dist/errors/index.d.ts +7 -0
- package/dist/errors/index.js +7 -0
- package/dist/errors/jwt.error.d.ts +9 -0
- package/dist/errors/jwt.error.js +12 -0
- package/dist/errors/magic-link.error.d.ts +18 -0
- package/dist/errors/magic-link.error.js +21 -0
- package/dist/errors/mfa-already-enrolled.error.d.ts +14 -0
- package/dist/errors/mfa-already-enrolled.error.js +17 -0
- package/dist/errors/oidc.error.d.ts +15 -0
- package/dist/errors/oidc.error.js +13 -0
- package/dist/errors/refresh-token.error.d.ts +10 -0
- package/dist/errors/refresh-token.error.js +12 -0
- package/dist/errors/secret-decryption.error.d.ts +3 -0
- package/dist/errors/secret-decryption.error.js +6 -0
- package/dist/events/authentication-events.interface.d.ts +121 -0
- package/dist/events/authentication-events.interface.js +1 -0
- package/dist/events/authentication-events.service.d.ts +19 -0
- package/dist/events/authentication-events.service.js +39 -0
- package/dist/events/authentication.channels.d.ts +4 -0
- package/dist/events/authentication.channels.js +11 -0
- package/dist/events/index.d.ts +3 -0
- package/dist/events/index.js +3 -0
- package/dist/guards/authentication.guard.d.ts +57 -0
- package/dist/guards/authentication.guard.js +171 -0
- package/dist/guards/index.d.ts +1 -0
- package/dist/guards/index.js +1 -0
- package/dist/index.d.ts +52 -0
- package/dist/index.js +52 -0
- package/dist/interceptors/authentication-scope.interceptor.d.ts +20 -0
- package/dist/interceptors/authentication-scope.interceptor.js +48 -0
- package/dist/interceptors/index.d.ts +1 -0
- package/dist/interceptors/index.js +1 -0
- package/dist/interfaces/api-key.interface.d.ts +41 -0
- package/dist/interfaces/api-key.interface.js +1 -0
- package/dist/interfaces/authenticate-options.interface.d.ts +31 -0
- package/dist/interfaces/authenticate-options.interface.js +1 -0
- package/dist/interfaces/authentication-error-options.interface.d.ts +8 -0
- package/dist/interfaces/authentication-error-options.interface.js +1 -0
- package/dist/interfaces/authentication-module-options.interface.d.ts +58 -0
- package/dist/interfaces/authentication-module-options.interface.js +1 -0
- package/dist/interfaces/authentication-registry.interface.d.ts +41 -0
- package/dist/interfaces/authentication-registry.interface.js +1 -0
- package/dist/interfaces/authentication-result.interface.d.ts +25 -0
- package/dist/interfaces/authentication-result.interface.js +1 -0
- package/dist/interfaces/authentication-storage.interface.d.ts +29 -0
- package/dist/interfaces/authentication-storage.interface.js +1 -0
- package/dist/interfaces/duration.interface.d.ts +2 -0
- package/dist/interfaces/duration.interface.js +1 -0
- package/dist/interfaces/email-token-store.interface.d.ts +52 -0
- package/dist/interfaces/email-token-store.interface.js +1 -0
- package/dist/interfaces/email-verification.interface.d.ts +18 -0
- package/dist/interfaces/email-verification.interface.js +1 -0
- package/dist/interfaces/index.d.ts +27 -0
- package/dist/interfaces/index.js +27 -0
- package/dist/interfaces/jwt-options.interface.d.ts +68 -0
- package/dist/interfaces/jwt-options.interface.js +1 -0
- package/dist/interfaces/jwt.interface.d.ts +36 -0
- package/dist/interfaces/jwt.interface.js +1 -0
- package/dist/interfaces/magic-link-store.interface.d.ts +36 -0
- package/dist/interfaces/magic-link-store.interface.js +1 -0
- package/dist/interfaces/magic-link.interface.d.ts +55 -0
- package/dist/interfaces/magic-link.interface.js +1 -0
- package/dist/interfaces/mfa-options.interface.d.ts +44 -0
- package/dist/interfaces/mfa-options.interface.js +1 -0
- package/dist/interfaces/mfa-store.interface.d.ts +70 -0
- package/dist/interfaces/mfa-store.interface.js +1 -0
- package/dist/interfaces/oidc-provider.interface.d.ts +56 -0
- package/dist/interfaces/oidc-provider.interface.js +1 -0
- package/dist/interfaces/oidc-state-store.interface.d.ts +25 -0
- package/dist/interfaces/oidc-state-store.interface.js +1 -0
- package/dist/interfaces/oidc.interface.d.ts +84 -0
- package/dist/interfaces/oidc.interface.js +1 -0
- package/dist/interfaces/password-hasher-options.interface.d.ts +17 -0
- package/dist/interfaces/password-hasher-options.interface.js +1 -0
- package/dist/interfaces/password-reset.interface.d.ts +49 -0
- package/dist/interfaces/password-reset.interface.js +1 -0
- package/dist/interfaces/refresh-token-options.interface.d.ts +10 -0
- package/dist/interfaces/refresh-token-options.interface.js +1 -0
- package/dist/interfaces/refresh-token-store.interface.d.ts +61 -0
- package/dist/interfaces/refresh-token-store.interface.js +1 -0
- package/dist/interfaces/session-store.interface.d.ts +53 -0
- package/dist/interfaces/session-store.interface.js +1 -0
- package/dist/interfaces/session.interface.d.ts +50 -0
- package/dist/interfaces/session.interface.js +1 -0
- package/dist/interfaces/sign-in-options.interface.d.ts +9 -0
- package/dist/interfaces/sign-in-options.interface.js +1 -0
- package/dist/interfaces/token.interface.d.ts +30 -0
- package/dist/interfaces/token.interface.js +1 -0
- package/dist/jwt/amr.util.d.ts +9 -0
- package/dist/jwt/amr.util.js +11 -0
- package/dist/jwt/in-memory-refresh-token.store.d.ts +29 -0
- package/dist/jwt/in-memory-refresh-token.store.js +67 -0
- package/dist/jwt/jwks.client.d.ts +34 -0
- package/dist/jwt/jwks.client.js +154 -0
- package/dist/jwt/jws.util.d.ts +45 -0
- package/dist/jwt/jws.util.js +282 -0
- package/dist/jwt/jwt-bearer.provider.d.ts +48 -0
- package/dist/jwt/jwt-bearer.provider.js +126 -0
- package/dist/jwt/jwt-signer.service.d.ts +19 -0
- package/dist/jwt/jwt-signer.service.js +36 -0
- package/dist/jwt/jwt-verifier.service.d.ts +22 -0
- package/dist/jwt/jwt-verifier.service.js +109 -0
- package/dist/jwt/token.service.d.ts +81 -0
- package/dist/jwt/token.service.js +230 -0
- package/dist/magic-link/in-memory-magic-link.store.d.ts +12 -0
- package/dist/magic-link/in-memory-magic-link.store.js +45 -0
- package/dist/magic-link/magic-link.handler.d.ts +25 -0
- package/dist/magic-link/magic-link.handler.js +14 -0
- package/dist/magic-link/magic-link.service.d.ts +79 -0
- package/dist/magic-link/magic-link.service.js +215 -0
- package/dist/mfa/in-memory-mfa.store.d.ts +25 -0
- package/dist/mfa/in-memory-mfa.store.js +73 -0
- package/dist/mfa/mfa.service.d.ts +104 -0
- package/dist/mfa/mfa.service.js +346 -0
- package/dist/mfa/otp.util.d.ts +7 -0
- package/dist/mfa/otp.util.js +52 -0
- package/dist/mfa/secret-cipher.service.d.ts +28 -0
- package/dist/mfa/secret-cipher.service.js +106 -0
- package/dist/oidc/in-memory-oidc-state.store.d.ts +13 -0
- package/dist/oidc/in-memory-oidc-state.store.js +45 -0
- package/dist/oidc/oidc-account.resolver.d.ts +23 -0
- package/dist/oidc/oidc-account.resolver.js +16 -0
- package/dist/oidc/oidc.client.d.ts +44 -0
- package/dist/oidc/oidc.client.js +286 -0
- package/dist/oidc/oidc.presets.d.ts +24 -0
- package/dist/oidc/oidc.presets.js +53 -0
- package/dist/oidc/oidc.service.d.ts +88 -0
- package/dist/oidc/oidc.service.js +259 -0
- package/dist/providers/api-key.provider.d.ts +51 -0
- package/dist/providers/api-key.provider.js +93 -0
- package/dist/providers/authentication.provider.d.ts +40 -0
- package/dist/providers/authentication.provider.js +33 -0
- package/dist/providers/index.d.ts +2 -0
- package/dist/providers/index.js +2 -0
- package/dist/services/authentication-registry.service.d.ts +75 -0
- package/dist/services/authentication-registry.service.js +235 -0
- package/dist/services/index.d.ts +3 -0
- package/dist/services/index.js +3 -0
- package/dist/services/password-hasher.service.d.ts +47 -0
- package/dist/services/password-hasher.service.js +170 -0
- package/dist/services/ws-authenticator.service.d.ts +38 -0
- package/dist/services/ws-authenticator.service.js +92 -0
- package/dist/session/cookies.util.d.ts +34 -0
- package/dist/session/cookies.util.js +82 -0
- package/dist/session/in-memory-session.store.d.ts +33 -0
- package/dist/session/in-memory-session.store.js +60 -0
- package/dist/session/session-cookie.provider.d.ts +36 -0
- package/dist/session/session-cookie.provider.js +58 -0
- package/dist/session/session.service.d.ts +97 -0
- package/dist/session/session.service.js +208 -0
- package/dist/session/sign-in.service.d.ts +87 -0
- package/dist/session/sign-in.service.js +206 -0
- package/dist/storage/authentication.storage.d.ts +130 -0
- package/dist/storage/authentication.storage.js +350 -0
- package/dist/storage/index.d.ts +1 -0
- package/dist/storage/index.js +1 -0
- package/dist/testing/index.d.ts +39 -0
- package/dist/testing/index.js +531 -0
- package/dist/utils/api-key.util.d.ts +10 -0
- package/dist/utils/api-key.util.js +10 -0
- package/dist/utils/auth-state.util.d.ts +54 -0
- package/dist/utils/auth-state.util.js +209 -0
- package/dist/utils/bearer.util.d.ts +9 -0
- package/dist/utils/bearer.util.js +21 -0
- package/dist/utils/cross-origin.util.d.ts +29 -0
- package/dist/utils/cross-origin.util.js +90 -0
- package/dist/utils/crypto.util.d.ts +19 -0
- package/dist/utils/crypto.util.js +38 -0
- package/dist/utils/duration.util.d.ts +9 -0
- package/dist/utils/duration.util.js +44 -0
- package/dist/utils/index.d.ts +8 -0
- package/dist/utils/index.js +8 -0
- package/dist/utils/options.util.d.ts +20 -0
- package/dist/utils/options.util.js +44 -0
- package/dist/utils/transport-errors.util.d.ts +33 -0
- package/dist/utils/transport-errors.util.js +70 -0
- 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
|
+
}
|