@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.
Files changed (54) hide show
  1. package/README.md +64 -6
  2. package/dist/account/password-reset.service.d.ts +1 -1
  3. package/dist/account/password-reset.service.js +1 -1
  4. package/dist/authentication.constants.d.ts +9 -0
  5. package/dist/authentication.constants.js +9 -0
  6. package/dist/authentication.module-definition.js +13 -4
  7. package/dist/decorators/authenticate.decorator.js +4 -1
  8. package/dist/decorators/current-user.decorator.d.ts +3 -1
  9. package/dist/decorators/current-user.decorator.js +2 -0
  10. package/dist/decorators/public.decorator.d.ts +3 -0
  11. package/dist/decorators/public.decorator.js +3 -0
  12. package/dist/errors/authentication.error.d.ts +4 -2
  13. package/dist/errors/authentication.error.js +6 -2
  14. package/dist/errors/magic-link.error.d.ts +1 -1
  15. package/dist/errors/magic-link.error.js +1 -1
  16. package/dist/events/authentication-events.interface.d.ts +16 -1
  17. package/dist/events/authentication-events.service.d.ts +2 -1
  18. package/dist/events/authentication-events.service.js +2 -1
  19. package/dist/guards/authentication.guard.d.ts +4 -2
  20. package/dist/guards/authentication.guard.js +9 -6
  21. package/dist/index.d.ts +7 -2
  22. package/dist/interceptors/authentication-scope.interceptor.js +2 -2
  23. package/dist/interfaces/authentication-error-options.interface.d.ts +12 -2
  24. package/dist/interfaces/authentication-module-options.interface.d.ts +13 -1
  25. package/dist/interfaces/authentication-result.interface.d.ts +7 -0
  26. package/dist/interfaces/jwt-options.interface.d.ts +13 -7
  27. package/dist/interfaces/jwt.interface.d.ts +4 -3
  28. package/dist/interfaces/session-store.interface.d.ts +29 -4
  29. package/dist/interfaces/token.interface.d.ts +31 -4
  30. package/dist/jwt/jwks.client.js +10 -3
  31. package/dist/jwt/jws.util.d.ts +10 -1
  32. package/dist/jwt/jws.util.js +108 -16
  33. package/dist/jwt/jwt-signer.service.js +2 -2
  34. package/dist/jwt/jwt-verifier.service.js +6 -4
  35. package/dist/jwt/token.service.d.ts +29 -8
  36. package/dist/jwt/token.service.js +48 -8
  37. package/dist/mfa/mfa.service.d.ts +11 -1
  38. package/dist/mfa/mfa.service.js +19 -9
  39. package/dist/oidc/oidc.client.d.ts +1 -1
  40. package/dist/oidc/oidc.client.js +1 -1
  41. package/dist/session/in-memory-session.store.d.ts +2 -0
  42. package/dist/session/in-memory-session.store.js +3 -1
  43. package/dist/session/session-cookie.provider.d.ts +17 -1
  44. package/dist/session/session-cookie.provider.js +16 -1
  45. package/dist/session/session.service.d.ts +18 -2
  46. package/dist/session/session.service.js +43 -8
  47. package/dist/session/sign-in.service.d.ts +6 -5
  48. package/dist/session/sign-in.service.js +6 -5
  49. package/dist/storage/authentication.storage.d.ts +25 -16
  50. package/dist/storage/authentication.storage.js +25 -16
  51. package/dist/testing/index.js +13 -5
  52. package/dist/utils/transport-errors.util.d.ts +23 -12
  53. package/dist/utils/transport-errors.util.js +34 -22
  54. 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
- [Overview & Tutorial](https://docs.nestjs.com/security/authentication)
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
- warnShortLifetimes(options, new Logger('AuthenticationModule'));
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: options.accessToken !== undefined,
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: (...dataOrPipes: (string | import("@nestjs/common").ParameterDecoratorOptions | import("@nestjs/common").PipeTransform<any, any> | import("@nestjs/common").Type<import("@nestjs/common").PipeTransform<any, any>> | undefined)[]) => ParameterDecorator;
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
- constructor(message?: string, { challenge, code, cause }?: AuthenticationErrorOptions);
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`, the way
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`, the way
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
- export type AuthenticationEvent = AuthenticationSignInEvent | AuthenticationSignOutEvent | AuthenticationMfaVerifiedEvent | AuthenticationMfaFailedEvent | AuthenticationMfaEnabledEvent | AuthenticationMfaDisabledEvent | AuthenticationRecoveryCodesGeneratedEvent | AuthenticationRefreshTokenReusedEvent | AuthenticationPasswordResetRequestedEvent | AuthenticationPasswordResetEvent | AuthenticationEmailVerifiedEvent | AuthenticationMagicLinkRefusedEvent;
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. Each event is also
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. Each event is also
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, {}, this.challenges(context, providers));
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
- const { status, message, code } = error;
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, { status: error.status, message: error.message, code: error.code, cause: error }, { challenge: error.challenge, adapterHost: this.adapterHost });
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
- /** Machine-readable reason, sent as the body's `error` field (`mfa_required`). */
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
- /** The error behind this one, for logs. */
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
- refreshToken?: RefreshTokenOptions;
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 HS256 secret (at least 32 bytes), or a public or
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, the key's own
38
- * algorithm for an asymmetric key, `['RS256', 'ES256']` for a JWKS.
39
- * `none` is never accepted.
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 HS256 secret (at least 32 bytes), or a private key for RS256, ES256
48
- * or EdDSA: a `KeyObject`, or PEM text (a string or a `Buffer`). A JWK, or
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
- /** Default: HS256 for a secret, the key's own algorithm for a private key. */
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 is rejected
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;