@nestjs/authentication 0.0.1 → 0.1.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/README.md +64 -6
- package/dist/account/password-reset.service.d.ts +9 -3
- package/dist/account/password-reset.service.js +16 -4
- package/dist/authentication.constants.d.ts +9 -0
- package/dist/authentication.constants.js +9 -0
- package/dist/authentication.module-definition.js +13 -4
- package/dist/decorators/authenticate.decorator.js +4 -1
- package/dist/decorators/current-user.decorator.d.ts +3 -1
- package/dist/decorators/current-user.decorator.js +2 -0
- package/dist/decorators/public.decorator.d.ts +3 -0
- package/dist/decorators/public.decorator.js +3 -0
- package/dist/errors/authentication.error.d.ts +4 -2
- package/dist/errors/authentication.error.js +6 -2
- package/dist/errors/magic-link.error.d.ts +5 -4
- package/dist/errors/magic-link.error.js +6 -5
- package/dist/events/authentication-events.interface.d.ts +16 -1
- package/dist/events/authentication-events.service.d.ts +2 -1
- package/dist/events/authentication-events.service.js +2 -1
- package/dist/guards/authentication.guard.d.ts +4 -2
- package/dist/guards/authentication.guard.js +9 -6
- package/dist/index.d.ts +7 -2
- package/dist/interceptors/authentication-scope.interceptor.js +2 -2
- package/dist/interfaces/authentication-error-options.interface.d.ts +12 -2
- package/dist/interfaces/authentication-module-options.interface.d.ts +13 -1
- package/dist/interfaces/authentication-result.interface.d.ts +7 -0
- package/dist/interfaces/jwt-options.interface.d.ts +13 -7
- package/dist/interfaces/jwt.interface.d.ts +4 -3
- package/dist/interfaces/magic-link.interface.d.ts +2 -1
- package/dist/interfaces/oidc.interface.d.ts +9 -1
- package/dist/interfaces/session-store.interface.d.ts +29 -4
- package/dist/interfaces/token.interface.d.ts +31 -4
- package/dist/jwt/jwks.client.js +10 -3
- package/dist/jwt/jws.util.d.ts +10 -1
- package/dist/jwt/jws.util.js +108 -16
- package/dist/jwt/jwt-signer.service.js +2 -2
- package/dist/jwt/jwt-verifier.service.js +6 -4
- package/dist/jwt/token.service.d.ts +29 -8
- package/dist/jwt/token.service.js +48 -8
- package/dist/magic-link/magic-link.service.d.ts +4 -1
- package/dist/magic-link/magic-link.service.js +40 -10
- package/dist/mfa/mfa.service.d.ts +11 -1
- package/dist/mfa/mfa.service.js +19 -9
- package/dist/oidc/oidc.client.d.ts +1 -1
- package/dist/oidc/oidc.client.js +1 -1
- package/dist/oidc/oidc.service.d.ts +1 -0
- package/dist/oidc/oidc.service.js +22 -8
- package/dist/session/in-memory-session.store.d.ts +2 -0
- package/dist/session/in-memory-session.store.js +3 -1
- package/dist/session/session-cookie.provider.d.ts +17 -1
- package/dist/session/session-cookie.provider.js +16 -1
- package/dist/session/session.service.d.ts +18 -2
- package/dist/session/session.service.js +43 -8
- package/dist/session/sign-in.service.d.ts +6 -5
- package/dist/session/sign-in.service.js +6 -5
- package/dist/storage/authentication.storage.d.ts +25 -16
- package/dist/storage/authentication.storage.js +25 -16
- package/dist/testing/index.js +13 -5
- package/dist/utils/transport-errors.util.d.ts +23 -12
- package/dist/utils/transport-errors.util.js +34 -22
- package/package.json +28 -28
package/README.md
CHANGED
|
@@ -2,11 +2,6 @@
|
|
|
2
2
|
<a href="http://nestjs.com/" target="blank"><img src="https://nestjs.com/img/logo-small.svg" width="120" alt="Nest Logo" /></a>
|
|
3
3
|
</p>
|
|
4
4
|
|
|
5
|
-
[travis-image]: https://api.travis-ci.org/nestjs/nest.svg?branch=master
|
|
6
|
-
[travis-url]: https://travis-ci.org/nestjs/nest
|
|
7
|
-
[linux-image]: https://img.shields.io/travis/nestjs/nest/master.svg?label=linux
|
|
8
|
-
[linux-url]: https://travis-ci.org/nestjs/nest
|
|
9
|
-
|
|
10
5
|
<p align="center">A progressive <a href="http://nodejs.org" target="blank">Node.js</a> framework for building efficient and scalable server-side applications.</p>
|
|
11
6
|
<p align="center">
|
|
12
7
|
<a href="https://www.npmjs.com/~nestjscore"><img src="https://img.shields.io/npm/v/@nestjs/core.svg" alt="NPM Version" /></a>
|
|
@@ -33,7 +28,70 @@ $ npm i --save @nestjs/authentication
|
|
|
33
28
|
|
|
34
29
|
## Quick Start
|
|
35
30
|
|
|
36
|
-
|
|
31
|
+
Import the module. Its global guard then requires a signed-in user on every route:
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
@Module({
|
|
35
|
+
imports: [
|
|
36
|
+
AuthenticationModule.forRoot({
|
|
37
|
+
accessToken: {
|
|
38
|
+
key: process.env.JWT_SECRET!,
|
|
39
|
+
issuer: 'https://api.example.com',
|
|
40
|
+
audience: 'web',
|
|
41
|
+
ttl: '15m',
|
|
42
|
+
},
|
|
43
|
+
}),
|
|
44
|
+
],
|
|
45
|
+
providers: [JwtAuth],
|
|
46
|
+
})
|
|
47
|
+
export class AppModule {}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Credential providers are ordinary injectable classes that register themselves with `AuthenticationRegistry`:
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
@Injectable()
|
|
54
|
+
export class JwtAuth extends JwtBearerProvider<User> {
|
|
55
|
+
constructor(
|
|
56
|
+
private readonly users: UsersRepository,
|
|
57
|
+
registry: AuthenticationRegistry,
|
|
58
|
+
) {
|
|
59
|
+
super(); // verifies the tokens the module's `accessToken` option signs
|
|
60
|
+
registry.registerProvider(this);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
validate({ sub }: JwtClaims) {
|
|
64
|
+
return sub ? this.users.findById(sub) : null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Then use `@Public()` to opt a route out, and `@Authenticate()` to change what a route requires:
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
@Controller()
|
|
73
|
+
export class AppController {
|
|
74
|
+
@Public()
|
|
75
|
+
@Get('health')
|
|
76
|
+
health() {
|
|
77
|
+
return 'ok';
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
@Get('me')
|
|
81
|
+
me(@CurrentUser() user: User) {
|
|
82
|
+
return user;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Anonymous callers get through too: `user` is then `null`.
|
|
86
|
+
@Authenticate({ optional: true })
|
|
87
|
+
@Get('greeting')
|
|
88
|
+
greeting(@CurrentUser() user: User | null) {
|
|
89
|
+
return user ? `Hello, ${user.email}` : 'Hello, guest';
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
The in-memory stores are fine in development. In production, startup fails until you register real stores with `AuthenticationStorage.registerSource()` for everything your features use (a `SessionCookieProvider` uses the `sessions` store, `accessToken` the `refreshTokens` store unless `refreshToken: false`, and `mfa`, off unless configured, the `mfa` store), or you set `allowInMemoryStorage: true`. Read [Overview & Tutorial](https://docs.nestjs.com/security/authentication) for sessions, refresh tokens, TOTP, magic links, OIDC and account flows.
|
|
37
95
|
|
|
38
96
|
## Support
|
|
39
97
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type OnModuleDestroy } from '@nestjs/common';
|
|
2
|
+
import { AuthenticationScope } from '../context/authentication-scope.service.js';
|
|
2
3
|
import { AuthenticationRegistry } from '../services/authentication-registry.service.js';
|
|
3
4
|
import { AuthenticationStorage } from '../storage/authentication.storage.js';
|
|
4
5
|
import { AuthenticationEvents } from '../events/authentication-events.service.js';
|
|
@@ -35,13 +36,14 @@ export declare class PasswordResetService implements OnModuleDestroy {
|
|
|
35
36
|
private readonly tokens;
|
|
36
37
|
private readonly signInService;
|
|
37
38
|
private readonly events;
|
|
39
|
+
private readonly scope;
|
|
38
40
|
private static readonly logger;
|
|
39
41
|
private readonly options?;
|
|
40
42
|
private readonly ttl;
|
|
41
43
|
private readonly inFlight;
|
|
42
44
|
constructor(storage: AuthenticationStorage, registry: AuthenticationRegistry, hasher: PasswordHasher, sessions: SessionService, tokens: TokenService, signInService: SignInService, options?: {
|
|
43
45
|
passwordReset?: PasswordResetOptions;
|
|
44
|
-
}, events?: AuthenticationEvents);
|
|
46
|
+
}, events?: AuthenticationEvents, scope?: AuthenticationScope);
|
|
45
47
|
/**
|
|
46
48
|
* Starts a reset for `email`, if it belongs to an account: the link goes
|
|
47
49
|
* to `PasswordResetHandler.send()`. Returns at once, before the account is
|
|
@@ -56,12 +58,16 @@ export declare class PasswordResetService implements OnModuleDestroy {
|
|
|
56
58
|
* changed its address or password since the link was sent. `null` too,
|
|
57
59
|
* before the token is looked up (so the link still works), for a password
|
|
58
60
|
* `PasswordHasher` refuses: not a string, empty, or over 4 KiB. Check your
|
|
59
|
-
* own password rules before calling it.
|
|
61
|
+
* own password rules before calling it. With `signIn`, a request from
|
|
62
|
+
* another origin than the app's own and `session.trustedOrigins` gets a
|
|
63
|
+
* `ForbiddenException` first, as `SignInService.signIn()` does, so the link
|
|
64
|
+
* and the password stay as they were. Without `signIn`, the origin is not
|
|
65
|
+
* checked.
|
|
60
66
|
*/
|
|
61
67
|
reset(token: string, password: string, { signIn }?: ResetPasswordOptions): Promise<PasswordResetResult | null>;
|
|
62
68
|
onModuleDestroy(): Promise<void>;
|
|
63
69
|
private issue;
|
|
64
|
-
/** Ends every session and refresh-token family of the user. */
|
|
70
|
+
/** Ends every session and refresh-token family (with refresh tokens on) of the user. */
|
|
65
71
|
private revokeSignIns;
|
|
66
72
|
/** The options and the handler; the module refuses to start with one and not the other. */
|
|
67
73
|
private feature;
|
|
@@ -12,6 +12,7 @@ var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
|
12
12
|
};
|
|
13
13
|
var PasswordResetService_1;
|
|
14
14
|
import { Inject, Injectable, Logger, Optional } from '@nestjs/common';
|
|
15
|
+
import { AuthenticationScope } from '../context/authentication-scope.service.js';
|
|
15
16
|
import { AuthenticationRegistry } from '../services/authentication-registry.service.js';
|
|
16
17
|
import { AuthenticationStorage } from '../storage/authentication.storage.js';
|
|
17
18
|
import { AUTHENTICATION_MODULE_OPTIONS } from '../authentication.constants.js';
|
|
@@ -45,7 +46,7 @@ import { isMailableEmail, normalizeEmail } from './email.util.js';
|
|
|
45
46
|
* the token: mail scanners and link previews follow GET links.
|
|
46
47
|
*/
|
|
47
48
|
let PasswordResetService = PasswordResetService_1 = class PasswordResetService {
|
|
48
|
-
constructor(storage, registry, hasher, sessions, tokens, signInService, options, events = new AuthenticationEvents()) {
|
|
49
|
+
constructor(storage, registry, hasher, sessions, tokens, signInService, options, events = new AuthenticationEvents(), scope = new AuthenticationScope()) {
|
|
49
50
|
this.storage = storage;
|
|
50
51
|
this.registry = registry;
|
|
51
52
|
this.hasher = hasher;
|
|
@@ -53,6 +54,7 @@ let PasswordResetService = PasswordResetService_1 = class PasswordResetService {
|
|
|
53
54
|
this.tokens = tokens;
|
|
54
55
|
this.signInService = signInService;
|
|
55
56
|
this.events = events;
|
|
57
|
+
this.scope = scope;
|
|
56
58
|
this.inFlight = new Set();
|
|
57
59
|
this.options = options?.passwordReset;
|
|
58
60
|
if (this.options) {
|
|
@@ -87,10 +89,19 @@ let PasswordResetService = PasswordResetService_1 = class PasswordResetService {
|
|
|
87
89
|
* changed its address or password since the link was sent. `null` too,
|
|
88
90
|
* before the token is looked up (so the link still works), for a password
|
|
89
91
|
* `PasswordHasher` refuses: not a string, empty, or over 4 KiB. Check your
|
|
90
|
-
* own password rules before calling it.
|
|
92
|
+
* own password rules before calling it. With `signIn`, a request from
|
|
93
|
+
* another origin than the app's own and `session.trustedOrigins` gets a
|
|
94
|
+
* `ForbiddenException` first, as `SignInService.signIn()` does, so the link
|
|
95
|
+
* and the password stay as they were. Without `signIn`, the origin is not
|
|
96
|
+
* checked.
|
|
91
97
|
*/
|
|
92
98
|
async reset(token, password, { signIn = false } = {}) {
|
|
93
99
|
const { handler } = this.feature();
|
|
100
|
+
// Signing in would refuse this request at the end, after the link is spent and the password
|
|
101
|
+
// changed: refuse it before, whatever the token is.
|
|
102
|
+
if (signIn) {
|
|
103
|
+
this.signInService.refuseCrossOrigin(this.scope.exchange()?.request);
|
|
104
|
+
}
|
|
94
105
|
if (typeof token !== 'string' || !TOKEN_PATTERN.test(token) || !isHashablePassword(password)) {
|
|
95
106
|
return null;
|
|
96
107
|
}
|
|
@@ -150,7 +161,7 @@ let PasswordResetService = PasswordResetService_1 = class PasswordResetService {
|
|
|
150
161
|
url.searchParams.set('token', token);
|
|
151
162
|
await handler.send({ userId: account.id, email: account.email, url: url.toString(), expiresAt });
|
|
152
163
|
}
|
|
153
|
-
/** Ends every session and refresh-token family of the user. */
|
|
164
|
+
/** Ends every session and refresh-token family (with refresh tokens on) of the user. */
|
|
154
165
|
async revokeSignIns(userId) {
|
|
155
166
|
await this.sessions.revokeAll(userId);
|
|
156
167
|
await this.tokens.revokeAll(userId);
|
|
@@ -178,7 +189,8 @@ PasswordResetService = PasswordResetService_1 = __decorate([
|
|
|
178
189
|
PasswordHasher,
|
|
179
190
|
SessionService,
|
|
180
191
|
TokenService,
|
|
181
|
-
SignInService, Object, AuthenticationEvents
|
|
192
|
+
SignInService, Object, AuthenticationEvents,
|
|
193
|
+
AuthenticationScope])
|
|
182
194
|
], PasswordResetService);
|
|
183
195
|
export { PasswordResetService };
|
|
184
196
|
/**
|
|
@@ -11,3 +11,12 @@ export declare const AUTHENTICATION_METADATA = "authentication:route";
|
|
|
11
11
|
* so every copy of either package agrees on it.
|
|
12
12
|
*/
|
|
13
13
|
export declare const AUTHENTICATION_GUARD_BRAND: unique symbol;
|
|
14
|
+
/**
|
|
15
|
+
* @internal Whether a route is public, written next to the route metadata by
|
|
16
|
+
* `@Public()` (`true`) and `@Authenticate()` (`false`), on the class or the
|
|
17
|
+
* method: the method's value, when it has one, wins. `@nestjs/authorization`
|
|
18
|
+
* reads it, without importing this package, so that a `@Public()` route
|
|
19
|
+
* needs no `@Can.Anyone()`. A registry symbol, so every copy of either
|
|
20
|
+
* package agrees on it.
|
|
21
|
+
*/
|
|
22
|
+
export declare const AUTHENTICATION_PUBLIC: unique symbol;
|
|
@@ -11,3 +11,12 @@ export const AUTHENTICATION_METADATA = 'authentication:route';
|
|
|
11
11
|
* so every copy of either package agrees on it.
|
|
12
12
|
*/
|
|
13
13
|
export const AUTHENTICATION_GUARD_BRAND = Symbol.for('@nestjs/authentication:guard');
|
|
14
|
+
/**
|
|
15
|
+
* @internal Whether a route is public, written next to the route metadata by
|
|
16
|
+
* `@Public()` (`true`) and `@Authenticate()` (`false`), on the class or the
|
|
17
|
+
* method: the method's value, when it has one, wins. `@nestjs/authorization`
|
|
18
|
+
* reads it, without importing this package, so that a `@Public()` route
|
|
19
|
+
* needs no `@Can.Anyone()`. A registry symbol, so every copy of either
|
|
20
|
+
* package agrees on it.
|
|
21
|
+
*/
|
|
22
|
+
export const AUTHENTICATION_PUBLIC = Symbol.for('@nestjs/authentication:public');
|
|
@@ -30,7 +30,9 @@ export const startupChecks = {
|
|
|
30
30
|
useFactory: (resolved) => {
|
|
31
31
|
const options = resolved ?? {};
|
|
32
32
|
checkKeys(options);
|
|
33
|
-
|
|
33
|
+
const logger = new Logger('AuthenticationModule');
|
|
34
|
+
warnShortLifetimes(options, logger);
|
|
35
|
+
warnIdleSwitches(options, logger);
|
|
34
36
|
return options;
|
|
35
37
|
},
|
|
36
38
|
};
|
|
@@ -49,7 +51,8 @@ export const storageRequirements = {
|
|
|
49
51
|
registry[LOCK_REGISTRY]();
|
|
50
52
|
const enabled = {
|
|
51
53
|
sessionCookie: registry.providers.some((provider) => provider instanceof SessionCookieProvider),
|
|
52
|
-
accessToken:
|
|
54
|
+
// With `accessToken`, unless turned off: they are what `issue()` stores.
|
|
55
|
+
refreshToken: options.accessToken !== undefined && options.refreshToken !== false,
|
|
53
56
|
mfa: options.mfa !== undefined,
|
|
54
57
|
// With their handlers: the registry refuses one without the other.
|
|
55
58
|
magicLink: options.magicLink !== undefined,
|
|
@@ -106,11 +109,17 @@ function checkKeys(options) {
|
|
|
106
109
|
(hint ? `: ${hint}.` : `. The options are ${[...KNOWN_OPTIONS].join(', ')}.`));
|
|
107
110
|
}
|
|
108
111
|
}
|
|
112
|
+
/** A switch that turns off what isn't on: harmless, and likely a forgotten option. */
|
|
113
|
+
function warnIdleSwitches(options, logger) {
|
|
114
|
+
if (options.refreshToken === false && options.accessToken === undefined) {
|
|
115
|
+
logger.warn('`refreshToken: false` has no effect without `accessToken`: refresh tokens are only issued with access tokens.');
|
|
116
|
+
}
|
|
117
|
+
}
|
|
109
118
|
/** The lifetimes: what `@nestjs/jwt` and jsonwebtoken users are used to giving in seconds. */
|
|
110
119
|
const LIFETIMES = [
|
|
111
120
|
['accessToken.ttl', (options) => options.accessToken?.ttl],
|
|
112
|
-
['refreshToken.ttl', (options) => options.refreshToken?.ttl],
|
|
113
|
-
['refreshToken.absoluteTtl', (options) => options.refreshToken?.absoluteTtl],
|
|
121
|
+
['refreshToken.ttl', (options) => (options.refreshToken || undefined)?.ttl],
|
|
122
|
+
['refreshToken.absoluteTtl', (options) => (options.refreshToken || undefined)?.absoluteTtl],
|
|
114
123
|
['session.absoluteTtl', (options) => options.session?.absoluteTtl],
|
|
115
124
|
['session.idleTtl', (options) => options.session?.idleTtl],
|
|
116
125
|
['mfa.pendingTtl', (options) => options.mfa?.pendingTtl],
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AUTHENTICATION_METADATA } from '../authentication.constants.js';
|
|
1
|
+
import { AUTHENTICATION_METADATA, AUTHENTICATION_PUBLIC } from '../authentication.constants.js';
|
|
2
2
|
/** Merges into the metadata already on the class or method, so decorators can be stacked. */
|
|
3
3
|
export function route(value) {
|
|
4
4
|
const defined = Object.fromEntries(Object.entries(value).filter(([, field]) => field !== undefined));
|
|
@@ -6,6 +6,9 @@ export function route(value) {
|
|
|
6
6
|
const holder = descriptor ? descriptor.value : target;
|
|
7
7
|
const existing = Reflect.getMetadata(AUTHENTICATION_METADATA, holder);
|
|
8
8
|
Reflect.defineMetadata(AUTHENTICATION_METADATA, { ...existing, ...defined }, holder);
|
|
9
|
+
if (value.public !== undefined) {
|
|
10
|
+
Reflect.defineMetadata(AUTHENTICATION_PUBLIC, value.public, holder);
|
|
11
|
+
}
|
|
9
12
|
};
|
|
10
13
|
return decorator;
|
|
11
14
|
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { createParamDecorator } from '@nestjs/common';
|
|
2
|
+
import type { AuthenticatedUser } from '../interfaces/authentication-result.interface.js';
|
|
1
3
|
/**
|
|
2
4
|
* The authenticated user, or `null` on `@Public()` and anonymous optional
|
|
3
5
|
* calls. `@CurrentUser('email')` picks one property; the key is checked
|
|
4
6
|
* against the `AuthenticationTypes` augmentation.
|
|
5
7
|
*/
|
|
6
|
-
export declare const CurrentUser:
|
|
8
|
+
export declare const CurrentUser: ReturnType<typeof createParamDecorator<keyof AuthenticatedUser | undefined>>;
|
|
@@ -5,6 +5,8 @@ import { resultOf } from '../utils/auth-state.util.js';
|
|
|
5
5
|
* calls. `@CurrentUser('email')` picks one property; the key is checked
|
|
6
6
|
* against the `AuthenticationTypes` augmentation.
|
|
7
7
|
*/
|
|
8
|
+
// Annotated so the emitted .d.ts keeps `keyof AuthenticatedUser`: inferred, it
|
|
9
|
+
// is resolved at build time against the unaugmented user and becomes `string`.
|
|
8
10
|
export const CurrentUser = createParamDecorator((key, context) => {
|
|
9
11
|
const user = resultOf(context)?.user ?? null;
|
|
10
12
|
return key && user ? user[key] : user;
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Skips authentication: no provider runs, and `@CurrentUser()` is `null`.
|
|
3
3
|
* For health checks, the public catalog, and the routes that sign users in.
|
|
4
|
+
* To `@nestjs/authorization` it means `@Can.Anyone()`: a `@Public()` method
|
|
5
|
+
* lifts its class's `@Can()`, and a `@Can()` on the method itself still
|
|
6
|
+
* applies, to a guest.
|
|
4
7
|
*/
|
|
5
8
|
export declare const Public: () => ClassDecorator & MethodDecorator;
|
|
@@ -2,5 +2,8 @@ import { route } from './authenticate.decorator.js';
|
|
|
2
2
|
/**
|
|
3
3
|
* Skips authentication: no provider runs, and `@CurrentUser()` is `null`.
|
|
4
4
|
* For health checks, the public catalog, and the routes that sign users in.
|
|
5
|
+
* To `@nestjs/authorization` it means `@Can.Anyone()`: a `@Public()` method
|
|
6
|
+
* lifts its class's `@Can()`, and a `@Can()` on the method itself still
|
|
7
|
+
* applies, to a guest.
|
|
5
8
|
*/
|
|
6
9
|
export const Public = () => route({ public: true });
|
|
@@ -10,7 +10,8 @@ import type { AuthenticationErrorOptions } from '../interfaces/authentication-er
|
|
|
10
10
|
* `TokenService.refresh()`), the module's interceptor turns it into the
|
|
11
11
|
* transport's error for its `status`: an HTTP 401 (409 for
|
|
12
12
|
* `MfaAlreadyEnrolledError`), GraphQL `UNAUTHENTICATED`, a `WsException`
|
|
13
|
-
* or an `RpcException`.
|
|
13
|
+
* or an `RpcException`. Its `code` is sent as the body's `errorCode`
|
|
14
|
+
* (and `error`), its `details` as `details`.
|
|
14
15
|
* - Elsewhere (a queue worker), it is a plain `Error`.
|
|
15
16
|
*/
|
|
16
17
|
export declare class AuthenticationError extends Error {
|
|
@@ -23,5 +24,6 @@ export declare class AuthenticationError extends Error {
|
|
|
23
24
|
readonly status: 401 | 403 | 409;
|
|
24
25
|
readonly challenge?: string;
|
|
25
26
|
readonly code?: string;
|
|
26
|
-
|
|
27
|
+
readonly details?: Record<string, unknown>;
|
|
28
|
+
constructor(message?: string, { challenge, code, details, cause }?: AuthenticationErrorOptions);
|
|
27
29
|
}
|
|
@@ -9,11 +9,12 @@
|
|
|
9
9
|
* `TokenService.refresh()`), the module's interceptor turns it into the
|
|
10
10
|
* transport's error for its `status`: an HTTP 401 (409 for
|
|
11
11
|
* `MfaAlreadyEnrolledError`), GraphQL `UNAUTHENTICATED`, a `WsException`
|
|
12
|
-
* or an `RpcException`.
|
|
12
|
+
* or an `RpcException`. Its `code` is sent as the body's `errorCode`
|
|
13
|
+
* (and `error`), its `details` as `details`.
|
|
13
14
|
* - Elsewhere (a queue worker), it is a plain `Error`.
|
|
14
15
|
*/
|
|
15
16
|
export class AuthenticationError extends Error {
|
|
16
|
-
constructor(message = 'Unauthorized', { challenge, code, cause } = {}) {
|
|
17
|
+
constructor(message = 'Unauthorized', { challenge, code, details, cause } = {}) {
|
|
17
18
|
super(message, cause === undefined ? undefined : { cause });
|
|
18
19
|
/**
|
|
19
20
|
* 401, or 409 for `MfaAlreadyEnrolledError`: the caller's fault, not an
|
|
@@ -29,5 +30,8 @@ export class AuthenticationError extends Error {
|
|
|
29
30
|
if (code !== undefined) {
|
|
30
31
|
this.code = code;
|
|
31
32
|
}
|
|
33
|
+
if (details !== undefined) {
|
|
34
|
+
this.details = details;
|
|
35
|
+
}
|
|
32
36
|
}
|
|
33
37
|
}
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import { AuthenticationError } from './authentication.error.js';
|
|
2
2
|
/**
|
|
3
3
|
* A link was presented in a browser other than the one that requested it
|
|
4
|
-
* (see `magicLink.bindToBrowser`)
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* (see `magicLink.bindToBrowser`), or in that browser after it requested
|
|
5
|
+
* five newer ones. The one refusal `consume()` tells apart: the customer
|
|
6
|
+
* can act on it (open the latest link where they asked for it, or request
|
|
7
|
+
* a new one here), and it is decided before the link is
|
|
7
8
|
* looked up, so it reveals nothing about the token. Unknown, used, expired
|
|
8
9
|
* and refused links share one answer, `null`.
|
|
9
10
|
*
|
|
10
11
|
* An `AuthenticationError`: a route that lets it escape answers 401 on any
|
|
11
|
-
* transport, with `not_this_browser` as the body's `error
|
|
12
|
+
* transport, with `not_this_browser` as the body's `errorCode` (and `error`), the way
|
|
12
13
|
* `TokenService.issue()` answers `mfa_required`.
|
|
13
14
|
*/
|
|
14
15
|
export declare class MagicLinkError extends AuthenticationError {
|
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
import { AuthenticationError } from './authentication.error.js';
|
|
2
2
|
/**
|
|
3
3
|
* A link was presented in a browser other than the one that requested it
|
|
4
|
-
* (see `magicLink.bindToBrowser`)
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* (see `magicLink.bindToBrowser`), or in that browser after it requested
|
|
5
|
+
* five newer ones. The one refusal `consume()` tells apart: the customer
|
|
6
|
+
* can act on it (open the latest link where they asked for it, or request
|
|
7
|
+
* a new one here), and it is decided before the link is
|
|
7
8
|
* looked up, so it reveals nothing about the token. Unknown, used, expired
|
|
8
9
|
* and refused links share one answer, `null`.
|
|
9
10
|
*
|
|
10
11
|
* An `AuthenticationError`: a route that lets it escape answers 401 on any
|
|
11
|
-
* transport, with `not_this_browser` as the body's `error
|
|
12
|
+
* transport, with `not_this_browser` as the body's `errorCode` (and `error`), the way
|
|
12
13
|
* `TokenService.issue()` answers `mfa_required`.
|
|
13
14
|
*/
|
|
14
15
|
export class MagicLinkError extends AuthenticationError {
|
|
15
16
|
constructor() {
|
|
16
|
-
super('Open the link in the browser you requested it from, or request a new one here', { code: 'not_this_browser' });
|
|
17
|
+
super('Open the latest link in the browser you requested it from, or request a new one here', { code: 'not_this_browser' });
|
|
17
18
|
/** Why the link was refused: the only reason apps are told. */
|
|
18
19
|
this.reason = 'not-this-browser';
|
|
19
20
|
this.name = 'MagicLinkError';
|
|
@@ -118,4 +118,19 @@ export interface AuthenticationMagicLinkRefusedEvent {
|
|
|
118
118
|
/** The address the link was sent to, once the link was looked up (`expired`, `refused`). */
|
|
119
119
|
email?: string;
|
|
120
120
|
}
|
|
121
|
-
|
|
121
|
+
/**
|
|
122
|
+
* A request's session was valid, but recording its activity
|
|
123
|
+
* (`SessionStore.touchSession()`) failed: a lock timeout, a read-only
|
|
124
|
+
* replica, an outage. The request went on with the session as read, and
|
|
125
|
+
* its idle timeout did not move: unless a later request records activity,
|
|
126
|
+
* the session ends at its previous idle deadline. Each request in a spell
|
|
127
|
+
* of failures publishes one, though `SessionService` logs only the first.
|
|
128
|
+
*/
|
|
129
|
+
export interface AuthenticationSessionTouchFailedEvent {
|
|
130
|
+
type: 'session-touch-failed';
|
|
131
|
+
userId: string;
|
|
132
|
+
sessionId: string;
|
|
133
|
+
/** What `touchSession()` rejected with. */
|
|
134
|
+
error: unknown;
|
|
135
|
+
}
|
|
136
|
+
export type AuthenticationEvent = AuthenticationSignInEvent | AuthenticationSignOutEvent | AuthenticationMfaVerifiedEvent | AuthenticationMfaFailedEvent | AuthenticationMfaEnabledEvent | AuthenticationMfaDisabledEvent | AuthenticationRecoveryCodesGeneratedEvent | AuthenticationRefreshTokenReusedEvent | AuthenticationPasswordResetRequestedEvent | AuthenticationPasswordResetEvent | AuthenticationEmailVerifiedEvent | AuthenticationMagicLinkRefusedEvent | AuthenticationSessionTouchFailedEvent;
|
|
@@ -3,7 +3,8 @@ import { type Observable } from 'rxjs';
|
|
|
3
3
|
import type { AuthenticationEvent } from './authentication-events.interface.js';
|
|
4
4
|
/**
|
|
5
5
|
* The audit trail of sign-ins, sign-outs, second-factor changes, password
|
|
6
|
-
* resets, email verifications and refused magic links
|
|
6
|
+
* resets, email verifications and refused magic links, and the session
|
|
7
|
+
* activity a store failed to record. Each event is also
|
|
7
8
|
* published on its `node:diagnostics_channel` channel
|
|
8
9
|
* (`nestjs:authentication:<type>`), for tooling that runs outside Nest.
|
|
9
10
|
* Listeners cannot change the outcome: a throwing channel subscriber is
|
|
@@ -9,7 +9,8 @@ import { Subject } from 'rxjs';
|
|
|
9
9
|
import { channelFor } from './authentication.channels.js';
|
|
10
10
|
/**
|
|
11
11
|
* The audit trail of sign-ins, sign-outs, second-factor changes, password
|
|
12
|
-
* resets, email verifications and refused magic links
|
|
12
|
+
* resets, email verifications and refused magic links, and the session
|
|
13
|
+
* activity a store failed to record. Each event is also
|
|
13
14
|
* published on its `node:diagnostics_channel` channel
|
|
14
15
|
* (`nestjs:authentication:<type>`), for tooling that runs outside Nest.
|
|
15
16
|
* Listeners cannot change the outcome: a throwing channel subscriber is
|
|
@@ -8,14 +8,16 @@ import { AUTHENTICATION_GUARD_BRAND } from '../authentication.constants.js';
|
|
|
8
8
|
*
|
|
9
9
|
* | Route | No credentials | Valid | Invalid |
|
|
10
10
|
* | --- | --- | --- | --- |
|
|
11
|
-
* | default | 401 | pass | 401 |
|
|
11
|
+
* | default | 401 `missing_credentials` | pass | 401 |
|
|
12
12
|
* | `@Authenticate({ optional: true })` | pass, user `null` | pass | 401 |
|
|
13
13
|
* | `@Public()` | providers not called | not called | not called |
|
|
14
14
|
*
|
|
15
15
|
* A result with `mfa: 'pending'` (second factor outstanding) does not count
|
|
16
16
|
* as signed in: 401 `mfa_required` where a user is required, anonymous on
|
|
17
17
|
* optional routes. `@Authenticate({ mfa: true })` needs `mfa: 'verified'`,
|
|
18
|
-
* and `@Authenticate({ verifiedEmail: true })` a verified address (403
|
|
18
|
+
* and `@Authenticate({ verifiedEmail: true })` a verified address (403
|
|
19
|
+
* `email_unverified`). The code is Nest's `errorCode`; a provider's
|
|
20
|
+
* `AuthenticationError` is answered with its own `code` and `details`.
|
|
19
21
|
*
|
|
20
22
|
* Providers run once per call: the raw result is cached on the request, the
|
|
21
23
|
* GraphQL operation's context, or per ws message. With
|
|
@@ -15,25 +15,29 @@ import { getRawResult, setAuthState, setRawResult } from '../utils/auth-state.ut
|
|
|
15
15
|
import { AuthenticationRegistry } from '../services/authentication-registry.service.js';
|
|
16
16
|
import { AUTHENTICATION_GUARD_BRAND, AUTHENTICATION_METADATA } from '../authentication.constants.js';
|
|
17
17
|
import { AuthenticationError } from '../errors/authentication.error.js';
|
|
18
|
-
import { refusal } from '../utils/transport-errors.util.js';
|
|
18
|
+
import { refusal, refusalOf } from '../utils/transport-errors.util.js';
|
|
19
19
|
/** Which provider produced a result, for `@Authenticate({ providers })` on a cached result. */
|
|
20
20
|
const producedBy = new WeakMap();
|
|
21
21
|
const MFA_REQUIRED = { message: 'Second factor required', code: 'mfa_required' };
|
|
22
22
|
const EMAIL_UNVERIFIED = { status: 403, message: 'Email address not verified', code: 'email_unverified' };
|
|
23
|
+
/** No provider recognised any credentials: `new UnauthorizedException()`'s body, with an `errorCode`. */
|
|
24
|
+
const MISSING_CREDENTIALS = { code: 'missing_credentials' };
|
|
23
25
|
/**
|
|
24
26
|
* Runs the registered providers in order and records the first result on
|
|
25
27
|
* the call.
|
|
26
28
|
*
|
|
27
29
|
* | Route | No credentials | Valid | Invalid |
|
|
28
30
|
* | --- | --- | --- | --- |
|
|
29
|
-
* | default | 401 | pass | 401 |
|
|
31
|
+
* | default | 401 `missing_credentials` | pass | 401 |
|
|
30
32
|
* | `@Authenticate({ optional: true })` | pass, user `null` | pass | 401 |
|
|
31
33
|
* | `@Public()` | providers not called | not called | not called |
|
|
32
34
|
*
|
|
33
35
|
* A result with `mfa: 'pending'` (second factor outstanding) does not count
|
|
34
36
|
* as signed in: 401 `mfa_required` where a user is required, anonymous on
|
|
35
37
|
* optional routes. `@Authenticate({ mfa: true })` needs `mfa: 'verified'`,
|
|
36
|
-
* and `@Authenticate({ verifiedEmail: true })` a verified address (403
|
|
38
|
+
* and `@Authenticate({ verifiedEmail: true })` a verified address (403
|
|
39
|
+
* `email_unverified`). The code is Nest's `errorCode`; a provider's
|
|
40
|
+
* `AuthenticationError` is answered with its own `code` and `details`.
|
|
37
41
|
*
|
|
38
42
|
* Providers run once per call: the raw result is cached on the request, the
|
|
39
43
|
* GraphQL operation's context, or per ws message. With
|
|
@@ -90,7 +94,7 @@ let AuthenticationGuard = class AuthenticationGuard {
|
|
|
90
94
|
if (pending) {
|
|
91
95
|
return this.fail(context, MFA_REQUIRED);
|
|
92
96
|
}
|
|
93
|
-
return this.fail(context,
|
|
97
|
+
return this.fail(context, MISSING_CREDENTIALS, this.challenges(context, providers));
|
|
94
98
|
}
|
|
95
99
|
if (route.mfa && result.mfa !== 'verified') {
|
|
96
100
|
return this.fail(context, MFA_REQUIRED);
|
|
@@ -126,8 +130,7 @@ let AuthenticationGuard = class AuthenticationGuard {
|
|
|
126
130
|
if (everyProvider || error.status === 401) {
|
|
127
131
|
this.forgetConnection(context);
|
|
128
132
|
}
|
|
129
|
-
|
|
130
|
-
return this.fail(context, { status, message, code, cause: error }, error.challenge);
|
|
133
|
+
return this.fail(context, refusalOf(error), error.challenge);
|
|
131
134
|
}
|
|
132
135
|
throw error; // a store outage is a 500, not a 401
|
|
133
136
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -11,7 +11,7 @@ export { AuthenticationProvider } from './providers/index.js';
|
|
|
11
11
|
export { ApiKeyProvider } from './providers/index.js';
|
|
12
12
|
export { AuthenticationError, JwtError, MagicLinkError, MfaAlreadyEnrolledError, RefreshTokenError, } from './errors/index.js';
|
|
13
13
|
export { AuthenticationEvents, type AuthenticationEmailVerifiedEvent, type AuthenticationEvent, type AuthenticationMagicLinkRefusedEvent, type AuthenticationMfaDisabledEvent, type AuthenticationMfaEnabledEvent, type AuthenticationMfaFailedEvent, type AuthenticationMfaVerifiedEvent, type AuthenticationPasswordResetEvent, type AuthenticationPasswordResetRequestedEvent, type AuthenticationRecoveryCodesGeneratedEvent, type AuthenticationRefreshTokenReusedEvent, type AuthenticationSignInEvent, type AuthenticationSignOutEvent, } from './events/index.js';
|
|
14
|
-
export type { ApiKeyProviderOptions, ApiKeyRecord, ApiKeySession, AuthenticateOptions, AuthenticationErrorOptions, AuthenticationHandlerName, AuthenticationHandlers, AuthenticationModuleOptions, AuthenticationOptionsFactory, AuthenticationResult, AuthenticationStorageContract, AuthenticationStorageRegisterOptions, AuthenticationStorageSources, CreatedMagicLink, CredentialProvider, Duration, EmailTokenPurpose, EmailTokenRecord, EmailTokenStore, EmailVerificationLink, EmailVerificationOptions, GeneratedApiKey, IssuedSession, IssueTokensOptions, JwksClientOptions, JwsAlgorithm, JwtBearerProviderOptions, JwtClaims, JwtSignerOptions, JwtVerifierOptions, MagicLink, MagicLinkAccount, MagicLinkOptions, MagicLinkRecord, MagicLinkRequest, MagicLinkStore, MfaOptions, MfaState, MfaStore, OAuthTokens, OidcOptions, OidcProfile, OidcProviderConfig, OidcRedirect, OidcRequest, OidcResolveContext, OidcStartOptions, OidcStateStore, OidcTransaction, PasswordHasherOptions, PasswordResetAccount, PasswordResetLink, PasswordResetOptions, PasswordResetResult, RefreshTokenOptions, RefreshTokenRecord, RefreshTokenStore, RegisterHandlerOptions, RegisterProviderOptions, ResetPasswordOptions, SessionOptions, SessionRecord, SessionStore, SignInOptions, TokenPair, TotpRecord, } from './interfaces/index.js';
|
|
14
|
+
export type { AccessTokenResult, ApiKeyProviderOptions, ApiKeyRecord, ApiKeySession, AuthenticateOptions, AuthenticationErrorOptions, AuthenticationHandlerName, AuthenticationHandlers, AuthenticationModuleOptions, AuthenticationOptionsFactory, AuthenticationResult, AuthenticationStorageContract, AuthenticationStorageRegisterOptions, AuthenticationStorageSources, CreatedMagicLink, CredentialProvider, Duration, EmailTokenPurpose, EmailTokenRecord, EmailTokenStore, EmailVerificationLink, EmailVerificationOptions, GeneratedApiKey, IssuedSession, IssuedTokens, IssueTokensOptions, JwksClientOptions, JwsAlgorithm, JwtBearerProviderOptions, JwtClaims, JwtSignerOptions, JwtVerifierOptions, MagicLink, MagicLinkAccount, MagicLinkOptions, MagicLinkRecord, MagicLinkRequest, MagicLinkStore, MfaOptions, MfaState, MfaStore, OAuthTokens, OidcOptions, OidcProfile, OidcProviderConfig, OidcRedirect, OidcRequest, OidcResolveContext, OidcStartOptions, OidcStateStore, OidcTransaction, PasswordHasherOptions, PasswordResetAccount, PasswordResetLink, PasswordResetOptions, PasswordResetResult, RefreshTokenOptions, RefreshTokenRecord, RefreshTokenStore, RegisterHandlerOptions, RegisterProviderOptions, ResetPasswordOptions, SessionExtra, SessionOptions, SessionRecord, SessionStore, SignInOptions, TokenPair, TotpRecord, } from './interfaces/index.js';
|
|
15
15
|
export { SessionCookieProvider } from './session/session-cookie.provider.js';
|
|
16
16
|
export { InMemorySessionStore } from './session/in-memory-session.store.js';
|
|
17
17
|
export { SessionService } from './session/session.service.js';
|
|
@@ -39,12 +39,17 @@ export { InMemoryOidcStateStore } from './oidc/in-memory-oidc-state.store.js';
|
|
|
39
39
|
export { github, google, microsoft } from './oidc/oidc.presets.js';
|
|
40
40
|
/**
|
|
41
41
|
* Augment once per app to type `@CurrentUser('key')`,
|
|
42
|
-
* `AuthenticationContext.user` and `AuthenticationContext.session
|
|
42
|
+
* `AuthenticationContext.user` and `AuthenticationContext.session`,
|
|
43
|
+
* (`sessionExtra`) what a `SessionStore` reads with each session for
|
|
44
|
+
* `SessionCookieProvider.validate()` (`SessionRecord.extra`), and
|
|
45
|
+
* (`refreshTokens: false`, with the `refreshToken: false` option) that
|
|
46
|
+
* `TokenService.issue()` returns no refresh token ({@link IssuedTokens}):
|
|
43
47
|
*
|
|
44
48
|
* @example
|
|
45
49
|
* declare module '@nestjs/authentication' {
|
|
46
50
|
* interface AuthenticationTypes {
|
|
47
51
|
* user: User;
|
|
52
|
+
* sessionExtra: { user: User };
|
|
48
53
|
* }
|
|
49
54
|
* }
|
|
50
55
|
*/
|
|
@@ -12,7 +12,7 @@ import { HttpAdapterHost } from '@nestjs/core';
|
|
|
12
12
|
import { catchError, from, mergeMap, Observable, throwError } from 'rxjs';
|
|
13
13
|
import { getAuthState } from '../utils/auth-state.util.js';
|
|
14
14
|
import { AuthenticationError } from '../errors/authentication.error.js';
|
|
15
|
-
import { refusal } from '../utils/transport-errors.util.js';
|
|
15
|
+
import { refusal, refusalOf } from '../utils/transport-errors.util.js';
|
|
16
16
|
import { AuthenticationScope, runCall } from '../context/authentication-scope.service.js';
|
|
17
17
|
/**
|
|
18
18
|
* @internal Opens the scope with what the guard recorded, around the
|
|
@@ -35,7 +35,7 @@ let AuthenticationScopeInterceptor = class AuthenticationScopeInterceptor {
|
|
|
35
35
|
if (!(error instanceof AuthenticationError)) {
|
|
36
36
|
return throwError(() => error);
|
|
37
37
|
}
|
|
38
|
-
const mapped = refusal(context,
|
|
38
|
+
const mapped = refusal(context, refusalOf(error), { challenge: error.challenge, adapterHost: this.adapterHost });
|
|
39
39
|
return from(mapped).pipe(mergeMap((exception) => throwError(() => exception)));
|
|
40
40
|
}));
|
|
41
41
|
}
|
|
@@ -1,8 +1,18 @@
|
|
|
1
1
|
export interface AuthenticationErrorOptions {
|
|
2
2
|
/** `WWW-Authenticate` challenge sent with the 401 over HTTP, e.g. `ApiKey header="x-api-key"`. */
|
|
3
3
|
challenge?: string;
|
|
4
|
-
/**
|
|
4
|
+
/**
|
|
5
|
+
* Machine-readable reason (`mfa_required`), sent as Nest's `errorCode`
|
|
6
|
+
* (the HTTP exception's `errorCode`, and the body's on every transport),
|
|
7
|
+
* and as the body's `error` field, which carried it first.
|
|
8
|
+
*/
|
|
5
9
|
code?: string;
|
|
6
|
-
/**
|
|
10
|
+
/**
|
|
11
|
+
* Machine-readable data for the client, sent as the body's `details` field
|
|
12
|
+
* as is (`{ retryAfter: 30 }`): keep it JSON-serialisable, and leave out
|
|
13
|
+
* anything the caller should not see. Not sent unless set.
|
|
14
|
+
*/
|
|
15
|
+
details?: Record<string, unknown>;
|
|
16
|
+
/** The error behind this one, for logs. Never sent. */
|
|
7
17
|
cause?: unknown;
|
|
8
18
|
}
|
|
@@ -26,7 +26,19 @@ export interface AuthenticationModuleOptions {
|
|
|
26
26
|
* verifies unless it is given its own `key` or `jwks`.
|
|
27
27
|
*/
|
|
28
28
|
accessToken?: JwtSignerOptions;
|
|
29
|
-
|
|
29
|
+
/**
|
|
30
|
+
* The refresh tokens `TokenService` issues with each access token, on by
|
|
31
|
+
* default with `accessToken` (which then needs a `RefreshTokenStore`).
|
|
32
|
+
* `false` turns them off: `issue()` returns an access token alone and
|
|
33
|
+
* starts no family, `refresh()` and `revoke()` throw, `revokeAll()` does
|
|
34
|
+
* nothing, and no `RefreshTokenStore` is needed. For a service that signs
|
|
35
|
+
* its own short-lived access tokens, or one that only verifies them
|
|
36
|
+
* (which can also leave `accessToken` out and give its
|
|
37
|
+
* `JwtBearerProvider` the key: `super({ key })`). Declare
|
|
38
|
+
* `refreshTokens: false` on `AuthenticationTypes` to type `issue()`
|
|
39
|
+
* accordingly ({@link IssuedTokens}).
|
|
40
|
+
*/
|
|
41
|
+
refreshToken?: RefreshTokenOptions | false;
|
|
30
42
|
/** Goes with a registered `OidcAccountResolver` (`'oidc'`): each fails at startup without the other. */
|
|
31
43
|
oidc?: OidcOptions;
|
|
32
44
|
/** Goes with a registered `PasswordResetHandler` (`'passwordReset'`): each fails at startup without the other. */
|