@nestjs/authentication 0.0.1 → 0.1.0
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 +1 -1
- package/dist/account/password-reset.service.js +1 -1
- 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 +1 -1
- package/dist/errors/magic-link.error.js +1 -1
- 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/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/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/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 +25 -25
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
|
|
|
@@ -61,7 +61,7 @@ export declare class PasswordResetService implements OnModuleDestroy {
|
|
|
61
61
|
reset(token: string, password: string, { signIn }?: ResetPasswordOptions): Promise<PasswordResetResult | null>;
|
|
62
62
|
onModuleDestroy(): Promise<void>;
|
|
63
63
|
private issue;
|
|
64
|
-
/** Ends every session and refresh-token family of the user. */
|
|
64
|
+
/** Ends every session and refresh-token family (with refresh tokens on) of the user. */
|
|
65
65
|
private revokeSignIns;
|
|
66
66
|
/** The options and the handler; the module refuses to start with one and not the other. */
|
|
67
67
|
private feature;
|
|
@@ -150,7 +150,7 @@ let PasswordResetService = PasswordResetService_1 = class PasswordResetService {
|
|
|
150
150
|
url.searchParams.set('token', token);
|
|
151
151
|
await handler.send({ userId: account.id, email: account.email, url: url.toString(), expiresAt });
|
|
152
152
|
}
|
|
153
|
-
/** Ends every session and refresh-token family of the user. */
|
|
153
|
+
/** Ends every session and refresh-token family (with refresh tokens on) of the user. */
|
|
154
154
|
async revokeSignIns(userId) {
|
|
155
155
|
await this.sessions.revokeAll(userId);
|
|
156
156
|
await this.tokens.revokeAll(userId);
|
|
@@ -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
|
}
|
|
@@ -8,7 +8,7 @@ import { AuthenticationError } from './authentication.error.js';
|
|
|
8
8
|
* and refused links share one answer, `null`.
|
|
9
9
|
*
|
|
10
10
|
* An `AuthenticationError`: a route that lets it escape answers 401 on any
|
|
11
|
-
* transport, with `not_this_browser` as the body's `error
|
|
11
|
+
* transport, with `not_this_browser` as the body's `errorCode` (and `error`), the way
|
|
12
12
|
* `TokenService.issue()` answers `mfa_required`.
|
|
13
13
|
*/
|
|
14
14
|
export declare class MagicLinkError extends AuthenticationError {
|
|
@@ -8,7 +8,7 @@ import { AuthenticationError } from './authentication.error.js';
|
|
|
8
8
|
* and refused links share one answer, `null`.
|
|
9
9
|
*
|
|
10
10
|
* An `AuthenticationError`: a route that lets it escape answers 401 on any
|
|
11
|
-
* transport, with `not_this_browser` as the body's `error
|
|
11
|
+
* transport, with `not_this_browser` as the body's `errorCode` (and `error`), the way
|
|
12
12
|
* `TokenService.issue()` answers `mfa_required`.
|
|
13
13
|
*/
|
|
14
14
|
export class MagicLinkError extends AuthenticationError {
|
|
@@ -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. */
|
|
@@ -10,6 +10,13 @@ export type AuthenticatedUser = AuthenticationTypes extends {
|
|
|
10
10
|
export type AuthenticatedSession = AuthenticationTypes extends {
|
|
11
11
|
session: infer S;
|
|
12
12
|
} ? S : unknown;
|
|
13
|
+
/**
|
|
14
|
+
* The type of `SessionRecord.extra` declared on {@link AuthenticationTypes}
|
|
15
|
+
* (`sessionExtra`), `unknown` otherwise.
|
|
16
|
+
*/
|
|
17
|
+
export type SessionExtra = AuthenticationTypes extends {
|
|
18
|
+
sessionExtra: infer E;
|
|
19
|
+
} ? E : unknown;
|
|
13
20
|
/**
|
|
14
21
|
* `pending`: first factor done, second factor outstanding. Such a result
|
|
15
22
|
* does not count as signed in: `SignInService.completeMfa()` finishes it.
|
|
@@ -19,7 +19,7 @@ export interface JwksClientOptions {
|
|
|
19
19
|
}
|
|
20
20
|
export interface JwtVerifierOptions extends ClaimRules {
|
|
21
21
|
/**
|
|
22
|
-
* A static key: an
|
|
22
|
+
* A static key: an HMAC secret (at least 32 bytes for HS256), or a public or
|
|
23
23
|
* private key as a `KeyObject` or PEM text (a string or a `Buffer`). A
|
|
24
24
|
* public key needs `issuer` and `audience`, as a `jwks` does: it is an
|
|
25
25
|
* identity provider's, which signs the tokens of all its clients with it.
|
|
@@ -34,9 +34,11 @@ export interface JwtVerifierOptions extends ClaimRules {
|
|
|
34
34
|
*/
|
|
35
35
|
jwks?: string | JwksClient;
|
|
36
36
|
/**
|
|
37
|
-
* Accepted `alg` values. Defaults: `['HS256']` for a secret,
|
|
38
|
-
*
|
|
39
|
-
* `
|
|
37
|
+
* Accepted `alg` values. Defaults: `['HS256']` for a secret, `['RS256']`
|
|
38
|
+
* for an RSA key, the curve's own (`ES256`, `ES384`, `ES512`) for an EC
|
|
39
|
+
* key, `['EdDSA']` for an Ed25519 key, and `['RS256', 'ES256']` for a JWKS.
|
|
40
|
+
* Others (`RS384`, `PS256`, `HS512`, …) are opted into here. `none` is
|
|
41
|
+
* never accepted, and HS algorithms never with a JWKS.
|
|
40
42
|
*/
|
|
41
43
|
algorithms?: JwsAlgorithm[];
|
|
42
44
|
/** Required `typ` header, e.g. `at+jwt` (RFC 9068). Not checked by default. */
|
|
@@ -44,12 +46,16 @@ export interface JwtVerifierOptions extends ClaimRules {
|
|
|
44
46
|
}
|
|
45
47
|
export interface JwtSignerOptions {
|
|
46
48
|
/**
|
|
47
|
-
* An
|
|
48
|
-
*
|
|
49
|
+
* An HMAC secret (at least as long as the hash: 32, 48 or 64 bytes), or an
|
|
50
|
+
* RSA, P-256, P-384, P-521 or Ed25519 private key: a `KeyObject`, or PEM
|
|
51
|
+
* text (a string or a `Buffer`). A JWK, or
|
|
49
52
|
* DER, is refused: pass `createPrivateKey({ key: jwk, format: 'jwk' })`.
|
|
50
53
|
*/
|
|
51
54
|
key: KeyObject | string | Buffer;
|
|
52
|
-
/**
|
|
55
|
+
/**
|
|
56
|
+
* Default: HS256 for a secret, RS256 for an RSA key, the curve's own for
|
|
57
|
+
* an EC key, EdDSA for an Ed25519 key.
|
|
58
|
+
*/
|
|
53
59
|
alg?: JwsAlgorithm;
|
|
54
60
|
/** Published in the header so verifiers can pick the key from a JWKS. */
|
|
55
61
|
kid?: string;
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import type { Duration } from './duration.interface.js';
|
|
2
2
|
/**
|
|
3
|
-
* Supported JWS algorithms. EdDSA means Ed25519 only: Ed448
|
|
4
|
-
* (untested here, and rare in practice).
|
|
3
|
+
* Supported JWS algorithms (RFC 7518 §3.1). EdDSA means Ed25519 only: Ed448
|
|
4
|
+
* is rejected (untested here, and rare in practice). PS256/384/512 take an
|
|
5
|
+
* RSA key, as a JWKS publishes it; Node's `rsa-pss` key type is rejected.
|
|
5
6
|
*/
|
|
6
|
-
export type JwsAlgorithm = 'HS256' | 'RS256' | 'ES256' | 'EdDSA';
|
|
7
|
+
export type JwsAlgorithm = 'HS256' | 'HS384' | 'HS512' | 'RS256' | 'RS384' | 'RS512' | 'PS256' | 'PS384' | 'PS512' | 'ES256' | 'ES384' | 'ES512' | 'EdDSA';
|
|
7
8
|
export interface JwtClaims {
|
|
8
9
|
iss?: string;
|
|
9
10
|
sub?: string;
|