@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.
Files changed (60) hide show
  1. package/README.md +64 -6
  2. package/dist/account/password-reset.service.d.ts +9 -3
  3. package/dist/account/password-reset.service.js +16 -4
  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 +5 -4
  15. package/dist/errors/magic-link.error.js +6 -5
  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/magic-link.interface.d.ts +2 -1
  29. package/dist/interfaces/oidc.interface.d.ts +9 -1
  30. package/dist/interfaces/session-store.interface.d.ts +29 -4
  31. package/dist/interfaces/token.interface.d.ts +31 -4
  32. package/dist/jwt/jwks.client.js +10 -3
  33. package/dist/jwt/jws.util.d.ts +10 -1
  34. package/dist/jwt/jws.util.js +108 -16
  35. package/dist/jwt/jwt-signer.service.js +2 -2
  36. package/dist/jwt/jwt-verifier.service.js +6 -4
  37. package/dist/jwt/token.service.d.ts +29 -8
  38. package/dist/jwt/token.service.js +48 -8
  39. package/dist/magic-link/magic-link.service.d.ts +4 -1
  40. package/dist/magic-link/magic-link.service.js +40 -10
  41. package/dist/mfa/mfa.service.d.ts +11 -1
  42. package/dist/mfa/mfa.service.js +19 -9
  43. package/dist/oidc/oidc.client.d.ts +1 -1
  44. package/dist/oidc/oidc.client.js +1 -1
  45. package/dist/oidc/oidc.service.d.ts +1 -0
  46. package/dist/oidc/oidc.service.js +22 -8
  47. package/dist/session/in-memory-session.store.d.ts +2 -0
  48. package/dist/session/in-memory-session.store.js +3 -1
  49. package/dist/session/session-cookie.provider.d.ts +17 -1
  50. package/dist/session/session-cookie.provider.js +16 -1
  51. package/dist/session/session.service.d.ts +18 -2
  52. package/dist/session/session.service.js +43 -8
  53. package/dist/session/sign-in.service.d.ts +6 -5
  54. package/dist/session/sign-in.service.js +6 -5
  55. package/dist/storage/authentication.storage.d.ts +25 -16
  56. package/dist/storage/authentication.storage.js +25 -16
  57. package/dist/testing/index.js +13 -5
  58. package/dist/utils/transport-errors.util.d.ts +23 -12
  59. package/dist/utils/transport-errors.util.js +34 -22
  60. package/package.json +28 -28
@@ -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;
@@ -23,7 +23,8 @@ export interface MagicLinkOptions {
23
23
  * Whether a link works only in the browser that requested it. Default
24
24
  * `true`: `create()` sets a transaction cookie, and `consume()` refuses a
25
25
  * link the browser holds no cookie for (`magic-link-refused`,
26
- * `not-this-browser`). Without it, anyone can request a link for their
26
+ * `not-this-browser`). The cookie keeps the last five links the browser
27
+ * requested: a "resend" does not void the link before it. Without it, anyone can request a link for their
27
28
  * own address and get someone else to open it, which signs that person's
28
29
  * browser in to the attacker's account (login CSRF). `false` trades that
29
30
  * for links that work on another device than the one that asked for them
@@ -45,8 +45,16 @@ export interface OidcOptions extends OidcClientOptions {
45
45
  * The public URL of your callback route, the redirect URI you register
46
46
  * with each provider. `:provider` is replaced by the provider's name:
47
47
  * `https://app.example.com/auth/oidc/:provider/callback`.
48
+ *
49
+ * An app served under several domains passes a function of the request
50
+ * instead, called on the login and again on the callback: both must
51
+ * answer the same URL, or the provider refuses the code. Check the host
52
+ * against your own list of domains rather than echoing the header, and
53
+ * register every URL it can answer with each provider. The transaction
54
+ * cookie is bound to the host that set it, so a login finishes on the
55
+ * domain where it started.
48
56
  */
49
- callbackUrl: string;
57
+ callbackUrl: string | ((request: OidcRequest) => string | Promise<string>);
50
58
  providers: Record<string, OidcProviderConfig>;
51
59
  /** Where to land after sign-in when the login had no `redirectTo`. Default `/`. */
52
60
  redirectAfterLogin?: string;
@@ -1,4 +1,4 @@
1
- import type { MfaState } from './authentication-result.interface.js';
1
+ import type { MfaState, SessionExtra } from './authentication-result.interface.js';
2
2
  export interface SessionRecord {
3
3
  /** SHA-256 of the cookie token. The token itself is never stored. */
4
4
  id: string;
@@ -11,6 +11,19 @@ export interface SessionRecord {
11
11
  mfa?: MfaState;
12
12
  /** App data: user agent, IP, device name, for "your sessions" pages. */
13
13
  metadata?: Record<string, unknown>;
14
+ /**
15
+ * Whatever `getSession()` read along with the session, typically its user
16
+ * from the same query (`JOIN users`), handed to
17
+ * `SessionCookieProvider.validate()` so a cookie request costs one read.
18
+ * Typed by `sessionExtra` on `AuthenticationTypes`.
19
+ *
20
+ * Never stored: `SessionService` leaves it out of every `createSession()`
21
+ * (rotations included), and `@CurrentSession()`, `request.session` and
22
+ * `AuthenticationContext.session` don't carry it. Join fresh data on every
23
+ * read; don't keep a copy of the user on the session row, which goes stale
24
+ * when the user changes.
25
+ */
26
+ extra?: SessionExtra;
14
27
  }
15
28
  /**
16
29
  * Where sessions live. Implement it on your database or Redis, and register
@@ -22,12 +35,16 @@ export interface SessionRecord {
22
35
  * from `SessionService`'s clock, never the store's.
23
36
  */
24
37
  export interface SessionStore {
25
- /** The session, or `undefined`. Expired sessions may be returned: `SessionService` checks expiry. */
38
+ /**
39
+ * The session, or `undefined`. Expired sessions may be returned: `SessionService` checks expiry.
40
+ * May set `extra` to what the same query read with it (the user, joined), for
41
+ * `SessionCookieProvider.validate()`.
42
+ */
26
43
  getSession(id: string): Promise<SessionRecord | undefined>;
27
44
  /**
28
45
  * Saves a new session (a fresh random id: a plain insert). A good place
29
46
  * to delete sessions whose `expiresAt` has passed, so the store stays
30
- * bounded.
47
+ * bounded. `record` never carries `extra`.
31
48
  */
32
49
  createSession(record: SessionRecord): Promise<void>;
33
50
  /**
@@ -36,6 +53,11 @@ export interface SessionStore {
36
53
  * WHERE id = ? AND last_active_at < ?`, `SET … XX` in Redis. A read
37
54
  * followed by a write lets a request that read the session before a
38
55
  * sign-out or rotation deleted it bring it back. Changes no other field.
56
+ *
57
+ * Best-effort: a rejection is published as `session-touch-failed` (every
58
+ * one) and logged (once per spell of failures), but the request goes on
59
+ * with the session as read. A missed touch only means the session goes idle at its previous
60
+ * deadline, unless a later request records activity.
39
61
  */
40
62
  touchSession(id: string, lastActiveAt: Date): Promise<void>;
41
63
  /**
@@ -46,7 +68,10 @@ export interface SessionStore {
46
68
  * rotations), only one may win.
47
69
  */
48
70
  deleteSession(id: string): Promise<boolean>;
49
- /** The user's sessions, in any order (`[]` when none), expired ones included. */
71
+ /**
72
+ * The user's sessions, in any order (`[]` when none), expired ones included.
73
+ * Without `extra`: they feed "your sessions" pages.
74
+ */
50
75
  listUserSessions(userId: string): Promise<SessionRecord[]>;
51
76
  /** Deletes every session of the user ("sign out everywhere"). */
52
77
  deleteUserSessions(userId: string): Promise<void>;
@@ -1,3 +1,4 @@
1
+ import type { AuthenticationTypes } from '../index.js';
1
2
  export interface IssueTokensOptions {
2
3
  /**
3
4
  * Claims that describe the sign-in (`amr`, `auth_time`, …): this access
@@ -19,12 +20,38 @@ export interface IssueTokensOptions {
19
20
  recoveryCode?: string;
20
21
  };
21
22
  }
22
- /** What a token endpoint returns to a client that signed in. */
23
- export interface TokenPair {
23
+ /**
24
+ * What `TokenService.issue()` returns with `refreshToken: false`: an access
25
+ * token alone, which the client replaces by signing in again.
26
+ */
27
+ export interface AccessTokenResult {
24
28
  /** A JWT signed with the `accessToken` options. */
25
29
  accessToken: string;
26
- /** Opaque, single use: `refresh()` exchanges it for a new pair. */
27
- refreshToken: string;
28
30
  /** Seconds until the access token expires (OAuth's `expires_in`). */
29
31
  expiresIn: number;
30
32
  }
33
+ /** What a token endpoint returns to a client that signed in. */
34
+ export interface TokenPair extends AccessTokenResult {
35
+ /** Opaque, single use: `refresh()` exchanges it for a new pair. */
36
+ refreshToken: string;
37
+ }
38
+ /**
39
+ * What `TokenService.issue()` returns: a {@link TokenPair}, or, for an app
40
+ * that declared `refreshTokens: false` on `AuthenticationTypes` (to go with
41
+ * the `refreshToken: false` option), an {@link AccessTokenResult}:
42
+ *
43
+ * ```ts
44
+ * declare module '@nestjs/authentication' {
45
+ * interface AuthenticationTypes {
46
+ * refreshTokens: false;
47
+ * }
48
+ * }
49
+ * ```
50
+ *
51
+ * The declaration is what the compiler sees; the option is what runs. An
52
+ * app that sets the option without the declaration gets no `refreshToken`
53
+ * at run time, typed as a string, and `refresh()` throws on it.
54
+ */
55
+ export type IssuedTokens<Types = AuthenticationTypes> = Types extends {
56
+ refreshTokens: false;
57
+ } ? AccessTokenResult : TokenPair;
@@ -3,12 +3,19 @@ import { durationOr } from '../utils/duration.util.js';
3
3
  import { JwtError } from '../errors/jwt.error.js';
4
4
  import { ASYMMETRIC_ALGORITHMS } from './jws.util.js';
5
5
  const LOOPBACK = new Set(['localhost', '127.0.0.1', '[::1]']);
6
+ /** The algorithm of each curve a JWK can name (`crv`, RFC 7518 §6.2.1.1). Other curves are skipped. */
7
+ const EC_ALGORITHMS = { 'P-256': 'ES256', 'P-384': 'ES384', 'P-521': 'ES512' };
8
+ /**
9
+ * What a JWK can verify, by its type and curve. An RSA key serves every RS and
10
+ * PS algorithm: its `alg`, when published, narrows that, and the verifier's
11
+ * `algorithms` decide which of them are accepted at all.
12
+ */
6
13
  function algorithmsFor(jwk) {
7
14
  if (jwk.kty === 'RSA') {
8
- return ['RS256'];
15
+ return ['RS256', 'RS384', 'RS512', 'PS256', 'PS384', 'PS512'];
9
16
  }
10
- if (jwk.kty === 'EC' && jwk.crv === 'P-256') {
11
- return ['ES256'];
17
+ if (jwk.kty === 'EC' && jwk.crv && EC_ALGORITHMS[jwk.crv]) {
18
+ return [EC_ALGORITHMS[jwk.crv]];
12
19
  }
13
20
  if (jwk.kty === 'OKP' && jwk.crv === 'Ed25519') {
14
21
  return ['EdDSA'];
@@ -1,5 +1,6 @@
1
1
  import { KeyObject } from 'node:crypto';
2
2
  import type { JwsAlgorithm, JwtClaims, ClaimRules } from '../interfaces/jwt.interface.js';
3
+ /** The algorithms a public key verifies: the only ones a JWKS can serve (HS secrets are never published). */
3
4
  export declare const ASYMMETRIC_ALGORITHMS: readonly JwsAlgorithm[];
4
5
  export interface JwsHeader {
5
6
  alg: string;
@@ -18,14 +19,22 @@ export interface JwsHeader {
18
19
  * `createPrivateKey`) and pass the `KeyObject`.
19
20
  */
20
21
  export declare function toKeyObject(key: KeyObject | string | Buffer): KeyObject;
21
- /** The algorithm a key implies: HS256 for a secret, the key's own for asymmetric keys. */
22
+ /**
23
+ * The algorithm a key implies: HS256 for a secret, RS256 for an RSA key (the
24
+ * key does not name a hash or padding: PS and the longer hashes are opted
25
+ * into with `alg`), and the curve's own for an EC key.
26
+ */
22
27
  export declare function algorithmFor(key: KeyObject): JwsAlgorithm | undefined;
28
+ /** The configuration error for a key no algorithm fits: an EC key names its curve, as EC keys of other curves are. */
29
+ export declare function unsupportedKeyError(key: KeyObject, owner: string): TypeError;
23
30
  /**
24
31
  * Whether `key` is appropriate for `alg`. This is the algorithm/key binding
25
32
  * that stops key-confusion attacks: an RSA public key can never be used as
26
33
  * an HMAC secret, a P-384 key never verifies ES256, and so on.
27
34
  */
28
35
  export declare function keyFits(alg: JwsAlgorithm, key: KeyObject): boolean;
36
+ /** HS256, HS384, HS512: signed and verified with the same secret, so no private key is involved. */
37
+ export declare const isHmac: (alg: JwsAlgorithm) => boolean;
29
38
  /** Throws a `TypeError` (configuration, not a token) unless `key` can serve `alg`. */
30
39
  export declare function assertKeyFits(alg: JwsAlgorithm, key: KeyObject, owner: string): void;
31
40
  /** Throws a `TypeError` (configuration, not a token) unless `key` can sign `alg`. */
@@ -1,7 +1,27 @@
1
- import { KeyObject, X509Certificate, createHmac, createPrivateKey, createPublicKey, createSecretKey, sign as cryptoSign, verify as cryptoVerify, timingSafeEqual, } from 'node:crypto';
1
+ import { KeyObject, X509Certificate, constants, createHmac, createPrivateKey, createPublicKey, createSecretKey, sign as cryptoSign, verify as cryptoVerify, timingSafeEqual, } from 'node:crypto';
2
2
  import { toMs } from '../utils/duration.util.js';
3
3
  import { JwtError } from '../errors/jwt.error.js';
4
- export const ASYMMETRIC_ALGORITHMS = ['RS256', 'ES256', 'EdDSA'];
4
+ /** The algorithms a public key verifies: the only ones a JWKS can serve (HS secrets are never published). */
5
+ export const ASYMMETRIC_ALGORITHMS = [
6
+ 'RS256',
7
+ 'RS384',
8
+ 'RS512',
9
+ 'PS256',
10
+ 'PS384',
11
+ 'PS512',
12
+ 'ES256',
13
+ 'ES384',
14
+ 'ES512',
15
+ 'EdDSA',
16
+ ];
17
+ /** `HS384` → `sha384`: the digest every algorithm but EdDSA names in its last three digits. */
18
+ const hashOf = (alg) => `sha${alg.slice(2)}`;
19
+ /** The curve each ECDSA algorithm is bound to (RFC 7518 §3.4), by its OpenSSL name. */
20
+ const CURVES = { ES256: 'prime256v1', ES384: 'secp384r1', ES512: 'secp521r1' };
21
+ /** The reverse: the one algorithm an EC key of each curve implies. Other curves (secp256k1, …) imply none. */
22
+ const CURVE_ALGORITHMS = { prime256v1: 'ES256', secp384r1: 'ES384', secp521r1: 'ES512' };
23
+ /** Raw r||s length: twice the curve's coordinate size (P-521's is 66 bytes, not 64). */
24
+ const ECDSA_SIGNATURE_BYTES = { ES256: 64, ES384: 96, ES512: 132 };
5
25
  const SEGMENT = /^[A-Za-z0-9_-]+$/;
6
26
  const PEM = /-----BEGIN ([A-Z0-9 ]+)-----/g;
7
27
  /**
@@ -40,7 +60,11 @@ export function toKeyObject(key) {
40
60
  throw new TypeError(`Cannot read the PEM ${label.toLowerCase()}: ${error.message}`);
41
61
  }
42
62
  }
43
- /** The algorithm a key implies: HS256 for a secret, the key's own for asymmetric keys. */
63
+ /**
64
+ * The algorithm a key implies: HS256 for a secret, RS256 for an RSA key (the
65
+ * key does not name a hash or padding: PS and the longer hashes are opted
66
+ * into with `alg`), and the curve's own for an EC key.
67
+ */
44
68
  export function algorithmFor(key) {
45
69
  if (key.type === 'secret') {
46
70
  return 'HS256';
@@ -49,13 +73,20 @@ export function algorithmFor(key) {
49
73
  case 'rsa':
50
74
  return 'RS256';
51
75
  case 'ec':
52
- return 'ES256';
76
+ return CURVE_ALGORITHMS[key.asymmetricKeyDetails?.namedCurve ?? ''];
53
77
  case 'ed25519':
54
78
  return 'EdDSA';
55
79
  default:
56
80
  return undefined;
57
81
  }
58
82
  }
83
+ /** The configuration error for a key no algorithm fits: an EC key names its curve, as EC keys of other curves are. */
84
+ export function unsupportedKeyError(key, owner) {
85
+ const curve = key.asymmetricKeyType === 'ec' ? key.asymmetricKeyDetails?.namedCurve : undefined;
86
+ return new TypeError(curve
87
+ ? `${owner}: EC keys on ${curve} are not supported (use P-256, P-384 or P-521).`
88
+ : `${owner}: ${key.asymmetricKeyType} keys are not supported.`);
89
+ }
59
90
  /**
60
91
  * Whether `key` is appropriate for `alg`. This is the algorithm/key binding
61
92
  * that stops key-confusion attacks: an RSA public key can never be used as
@@ -64,24 +95,53 @@ export function algorithmFor(key) {
64
95
  export function keyFits(alg, key) {
65
96
  const details = key.asymmetricKeyDetails;
66
97
  switch (alg) {
98
+ // A secret at least as long as the hash's output (RFC 7518 §3.2).
67
99
  case 'HS256':
68
100
  return key.type === 'secret' && (key.symmetricKeySize ?? 0) >= 32;
101
+ case 'HS384':
102
+ return key.type === 'secret' && (key.symmetricKeySize ?? 0) >= 48;
103
+ case 'HS512':
104
+ return key.type === 'secret' && (key.symmetricKeySize ?? 0) >= 64;
105
+ // One RSA key serves both paddings and every hash: the header names which, and the verifier's
106
+ // `algorithms` decide which are accepted. Node's `rsa-pss` keys are refused (asymmetricKeyType
107
+ // 'rsa-pss'): they restrict their own hash and salt, which a JWKS cannot even publish.
69
108
  case 'RS256':
109
+ case 'RS384':
110
+ case 'RS512':
111
+ case 'PS256':
112
+ case 'PS384':
113
+ case 'PS512':
70
114
  return key.asymmetricKeyType === 'rsa' && (details?.modulusLength ?? 0) >= 2048;
115
+ // Each ECDSA algorithm takes its own curve only: an ES256 token never verifies with a P-384 key.
71
116
  case 'ES256':
72
- return key.asymmetricKeyType === 'ec' && details?.namedCurve === 'prime256v1';
117
+ case 'ES384':
118
+ case 'ES512':
119
+ return key.asymmetricKeyType === 'ec' && details?.namedCurve === CURVES[alg];
73
120
  case 'EdDSA':
74
121
  return key.asymmetricKeyType === 'ed25519';
75
122
  default:
76
123
  return false;
77
124
  }
78
125
  }
126
+ /** What each algorithm needs, for the configuration error that names it. */
127
+ const RSA_KEY = 'an RSA key of at least 2048 bits';
79
128
  const REQUIREMENTS = {
80
129
  HS256: 'a secret of at least 32 bytes (`openssl rand -base64 32`)',
81
- RS256: 'an RSA key of at least 2048 bits',
130
+ HS384: 'a secret of at least 48 bytes (`openssl rand -base64 48`)',
131
+ HS512: 'a secret of at least 64 bytes (`openssl rand -base64 64`)',
132
+ RS256: RSA_KEY,
133
+ RS384: RSA_KEY,
134
+ RS512: RSA_KEY,
135
+ PS256: RSA_KEY,
136
+ PS384: RSA_KEY,
137
+ PS512: RSA_KEY,
82
138
  ES256: 'a P-256 key',
139
+ ES384: 'a P-384 key',
140
+ ES512: 'a P-521 key',
83
141
  EdDSA: 'an Ed25519 key',
84
142
  };
143
+ /** HS256, HS384, HS512: signed and verified with the same secret, so no private key is involved. */
144
+ export const isHmac = (alg) => alg.startsWith('HS');
85
145
  /** Throws a `TypeError` (configuration, not a token) unless `key` can serve `alg`. */
86
146
  export function assertKeyFits(alg, key, owner) {
87
147
  if (!keyFits(alg, key)) {
@@ -91,7 +151,7 @@ export function assertKeyFits(alg, key, owner) {
91
151
  /** Throws a `TypeError` (configuration, not a token) unless `key` can sign `alg`. */
92
152
  export function assertSigningKey(alg, key, owner) {
93
153
  assertKeyFits(alg, key, owner);
94
- if (alg !== 'HS256' && key.type !== 'private') {
154
+ if (!isHmac(alg) && key.type !== 'private') {
95
155
  throw new TypeError(`${owner}: ${alg} signing needs a private key.`);
96
156
  }
97
157
  }
@@ -99,16 +159,28 @@ export function signJws(header, payload, key) {
99
159
  assertSigningKey(header.alg, key, 'JwtSigner');
100
160
  const input = `${encode(header)}.${encode(payload)}`;
101
161
  const data = Buffer.from(input);
162
+ const { alg } = header;
102
163
  let signature;
103
- switch (header.alg) {
164
+ switch (alg) {
104
165
  case 'HS256':
105
- signature = createHmac('sha256', key).update(data).digest();
166
+ case 'HS384':
167
+ case 'HS512':
168
+ signature = createHmac(hashOf(alg), key).update(data).digest();
106
169
  break;
107
170
  case 'RS256':
108
- signature = cryptoSign('sha256', data, key);
171
+ case 'RS384':
172
+ case 'RS512':
173
+ signature = cryptoSign(hashOf(alg), data, key); // PKCS#1 v1.5 padding, Node's default for RSA
174
+ break;
175
+ case 'PS256':
176
+ case 'PS384':
177
+ case 'PS512':
178
+ signature = cryptoSign(hashOf(alg), data, pss(key));
109
179
  break;
110
180
  case 'ES256':
111
- signature = cryptoSign('sha256', data, { key, dsaEncoding: 'ieee-p1363' });
181
+ case 'ES384':
182
+ case 'ES512':
183
+ signature = cryptoSign(hashOf(alg), data, { key, dsaEncoding: 'ieee-p1363' });
112
184
  break;
113
185
  case 'EdDSA':
114
186
  signature = cryptoSign(null, data, key);
@@ -147,19 +219,31 @@ export function verifySignature(alg, key, decoded) {
147
219
  const { signingInput, signature } = decoded;
148
220
  let valid;
149
221
  switch (alg) {
150
- case 'HS256': {
151
- const expected = createHmac('sha256', publicKey).update(signingInput).digest();
222
+ case 'HS256':
223
+ case 'HS384':
224
+ case 'HS512': {
225
+ const expected = createHmac(hashOf(alg), publicKey).update(signingInput).digest();
152
226
  valid = signature.length === expected.length && timingSafeEqual(signature, expected);
153
227
  break;
154
228
  }
155
229
  case 'RS256':
156
- valid = cryptoVerify('sha256', signingInput, publicKey, signature);
230
+ case 'RS384':
231
+ case 'RS512':
232
+ valid = cryptoVerify(hashOf(alg), signingInput, publicKey, signature);
233
+ break;
234
+ // Same key as RS, other padding: a PKCS#1 v1.5 signature presented as PS (or the reverse) fails here.
235
+ case 'PS256':
236
+ case 'PS384':
237
+ case 'PS512':
238
+ valid = cryptoVerify(hashOf(alg), signingInput, pss(publicKey), signature);
157
239
  break;
158
240
  case 'ES256':
241
+ case 'ES384':
242
+ case 'ES512':
159
243
  // JWS uses the raw r||s form (RFC 7518 §3.4), not DER.
160
244
  valid =
161
- signature.length === 64 &&
162
- cryptoVerify('sha256', signingInput, { key: publicKey, dsaEncoding: 'ieee-p1363' }, signature);
245
+ signature.length === ECDSA_SIGNATURE_BYTES[alg] &&
246
+ cryptoVerify(hashOf(alg), signingInput, { key: publicKey, dsaEncoding: 'ieee-p1363' }, signature);
163
247
  break;
164
248
  case 'EdDSA':
165
249
  valid = signature.length === 64 && cryptoVerify(null, signingInput, publicKey, signature);
@@ -171,6 +255,14 @@ export function verifySignature(alg, key, decoded) {
171
255
  throw new JwtError('invalid signature');
172
256
  }
173
257
  }
258
+ /**
259
+ * RSASSA-PSS options (RFC 7518 §3.5): MGF1 on the same hash (Node's default),
260
+ * and a salt exactly as long as the hash. Verifying with this exact length,
261
+ * not "any", refuses signatures made with another salt length.
262
+ */
263
+ function pss(key) {
264
+ return { key, padding: constants.RSA_PKCS1_PSS_PADDING, saltLength: constants.RSA_PSS_SALTLEN_DIGEST };
265
+ }
174
266
  export function validateClaims(claims, rules) {
175
267
  // JWT times are seconds (RFC 7519 §2, NumericDate).
176
268
  const now = Math.floor((rules.now?.() ?? Date.now()) / 1000);
@@ -1,6 +1,6 @@
1
1
  import { randomToken } from '../utils/crypto.util.js';
2
2
  import { durationOr, toMs } from '../utils/duration.util.js';
3
- import { algorithmFor, assertSigningKey, signJws, toKeyObject } from './jws.util.js';
3
+ import { algorithmFor, assertSigningKey, signJws, toKeyObject, unsupportedKeyError } from './jws.util.js';
4
4
  /**
5
5
  * Issues signed JWTs. Adds `iat`, `exp`, `jti` and the configured
6
6
  * `iss`/`aud`; callers pass `sub` and custom claims. The module's
@@ -14,7 +14,7 @@ export class JwtSigner {
14
14
  this.key = toKeyObject(options.key);
15
15
  const alg = options.alg ?? algorithmFor(this.key);
16
16
  if (!alg) {
17
- throw new TypeError(`JwtSigner: ${this.key.asymmetricKeyType} keys are not supported.`);
17
+ throw unsupportedKeyError(this.key, 'JwtSigner');
18
18
  }
19
19
  assertSigningKey(alg, this.key, 'JwtSigner');
20
20
  this.alg = alg;
@@ -2,7 +2,7 @@ import { createPublicKey } from 'node:crypto';
2
2
  import { toMs } from '../utils/duration.util.js';
3
3
  import { JwtError } from '../errors/jwt.error.js';
4
4
  import { JwksClient } from './jwks.client.js';
5
- import { algorithmFor, assertKeyFits, decodeJws, toKeyObject, validateClaims, verifySignature } from './jws.util.js';
5
+ import { algorithmFor, assertKeyFits, decodeJws, isHmac, toKeyObject, unsupportedKeyError, validateClaims, verifySignature, } from './jws.util.js';
6
6
  /**
7
7
  * Verifies compact JWS tokens and their registered claims. `verify()`
8
8
  * throws {@link JwtError}; a configuration that could not verify anything
@@ -26,7 +26,7 @@ export class JwtVerifier {
26
26
  if (this.key) {
27
27
  const implied = algorithmFor(this.key);
28
28
  if (!implied) {
29
- throw new TypeError(`JwtVerifier: ${this.key.asymmetricKeyType} keys are not supported.`);
29
+ throw unsupportedKeyError(this.key, 'JwtVerifier');
30
30
  }
31
31
  this.algorithms = options.algorithms ?? [implied];
32
32
  for (const alg of this.algorithms) {
@@ -40,8 +40,10 @@ export class JwtVerifier {
40
40
  }
41
41
  else {
42
42
  this.algorithms = options.algorithms ?? ['RS256', 'ES256'];
43
- if (this.algorithms.includes('HS256')) {
44
- throw new TypeError('JwtVerifier: HS256 cannot be combined with a JWKS.');
43
+ // A JWKS publishes keys to everyone: an HMAC secret taken from one would let anyone sign.
44
+ const hmac = this.algorithms.find(isHmac);
45
+ if (hmac) {
46
+ throw new TypeError(`JwtVerifier: ${hmac} cannot be combined with a JWKS.`);
45
47
  }
46
48
  requireIssuerAndAudience(options, '`jwks`');
47
49
  }
@@ -3,13 +3,13 @@ import { AuthenticationEvents } from '../events/authentication-events.service.js
3
3
  import { MfaService } from '../mfa/mfa.service.js';
4
4
  import type { JwtSignerOptions } from '../interfaces/jwt-options.interface.js';
5
5
  import type { RefreshTokenOptions } from '../interfaces/refresh-token-options.interface.js';
6
- import type { IssueTokensOptions, TokenPair } from '../interfaces/token.interface.js';
6
+ import type { IssueTokensOptions, IssuedTokens, TokenPair } from '../interfaces/token.interface.js';
7
7
  /**
8
8
  * Tokens for clients that do not keep cookies (mobile apps, CLIs): a
9
9
  * short-lived JWT access token, and an opaque refresh token that renews it.
10
10
  *
11
11
  * - `issue()` is to token clients what `SignInService.signIn()` is to
12
- * browsers: a user with a confirmed authenticator gets no tokens without
12
+ * browsers: with `mfa` configured, a user with a confirmed authenticator gets no tokens without
13
13
  * a second factor.
14
14
  * - Access tokens are signed with the module's `accessToken` options, the
15
15
  * same ones `JwtBearerProvider` verifies with by default.
@@ -19,6 +19,12 @@ import type { IssueTokensOptions, TokenPair } from '../interfaces/token.interfac
19
19
  * concurrent presentations (`markRefreshTokenUsed()` is a compare-and-set), means two
20
20
  * parties hold it: the whole family is revoked, and both have to sign in
21
21
  * again (OAuth 2.0 Security BCP §4.14.2).
22
+ * - With `refreshToken: false`, there are none: `issue()` returns an access
23
+ * token alone and stores nothing, `refresh()` and `revoke()` throw, and
24
+ * `revokeAll()` does nothing. For a service that signs short-lived access
25
+ * tokens and asks the client to sign in again; a service that only
26
+ * verifies tokens can also leave `accessToken` out and give its
27
+ * `JwtBearerProvider` the key (`super({ key })`).
22
28
  */
23
29
  export declare class TokenService {
24
30
  private readonly storage;
@@ -32,14 +38,18 @@ export declare class TokenService {
32
38
  private readonly ttl;
33
39
  private readonly absoluteTtl;
34
40
  private readonly clock?;
41
+ /** `refreshToken: false`: access tokens only, no families. */
42
+ private readonly refreshTokensOff;
35
43
  constructor(storage: AuthenticationStorage, options?: {
36
44
  accessToken?: JwtSignerOptions;
37
- refreshToken?: RefreshTokenOptions;
45
+ refreshToken?: RefreshTokenOptions | false;
38
46
  }, events?: AuthenticationEvents, mfa?: MfaService);
39
47
  /**
40
48
  * At sign-in: signs an access token and starts a refresh-token family.
49
+ * With `refreshToken: false`, returns the access token alone (typed by
50
+ * {@link IssuedTokens}), and stores nothing.
41
51
  *
42
- * A user with a confirmed authenticator must send a second factor
52
+ * With `mfa` configured, a user with a confirmed authenticator must send a second factor
43
53
  * (`secondFactor`). Without one, `issue()` throws an
44
54
  * `AuthenticationError` whose body says `mfa_required`, and counts
45
55
  * nothing against the user's lockout; with a wrong one, `Invalid code`.
@@ -47,11 +57,12 @@ export declare class TokenService {
47
57
  * `@Authenticate({ mfa: true })` accepts; `claims.amr` cannot (see
48
58
  * {@link IssueTokensOptions.claims}).
49
59
  */
50
- issue(userId: string, { claims, method, secondFactor }?: IssueTokensOptions): Promise<TokenPair>;
60
+ issue(userId: string, { claims, method, secondFactor }?: IssueTokensOptions): Promise<IssuedTokens>;
51
61
  /**
52
62
  * Spends a refresh token: returns its successor, and an access token with
53
63
  * the family's claims. Throws {@link RefreshTokenError} (`invalid`,
54
- * `expired`, `reused`), which a route answers with 401.
64
+ * `expired`, `reused`), which a route answers with 401. Throws without
65
+ * `accessToken`, and with `refreshToken: false`.
55
66
  */
56
67
  refresh(refreshToken: string): Promise<TokenPair>;
57
68
  /** Two parties hold the token: the family is revoked, and the theft reported. */
@@ -60,14 +71,24 @@ export declare class TokenService {
60
71
  * Signs out the client holding `refreshToken` (one device): revokes its
61
72
  * family. Spent tokens of the family work too. Resolves `false` for
62
73
  * unknown or malformed tokens, which a route should not reveal (RFC 7009
63
- * §2.2).
74
+ * §2.2). Throws without `accessToken`, and with `refreshToken: false`, as
75
+ * `refresh()` does.
64
76
  */
65
77
  revoke(refreshToken: string): Promise<boolean>;
66
- /** Signs the user out of every token client. Issued access tokens live until they expire. */
78
+ /**
79
+ * Signs the user out of every token client. Issued access tokens live
80
+ * until they expire. Without `accessToken`, or with `refreshToken: false`,
81
+ * this app issues no refresh tokens, and there is nothing to revoke: the
82
+ * store is not read. Apps that
83
+ * share a user database configure `accessToken` in all of them if any of
84
+ * them issues tokens: otherwise this one's sign-outs everywhere leave the
85
+ * token clients of the others signed in.
86
+ */
67
87
  revokeAll(userId: string): Promise<void>;
68
88
  /** `true` when a second factor was required and verified; throws when one is required and missing or wrong. */
69
89
  private checkSecondFactor;
70
90
  private pair;
91
+ private requireRefreshTokens;
71
92
  private requireSigner;
72
93
  private create;
73
94
  /**