@nestjs/authentication 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +23 -0
- package/README.md +50 -0
- package/dist/account/email-verification.handler.d.ts +23 -0
- package/dist/account/email-verification.handler.js +17 -0
- package/dist/account/email-verification.service.d.ts +46 -0
- package/dist/account/email-verification.service.js +111 -0
- package/dist/account/email.util.d.ts +14 -0
- package/dist/account/email.util.js +28 -0
- package/dist/account/in-memory-email-token.store.d.ts +12 -0
- package/dist/account/in-memory-email-token.store.js +60 -0
- package/dist/account/password-reset.handler.d.ts +19 -0
- package/dist/account/password-reset.handler.js +9 -0
- package/dist/account/password-reset.service.d.ts +69 -0
- package/dist/account/password-reset.service.js +204 -0
- package/dist/authentication.constants.d.ts +13 -0
- package/dist/authentication.constants.js +13 -0
- package/dist/authentication.module-definition.d.ts +21 -0
- package/dist/authentication.module-definition.js +147 -0
- package/dist/authentication.module.d.ts +34 -0
- package/dist/authentication.module.js +139 -0
- package/dist/context/authentication-scope.service.d.ts +39 -0
- package/dist/context/authentication-scope.service.js +64 -0
- package/dist/context/authentication.context.d.ts +28 -0
- package/dist/context/authentication.context.js +58 -0
- package/dist/context/index.d.ts +2 -0
- package/dist/context/index.js +2 -0
- package/dist/decorators/authenticate.decorator.d.ts +10 -0
- package/dist/decorators/authenticate.decorator.js +18 -0
- package/dist/decorators/current-session.decorator.d.ts +2 -0
- package/dist/decorators/current-session.decorator.js +6 -0
- package/dist/decorators/current-user.decorator.d.ts +6 -0
- package/dist/decorators/current-user.decorator.js +11 -0
- package/dist/decorators/index.d.ts +4 -0
- package/dist/decorators/index.js +4 -0
- package/dist/decorators/public.decorator.d.ts +5 -0
- package/dist/decorators/public.decorator.js +6 -0
- package/dist/errors/authentication.error.d.ts +27 -0
- package/dist/errors/authentication.error.js +33 -0
- package/dist/errors/index.d.ts +7 -0
- package/dist/errors/index.js +7 -0
- package/dist/errors/jwt.error.d.ts +9 -0
- package/dist/errors/jwt.error.js +12 -0
- package/dist/errors/magic-link.error.d.ts +18 -0
- package/dist/errors/magic-link.error.js +21 -0
- package/dist/errors/mfa-already-enrolled.error.d.ts +14 -0
- package/dist/errors/mfa-already-enrolled.error.js +17 -0
- package/dist/errors/oidc.error.d.ts +15 -0
- package/dist/errors/oidc.error.js +13 -0
- package/dist/errors/refresh-token.error.d.ts +10 -0
- package/dist/errors/refresh-token.error.js +12 -0
- package/dist/errors/secret-decryption.error.d.ts +3 -0
- package/dist/errors/secret-decryption.error.js +6 -0
- package/dist/events/authentication-events.interface.d.ts +121 -0
- package/dist/events/authentication-events.interface.js +1 -0
- package/dist/events/authentication-events.service.d.ts +19 -0
- package/dist/events/authentication-events.service.js +39 -0
- package/dist/events/authentication.channels.d.ts +4 -0
- package/dist/events/authentication.channels.js +11 -0
- package/dist/events/index.d.ts +3 -0
- package/dist/events/index.js +3 -0
- package/dist/guards/authentication.guard.d.ts +57 -0
- package/dist/guards/authentication.guard.js +171 -0
- package/dist/guards/index.d.ts +1 -0
- package/dist/guards/index.js +1 -0
- package/dist/index.d.ts +52 -0
- package/dist/index.js +52 -0
- package/dist/interceptors/authentication-scope.interceptor.d.ts +20 -0
- package/dist/interceptors/authentication-scope.interceptor.js +48 -0
- package/dist/interceptors/index.d.ts +1 -0
- package/dist/interceptors/index.js +1 -0
- package/dist/interfaces/api-key.interface.d.ts +41 -0
- package/dist/interfaces/api-key.interface.js +1 -0
- package/dist/interfaces/authenticate-options.interface.d.ts +31 -0
- package/dist/interfaces/authenticate-options.interface.js +1 -0
- package/dist/interfaces/authentication-error-options.interface.d.ts +8 -0
- package/dist/interfaces/authentication-error-options.interface.js +1 -0
- package/dist/interfaces/authentication-module-options.interface.d.ts +58 -0
- package/dist/interfaces/authentication-module-options.interface.js +1 -0
- package/dist/interfaces/authentication-registry.interface.d.ts +41 -0
- package/dist/interfaces/authentication-registry.interface.js +1 -0
- package/dist/interfaces/authentication-result.interface.d.ts +25 -0
- package/dist/interfaces/authentication-result.interface.js +1 -0
- package/dist/interfaces/authentication-storage.interface.d.ts +29 -0
- package/dist/interfaces/authentication-storage.interface.js +1 -0
- package/dist/interfaces/duration.interface.d.ts +2 -0
- package/dist/interfaces/duration.interface.js +1 -0
- package/dist/interfaces/email-token-store.interface.d.ts +52 -0
- package/dist/interfaces/email-token-store.interface.js +1 -0
- package/dist/interfaces/email-verification.interface.d.ts +18 -0
- package/dist/interfaces/email-verification.interface.js +1 -0
- package/dist/interfaces/index.d.ts +27 -0
- package/dist/interfaces/index.js +27 -0
- package/dist/interfaces/jwt-options.interface.d.ts +68 -0
- package/dist/interfaces/jwt-options.interface.js +1 -0
- package/dist/interfaces/jwt.interface.d.ts +36 -0
- package/dist/interfaces/jwt.interface.js +1 -0
- package/dist/interfaces/magic-link-store.interface.d.ts +36 -0
- package/dist/interfaces/magic-link-store.interface.js +1 -0
- package/dist/interfaces/magic-link.interface.d.ts +55 -0
- package/dist/interfaces/magic-link.interface.js +1 -0
- package/dist/interfaces/mfa-options.interface.d.ts +44 -0
- package/dist/interfaces/mfa-options.interface.js +1 -0
- package/dist/interfaces/mfa-store.interface.d.ts +70 -0
- package/dist/interfaces/mfa-store.interface.js +1 -0
- package/dist/interfaces/oidc-provider.interface.d.ts +56 -0
- package/dist/interfaces/oidc-provider.interface.js +1 -0
- package/dist/interfaces/oidc-state-store.interface.d.ts +25 -0
- package/dist/interfaces/oidc-state-store.interface.js +1 -0
- package/dist/interfaces/oidc.interface.d.ts +84 -0
- package/dist/interfaces/oidc.interface.js +1 -0
- package/dist/interfaces/password-hasher-options.interface.d.ts +17 -0
- package/dist/interfaces/password-hasher-options.interface.js +1 -0
- package/dist/interfaces/password-reset.interface.d.ts +49 -0
- package/dist/interfaces/password-reset.interface.js +1 -0
- package/dist/interfaces/refresh-token-options.interface.d.ts +10 -0
- package/dist/interfaces/refresh-token-options.interface.js +1 -0
- package/dist/interfaces/refresh-token-store.interface.d.ts +61 -0
- package/dist/interfaces/refresh-token-store.interface.js +1 -0
- package/dist/interfaces/session-store.interface.d.ts +53 -0
- package/dist/interfaces/session-store.interface.js +1 -0
- package/dist/interfaces/session.interface.d.ts +50 -0
- package/dist/interfaces/session.interface.js +1 -0
- package/dist/interfaces/sign-in-options.interface.d.ts +9 -0
- package/dist/interfaces/sign-in-options.interface.js +1 -0
- package/dist/interfaces/token.interface.d.ts +30 -0
- package/dist/interfaces/token.interface.js +1 -0
- package/dist/jwt/amr.util.d.ts +9 -0
- package/dist/jwt/amr.util.js +11 -0
- package/dist/jwt/in-memory-refresh-token.store.d.ts +29 -0
- package/dist/jwt/in-memory-refresh-token.store.js +67 -0
- package/dist/jwt/jwks.client.d.ts +34 -0
- package/dist/jwt/jwks.client.js +154 -0
- package/dist/jwt/jws.util.d.ts +45 -0
- package/dist/jwt/jws.util.js +282 -0
- package/dist/jwt/jwt-bearer.provider.d.ts +48 -0
- package/dist/jwt/jwt-bearer.provider.js +126 -0
- package/dist/jwt/jwt-signer.service.d.ts +19 -0
- package/dist/jwt/jwt-signer.service.js +36 -0
- package/dist/jwt/jwt-verifier.service.d.ts +22 -0
- package/dist/jwt/jwt-verifier.service.js +109 -0
- package/dist/jwt/token.service.d.ts +81 -0
- package/dist/jwt/token.service.js +230 -0
- package/dist/magic-link/in-memory-magic-link.store.d.ts +12 -0
- package/dist/magic-link/in-memory-magic-link.store.js +45 -0
- package/dist/magic-link/magic-link.handler.d.ts +25 -0
- package/dist/magic-link/magic-link.handler.js +14 -0
- package/dist/magic-link/magic-link.service.d.ts +79 -0
- package/dist/magic-link/magic-link.service.js +215 -0
- package/dist/mfa/in-memory-mfa.store.d.ts +25 -0
- package/dist/mfa/in-memory-mfa.store.js +73 -0
- package/dist/mfa/mfa.service.d.ts +104 -0
- package/dist/mfa/mfa.service.js +346 -0
- package/dist/mfa/otp.util.d.ts +7 -0
- package/dist/mfa/otp.util.js +52 -0
- package/dist/mfa/secret-cipher.service.d.ts +28 -0
- package/dist/mfa/secret-cipher.service.js +106 -0
- package/dist/oidc/in-memory-oidc-state.store.d.ts +13 -0
- package/dist/oidc/in-memory-oidc-state.store.js +45 -0
- package/dist/oidc/oidc-account.resolver.d.ts +23 -0
- package/dist/oidc/oidc-account.resolver.js +16 -0
- package/dist/oidc/oidc.client.d.ts +44 -0
- package/dist/oidc/oidc.client.js +286 -0
- package/dist/oidc/oidc.presets.d.ts +24 -0
- package/dist/oidc/oidc.presets.js +53 -0
- package/dist/oidc/oidc.service.d.ts +88 -0
- package/dist/oidc/oidc.service.js +259 -0
- package/dist/providers/api-key.provider.d.ts +51 -0
- package/dist/providers/api-key.provider.js +93 -0
- package/dist/providers/authentication.provider.d.ts +40 -0
- package/dist/providers/authentication.provider.js +33 -0
- package/dist/providers/index.d.ts +2 -0
- package/dist/providers/index.js +2 -0
- package/dist/services/authentication-registry.service.d.ts +75 -0
- package/dist/services/authentication-registry.service.js +235 -0
- package/dist/services/index.d.ts +3 -0
- package/dist/services/index.js +3 -0
- package/dist/services/password-hasher.service.d.ts +47 -0
- package/dist/services/password-hasher.service.js +170 -0
- package/dist/services/ws-authenticator.service.d.ts +38 -0
- package/dist/services/ws-authenticator.service.js +92 -0
- package/dist/session/cookies.util.d.ts +34 -0
- package/dist/session/cookies.util.js +82 -0
- package/dist/session/in-memory-session.store.d.ts +33 -0
- package/dist/session/in-memory-session.store.js +60 -0
- package/dist/session/session-cookie.provider.d.ts +36 -0
- package/dist/session/session-cookie.provider.js +58 -0
- package/dist/session/session.service.d.ts +97 -0
- package/dist/session/session.service.js +208 -0
- package/dist/session/sign-in.service.d.ts +87 -0
- package/dist/session/sign-in.service.js +206 -0
- package/dist/storage/authentication.storage.d.ts +130 -0
- package/dist/storage/authentication.storage.js +350 -0
- package/dist/storage/index.d.ts +1 -0
- package/dist/storage/index.js +1 -0
- package/dist/testing/index.d.ts +39 -0
- package/dist/testing/index.js +531 -0
- package/dist/utils/api-key.util.d.ts +10 -0
- package/dist/utils/api-key.util.js +10 -0
- package/dist/utils/auth-state.util.d.ts +54 -0
- package/dist/utils/auth-state.util.js +209 -0
- package/dist/utils/bearer.util.d.ts +9 -0
- package/dist/utils/bearer.util.js +21 -0
- package/dist/utils/cross-origin.util.d.ts +29 -0
- package/dist/utils/cross-origin.util.js +90 -0
- package/dist/utils/crypto.util.d.ts +19 -0
- package/dist/utils/crypto.util.js +38 -0
- package/dist/utils/duration.util.d.ts +9 -0
- package/dist/utils/duration.util.js +44 -0
- package/dist/utils/index.d.ts +8 -0
- package/dist/utils/index.js +8 -0
- package/dist/utils/options.util.d.ts +20 -0
- package/dist/utils/options.util.js +44 -0
- package/dist/utils/transport-errors.util.d.ts +33 -0
- package/dist/utils/transport-errors.util.js +70 -0
- package/package.json +108 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
(The MIT License)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Kamil Mysliwiec
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
6
|
+
a copy of this software and associated documentation files (the
|
|
7
|
+
'Software'), to deal in the Software without restriction, including
|
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
|
11
|
+
the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be
|
|
14
|
+
included in all copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
19
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
|
20
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
|
21
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
|
22
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
23
|
+
|
package/README.md
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<a href="http://nestjs.com/" target="blank"><img src="https://nestjs.com/img/logo-small.svg" width="120" alt="Nest Logo" /></a>
|
|
3
|
+
</p>
|
|
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
|
+
<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
|
+
<p align="center">
|
|
12
|
+
<a href="https://www.npmjs.com/~nestjscore"><img src="https://img.shields.io/npm/v/@nestjs/core.svg" alt="NPM Version" /></a>
|
|
13
|
+
<a href="https://www.npmjs.com/~nestjscore"><img src="https://img.shields.io/npm/l/@nestjs/core.svg" alt="Package License" /></a>
|
|
14
|
+
<a href="https://www.npmjs.com/~nestjscore"><img src="https://img.shields.io/npm/dm/@nestjs/core.svg" alt="NPM Downloads" /></a>
|
|
15
|
+
<a href="https://discord.gg/G7Qnnhy" target="_blank"><img src="https://img.shields.io/badge/discord-online-brightgreen.svg" alt="Discord"/></a>
|
|
16
|
+
<a href="https://opencollective.com/nest#backer"><img src="https://opencollective.com/nest/backers/badge.svg" alt="Backers on Open Collective" /></a>
|
|
17
|
+
<a href="https://opencollective.com/nest#sponsor"><img src="https://opencollective.com/nest/sponsors/badge.svg" alt="Sponsors on Open Collective" /></a>
|
|
18
|
+
<a href="https://paypal.me/kamilmysliwiec"><img src="https://img.shields.io/badge/Donate-PayPal-dc3d53.svg"/></a>
|
|
19
|
+
<a href="https://twitter.com/nestframework"><img src="https://img.shields.io/twitter/follow/nestframework.svg?style=social&label=Follow"></a>
|
|
20
|
+
</p>
|
|
21
|
+
<!--[](https://opencollective.com/nest#backer)
|
|
22
|
+
[](https://opencollective.com/nest#sponsor)-->
|
|
23
|
+
|
|
24
|
+
## Description
|
|
25
|
+
|
|
26
|
+
Authentication module for [Nest](https://github.com/nestjs/nest): a global guard with `@Public()` and `@Authenticate()`, credential providers as ordinary injectable classes (bearer JWT, cookie sessions, your own), server-side sessions with rotation, refresh tokens, TOTP second factor, magic links, OIDC sign-in, email verification and password reset, across HTTP, GraphQL, WebSockets and microservices, with no third-party dependencies.
|
|
27
|
+
|
|
28
|
+
## Installation
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
$ npm i --save @nestjs/authentication
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Quick Start
|
|
35
|
+
|
|
36
|
+
[Overview & Tutorial](https://docs.nestjs.com/security/authentication)
|
|
37
|
+
|
|
38
|
+
## Support
|
|
39
|
+
|
|
40
|
+
Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support).
|
|
41
|
+
|
|
42
|
+
## Stay in touch
|
|
43
|
+
|
|
44
|
+
- Author - [Kamil Myśliwiec](https://twitter.com/kammysliwiec)
|
|
45
|
+
- Website - [https://nestjs.com](https://nestjs.com/)
|
|
46
|
+
- Twitter - [@nestframework](https://twitter.com/nestframework)
|
|
47
|
+
|
|
48
|
+
## License
|
|
49
|
+
|
|
50
|
+
Nest is [MIT licensed](LICENSE).
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { EmailVerificationLink } from '../interfaces/email-verification.interface.js';
|
|
2
|
+
/**
|
|
3
|
+
* Implemented by the app: delivers verification links, and records a
|
|
4
|
+
* verified address in the app's users table (the module ships no mailer and
|
|
5
|
+
* never reads that table). An ordinary provider of one of your modules,
|
|
6
|
+
* which registers itself from its constructor:
|
|
7
|
+
* `registry.registerHandler('emailVerification', this)`.
|
|
8
|
+
*/
|
|
9
|
+
export declare abstract class EmailVerificationHandler {
|
|
10
|
+
abstract send(link: EmailVerificationLink): void | Promise<void>;
|
|
11
|
+
/**
|
|
12
|
+
* Marks the user's address verified, but only if it is still `email`
|
|
13
|
+
* (one conditional update: `UPDATE users SET email_verified = true WHERE
|
|
14
|
+
* id = ? AND email = ?`). Resolves `false` when the address changed since
|
|
15
|
+
* the link was sent, or the user is gone: the link then verifies nothing.
|
|
16
|
+
*/
|
|
17
|
+
abstract markVerified(userId: string, email: string): boolean | Promise<boolean>;
|
|
18
|
+
/**
|
|
19
|
+
* Whether a signed-in user's current address is verified, for
|
|
20
|
+
* `@Authenticate({ verifiedEmail: true })`. Default: `user.emailVerified === true`.
|
|
21
|
+
*/
|
|
22
|
+
isVerified(user: unknown): boolean | Promise<boolean>;
|
|
23
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { hasVerifiedEmail } from './email.util.js';
|
|
2
|
+
/**
|
|
3
|
+
* Implemented by the app: delivers verification links, and records a
|
|
4
|
+
* verified address in the app's users table (the module ships no mailer and
|
|
5
|
+
* never reads that table). An ordinary provider of one of your modules,
|
|
6
|
+
* which registers itself from its constructor:
|
|
7
|
+
* `registry.registerHandler('emailVerification', this)`.
|
|
8
|
+
*/
|
|
9
|
+
export class EmailVerificationHandler {
|
|
10
|
+
/**
|
|
11
|
+
* Whether a signed-in user's current address is verified, for
|
|
12
|
+
* `@Authenticate({ verifiedEmail: true })`. Default: `user.emailVerified === true`.
|
|
13
|
+
*/
|
|
14
|
+
isVerified(user) {
|
|
15
|
+
return hasVerifiedEmail(user);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { AuthenticationRegistry } from '../services/authentication-registry.service.js';
|
|
2
|
+
import { AuthenticationStorage } from '../storage/authentication.storage.js';
|
|
3
|
+
import { AuthenticationEvents } from '../events/authentication-events.service.js';
|
|
4
|
+
import type { EmailVerificationOptions } from '../interfaces/email-verification.interface.js';
|
|
5
|
+
/**
|
|
6
|
+
* Email verification links: 256-bit single-use tokens, stored hashed, bound
|
|
7
|
+
* to the user and to the address they were sent to. Changing the address
|
|
8
|
+
* makes the links sent to the old one verify nothing.
|
|
9
|
+
*
|
|
10
|
+
* Point the link at a page that POSTs the token back: mail scanners and link
|
|
11
|
+
* previews follow GET links, and would use it up.
|
|
12
|
+
*/
|
|
13
|
+
export declare class EmailVerificationService {
|
|
14
|
+
private readonly storage;
|
|
15
|
+
private readonly registry;
|
|
16
|
+
private readonly events;
|
|
17
|
+
private readonly options?;
|
|
18
|
+
private readonly ttl;
|
|
19
|
+
constructor(storage: AuthenticationStorage, registry: AuthenticationRegistry, options?: {
|
|
20
|
+
emailVerification?: EmailVerificationOptions;
|
|
21
|
+
}, events?: AuthenticationEvents);
|
|
22
|
+
/**
|
|
23
|
+
* Sends a verification link for the user's current address, through
|
|
24
|
+
* `EmailVerificationHandler.send()`. Call it at sign-up, when the address
|
|
25
|
+
* changes, and when the user asks for a new link. The token is not returned.
|
|
26
|
+
*/
|
|
27
|
+
send(user: {
|
|
28
|
+
id: string;
|
|
29
|
+
email: string;
|
|
30
|
+
}): Promise<{
|
|
31
|
+
expiresAt: Date;
|
|
32
|
+
}>;
|
|
33
|
+
/**
|
|
34
|
+
* Burns the token and asks `EmailVerificationHandler.markVerified()` to
|
|
35
|
+
* record the address, if it is still the user's. Resolves the verified
|
|
36
|
+
* address, or `null` for unknown, used or expired tokens and for an
|
|
37
|
+
* address the user has since changed.
|
|
38
|
+
*/
|
|
39
|
+
verify(token: string): Promise<{
|
|
40
|
+
userId: string;
|
|
41
|
+
email: string;
|
|
42
|
+
} | null>;
|
|
43
|
+
/** The options and the handler; the module refuses to start with one and not the other. */
|
|
44
|
+
private feature;
|
|
45
|
+
private now;
|
|
46
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
8
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
9
|
+
};
|
|
10
|
+
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
11
|
+
return function (target, key) { decorator(target, key, paramIndex); }
|
|
12
|
+
};
|
|
13
|
+
import { Inject, Injectable, Optional } from '@nestjs/common';
|
|
14
|
+
import { AuthenticationRegistry } from '../services/authentication-registry.service.js';
|
|
15
|
+
import { AuthenticationStorage } from '../storage/authentication.storage.js';
|
|
16
|
+
import { AUTHENTICATION_MODULE_OPTIONS } from '../authentication.constants.js';
|
|
17
|
+
import { TOKEN_PATTERN, randomToken, sha256 } from '../utils/crypto.util.js';
|
|
18
|
+
import { durationOr, hasExpired } from '../utils/duration.util.js';
|
|
19
|
+
import { AuthenticationEvents } from '../events/authentication-events.service.js';
|
|
20
|
+
import { requireUrlOption } from '../utils/options.util.js';
|
|
21
|
+
/**
|
|
22
|
+
* Email verification links: 256-bit single-use tokens, stored hashed, bound
|
|
23
|
+
* to the user and to the address they were sent to. Changing the address
|
|
24
|
+
* makes the links sent to the old one verify nothing.
|
|
25
|
+
*
|
|
26
|
+
* Point the link at a page that POSTs the token back: mail scanners and link
|
|
27
|
+
* previews follow GET links, and would use it up.
|
|
28
|
+
*/
|
|
29
|
+
let EmailVerificationService = class EmailVerificationService {
|
|
30
|
+
constructor(storage, registry, options, events = new AuthenticationEvents()) {
|
|
31
|
+
this.storage = storage;
|
|
32
|
+
this.registry = registry;
|
|
33
|
+
this.events = events;
|
|
34
|
+
this.options = options?.emailVerification;
|
|
35
|
+
if (this.options) {
|
|
36
|
+
requireUrlOption(this.options.url, 'emailVerification.url', 'the page that receives the link');
|
|
37
|
+
}
|
|
38
|
+
this.ttl = durationOr(this.options?.ttl, '24h');
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Sends a verification link for the user's current address, through
|
|
42
|
+
* `EmailVerificationHandler.send()`. Call it at sign-up, when the address
|
|
43
|
+
* changes, and when the user asks for a new link. The token is not returned.
|
|
44
|
+
*/
|
|
45
|
+
async send(user) {
|
|
46
|
+
const { options, handler } = this.feature();
|
|
47
|
+
// The address as the account has it: `markVerified()` compares it with the stored one, which
|
|
48
|
+
// may not be lowercase.
|
|
49
|
+
if (typeof user?.email !== 'string' || user.email.trim() === '') {
|
|
50
|
+
throw new TypeError('EmailVerificationService.send(): the user needs an `email` to send the link to.');
|
|
51
|
+
}
|
|
52
|
+
const email = user.email;
|
|
53
|
+
const token = randomToken();
|
|
54
|
+
const now = this.now();
|
|
55
|
+
const expiresAt = new Date(now + this.ttl);
|
|
56
|
+
await this.storage.emailTokens.saveEmailToken({
|
|
57
|
+
id: sha256(token),
|
|
58
|
+
purpose: 'email-verification',
|
|
59
|
+
userId: user.id,
|
|
60
|
+
email,
|
|
61
|
+
createdAt: new Date(now),
|
|
62
|
+
expiresAt,
|
|
63
|
+
});
|
|
64
|
+
const url = new URL(options.url);
|
|
65
|
+
url.searchParams.set('token', token);
|
|
66
|
+
await handler.send({ userId: user.id, email, url: url.toString(), expiresAt });
|
|
67
|
+
return { expiresAt };
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Burns the token and asks `EmailVerificationHandler.markVerified()` to
|
|
71
|
+
* record the address, if it is still the user's. Resolves the verified
|
|
72
|
+
* address, or `null` for unknown, used or expired tokens and for an
|
|
73
|
+
* address the user has since changed.
|
|
74
|
+
*/
|
|
75
|
+
async verify(token) {
|
|
76
|
+
const { handler } = this.feature();
|
|
77
|
+
if (typeof token !== 'string' || !TOKEN_PATTERN.test(token)) {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
const record = await this.storage.emailTokens.consumeEmailToken(sha256(token), 'email-verification');
|
|
81
|
+
if (!record || hasExpired(record.expiresAt, this.now())) {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
if (!(await handler.markVerified(record.userId, record.email))) {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
await this.storage.emailTokens.deleteUserEmailTokens(record.userId, 'email-verification');
|
|
88
|
+
this.events.emit({ type: 'email-verified', userId: record.userId, email: record.email });
|
|
89
|
+
return { userId: record.userId, email: record.email };
|
|
90
|
+
}
|
|
91
|
+
/** The options and the handler; the module refuses to start with one and not the other. */
|
|
92
|
+
feature() {
|
|
93
|
+
const handler = this.registry.handler('emailVerification');
|
|
94
|
+
if (!this.options || !handler) {
|
|
95
|
+
throw new Error('EmailVerificationService: email verification is not enabled. Configure `emailVerification` in the ' +
|
|
96
|
+
"AuthenticationModule options, and register an EmailVerificationHandler: `registry.registerHandler('emailVerification', this)`.");
|
|
97
|
+
}
|
|
98
|
+
return { options: this.options, handler };
|
|
99
|
+
}
|
|
100
|
+
now() {
|
|
101
|
+
return this.options?.now?.() ?? Date.now();
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
EmailVerificationService = __decorate([
|
|
105
|
+
Injectable(),
|
|
106
|
+
__param(2, Optional()),
|
|
107
|
+
__param(2, Inject(AUTHENTICATION_MODULE_OPTIONS)),
|
|
108
|
+
__metadata("design:paramtypes", [AuthenticationStorage,
|
|
109
|
+
AuthenticationRegistry, Object, AuthenticationEvents])
|
|
110
|
+
], EmailVerificationService);
|
|
111
|
+
export { EmailVerificationService };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** @internal What `@Authenticate({ verifiedEmail: true })` checks without a handler. */
|
|
2
|
+
export declare function hasVerifiedEmail(user: unknown): boolean;
|
|
3
|
+
/**
|
|
4
|
+
* @internal The address as links carry it: trimmed, lowercased, and in
|
|
5
|
+
* Unicode NFC, so an address typed with a combining accent is the same
|
|
6
|
+
* address as the one stored precomposed.
|
|
7
|
+
*/
|
|
8
|
+
export declare function normalizeEmail(email: string): string;
|
|
9
|
+
/**
|
|
10
|
+
* @internal An address worth mailing a link to: `local@domain`, at most 254
|
|
11
|
+
* characters (RFC 5321 §4.5.3.1.3), without whitespace or control
|
|
12
|
+
* characters, which a mailer could take for header syntax.
|
|
13
|
+
*/
|
|
14
|
+
export declare function isMailableEmail(normalized: string): boolean;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/** @internal What `@Authenticate({ verifiedEmail: true })` checks without a handler. */
|
|
2
|
+
export function hasVerifiedEmail(user) {
|
|
3
|
+
return typeof user === 'object' && user !== null && user.emailVerified === true;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* @internal The address as links carry it: trimmed, lowercased, and in
|
|
7
|
+
* Unicode NFC, so an address typed with a combining accent is the same
|
|
8
|
+
* address as the one stored precomposed.
|
|
9
|
+
*/
|
|
10
|
+
export function normalizeEmail(email) {
|
|
11
|
+
if (typeof email !== 'string') {
|
|
12
|
+
throw new TypeError('An email address must be a string');
|
|
13
|
+
}
|
|
14
|
+
return email.trim().normalize('NFC').toLowerCase();
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* @internal An address worth mailing a link to: `local@domain`, at most 254
|
|
18
|
+
* characters (RFC 5321 §4.5.3.1.3), without whitespace or control
|
|
19
|
+
* characters, which a mailer could take for header syntax.
|
|
20
|
+
*/
|
|
21
|
+
export function isMailableEmail(normalized) {
|
|
22
|
+
// oxlint-disable-next-line no-control-regex -- control characters are what it refuses
|
|
23
|
+
if (normalized.length > 254 || /[\s\x00-\x1f\x7f]/.test(normalized)) {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
const at = normalized.lastIndexOf('@');
|
|
27
|
+
return at > 0 && at < normalized.length - 1;
|
|
28
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { EmailTokenPurpose, EmailTokenRecord, EmailTokenStore } from '../interfaces/email-token-store.interface.js';
|
|
2
|
+
/**
|
|
3
|
+
* Process-local store: the default when no source is registered, and a
|
|
4
|
+
* test double. Drops expired tokens as new ones are saved, and holds at
|
|
5
|
+
* most 10,000, dropping the one that expires first.
|
|
6
|
+
*/
|
|
7
|
+
export declare class InMemoryEmailTokenStore implements EmailTokenStore {
|
|
8
|
+
private readonly tokens;
|
|
9
|
+
saveEmailToken(record: EmailTokenRecord): Promise<void>;
|
|
10
|
+
consumeEmailToken(id: string, purpose: EmailTokenPurpose): Promise<EmailTokenRecord | undefined>;
|
|
11
|
+
deleteUserEmailTokens(userId: string, purpose: EmailTokenPurpose): Promise<void>;
|
|
12
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
import { Injectable } from '@nestjs/common';
|
|
8
|
+
const MAX_PENDING = 10_000;
|
|
9
|
+
/**
|
|
10
|
+
* Process-local store: the default when no source is registered, and a
|
|
11
|
+
* test double. Drops expired tokens as new ones are saved, and holds at
|
|
12
|
+
* most 10,000, dropping the one that expires first.
|
|
13
|
+
*/
|
|
14
|
+
let InMemoryEmailTokenStore = class InMemoryEmailTokenStore {
|
|
15
|
+
constructor() {
|
|
16
|
+
this.tokens = new Map();
|
|
17
|
+
}
|
|
18
|
+
async saveEmailToken(record) {
|
|
19
|
+
const now = record.createdAt.getTime();
|
|
20
|
+
// Insertion order is close to expiry order (one lifetime per purpose): stop at the first
|
|
21
|
+
// live entry; the cap bounds whatever that leaves behind.
|
|
22
|
+
for (const [id, token] of this.tokens) {
|
|
23
|
+
if (token.expiresAt.getTime() > now) {
|
|
24
|
+
break;
|
|
25
|
+
}
|
|
26
|
+
this.tokens.delete(id);
|
|
27
|
+
}
|
|
28
|
+
this.tokens.set(record.id, { ...record });
|
|
29
|
+
// Past the cap, the token that expires first goes, not the oldest: purposes have their own
|
|
30
|
+
// lifetimes, and a flood of one-hour reset links must not push out a day-long verification link.
|
|
31
|
+
if (this.tokens.size > MAX_PENDING) {
|
|
32
|
+
let first;
|
|
33
|
+
for (const token of this.tokens.values()) {
|
|
34
|
+
if (!first || token.expiresAt.getTime() < first.expiresAt.getTime()) {
|
|
35
|
+
first = token;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
this.tokens.delete(first.id);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
async consumeEmailToken(id, purpose) {
|
|
42
|
+
const record = this.tokens.get(id);
|
|
43
|
+
if (record?.purpose !== purpose) {
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
this.tokens.delete(id);
|
|
47
|
+
return record;
|
|
48
|
+
}
|
|
49
|
+
async deleteUserEmailTokens(userId, purpose) {
|
|
50
|
+
for (const [id, token] of this.tokens) {
|
|
51
|
+
if (token.userId === userId && token.purpose === purpose) {
|
|
52
|
+
this.tokens.delete(id);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
InMemoryEmailTokenStore = __decorate([
|
|
58
|
+
Injectable()
|
|
59
|
+
], InMemoryEmailTokenStore);
|
|
60
|
+
export { InMemoryEmailTokenStore };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { PasswordResetAccount, PasswordResetLink } from '../interfaces/password-reset.interface.js';
|
|
2
|
+
/**
|
|
3
|
+
* Implemented by the app: finds accounts by address, delivers reset links,
|
|
4
|
+
* and stores new password hashes (the module ships no mailer and never reads
|
|
5
|
+
* your users table). An ordinary provider of one of your modules, which
|
|
6
|
+
* registers itself from its constructor:
|
|
7
|
+
* `registry.registerHandler('passwordReset', this)`.
|
|
8
|
+
*/
|
|
9
|
+
export declare abstract class PasswordResetHandler {
|
|
10
|
+
/**
|
|
11
|
+
* The account whose address this is, with the address and the password
|
|
12
|
+
* hash it has stored, or `null`. Called with the address trimmed,
|
|
13
|
+
* lowercased and in Unicode NFC.
|
|
14
|
+
*/
|
|
15
|
+
abstract findUser(email: string): PasswordResetAccount | null | Promise<PasswordResetAccount | null>;
|
|
16
|
+
abstract send(link: PasswordResetLink): void | Promise<void>;
|
|
17
|
+
/** Stores the new password's hash (from `PasswordHasher`). */
|
|
18
|
+
abstract updatePassword(userId: string, passwordHash: string): void | Promise<void>;
|
|
19
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Implemented by the app: finds accounts by address, delivers reset links,
|
|
3
|
+
* and stores new password hashes (the module ships no mailer and never reads
|
|
4
|
+
* your users table). An ordinary provider of one of your modules, which
|
|
5
|
+
* registers itself from its constructor:
|
|
6
|
+
* `registry.registerHandler('passwordReset', this)`.
|
|
7
|
+
*/
|
|
8
|
+
export class PasswordResetHandler {
|
|
9
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { type OnModuleDestroy } from '@nestjs/common';
|
|
2
|
+
import { AuthenticationRegistry } from '../services/authentication-registry.service.js';
|
|
3
|
+
import { AuthenticationStorage } from '../storage/authentication.storage.js';
|
|
4
|
+
import { AuthenticationEvents } from '../events/authentication-events.service.js';
|
|
5
|
+
import { TokenService } from '../jwt/token.service.js';
|
|
6
|
+
import { PasswordHasher } from '../services/password-hasher.service.js';
|
|
7
|
+
import { SessionService } from '../session/session.service.js';
|
|
8
|
+
import { SignInService } from '../session/sign-in.service.js';
|
|
9
|
+
import type { PasswordResetOptions, ResetPasswordOptions, PasswordResetResult } from '../interfaces/password-reset.interface.js';
|
|
10
|
+
/**
|
|
11
|
+
* "Forgot your password?": reset links that are 256-bit single-use tokens,
|
|
12
|
+
* stored hashed, short-lived, and bound to the address they were sent to and
|
|
13
|
+
* to the password they replace.
|
|
14
|
+
*
|
|
15
|
+
* - `request(email)` answers the same way, at once, whether or not the
|
|
16
|
+
* address has an account: finding the account, storing the token and
|
|
17
|
+
* sending the link happen after it returned, so neither the answer nor
|
|
18
|
+
* its timing reveals which addresses are registered. The link goes to the
|
|
19
|
+
* address the account has stored, never to the one typed in the form.
|
|
20
|
+
* - `reset(token, password)` burns the token, stores the new password's
|
|
21
|
+
* hash, invalidates the user's other reset links, and ends every session
|
|
22
|
+
* and refresh-token family of the user (whoever knew the old password is
|
|
23
|
+
* signed out). It also marks the address verified when email
|
|
24
|
+
* verification is configured: the link proved it. API keys live in your
|
|
25
|
+
* own table: revoke those yourself, on the `password-reset` event.
|
|
26
|
+
*
|
|
27
|
+
* Point the link at a page that asks for the new password and POSTs it with
|
|
28
|
+
* the token: mail scanners and link previews follow GET links.
|
|
29
|
+
*/
|
|
30
|
+
export declare class PasswordResetService implements OnModuleDestroy {
|
|
31
|
+
private readonly storage;
|
|
32
|
+
private readonly registry;
|
|
33
|
+
private readonly hasher;
|
|
34
|
+
private readonly sessions;
|
|
35
|
+
private readonly tokens;
|
|
36
|
+
private readonly signInService;
|
|
37
|
+
private readonly events;
|
|
38
|
+
private static readonly logger;
|
|
39
|
+
private readonly options?;
|
|
40
|
+
private readonly ttl;
|
|
41
|
+
private readonly inFlight;
|
|
42
|
+
constructor(storage: AuthenticationStorage, registry: AuthenticationRegistry, hasher: PasswordHasher, sessions: SessionService, tokens: TokenService, signInService: SignInService, options?: {
|
|
43
|
+
passwordReset?: PasswordResetOptions;
|
|
44
|
+
}, events?: AuthenticationEvents);
|
|
45
|
+
/**
|
|
46
|
+
* Starts a reset for `email`, if it belongs to an account: the link goes
|
|
47
|
+
* to `PasswordResetHandler.send()`. Returns at once, before the account is
|
|
48
|
+
* looked up, so the caller answers every address the same way and in the
|
|
49
|
+
* same time. Failures are logged, not thrown. On shutdown, the module waits
|
|
50
|
+
* for the requests in flight.
|
|
51
|
+
*/
|
|
52
|
+
request(email: string): void;
|
|
53
|
+
/**
|
|
54
|
+
* Sets a new password with a reset token. `null` (and nothing changed)
|
|
55
|
+
* for an unknown, used or expired token, and for one whose account
|
|
56
|
+
* changed its address or password since the link was sent. `null` too,
|
|
57
|
+
* before the token is looked up (so the link still works), for a password
|
|
58
|
+
* `PasswordHasher` refuses: not a string, empty, or over 4 KiB. Check your
|
|
59
|
+
* own password rules before calling it.
|
|
60
|
+
*/
|
|
61
|
+
reset(token: string, password: string, { signIn }?: ResetPasswordOptions): Promise<PasswordResetResult | null>;
|
|
62
|
+
onModuleDestroy(): Promise<void>;
|
|
63
|
+
private issue;
|
|
64
|
+
/** Ends every session and refresh-token family of the user. */
|
|
65
|
+
private revokeSignIns;
|
|
66
|
+
/** The options and the handler; the module refuses to start with one and not the other. */
|
|
67
|
+
private feature;
|
|
68
|
+
private now;
|
|
69
|
+
}
|