@ambushsoftworks/nestjs-auth-graphql 0.18.0 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +101 -0
- package/README.md +273 -5
- package/dist/auth.module.d.ts +10 -2
- package/dist/auth.module.d.ts.map +1 -1
- package/dist/auth.module.js +71 -3
- package/dist/auth.module.js.map +1 -1
- package/dist/constants.d.ts +1 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +2 -1
- package/dist/constants.js.map +1 -1
- package/dist/decorators/allow-guest-session.decorator.d.ts +3 -0
- package/dist/decorators/allow-guest-session.decorator.d.ts.map +1 -0
- package/dist/decorators/allow-guest-session.decorator.js +8 -0
- package/dist/decorators/allow-guest-session.decorator.js.map +1 -0
- package/dist/decorators/current-guest.decorator.d.ts +2 -0
- package/dist/decorators/current-guest.decorator.d.ts.map +1 -0
- package/dist/decorators/current-guest.decorator.js +7 -0
- package/dist/decorators/current-guest.decorator.js.map +1 -0
- package/dist/exceptions/guest-session.exceptions.d.ts +9 -0
- package/dist/exceptions/guest-session.exceptions.d.ts.map +1 -0
- package/dist/exceptions/guest-session.exceptions.js +20 -0
- package/dist/exceptions/guest-session.exceptions.js.map +1 -0
- package/dist/exceptions/signup-disabled.exception.d.ts +5 -0
- package/dist/exceptions/signup-disabled.exception.d.ts.map +1 -0
- package/dist/exceptions/signup-disabled.exception.js +11 -0
- package/dist/exceptions/signup-disabled.exception.js.map +1 -0
- package/dist/guards/create-auth-guard.d.ts.map +1 -1
- package/dist/guards/create-auth-guard.js +29 -1
- package/dist/guards/create-auth-guard.js.map +1 -1
- package/dist/guards/guest-session.guard.d.ts +14 -0
- package/dist/guards/guest-session.guard.d.ts.map +1 -0
- package/dist/guards/guest-session.guard.js +63 -0
- package/dist/guards/guest-session.guard.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -1
- package/dist/interfaces/auth-logger.interface.d.ts +6 -1
- package/dist/interfaces/auth-logger.interface.d.ts.map +1 -1
- package/dist/interfaces/auth-logger.interface.js +5 -0
- package/dist/interfaces/auth-logger.interface.js.map +1 -1
- package/dist/interfaces/guest-session-repository.interface.d.ts +37 -0
- package/dist/interfaces/guest-session-repository.interface.d.ts.map +1 -0
- package/dist/interfaces/guest-session-repository.interface.js +3 -0
- package/dist/interfaces/guest-session-repository.interface.js.map +1 -0
- package/dist/interfaces/index.d.ts +1 -0
- package/dist/interfaces/index.d.ts.map +1 -1
- package/dist/interfaces/index.js +1 -0
- package/dist/interfaces/index.js.map +1 -1
- package/dist/interfaces/user-repository.interface.d.ts +29 -27
- package/dist/interfaces/user-repository.interface.d.ts.map +1 -1
- package/dist/repositories/in-memory-guest-session.repository.d.ts +18 -0
- package/dist/repositories/in-memory-guest-session.repository.d.ts.map +1 -0
- package/dist/repositories/in-memory-guest-session.repository.js +83 -0
- package/dist/repositories/in-memory-guest-session.repository.js.map +1 -0
- package/dist/services/auth.service.d.ts +8 -0
- package/dist/services/auth.service.d.ts.map +1 -1
- package/dist/services/auth.service.js +31 -4
- package/dist/services/auth.service.js.map +1 -1
- package/dist/services/biometric-auth.service.d.ts.map +1 -1
- package/dist/services/biometric-auth.service.js +1 -1
- package/dist/services/biometric-auth.service.js.map +1 -1
- package/dist/services/guest-session.service.d.ts +80 -0
- package/dist/services/guest-session.service.d.ts.map +1 -0
- package/dist/services/guest-session.service.js +293 -0
- package/dist/services/guest-session.service.js.map +1 -0
- package/dist/services/passkey.service.d.ts.map +1 -1
- package/dist/services/passkey.service.js +7 -2
- package/dist/services/passkey.service.js.map +1 -1
- package/dist/services/password-hash.service.d.ts +7 -0
- package/dist/services/password-hash.service.d.ts.map +1 -0
- package/dist/services/password-hash.service.js +33 -0
- package/dist/services/password-hash.service.js.map +1 -0
- package/dist/testing/guest-session-repository.contract.d.ts +18 -0
- package/dist/testing/guest-session-repository.contract.d.ts.map +1 -0
- package/dist/testing/guest-session-repository.contract.js +368 -0
- package/dist/testing/guest-session-repository.contract.js.map +1 -0
- package/dist/testing/index.d.ts +1 -0
- package/dist/testing/index.d.ts.map +1 -1
- package/dist/testing/index.js +4 -1
- package/dist/testing/index.js.map +1 -1
- package/dist/utils/password-hash.d.ts +3 -0
- package/dist/utils/password-hash.d.ts.map +1 -0
- package/dist/utils/password-hash.js +43 -0
- package/dist/utils/password-hash.js.map +1 -0
- package/examples/guest-sessions/prisma-guest-session.repository.ts +86 -0
- package/examples/guest-sessions/schema.prisma +33 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -27,6 +27,107 @@ for buried obligations. **The marker was introduced in 0.9.0 and has not been
|
|
|
27
27
|
retro-applied**, so it is reliable from 0.9.0 onward only. Convention adopted
|
|
28
28
|
from `@ambushsoftworks/nestjs-payments-graphql`.
|
|
29
29
|
|
|
30
|
+
## [0.20.0] - 2026-10-10
|
|
31
|
+
|
|
32
|
+
For apps whose accounts are created by an operator, not by sign-up. Five
|
|
33
|
+
requests from Posada San Carlos. Nothing here requires a change to an app that
|
|
34
|
+
compiles today, with one exception under "Changed — breaking".
|
|
35
|
+
|
|
36
|
+
### Added
|
|
37
|
+
|
|
38
|
+
- **`features.signup: false`** refuses every path that would create an account
|
|
39
|
+
with `SIGNUP_DISABLED` (403), before it reaches `create`: password sign-up,
|
|
40
|
+
passwordless sign-up, and a first sign-in with Google or Facebook. Signing in
|
|
41
|
+
to an existing account is unaffected. Default `true`.
|
|
42
|
+
- **`PasswordHashService`**: `hash(password)` hashes exactly as sign-up does, at
|
|
43
|
+
the module's `bcryptRounds`, for accounts created by a script. Available from
|
|
44
|
+
`NestFactory.createApplicationContext`. Sign-up, change-password and
|
|
45
|
+
reset-password now go through the same function.
|
|
46
|
+
- **`AuthUserRepository`**, the type `userRepositoryInstance` now accepts: five
|
|
47
|
+
required methods (`findByEmail`, `findById`, `update`, `updatePassword`,
|
|
48
|
+
`markEmailVerified`) and the rest optional. The README Quick Start's core-only
|
|
49
|
+
repository now compiles without a cast.
|
|
50
|
+
- **A boot check on the user repository**: `InvalidAuthConfigException` naming
|
|
51
|
+
the method, when `oauth.google` or `oauth.facebook` is set without
|
|
52
|
+
`findByOAuthProvider`, or `smsServiceInstance` without `findByPhoneNumber`.
|
|
53
|
+
- `AuthService.issueLoginSession`, used by the package's own passkey and
|
|
54
|
+
device-key sign-in. `SignupDisabledException`.
|
|
55
|
+
|
|
56
|
+
### Changed
|
|
57
|
+
|
|
58
|
+
- **`onLogin` now fires for passkey and device-key sign-in**, as it always has
|
|
59
|
+
for password, Google and Facebook. If your hook sends a "new sign-in" email,
|
|
60
|
+
people who sign in with a passkey will start to receive it. An error thrown
|
|
61
|
+
by the hook on these two paths is logged and does not fail the sign-in.
|
|
62
|
+
- **No boot warning about Google or Facebook when you have not configured
|
|
63
|
+
them.** The two "OAuth not configured" warnings are now logged only when an
|
|
64
|
+
`oauth.google` or `oauth.facebook` block is present but incomplete.
|
|
65
|
+
|
|
66
|
+
### Changed — breaking (types only)
|
|
67
|
+
|
|
68
|
+
- **27 methods of `IUserRepository` are now optional and `@deprecated`**: the
|
|
69
|
+
package never called them. Among them `updateLastLoginAt` (record a login in
|
|
70
|
+
`onLogin` instead), `incrementFailedLoginAttempts`, `updateEmail`, every
|
|
71
|
+
per-provider `find…`, `link…` and `unlink…` method, and all of
|
|
72
|
+
`IUserRepositoryBiometric`. A repository that implements them keeps
|
|
73
|
+
compiling. **Code of yours that calls one of them through the package's
|
|
74
|
+
interface type** will now be told it may be undefined; call it on your own
|
|
75
|
+
class type instead. They will be removed from the interface at the next
|
|
76
|
+
breaking release.
|
|
77
|
+
|
|
78
|
+
### Deprecated
|
|
79
|
+
|
|
80
|
+
- `IUserRepositoryBiometric`, and the 27 methods above.
|
|
81
|
+
|
|
82
|
+
## [0.19.0] - 2026-10-08
|
|
83
|
+
|
|
84
|
+
**Guest sessions**: a session for someone who is not a user, which works only on
|
|
85
|
+
routes that opt in, for the things you granted, until the dates you set.
|
|
86
|
+
Additive; nothing changes for an app that does not use it. Requested by Ariadne,
|
|
87
|
+
for outside translators working on one job.
|
|
88
|
+
|
|
89
|
+
### ⚠ External configuration required
|
|
90
|
+
|
|
91
|
+
Only to use guest sessions:
|
|
92
|
+
- **Two tables for `IGuestSessionRepository`**, with no foreign key to users.
|
|
93
|
+
Schema and Prisma adapter in `examples/guest-sessions/`. Run
|
|
94
|
+
`assertGuestSessionRepositoryContract` against it on your real database.
|
|
95
|
+
- **Your frontend handles `GUEST_SESSION_INVALID`** by sending the person to
|
|
96
|
+
prove their address again. It must not be treated like `UNAUTHENTICATED`:
|
|
97
|
+
there is no refresh for a guest.
|
|
98
|
+
- Without `features.cookieAuth`, the client keeps the token `issue` returns and
|
|
99
|
+
sends it as `Authorization: Guest <token>`. **The token changes whenever a
|
|
100
|
+
grant is added**; store the new one.
|
|
101
|
+
|
|
102
|
+
### Added
|
|
103
|
+
|
|
104
|
+
- **`GuestSessionService`**: `issue({ principal, grant })` starts a session or
|
|
105
|
+
adds a grant to the one the browser holds; `revokeGrant({ kind, id })`,
|
|
106
|
+
`revoke(sessionId)` and `end(request, res)` stop them. See the README, "Guest
|
|
107
|
+
Sessions".
|
|
108
|
+
- **`@AllowGuestSession(kind)`**, on a method, lets a guest with a standing
|
|
109
|
+
grant of that kind reach the handler. Guards made by `createAuthGuard` read
|
|
110
|
+
it, trying the user first; so does the new **`GuestSessionGuard`**, which
|
|
111
|
+
admits guests only. **`@CurrentGuest()`** gives the handler the guest and
|
|
112
|
+
their grants of that kind.
|
|
113
|
+
- **`IGuestSessionRepository`**, `InMemoryGuestSessionRepository`, and
|
|
114
|
+
`guestSessionRepositoryInstance`, which enables the feature.
|
|
115
|
+
- **`assertGuestSessionRepositoryContract`** in
|
|
116
|
+
`@ambushsoftworks/nestjs-auth-graphql/testing`.
|
|
117
|
+
- **`guestSession`** options: `maxGrantsPerSession` (default 10; `1` gives one
|
|
118
|
+
grant per session) and `maxLifetimeDays` (default 30).
|
|
119
|
+
- Security events `GUEST_SESSION_ISSUED`, `GUEST_SESSION_REJECTED`,
|
|
120
|
+
`GUEST_SESSION_REVOKED`, `GUEST_SESSION_ENDED` and `GUEST_GRANT_REVOKED`.
|
|
121
|
+
- Error codes `GUEST_SESSION_INVALID` (401) and `GUEST_SESSION_UNAVAILABLE`
|
|
122
|
+
(503).
|
|
123
|
+
|
|
124
|
+
### Changed
|
|
125
|
+
|
|
126
|
+
- A guard made by `createAuthGuard` now reads `@AllowGuestSession` on the
|
|
127
|
+
handler. On a handler without it, which is every handler in an existing app,
|
|
128
|
+
nothing changes: no guest credential is read. `JwtAuthGuard` is unchanged and
|
|
129
|
+
ignores the decorator.
|
|
130
|
+
|
|
30
131
|
## [0.18.0] - 2026-10-07
|
|
31
132
|
|
|
32
133
|
**Email challenges**: one-time codes that prove someone holds an email address,
|
package/README.md
CHANGED
|
@@ -22,6 +22,8 @@ Production-grade authentication module for NestJS GraphQL + REST APIs. JWT, cook
|
|
|
22
22
|
- [Passkeys](#passkeys)
|
|
23
23
|
- [Step-Up Re-authentication](#step-up-re-authentication)
|
|
24
24
|
- [Email Challenges](#email-challenges)
|
|
25
|
+
- [Guest Sessions](#guest-sessions)
|
|
26
|
+
- [Accounts Created by an Operator](#accounts-created-by-an-operator)
|
|
25
27
|
- [Brute Force Protection](#brute-force-protection)
|
|
26
28
|
- [Password Reset](#password-reset)
|
|
27
29
|
- [Password Policy](#password-policy)
|
|
@@ -44,6 +46,7 @@ Production-grade authentication module for NestJS GraphQL + REST APIs. JWT, cook
|
|
|
44
46
|
- [Passkey Options (`passkey`)](#passkey-options-passkey)
|
|
45
47
|
- [Step-Up Options (`stepUp`)](#step-up-options-stepup)
|
|
46
48
|
- [Email Challenge Options (`emailChallenge`)](#email-challenge-options-emailchallenge)
|
|
49
|
+
- [Guest Session Options (`guestSession`)](#guest-session-options-guestsession)
|
|
47
50
|
- [SendGrid Options (`sendgrid`)](#sendgrid-options-sendgrid)
|
|
48
51
|
- [Twilio Options (`twilio`)](#twilio-options-twilio)
|
|
49
52
|
- [Optional Instance Options](#optional-instance-options)
|
|
@@ -102,13 +105,33 @@ import { IUserRepositoryCore, CreateUserData } from '@ambushsoftworks/nestjs-aut
|
|
|
102
105
|
|
|
103
106
|
@Injectable()
|
|
104
107
|
export class UsersRepository implements IUserRepositoryCore<User> {
|
|
105
|
-
async findByEmail(email: string): Promise<User | null> { /* ... */ }
|
|
108
|
+
async findByEmail(email: string, realm?: string): Promise<User | null> { /* ... */ }
|
|
106
109
|
async findById(id: string): Promise<User | null> { /* ... */ }
|
|
107
110
|
async create(data: CreateUserData): Promise<User> { /* ... */ }
|
|
108
|
-
|
|
111
|
+
async update(id: string, data: Partial<User>): Promise<User> { /* ... */ }
|
|
112
|
+
async updatePassword(id: string, passwordHash: string): Promise<User> { /* ... */ }
|
|
113
|
+
async markEmailVerified(id: string): Promise<User> { /* ... */ }
|
|
114
|
+
async delete(id: string): Promise<void> { /* ... */ }
|
|
109
115
|
}
|
|
110
116
|
```
|
|
111
117
|
|
|
118
|
+
Those seven are all of `IUserRepositoryCore`. What the package calls, and when:
|
|
119
|
+
|
|
120
|
+
| Method | Called by |
|
|
121
|
+
|---|---|
|
|
122
|
+
| `findByEmail`, `findById` | Every sign-in and every authenticated request. Boot fails without them |
|
|
123
|
+
| `update`, `updatePassword`, `markEmailVerified` | Account changes, password change and reset, email verification |
|
|
124
|
+
| `create` | Sign-up. Never, with `features.signup: false` |
|
|
125
|
+
| `delete` | Passwordless sign-up only, to undo a half-made account |
|
|
126
|
+
| `findByOAuthProvider` | Google and Facebook sign-in. **Boot fails without it once `oauth.google` or `oauth.facebook` is set** |
|
|
127
|
+
| `findByPhoneNumber` | Phone verification. **Boot fails without it once `smsServiceInstance` is set** |
|
|
128
|
+
|
|
129
|
+
`userRepositoryInstance` accepts any repository with the first five
|
|
130
|
+
(`AuthUserRepository`), so an app that only signs existing users in implements
|
|
131
|
+
five methods and no stubs. The other methods on `IUserRepository` are optional
|
|
132
|
+
and deprecated: the package never called them. See
|
|
133
|
+
[Accounts Created by an Operator](#accounts-created-by-an-operator).
|
|
134
|
+
|
|
112
135
|
```typescript
|
|
113
136
|
// refresh-token.repository.ts
|
|
114
137
|
import { Injectable } from '@nestjs/common';
|
|
@@ -1625,6 +1648,234 @@ It calls `deleteExpiredChallenges`, which removes every expired challenge in
|
|
|
1625
1648
|
that database. `InMemoryEmailChallengeRepository` is for tests and a single
|
|
1626
1649
|
instance.
|
|
1627
1650
|
|
|
1651
|
+
### Guest Sessions
|
|
1652
|
+
|
|
1653
|
+
A session for someone who is **not a user**: it works only on the routes you
|
|
1654
|
+
mark, for the things you granted, until the dates you set. For the outside
|
|
1655
|
+
translator who has proved their address with an [email challenge](#email-challenges)
|
|
1656
|
+
and now needs to work on one job. Since 0.19.0.
|
|
1657
|
+
|
|
1658
|
+
```typescript
|
|
1659
|
+
// 1. After your own check that they hold the address (a verified email challenge):
|
|
1660
|
+
const issued = await this.guestSessions.issue(
|
|
1661
|
+
{
|
|
1662
|
+
principal: { id: translator.id, email: job.translatorEmail }, // your id for the person
|
|
1663
|
+
grant: { kind: 'translation-job', id: job.id, expiresAt: job.dueAt },
|
|
1664
|
+
},
|
|
1665
|
+
{ request: ctx.req, res: ctx.res, realm, ipAddress: extractIpAddress(ctx.req) },
|
|
1666
|
+
);
|
|
1667
|
+
|
|
1668
|
+
// 2. On the routes a guest may reach, and no others:
|
|
1669
|
+
@Mutation(() => UploadResult)
|
|
1670
|
+
@UseGuards(CsrfGuard, JwtGuard) // a guard from createAuthGuard
|
|
1671
|
+
@AllowGuestSession('translation-job') // on the method
|
|
1672
|
+
upload(@Args('input') input: UploadInput,
|
|
1673
|
+
@CurrentUser() user: User | undefined,
|
|
1674
|
+
@CurrentGuest() guest: GuestPrincipal | undefined) {
|
|
1675
|
+
if (guest && !guest.grants.some((g) => g.id === input.jobId)) throw new ForbiddenException();
|
|
1676
|
+
// ...
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1679
|
+
// 3. When the job is delivered or taken back:
|
|
1680
|
+
await this.guestSessions.revokeGrant({ kind: 'translation-job', id: job.id }, { realm });
|
|
1681
|
+
```
|
|
1682
|
+
|
|
1683
|
+
**Enable it** with `guestSessionRepositoryInstance`. There is no flag, and no
|
|
1684
|
+
fallback store.
|
|
1685
|
+
|
|
1686
|
+
#### What a guest can and cannot reach
|
|
1687
|
+
|
|
1688
|
+
| | |
|
|
1689
|
+
|---|---|
|
|
1690
|
+
| A handler with `@AllowGuestSession(kind)` | Admitted, if the session holds a standing grant of that kind |
|
|
1691
|
+
| Every other handler | **Refused. The guest credential is not even read** |
|
|
1692
|
+
| `@CurrentUser()`, `request.user` | Empty for a guest. The guest is on `request.guest` / `@CurrentGuest()` |
|
|
1693
|
+
| `PermissionGuard`, `TenantGuard`, `ScopeGuard` | Refuse a guest: there is no user for them to look up |
|
|
1694
|
+
| Token refresh | None. When a grant ends, the person proves their address again |
|
|
1695
|
+
| GraphQL subscriptions | Not accepted |
|
|
1696
|
+
|
|
1697
|
+
`@AllowGuestSession` is read **from the method only**. On a class it does
|
|
1698
|
+
nothing, so decorating a resolver cannot open every method on it.
|
|
1699
|
+
|
|
1700
|
+
| Guard on the route | With `@AllowGuestSession(kind)` |
|
|
1701
|
+
|---|---|
|
|
1702
|
+
| One made by `createAuthGuard([...])` | Users and guests. The user strategies run first, so a signed-in user is handled exactly as on any other route; only if none authenticates is the guest session looked at |
|
|
1703
|
+
| `GuestSessionGuard` | Guests only |
|
|
1704
|
+
| `JwtAuthGuard` | Users only: it ignores the decorator |
|
|
1705
|
+
|
|
1706
|
+
#### What the package checks, and what is yours
|
|
1707
|
+
|
|
1708
|
+
The package guarantees that the guest holds a standing grant of the **kind** the
|
|
1709
|
+
route names. **Which one is your check**, in every opted-in handler: compare
|
|
1710
|
+
what the request asks for with `guest.grants`. A route marked for
|
|
1711
|
+
`translation-job` is reachable by every guest with any translation job.
|
|
1712
|
+
|
|
1713
|
+
Two more things are yours:
|
|
1714
|
+
|
|
1715
|
+
- **What the response contains.** Field resolvers do not run guards by default.
|
|
1716
|
+
A guest-allowed query returning `Job { client { ... } }` exposes whatever
|
|
1717
|
+
those fields resolve. Return guest-shaped types from guest routes.
|
|
1718
|
+
- **Do not make a guest route `@Public()`.** `CsrfGuard` skips public routes,
|
|
1719
|
+
and a guest cookie needs its CSRF check like any other cookie session. That
|
|
1720
|
+
is why the route keeps its normal guard and adds the decorator.
|
|
1721
|
+
|
|
1722
|
+
#### Several grants in one session
|
|
1723
|
+
|
|
1724
|
+
A translator with two open jobs uses one browser. When `issue` is given the
|
|
1725
|
+
request, and that browser already holds a guest session **for the same
|
|
1726
|
+
`principal.id`, address and realm**, the new grant joins it. Otherwise a new
|
|
1727
|
+
session starts.
|
|
1728
|
+
|
|
1729
|
+
| Behaviour | Detail |
|
|
1730
|
+
|---|---|
|
|
1731
|
+
| The token changes when a grant is added | The old one stops working, so an older copy of it never gains the new job. With cookie auth the cookie is replaced for you |
|
|
1732
|
+
| `guestSession.maxGrantsPerSession` (default 10) | When full, the grant that ends soonest is revoked to make room |
|
|
1733
|
+
| `maxGrantsPerSession: 1` | One grant per session: each new one replaces the last |
|
|
1734
|
+
| The same `kind` and `id` again | Replaces the earlier grant, taking the new end date |
|
|
1735
|
+
| Each grant has its own `expiresAt` | Never moved. At most `guestSession.maxLifetimeDays` (default 30) ahead |
|
|
1736
|
+
|
|
1737
|
+
#### Credential and codes
|
|
1738
|
+
|
|
1739
|
+
The credential is an opaque random token, stored only as a keyed hash. It is not
|
|
1740
|
+
a JWT, so nothing that verifies access tokens can accept it.
|
|
1741
|
+
|
|
1742
|
+
- **With `features.cookieAuth`** and `res` passed to `issue`, it is set as the
|
|
1743
|
+
HttpOnly cookie `guest_session` (`__Host-guest_session` with
|
|
1744
|
+
`cookie.useHostPrefix`), with your cookie options, lasting until the session's
|
|
1745
|
+
last grant ends. It is a different cookie from the access token, so a staff
|
|
1746
|
+
member who opens a translator's link stays signed in.
|
|
1747
|
+
- **Otherwise** the client sends `Authorization: Guest <token>`.
|
|
1748
|
+
|
|
1749
|
+
| Answer | When | Tell the person |
|
|
1750
|
+
|---|---|---|
|
|
1751
|
+
| The route's usual `UNAUTHENTICATED` | No credential at all | Sign in, or open your link |
|
|
1752
|
+
| `GUEST_SESSION_INVALID` (401) | A guest credential that cannot be used here: revoked, ended, another realm, another kind, or unknown | Prove your address again |
|
|
1753
|
+
| `GUEST_SESSION_UNAVAILABLE` (503) | `issue` without a repository | |
|
|
1754
|
+
|
|
1755
|
+
`GUEST_SESSION_INVALID` is deliberately not `UNAUTHENTICATED`: clients treat
|
|
1756
|
+
that as "refresh and retry", which can never work for a guest. The same code is
|
|
1757
|
+
in the body on REST routes. The exact cause (`revoked`, `expired`,
|
|
1758
|
+
`realm_mismatch`, `kind_not_allowed`) is in the `GUEST_SESSION_REJECTED`
|
|
1759
|
+
security event.
|
|
1760
|
+
|
|
1761
|
+
`end(request, res)` is the guest's own "I'm done": it revokes the session and
|
|
1762
|
+
clears the cookie. Put `CsrfGuard` on that route too.
|
|
1763
|
+
|
|
1764
|
+
#### Implementing `IGuestSessionRepository`
|
|
1765
|
+
|
|
1766
|
+
Two tables, no relation to your users. Reference schema and Prisma adapter:
|
|
1767
|
+
[`examples/guest-sessions/`](examples/guest-sessions/).
|
|
1768
|
+
|
|
1769
|
+
```prisma
|
|
1770
|
+
model GuestSession {
|
|
1771
|
+
id String @id
|
|
1772
|
+
tokenHash String @unique // read on every guest request
|
|
1773
|
+
principalId String
|
|
1774
|
+
email String
|
|
1775
|
+
realm String?
|
|
1776
|
+
revokedAt DateTime?
|
|
1777
|
+
createdAt DateTime
|
|
1778
|
+
grants GuestGrant[]
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1781
|
+
model GuestGrant {
|
|
1782
|
+
id String @id
|
|
1783
|
+
sessionId String
|
|
1784
|
+
kind String
|
|
1785
|
+
grantId String
|
|
1786
|
+
realm String?
|
|
1787
|
+
expiresAt DateTime
|
|
1788
|
+
revokedAt DateTime?
|
|
1789
|
+
createdAt DateTime
|
|
1790
|
+
session GuestSession @relation(fields: [sessionId], references: [id], onDelete: Cascade)
|
|
1791
|
+
|
|
1792
|
+
@@index([sessionId])
|
|
1793
|
+
@@index([kind, grantId])
|
|
1794
|
+
@@index([expiresAt])
|
|
1795
|
+
}
|
|
1796
|
+
```
|
|
1797
|
+
|
|
1798
|
+
Two rules are the ones adapters get wrong, and the contract checks both:
|
|
1799
|
+
|
|
1800
|
+
- **`rotateToken` is one conditional write** (`UPDATE … SET tokenHash = new WHERE
|
|
1801
|
+
tokenHash = old AND revokedAt IS NULL`), never a read followed by a write.
|
|
1802
|
+
- **`realm: null` means "no realm", never "any realm".** Pass it through as
|
|
1803
|
+
given; `where: { realm: undefined }` drops the condition.
|
|
1804
|
+
|
|
1805
|
+
```typescript
|
|
1806
|
+
import { assertGuestSessionRepositoryContract } from '@ambushsoftworks/nestjs-auth-graphql/testing';
|
|
1807
|
+
|
|
1808
|
+
it('meets the guest session repository contract', async () => {
|
|
1809
|
+
await assertGuestSessionRepositoryContract(() => new PrismaGuestSessionRepository(prisma), {
|
|
1810
|
+
realms: false, // if you do not use realms
|
|
1811
|
+
cleanup: async ({ sessionIds, grantIds }) => {
|
|
1812
|
+
await prisma.guestGrant.deleteMany({ where: { id: { in: grantIds } } });
|
|
1813
|
+
await prisma.guestSession.deleteMany({ where: { id: { in: sessionIds } } });
|
|
1814
|
+
},
|
|
1815
|
+
});
|
|
1816
|
+
});
|
|
1817
|
+
```
|
|
1818
|
+
|
|
1819
|
+
It calls `deleteExpired`, which removes every expired grant and every session
|
|
1820
|
+
without grants in that database. `InMemoryGuestSessionRepository` is for tests
|
|
1821
|
+
and a single instance. The store is read once per request that presents a guest
|
|
1822
|
+
credential on an opted-in route, and never written per request.
|
|
1823
|
+
|
|
1824
|
+
### Accounts Created by an Operator
|
|
1825
|
+
|
|
1826
|
+
For an app with no public registration: staff accounts made by a script or an
|
|
1827
|
+
admin screen. Three things make that a property of the configuration instead of
|
|
1828
|
+
a convention in your code. Since 0.20.0.
|
|
1829
|
+
|
|
1830
|
+
**1. Switch sign-up off.**
|
|
1831
|
+
|
|
1832
|
+
```typescript
|
|
1833
|
+
features: { signup: false }
|
|
1834
|
+
```
|
|
1835
|
+
|
|
1836
|
+
Every path in the package that would create a user then refuses with
|
|
1837
|
+
`SIGNUP_DISABLED` (403), before it reaches `create`: password sign-up,
|
|
1838
|
+
passwordless sign-up (no code is emailed), and a first sign-in with Google or
|
|
1839
|
+
Facebook. Signing in to an existing account, by any method, is unaffected. The
|
|
1840
|
+
answer is the same for every address.
|
|
1841
|
+
|
|
1842
|
+
**2. Hash passwords with the package.**
|
|
1843
|
+
|
|
1844
|
+
```typescript
|
|
1845
|
+
// scripts/create-staff.ts
|
|
1846
|
+
const app = await NestFactory.createApplicationContext(AppModule);
|
|
1847
|
+
const passwordHash = await app.get(PasswordHashService).hash(password);
|
|
1848
|
+
await staffUsers.insert({ email, passwordHash, emailVerified: true });
|
|
1849
|
+
await app.close();
|
|
1850
|
+
```
|
|
1851
|
+
|
|
1852
|
+
`PasswordHashService` hashes exactly as sign-up, change-password and
|
|
1853
|
+
reset-password do, at the module's `bcryptRounds`, because all four go through
|
|
1854
|
+
one function. Do not call `bcrypt` yourself: if the package's hashing ever
|
|
1855
|
+
changes, a hash made on the side keeps the old shape and nothing fails until a
|
|
1856
|
+
login does. It hashes whatever it is given; check strength first with
|
|
1857
|
+
`PasswordValidationService` if the operator types the password.
|
|
1858
|
+
|
|
1859
|
+
**3. Implement only what you use.**
|
|
1860
|
+
|
|
1861
|
+
```typescript
|
|
1862
|
+
@Injectable()
|
|
1863
|
+
export class StaffUserRepository implements AuthUserRepository<StaffUser> {
|
|
1864
|
+
findByEmail(email: string) { /* ... */ }
|
|
1865
|
+
findById(id: string) { /* ... */ }
|
|
1866
|
+
update(id: string, data: Partial<StaffUser>) { /* ... */ }
|
|
1867
|
+
updatePassword(id: string, passwordHash: string) { /* ... */ }
|
|
1868
|
+
markEmailVerified(id: string) { /* ... */ }
|
|
1869
|
+
}
|
|
1870
|
+
```
|
|
1871
|
+
|
|
1872
|
+
No `create`, and no OAuth, biometric or phone methods. If you later configure a
|
|
1873
|
+
feature that needs one, the module refuses to boot and names the method.
|
|
1874
|
+
|
|
1875
|
+
To record when someone last signed in, use the `onLogin` hook. It fires for
|
|
1876
|
+
every sign-in the package verifies: password, Google, Facebook, passkey and
|
|
1877
|
+
device key.
|
|
1878
|
+
|
|
1628
1879
|
### Brute Force Protection
|
|
1629
1880
|
|
|
1630
1881
|
Provide a `bruteForceRepositoryInstance` and the module tracks failed login attempts and temporarily locks accounts. Without one, `NoOpBruteForceRepository` is used and no account is ever locked; there is no flag to set. The lockout policy (attempt thresholds, lockout duration) is determined by your `IBruteForceRepository` implementation.
|
|
@@ -1861,7 +2112,7 @@ React to auth events without modifying core logic:
|
|
|
1861
2112
|
```typescript
|
|
1862
2113
|
lifecycleHooksInstance: {
|
|
1863
2114
|
async onSignup(user) { /* create default settings, send analytics */ },
|
|
1864
|
-
async onLogin(user) { /* update metrics */ },
|
|
2115
|
+
async onLogin(user) { /* record lastLoginAt, update metrics */ },
|
|
1865
2116
|
async onLogout(user) { /* cleanup */ },
|
|
1866
2117
|
async onEmailVerified(user) { /* unlock features */ },
|
|
1867
2118
|
async onPhoneVerified(user) { /* enable 2FA */ },
|
|
@@ -1880,6 +2131,8 @@ lifecycleHooksInstance: {
|
|
|
1880
2131
|
|
|
1881
2132
|
All hooks are optional. Async hooks are awaited but failures are logged and do not block the auth operation; the passkey hooks are not awaited. The package sends the passkey notification emails itself (see [Notification emails](#notification-emails)); don't send them again from these hooks. `onAuthFailure` and `onAccountLocked` are synchronous (fire-and-forget) — both are security signals and the underlying state (lock recorded, attempt logged) is already persisted by the time the hook fires, so hook failures must not roll it back.
|
|
1882
2133
|
|
|
2134
|
+
`onLogin` fires for every sign-in the package verifies: password, Google, Facebook, and since 0.20.0 passkey and device-key sign-in too. It is the place to record `lastLoginAt`; `IUserRepository.updateLastLoginAt` was never called and is deprecated. It does not fire for a token refresh, for sign-up (`onSignup` does), or for a session you issue yourself through `AuthService.issueAuthSession`.
|
|
2135
|
+
|
|
1883
2136
|
`onAccountLocked` fires from `BruteForceProtectionService.recordFailedAttempt` on the transition from "not locked" → "locked". Subsequent failed attempts while the account remains locked do **not** re-fire (use `onAuthFailure` for per-attempt signal). Wire it to send the account-locked email yourself — the package no longer ships a `sendAccountLockedEmail` method on `IEmailService`; notification surface area beyond verification, password-reset, and password-changed is consumer-owned via this hook.
|
|
1884
2137
|
|
|
1885
2138
|
### Account Status
|
|
@@ -2006,6 +2259,7 @@ These sit beside `useFactory`, not in the object it returns.
|
|
|
2006
2259
|
| `cookieAuth` | `false` | Token delivery via HttpOnly cookies |
|
|
2007
2260
|
| `preventEnumerationOnSignup` | `false` | Return synthetic success for duplicate emails |
|
|
2008
2261
|
| `verificationMode` | `'code'` | `'code'` for 6-digit codes, `'token'` for URL tokens |
|
|
2262
|
+
| `signup` | `true` | `false` refuses every path that would create an account, with `SIGNUP_DISABLED`. See [Accounts Created by an Operator](#accounts-created-by-an-operator) |
|
|
2009
2263
|
|
|
2010
2264
|
Capabilities are not switched on by flags. They follow from what you configure:
|
|
2011
2265
|
|
|
@@ -2013,7 +2267,7 @@ Capabilities are not switched on by flags. They follow from what you configure:
|
|
|
2013
2267
|
|------------|------------|
|
|
2014
2268
|
| Email verification | `email` or `emailServiceInstance` to deliver, `verificationRepositoryInstance` to store codes |
|
|
2015
2269
|
| SMS verification | `smsServiceInstance`, plus `verificationRepositoryInstance` |
|
|
2016
|
-
| Google / Facebook sign-in | `oauth.google` / `oauth.facebook`, plus `encryptionKey
|
|
2270
|
+
| Google / Facebook sign-in | `oauth.google` / `oauth.facebook`, plus `encryptionKey`. With no block for a provider, nothing is logged about it; a block that is present but incomplete is a boot warning |
|
|
2017
2271
|
| Biometric authentication | `biometricRepositoryInstance` |
|
|
2018
2272
|
| Account lockout | `bruteForceRepositoryInstance` |
|
|
2019
2273
|
| Multi-tenancy | `tenantRepositoryInstance` and `tenantExtractorInstance`, with `TenantGuard` in your guard chain |
|
|
@@ -2142,6 +2396,15 @@ Read only with `emailChallengeRepositoryInstance`. See [Email Challenges](#email
|
|
|
2142
2396
|
| `issueLimits.perHour` | `number` | `5` | Codes an hour for one address and purpose. At least 1 |
|
|
2143
2397
|
| `issueLimits.perIpPerHour` | `number` | `30` | Codes an hour from one IP, across addresses. `0` turns it off |
|
|
2144
2398
|
|
|
2399
|
+
### Guest Session Options (`guestSession`)
|
|
2400
|
+
|
|
2401
|
+
Read only with `guestSessionRepositoryInstance`. See [Guest Sessions](#guest-sessions).
|
|
2402
|
+
|
|
2403
|
+
| Option | Type | Default | Description |
|
|
2404
|
+
|--------|------|---------|-------------|
|
|
2405
|
+
| `maxGrantsPerSession` | `number` | `10` | Standing grants one session may hold, 1 to 100. `1` gives one grant per session |
|
|
2406
|
+
| `maxLifetimeDays` | `number` | `30` | The furthest ahead a grant's `expiresAt` may be, 1 to 365 |
|
|
2407
|
+
|
|
2145
2408
|
### Optional Instance Options
|
|
2146
2409
|
|
|
2147
2410
|
| Option | Interface | Fallback |
|
|
@@ -2154,6 +2417,7 @@ Read only with `emailChallengeRepositoryInstance`. See [Email Challenges](#email
|
|
|
2154
2417
|
| `biometricRepositoryInstance` | `IBiometricRepository` | none: biometrics disabled |
|
|
2155
2418
|
| `passkeyRepositoryInstance` | `IPasskeyRepository` | none: passkeys disabled |
|
|
2156
2419
|
| `emailChallengeRepositoryInstance` | `IEmailChallengeRepository` | none: email challenges unavailable |
|
|
2420
|
+
| `guestSessionRepositoryInstance` | `IGuestSessionRepository` | none: guest sessions disabled |
|
|
2157
2421
|
| `authLoggerInstance` | `IAuthLogger` | `ConsoleAuthLogger` |
|
|
2158
2422
|
| `tenantRepositoryInstance` | `ITenantRepository` | `null` |
|
|
2159
2423
|
| `tenantExtractorInstance` | `ITenantExtractor` | `null` |
|
|
@@ -2176,6 +2440,8 @@ Read only with `emailChallengeRepositoryInstance`. See [Email Challenges](#email
|
|
|
2176
2440
|
| `@RequirePermissions('p1', 'p2')` | Method/Class | Require specific permissions |
|
|
2177
2441
|
| `@RequireScopes('s1', 's2')` | Method/Class | Require API key scopes, enforced by `ScopeGuard`. People are unaffected. See [API Key Authentication](#api-key-authentication) |
|
|
2178
2442
|
| `@CurrentUser()` | Parameter | Inject authenticated user |
|
|
2443
|
+
| `@AllowGuestSession('kind')` | **Method only** | Let a guest session with a standing grant of that kind reach the handler. See [Guest Sessions](#guest-sessions) |
|
|
2444
|
+
| `@CurrentGuest()` | Parameter | Inject the admitted guest, or `undefined` for a user |
|
|
2179
2445
|
| `@CurrentTenant()` | Parameter | Inject resolved tenant context |
|
|
2180
2446
|
| `@CurrentRealm()` | Parameter | Inject resolved realm from request context |
|
|
2181
2447
|
| `@ResourceScope('resourceType', 'argName')` | Method | Resource-level permission scoping |
|
|
@@ -2184,7 +2450,8 @@ Read only with `emailChallengeRepositoryInstance`. See [Email Challenges](#email
|
|
|
2184
2450
|
|
|
2185
2451
|
| Guard | Description |
|
|
2186
2452
|
|-------|-------------|
|
|
2187
|
-
| `createAuthGuard(strategies, options)` | Factory for multi-strategy auth guards |
|
|
2453
|
+
| `createAuthGuard(strategies, options)` | Factory for multi-strategy auth guards. Also admits guests on handlers marked `@AllowGuestSession()`, after trying the user strategies |
|
|
2454
|
+
| `GuestSessionGuard` | Guests only, on handlers marked `@AllowGuestSession()` |
|
|
2188
2455
|
| `CsrfGuard` | CSRF header validation for cookie auth |
|
|
2189
2456
|
| `TenantGuard` | Multi-tenant context resolution |
|
|
2190
2457
|
| `PermissionGuard` | Permission checking against tenant context |
|
|
@@ -2282,6 +2549,7 @@ export class CustomGuard implements CanActivate {
|
|
|
2282
2549
|
- **`__Host-` cookie prefix** support for enhanced cookie security
|
|
2283
2550
|
- **Separate HMAC secrets** for refresh tokens, verification codes, and OAuth state
|
|
2284
2551
|
- **Realm-based identity isolation** with JWT realm claim validation, realm-scoped rate limiting and lockout, and auth flows that refuse a request without a realm
|
|
2552
|
+
- **Guest sessions for non-users** -- an opaque token stored as a keyed hash, never a JWT, kept off `request.user`, refused by every route that has not opted in by name, with a hard end per grant, revocation effective on the next request, and the token replaced whenever a grant is added (see [Guest Sessions](#guest-sessions))
|
|
2285
2553
|
- **Email challenges for non-users** -- one-time codes whose address, purpose and code are stored only as keyed hashes, bound to the address, purpose and realm they were issued for, with attempts and single use enforced by atomic writes in your store (see [Email Challenges](#email-challenges))
|
|
2286
2554
|
- **Pinned JWT algorithms** -- the package's own `jwt` strategy verifies `HS256` only, and `createExternalJwtStrategy` requires an explicit `algorithms` list from one family (see [External JWTs](#external-jwts))
|
|
2287
2555
|
- **API keys with scopes and expiry**, refused hard when they carry your prefix and match no key, so no other strategy accepts them
|
package/dist/auth.module.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { DynamicModule, MiddlewareConsumer, NestModule } from '@nestjs/common';
|
|
2
2
|
import { ModuleMetadata } from '@nestjs/common/interfaces';
|
|
3
|
-
import {
|
|
3
|
+
import { AuthUserRepository } from './interfaces/user-repository.interface';
|
|
4
4
|
import { IAuthUser } from './interfaces/auth-user.interface';
|
|
5
5
|
import { IEmailService } from './interfaces/email-service.interface';
|
|
6
6
|
import { ISmsService } from './interfaces/sms-service.interface';
|
|
@@ -11,6 +11,7 @@ import { IBruteForceRepository } from './interfaces/brute-force-repository.inter
|
|
|
11
11
|
import { IBiometricRepository } from './interfaces/biometric-repository.interface';
|
|
12
12
|
import { IPasskeyRepository } from './interfaces/passkey-repository.interface';
|
|
13
13
|
import { IEmailChallengeRepository } from './interfaces/email-challenge-repository.interface';
|
|
14
|
+
import { IGuestSessionRepository } from './interfaces/guest-session-repository.interface';
|
|
14
15
|
import { PasskeyRelyingPartyConfig } from './utils/passkey-rp';
|
|
15
16
|
import { IAuthLogger } from './interfaces/auth-logger.interface';
|
|
16
17
|
import { ITenantRepository } from './interfaces/tenant-repository.interface';
|
|
@@ -27,7 +28,7 @@ import { IEmailSender } from './interfaces/email-sender.interface';
|
|
|
27
28
|
import { IEmailTemplateRenderer } from './interfaces/email-template-renderer.interface';
|
|
28
29
|
import { EmailBrandingConfig } from './interfaces/email-branding-config.interface';
|
|
29
30
|
export interface AuthModuleOptions {
|
|
30
|
-
userRepositoryInstance:
|
|
31
|
+
userRepositoryInstance: AuthUserRepository;
|
|
31
32
|
refreshTokenRepositoryInstance: IRefreshTokenRepository;
|
|
32
33
|
emailServiceInstance?: IEmailService;
|
|
33
34
|
smsServiceInstance?: ISmsService;
|
|
@@ -37,6 +38,7 @@ export interface AuthModuleOptions {
|
|
|
37
38
|
biometricRepositoryInstance?: IBiometricRepository;
|
|
38
39
|
passkeyRepositoryInstance?: IPasskeyRepository;
|
|
39
40
|
emailChallengeRepositoryInstance?: IEmailChallengeRepository;
|
|
41
|
+
guestSessionRepositoryInstance?: IGuestSessionRepository;
|
|
40
42
|
authLoggerInstance?: IAuthLogger;
|
|
41
43
|
refreshTokenSecret?: string;
|
|
42
44
|
verificationCodeSecret?: string;
|
|
@@ -73,6 +75,7 @@ export interface AuthModuleOptions {
|
|
|
73
75
|
preventEnumerationOnSignup?: boolean;
|
|
74
76
|
cookieAuth?: boolean;
|
|
75
77
|
verificationMode?: 'code' | 'token';
|
|
78
|
+
signup?: boolean;
|
|
76
79
|
};
|
|
77
80
|
cookie?: {
|
|
78
81
|
httpOnly?: boolean;
|
|
@@ -158,6 +161,10 @@ export interface AuthModuleOptions {
|
|
|
158
161
|
perIpPerHour?: number;
|
|
159
162
|
};
|
|
160
163
|
};
|
|
164
|
+
guestSession?: {
|
|
165
|
+
maxGrantsPerSession?: number;
|
|
166
|
+
maxLifetimeDays?: number;
|
|
167
|
+
};
|
|
161
168
|
stepUp?: {
|
|
162
169
|
ttlSeconds?: number;
|
|
163
170
|
issueOnLogin?: boolean;
|
|
@@ -178,6 +185,7 @@ export declare function validateAuthModuleOptions(opts: AuthModuleOptions): void
|
|
|
178
185
|
export declare function describeIgnoredFeatureFlags(opts: AuthModuleOptions): string[];
|
|
179
186
|
export declare function describeIgnoredOptions(opts: AuthModuleOptions): string[];
|
|
180
187
|
export declare function describeInMemoryDefaults(opts: AuthModuleOptions): string[];
|
|
188
|
+
export declare function validateUserRepositoryMethods(opts: AuthModuleOptions): void;
|
|
181
189
|
export { AUTH_MODULE_OPTIONS, USER_REPOSITORY, REFRESH_TOKEN_REPOSITORY, EMAIL_SERVICE, SMS_SERVICE, AUTH_LIFECYCLE_HOOKS, VERIFICATION_REPOSITORY, BRUTE_FORCE_REPOSITORY, BIOMETRIC_REPOSITORY, AUTH_LOGGER, RATE_LIMITER, TENANT_REPOSITORY, TENANT_EXTRACTOR, RESOURCE_PERMISSION_REPOSITORY, JWT_PAYLOAD_FACTORY, API_KEY_REPOSITORY, } from './constants';
|
|
182
190
|
export declare class AuthModule implements NestModule {
|
|
183
191
|
configure(consumer: MiddlewareConsumer): void;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth.module.d.ts","sourceRoot":"","sources":["../src/auth.module.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,aAAa,EACb,kBAAkB,EAElB,UAAU,EAGX,MAAM,gBAAgB,CAAC;AAIxB,OAAO,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;
|
|
1
|
+
{"version":3,"file":"auth.module.d.ts","sourceRoot":"","sources":["../src/auth.module.ts"],"names":[],"mappings":"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"}
|