@ambushsoftworks/nestjs-auth-graphql 0.9.0-rc.1 → 0.9.0-rc.2
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 +787 -0
- package/dist/auth.module.d.ts +2 -0
- package/dist/auth.module.d.ts.map +1 -1
- package/dist/auth.module.js.map +1 -1
- package/dist/exceptions/account-inactive.exception.d.ts +5 -0
- package/dist/exceptions/account-inactive.exception.d.ts.map +1 -0
- package/dist/exceptions/account-inactive.exception.js +16 -0
- package/dist/exceptions/account-inactive.exception.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/interfaces/auth-logger.interface.d.ts +1 -0
- package/dist/interfaces/auth-logger.interface.d.ts.map +1 -1
- package/dist/interfaces/auth-logger.interface.js +1 -0
- package/dist/interfaces/auth-logger.interface.js.map +1 -1
- package/dist/interfaces/auth-user.interface.d.ts.map +1 -1
- package/dist/interfaces/user-repository.interface.d.ts.map +1 -1
- package/dist/resolvers/auth.resolver.d.ts +73 -0
- package/dist/resolvers/auth.resolver.d.ts.map +1 -0
- package/dist/resolvers/auth.resolver.js +472 -0
- package/dist/resolvers/auth.resolver.js.map +1 -0
- package/dist/services/auth.service.d.ts +2 -0
- package/dist/services/auth.service.d.ts.map +1 -1
- package/dist/services/auth.service.js +18 -0
- package/dist/services/auth.service.js.map +1 -1
- package/dist/services/password-validation.service.js +1 -1
- package/dist/services/password-validation.service.js.map +1 -1
- package/dist/services/refresh-token.service.d.ts.map +1 -1
- package/dist/services/refresh-token.service.js +1 -0
- package/dist/services/refresh-token.service.js.map +1 -1
- package/dist/utils/account-status.d.ts +3 -0
- package/dist/utils/account-status.d.ts.map +1 -0
- package/dist/utils/account-status.js +19 -0
- package/dist/utils/account-status.js.map +1 -0
- package/dist/utils/passport-inspector.d.ts +11 -0
- package/dist/utils/passport-inspector.d.ts.map +1 -0
- package/dist/utils/passport-inspector.js +48 -0
- package/dist/utils/passport-inspector.js.map +1 -0
- package/package.json +5 -3
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,787 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## Reading this file
|
|
9
|
+
|
|
10
|
+
Alongside the standard Keep-a-Changelog sections, releases may carry:
|
|
11
|
+
|
|
12
|
+
### ⚠ External configuration required
|
|
13
|
+
|
|
14
|
+
Steps that must be performed **outside your codebase** — a frontend route, an
|
|
15
|
+
OAuth provider console, a DNS record, an environment variable in your
|
|
16
|
+
deployment platform — for the release to work.
|
|
17
|
+
|
|
18
|
+
These get their own heading because they are a different species from API
|
|
19
|
+
changes. A removed export, a new required field, a changed signature: your
|
|
20
|
+
compiler, your tests, or a code review will catch every one of them. A reset
|
|
21
|
+
link pointing at a route your frontend does not serve is invisible to all
|
|
22
|
+
three, and surfaces as a user who cannot complete a flow.
|
|
23
|
+
|
|
24
|
+
Grep for `⚠ External configuration required` across the versions you are
|
|
25
|
+
skipping to build an upgrade checklist mechanically, rather than reading prose
|
|
26
|
+
for buried obligations. **The marker was introduced in 0.9.0 and has not been
|
|
27
|
+
retro-applied**, so it is reliable from 0.9.0 onward only. Convention adopted
|
|
28
|
+
from `@ambushsoftworks/nestjs-payments-graphql`.
|
|
29
|
+
|
|
30
|
+
## [0.9.0] - Unreleased
|
|
31
|
+
|
|
32
|
+
Three independent workstreams land here: a trim of the email interface surface
|
|
33
|
+
down to what the package actually fires, a fix making
|
|
34
|
+
`verificationMode: 'token'` functional for the first time, and enforcement of
|
|
35
|
+
account status — which had never been checked at all. See **Security** first.
|
|
36
|
+
|
|
37
|
+
> Published as a release candidate under the `next` dist-tag first, so the
|
|
38
|
+
> verification and password-reset emails could be confirmed through a real
|
|
39
|
+
> mail client before reaching `latest`. That last hop — a mail client
|
|
40
|
+
> rendering the link — is exactly where the original defect lived and the one
|
|
41
|
+
> thing no test suite in this repo can exercise. Install the candidate with
|
|
42
|
+
> `npm install @ambushsoftworks/nestjs-auth-graphql@next`.
|
|
43
|
+
>
|
|
44
|
+
> **`0.9.0-rc.2` supersedes `rc.1`.** `rc.1` predates the account-status fix
|
|
45
|
+
> in **Security** below and still has the suspension window open — test
|
|
46
|
+
> against `rc.2`, not `rc.1`.
|
|
47
|
+
|
|
48
|
+
### ⚠ External configuration required
|
|
49
|
+
- **`verificationMode: 'token'` requires your frontend to serve two routes.**
|
|
50
|
+
Token-mode links point at `<verification.baseUrl>/verify-email` and
|
|
51
|
+
`<verification.baseUrl>/reset-password`, each carrying `token` and `email`
|
|
52
|
+
query parameters. Your app must serve both paths, read both parameters, and
|
|
53
|
+
pass them to `verifyEmail` / `resetPassword`. Nothing in this package can
|
|
54
|
+
check that — a link to a route you do not serve compiles, tests green, sends
|
|
55
|
+
successfully, and 404s in the recipient's browser. `verification.baseUrl`
|
|
56
|
+
itself IS validated at boot; the routes behind it cannot be.
|
|
57
|
+
**Not applicable in `'code'` mode**, which is the default and ships no link.
|
|
58
|
+
|
|
59
|
+
### Security
|
|
60
|
+
- **`status` and `deletedAt` were never checked — suspending a user revoked nothing.**
|
|
61
|
+
`IAuthUser.status` was documented as *"Checked during login to prevent
|
|
62
|
+
suspended/deleted users from authenticating"*. Nothing checked it, anywhere:
|
|
63
|
+
outside the `UserStatus` enum declaration and that docblock, `SUSPENDED` and
|
|
64
|
+
`DELETED` did not appear in `src/`. `login()` gated on rate limit, lockout,
|
|
65
|
+
user existence, password presence and password validity — never status.
|
|
66
|
+
`validateUser()`, the JWT path on every authenticated request, verified only
|
|
67
|
+
that the user still existed.
|
|
68
|
+
|
|
69
|
+
What provided revocation in practice was an **undocumented contract**: because
|
|
70
|
+
`refreshToken()` calls `findById` and rejects on `null`, revocation worked only
|
|
71
|
+
for consumers whose repository happened to filter revoked users. A consumer
|
|
72
|
+
who read the docblock and reasonably concluded the package handled it had an
|
|
73
|
+
**unbounded** exposure window — a suspended user kept refreshing indefinitely,
|
|
74
|
+
minting a fresh access token each time. Reported by the
|
|
75
|
+
nestjs-account-management team, who found it while planning a `suspendAccount`
|
|
76
|
+
operation; two consumers were affected in production.
|
|
77
|
+
|
|
78
|
+
Status is now enforced in four places: `login()` (after password verification,
|
|
79
|
+
so it cannot become an enumeration oracle), `validateUser()`, both branches of
|
|
80
|
+
`refreshToken()` including the grace-period cache, and as a backstop in the
|
|
81
|
+
private token-issuance sink every flow funnels through — so no path, including
|
|
82
|
+
`issueAuthSession`, can mint tokens for an inactive account. Rejection is
|
|
83
|
+
`AccountInactiveException` (code `ACCOUNT_INACTIVE`, HTTP 403) and logs the new
|
|
84
|
+
`ACCOUNT_INACTIVE_BLOCKED` security event with the phase.
|
|
85
|
+
|
|
86
|
+
**The default is a deny-list, and that is deliberate.** An account is inactive
|
|
87
|
+
when `deletedAt` is set, or `status` is `SUSPENDED`/`DELETED` compared
|
|
88
|
+
case-insensitively. It is *not* "anything that is not `ACTIVE`": `status` is
|
|
89
|
+
typed `string` so consumers can map their own model, and an allow-list would
|
|
90
|
+
have locked out every user of a consumer storing `'active'`, `'ENABLED'` or
|
|
91
|
+
`''` the moment they upgraded. A deny-list cannot produce a false rejection.
|
|
92
|
+
**If your vocabulary differs from `UserStatus`, the default cannot recognize
|
|
93
|
+
it** — supply the new `isUserActive` option.
|
|
94
|
+
|
|
95
|
+
**`jwtValidation: 'payload-only'` bounds revocation by token TTL rather than
|
|
96
|
+
making it immediate.** That mode skips the per-request lookup by design, so
|
|
97
|
+
there is nothing to check; a suspended user keeps working until their access
|
|
98
|
+
token expires. Refresh is still blocked, so the window is exactly one
|
|
99
|
+
access-token lifetime (15 minutes by default). This was previously
|
|
100
|
+
undocumented — the option's docblock said only that it "does not check if user
|
|
101
|
+
still exists" — and now states the consequence. No behavioral change to that
|
|
102
|
+
mode in this release.
|
|
103
|
+
|
|
104
|
+
### Added
|
|
105
|
+
- **`isUserActive?: (user: IAuthUser) => boolean` module option.** Overrides the
|
|
106
|
+
default account-active predicate for consumers whose status vocabulary differs
|
|
107
|
+
from `UserStatus`. Called on login, on refresh, on every request in
|
|
108
|
+
`jwtValidation: 'full'` mode, and before any token pair is minted. Must be pure
|
|
109
|
+
and synchronous — it runs on the authenticated request path and receives the
|
|
110
|
+
already-loaded user. Also exported: `isUserActiveByDefault` (the default
|
|
111
|
+
predicate) and `AccountInactiveException`.
|
|
112
|
+
|
|
113
|
+
### Fixed
|
|
114
|
+
- **`verificationMode: 'token'` produced unusable password-reset emails.** `AuthService.requestPasswordReset` built the reset URL by string concatenation (`${process.env.FRONTEND_URL}/reset-password`) with no query parameters, then passed the freshly minted credential separately — which `ConfigurableEmailService` discards in token mode. The recipient received a "Reset Password" button pointing at a bare page URL, with no path by which they could ever learn the token. Reported by the Ariadne team; reproduced end to end. No consumer configuration could work around it: both defects sat between `requestPasswordReset` and the renderer.
|
|
115
|
+
- **`verificationMode: 'token'` produced unusable verification emails.** `ConfigurableEmailService.sendVerificationEmail` assigned the raw `code` to the renderer's `verificationUrl` parameter, so the "Verify Email" button's href was the 6-digit code itself (e.g. `href="483920"`), which mail clients resolve as a relative path. Both parameters are typed `string`, so the compiler could not catch it.
|
|
116
|
+
- **`VerificationService.buildVerificationUrl` was never called by any send path.** The package shipped a correct URL builder (setting both `token` and `email`) alongside a second, incorrect one; the send paths used the one that loses the token. There is now exactly one place that knows the URL shape.
|
|
117
|
+
- **Token mode was never wired into the auth flows at all.** `AuthService` called `generateCode`/`validateCode` unconditionally; `generateToken`, `verifyToken`, and `buildVerificationUrl` existed and were unit tested but unreachable from signup, resend, verify, request-reset, or reset. `ConfigurableEmailService` was the only consumer of the `verificationMode` flag, and read it at a layer that has a credential but no URL — which is why it fabricated one.
|
|
118
|
+
- **`expiresInMinutes` was accepted and discarded** (`void expiresInMinutes`) by `ConfigurableEmailService.sendVerificationEmail`, so email copy could not state an accurate expiry. It is now plumbed to the renderer and reflects the active mode's real expiry (15 min for codes, `verification.tokenExpiresInMinutes` for tokens).
|
|
119
|
+
|
|
120
|
+
### Added
|
|
121
|
+
- **`IAuthLifecycleHooks.onAccountLocked(userId, email, ipAddress, lockDurationSeconds, realm?)`** — fires from `BruteForceProtectionService.recordFailedAttempt` on the not-locked → locked transition. Subsequent failed attempts while already locked do NOT re-fire; use `onAuthFailure` for per-attempt signal. Fire-and-forget (`void`, not `Promise`) — the lock is already recorded by the time the hook runs, and hook failures are caught and logged rather than rolling it back. This is the wiring point for account-locked notification email now that the package no longer sends one itself.
|
|
122
|
+
- **`AuthService.readRefreshTokenFromCookie(req)`** — public symmetric counterpart to `setAuthCookies` / `clearAuthCookies`. Reads `refresh_token` / `__Host-refresh_token` from the request. Useful when wrapping the resolver methods yourself.
|
|
123
|
+
- **`VerificationService.issueCredential(identifier, type, realm?)`** — mode-agnostic issuance returning `IssuedVerificationCredential` (`{ credential, url?, expiresInMinutes, mode }`). Mints the credential and builds its matching URL under a single mode decision, so the two cannot describe different verification schemes.
|
|
124
|
+
- **`VerificationService.verifyCredential(identifier, type, submitted, realm?)`** — the counterpart; branches on the same mode, making it structurally impossible to issue under one scheme and validate under the other.
|
|
125
|
+
- **`IssuedVerificationCredential`** — new exported interface.
|
|
126
|
+
- **`VerificationService.codeExpiresInMinutes` / `tokenExpiresInMinutes` / `credentialExpiresInMinutes`** getters — single source for expiry values previously duplicated as literals across services.
|
|
127
|
+
- **Boot-time validation: `verificationMode: 'token'` requires an absolute http(s) `verification.baseUrl`**, enforced by `validateAuthModuleOptions` → `InvalidAuthConfigException`. Token-mode emails carry no readable credential, so a missing base URL yields mail that delivers successfully and cannot be acted on; this makes it a deploy-time failure rather than a support ticket. Follows the existing `bruteForce.ipRateLimit` ↔ `rateLimiterInstance` precedent. **Code mode is unaffected — `baseUrl` remains optional there.**
|
|
128
|
+
- **`ConfigurableEmailService` refuses to send a token-mode email with no link**, throwing `InternalServerErrorException` rather than delivering an unusable message.
|
|
129
|
+
- Expiry line in the default renderer (HTML + plain text), sourced from the caller and omitted entirely when unknown rather than guessing.
|
|
130
|
+
- **`NoOpEmailSender` now logs every link in the message it would have sent** (HTML entities decoded, duplicates collapsed, and an explicit "contains no links" line for code-mode mail). Previously it logged only recipient, sender, and subject, so locally "an email was sent" was the entirety of what a developer could observe and a dead href was undetectable without a real mailbox — the exact blind spot that let unusable links reach production. Walking an auth flow locally is now: trigger it, read the link from the log, open it. Suggested by the Ariadne team, who had independently built the same thing on their side.
|
|
131
|
+
|
|
132
|
+
- **`RefreshTokenService` held the Node event loop open.** Its constructor
|
|
133
|
+
starts a 60-second sweep over the in-memory refresh-token cache, and the
|
|
134
|
+
timer was never `unref()`d. `onModuleDestroy` clears it, so a normal NestJS
|
|
135
|
+
shutdown was unaffected — but any process that finished its work and expected
|
|
136
|
+
to exit (a script, a CLI, a short-lived job importing the package) would hang
|
|
137
|
+
until something killed it, and Jest workers had to be force-exited. The timer
|
|
138
|
+
is now unref'd: it still fires for as long as the process is alive for its own
|
|
139
|
+
reasons, but no longer keeps it alive. `onModuleDestroy` still clears it.
|
|
140
|
+
|
|
141
|
+
### Fixed (release tooling)
|
|
142
|
+
- **`npm run lint` had never worked.** Five ESLint packages were in
|
|
143
|
+
`devDependencies` and the script had been present for months, but no config
|
|
144
|
+
file existed anywhere, so it failed on "couldn't find a configuration file" —
|
|
145
|
+
and CI never invoked it, so nothing surfaced. The glob also referenced a
|
|
146
|
+
`tests/` directory that does not exist, which fails ESLint on its own. Adds
|
|
147
|
+
`.eslintrc.js`, splits `lint` (checks) from `lint:fix` (mutates) so CI can
|
|
148
|
+
run the check form, and wires `npm run lint` into the pipeline. The existing
|
|
149
|
+
codebase produced exactly one violation.
|
|
150
|
+
- **Test job now runs on the default branch.** It ran only on merge requests
|
|
151
|
+
and tags, so a semantic conflict introduced by a merge — green on both sides
|
|
152
|
+
in isolation — would not surface until someone cut a release.
|
|
153
|
+
- **`--forceExit` removed from the CI test command**, now that nothing holds
|
|
154
|
+
the event loop open. Force-exiting masks precisely the leak fixed above.
|
|
155
|
+
- **`CHANGELOG.md` is now published to npm** (added to `files`), so consumers
|
|
156
|
+
can read release notes without leaving the registry.
|
|
157
|
+
- **Prerelease tags no longer publish as `latest`.** `.gitlab-ci.yml` now detects a prerelease tag (`v1.2.3-rc.1`, `-alpha`, `-beta`) and publishes under the `next` dist-tag. Previously `npm publish` defaulted to `latest` for every tag, so cutting an RC would have served it to anyone running a bare `npm install`.
|
|
158
|
+
|
|
159
|
+
### Changed
|
|
160
|
+
- **`BaseAuthResolver.performRefreshToken` and `performLogout` are now cookie/input symmetric.** When `features.cookieAuth` is enabled and `input.refreshToken` is empty, both read the refresh cookie instead. Browser SPAs cannot read HttpOnly cookies, so they send an empty input and rely on the credentialed request carrying the cookie. Explicit `input.refreshToken` still takes precedence for mobile/CLI callers. Both empty → `UnauthorizedException('Refresh token required')`. **Consumer DTOs must mark `refreshToken` `@IsOptional()`** (or `@IsString()` without `@IsNotEmpty()`) when targeting browser cookie auth, otherwise validation rejects the empty value before the resolver ever runs.
|
|
161
|
+
- **`IEmailService.sendVerificationEmail`** gained an optional 4th parameter `verificationUrl?: string`; **`sendPasswordResetEmail`** gained an optional 4th parameter `expiresInMinutes?: number`. Both are optional, so existing implementations continue to satisfy the interface unchanged.
|
|
162
|
+
- **`IEmailTemplateRenderer.renderVerificationEmail` / `renderPasswordResetEmail`** params gained optional `expiresInMinutes?: number`.
|
|
163
|
+
- **Base URL resolution consolidated on `verification.baseUrl`.** `AuthService` no longer reads `process.env.FRONTEND_URL` directly. In code mode the legacy env var is still honored as a fallback (deprecated, logged once); in token mode `verification.baseUrl` is required and the env fallback is deliberately not consulted.
|
|
164
|
+
- Default renderer copy is now mode-aware — a token-mode verification email no longer instructs the reader to "enter the code below" when the email contains no code.
|
|
165
|
+
- `PASSWORD_RESET_COMPLETED` security-event metadata now reports the real `method` (`'code'` or `'token'`) instead of a hardcoded `'code'`.
|
|
166
|
+
|
|
167
|
+
### Breaking
|
|
168
|
+
- **`IEmailService` — four notification methods removed.** The package never fired these on its own; they were interface surface consumers had to implement for no benefit. Notification beyond verification, password-reset, and password-changed is now consumer-owned via lifecycle hooks. Only affects consumers who implement `IEmailService` **directly** — the bundled `ConfigurableEmailService`, `SendGridEmailService`, and `NoOpEmailService` are updated, so `emailServiceInstance` / `email` users need no action.
|
|
169
|
+
- `sendAccountLockedEmail` → wire from the new `onAccountLocked` hook
|
|
170
|
+
- `sendWelcomeEmail` → wire from `onSignup` (or `onEmailVerified`)
|
|
171
|
+
- `sendAccountLinkedEmail` → wire from `onOAuthAccountLinked`
|
|
172
|
+
- `sendAccountUnlinkedEmail` → wire from your unlink resolver directly
|
|
173
|
+
- **`IEmailTemplateRenderer.renderLoginAlertEmail` removed.** The package never fired a login-from-new-device event, so nothing rendered it. Device-fingerprint storage and the send belong on the consumer side.
|
|
174
|
+
- **The corresponding renderers were deliberately kept.** `renderWelcomeEmail`, `renderAccountLockedEmail`, and `renderEmailChangedEmail` remain on `IEmailTemplateRenderer` and on `DefaultEmailTemplateRenderer`. Migration path for each removed send method is therefore: call the renderer you already have from your hook, then hand the result to your own `IEmailSender` — you keep the branded templates without the package pretending to own the trigger.
|
|
175
|
+
|
|
176
|
+
### Documentation
|
|
177
|
+
- **`IBruteForceRepository.deleteOlderThan` contract clarified.** Documented explicitly that it is a retention operation ("prune whatever attempt history you keep, if any"), not a guarantee the caller can rely on, and that **a no-op returning `0` is a valid implementation** for schemas keeping no attempt history — e.g. `failedLoginAttempts` / `lockedUntil` as columns on the user row, overwritten in place. Raised as a question by the Ariadne team, whose schema is exactly that shape.
|
|
178
|
+
- README: the two verification modes differ in **where the secret lives**, not merely in formatting — token mode puts the credential in the link, code mode keeps it in the body so it never reaches browser history, referrer headers, or proxy logs.
|
|
179
|
+
- README: new "Migrating to v0.9.0" section covering both workstreams — the removed interface methods with their hook-based replacements, and the action-required items for token-mode consumers (`verification.baseUrl` now required at boot; `resetPassword`/`verifyEmail` now receive a 128-character token, so input DTO constraints sized for 6-digit codes must be relaxed).
|
|
180
|
+
- README: "Verification Modes" section rewritten with a code-vs-token comparison table, the exact link format the package emits, base-URL resolution order, and guidance for custom `IEmailService` implementations.
|
|
181
|
+
- CLAUDE.md: documents the single-place-for-the-mode-branch rule, the prohibition on calling `generateCode`/`generateToken` directly from `AuthService`, and the testing rule derived from how these bugs escaped.
|
|
182
|
+
- LOCAL_DEVELOPMENT.md: added guidance for pnpm/yarn consumers (`link:` protocol rather than `npm link`), `npm pack` as the recommended way to verify a fix pre-release, and a rewritten "Nest can't resolve dependencies" entry covering duplicate peer-dependency resolution under symlinks.
|
|
183
|
+
|
|
184
|
+
### Notes
|
|
185
|
+
- **Backward compatibility of the verification fix:** code mode — the default, and what every existing consumer runs unless they opted into `'token'` — behaves identically, including the `FRONTEND_URL` fallback chain. Token mode was non-functional before this release, so there is no working behavior to preserve. The breaking changes in this release come from the email interface trim above, not from the verification fix.
|
|
186
|
+
- **Why not a one-line patch:** wiring `buildVerificationUrl` into the existing send path would have produced a working link carrying a **6-digit code** as its `token` parameter — functional, but shipping code-mode entropy in a URL that lands in inboxes, referrer headers, and proxy logs. Token mode now mints real tokens end to end.
|
|
187
|
+
- **Test-design change.** Both bugs survived because each side of the seam was unit tested against arguments the real call site never produced: `configurable-email.service.spec.ts` passed a ready-made URL in as the `code` parameter, so the assertion passed while production sent a bare digit string. New tests parse the rendered URL and feed the credential back through `verifyEmail`/`resetPassword` — see `auth.service.verification-mode.spec.ts` and `verification-credential.spec.ts`. Both fixes were verified by reintroducing each bug and confirming the new tests fail.
|
|
188
|
+
|
|
189
|
+
## [0.8.0] - 2026-05-14
|
|
190
|
+
|
|
191
|
+
### Added
|
|
192
|
+
- **Async `IRealmExtractor` and `ITenantExtractor` returns** — both interfaces now accept `string | null | Promise<string | null>` from their extractor method. `RealmMiddleware` and `TenantGuard` await the result when a Promise is returned, with no microtask overhead for sync returns (`instanceof Promise` gate). Enables consumers to resolve realm/tenant from a database lookup without bootstrap-cache workarounds. Async extractor throws (rejected Promise or sync `throw`) are treated as `400 Bad Request`, identical to the existing sync-`null` path; the internal cause is logged server-side via Nest's `Logger` but is NOT leaked to the client. Backward-compatible — existing sync extractors compile unchanged.
|
|
193
|
+
- **`BruteForceProtectionService.cleanupOldAttempts(olderThan: Date): Promise<number>`** — implemented. Calls through to the new `IBruteForceRepository.deleteOlderThan(beforeDate)` method and returns the deletion count. The package does not register a scheduler — wire your own cron with your own retention policy. See README "Brute Force Protection" section for an `@nestjs/schedule` example.
|
|
194
|
+
- **`BruteForceProtectionService.checkIpRateLimit(ipAddress, realm?)`** — opt-in implementation behind the new `bruteForce.ipRateLimit` config slot. Reuses the existing `IRateLimiter` infrastructure (the same one used for password-reset throttling) — NO new interface or injection token. Behavior is unchanged when `bruteForce.ipRateLimit` is omitted: returns `false`. The previous per-request "not implemented" WARN log is replaced with a one-time WARN on first call. When `bruteForce.ipRateLimit` IS configured, consumers MUST also explicitly provide `rateLimiterInstance` — `AuthModule.forRootAsync` throws `InvalidAuthConfigException` at boot otherwise (the default `InMemoryRateLimiterService` is silently broken in multi-replica deployments and is not auto-applied for IP rate limiting). Realm-aware: when `realm` is provided, rate-limit keys are prefixed with the realm to prevent cross-realm collisions on a shared store.
|
|
195
|
+
- **`AuthService.issueAuthSession(userId, res?, realm?)`** — new public method that mints an access + refresh pair, persists the refresh token, and sets cookies when `cookieAuth` is enabled AND `res` is supplied. Use after `changePassword()` to keep the current device logged in (other devices stay logged out — `changePassword` already revokes all refresh tokens). Also useful for admin-driven account creation, post-MFA-enrollment session bootstrap, and any flow that needs a session without going through `login()`. The caller is responsible for verifying the user's identity — this method takes a `userId` and trusts it.
|
|
196
|
+
- **`SecurityEvent.IP_RATE_LIMIT_EXCEEDED`** — emitted by `checkIpRateLimit` when an IP exceeds the configured `bruteForce.ipRateLimit` threshold. Metadata: `ipHash`, `realm`, `remainingAttempts`, `resetAt`, `maxAttempts`, `windowMs`. Raw IP is never logged.
|
|
197
|
+
- **`SecurityEvent.SESSION_ISSUED`** — emitted by `issueAuthSession`. Distinct from `LOGIN_SUCCESS` because no credential was verified at this layer. Metadata: `userId`, `realm`, `method: 'issueAuthSession'`.
|
|
198
|
+
- **`hashIp(ipAddress, salt)` utility** — exported HMAC-SHA256-based IP hashing helper used by `checkIpRateLimit`. Pure function: the salt is passed in by the caller (resolved inside `BruteForceProtectionService` from `AUTH_IP_HASH_SALT` env var, falling back to `options.jwtSecret`, with a one-time INFO log on the fallback path when `bruteForce.ipRateLimit` is configured). Threading the salt through the options object (rather than reading `process.env.JWT_SECRET` directly) keeps the package compatible with consumers wiring their JWT secret via `@nestjs/config` / a secrets manager. Output is a 16-character hex prefix.
|
|
199
|
+
- **`InvalidAuthConfigException`** — new exported exception thrown at boot from `AuthModule.forRootAsync` when configuration is internally inconsistent (currently: `bruteForce.ipRateLimit` set without explicit `rateLimiterInstance`). Extends native `Error`, NOT `HttpException` — this is a startup wiring error, not a runtime HTTP error.
|
|
200
|
+
- **`validateAuthModuleOptions(opts)`** — exported (for unit testing) helper invoked inside the `forRootAsync` factory. Currently checks the `bruteForce.ipRateLimit` ↔ `rateLimiterInstance` invariant.
|
|
201
|
+
- **`AUTH_IP_HASH_SALT` env var** — recommended dedicated salt for `hashIp`. Falls back to `JWT_SECRET` with a one-time INFO log when unset.
|
|
202
|
+
|
|
203
|
+
### Changed
|
|
204
|
+
- **`BruteForceProtectionService` constructor** — now requires `AUTH_MODULE_OPTIONS` and `RATE_LIMITER` injections in addition to the existing args. Internal change; Nest DI handles it. Consumer test code that constructs the service directly with `new BruteForceProtectionService(...)` must update its argument list.
|
|
205
|
+
- **`RealmMiddleware.use` is now async** (`Promise<void>`). Express middleware accepts both shapes — no impact on consumers.
|
|
206
|
+
|
|
207
|
+
### Breaking
|
|
208
|
+
- **`BruteForceProtectionService.cleanupOldAttempts(olderThan: Date)`** — signature changed from no-arg `Promise<void>` to 1-arg `Promise<number>`. The previous no-arg version was a documented stub that did nothing, so callers wired into a cron were no-op'ing. Migration: `await bruteForce.cleanupOldAttempts(new Date(Date.now() - 90 * 24 * 60 * 60 * 1000));` (90-day retention).
|
|
209
|
+
- **`IBruteForceRepository`** — new required method `deleteOlderThan(beforeDate: Date): Promise<number>`. Consumer implementations must add this method. Reference: `NoOpBruteForceRepository.deleteOlderThan` returns `0`. Prisma example: `return (await this.prisma.bruteForceAttempt.deleteMany({ where: { createdAt: { lt: beforeDate } } })).count;`.
|
|
210
|
+
|
|
211
|
+
### Documentation
|
|
212
|
+
- Tightened JSDoc on every `BaseAuthResolver.perform*` method to include a copy-paste-ready `@Mutation`/`@Query` snippet — actionable guidance instead of "consumer MUST override and add @Mutation."
|
|
213
|
+
- New reference file `examples/full-resolver-template.ts` shipped in the published package. Consumers can copy this file wholesale as a starting point for their own AuthResolver. Listed in `package.json` `files` array (alongside `dist`, `README.md`, `LICENSE`).
|
|
214
|
+
- README updated: new "IP Rate Limiting" and "Brute-Force Cleanup" subsections under Brute Force Protection; new "Stay Logged In After Password Change" subsection covering the `changePassword` + `issueAuthSession` two-call pattern; async-extractor note added to the Realm-Based Identity Isolation and Multi-Tenancy sections; configuration table extended with `bruteForce.ipRateLimit` and `AUTH_IP_HASH_SALT`; security-events list extended with `IP_RATE_LIMIT_EXCEEDED` and `SESSION_ISSUED`; `BaseAuthResolver` section now points consumers at `examples/full-resolver-template.ts`.
|
|
215
|
+
|
|
216
|
+
### Notes
|
|
217
|
+
- IP rate limit threshold is inclusive: `maxAttempts` is the first BLOCKED attempt, not the first allowed-over. With `maxAttempts: 5`, the user is blocked starting on the 5th attempt within the window.
|
|
218
|
+
- Auto-cron registration (`@nestjs/schedule`) was considered for `cleanupOldAttempts` and explicitly rejected: pinning consumers to a scheduler is the wrong layer. The package exposes the working primitive; consumers wire their own cron.
|
|
219
|
+
- A separate `IRateLimitStore` interface was considered for IP rate limiting and rejected: the existing `IRateLimiter` already covers the use case (its JSDoc explicitly lists "login attempt throttling"). Adding a parallel store would force consumers to wire two equivalent interfaces.
|
|
220
|
+
- A `BaseAuthResolver` mutation-exposure feature was requested and rejected. The package's compiled output cannot register `@Mutation` decorators that consumers' GraphQL schemas pick up — TypeScript decorator metadata is not preserved across module boundaries (see `src/resolvers/base-auth.resolver.ts:38-40`). The two consolation deliverables above replace the rejected feature.
|
|
221
|
+
- A `pwv` (password-version) JWT claim with immediate access-token revocation on `changePassword` is out of scope for this release; tracked for a future minor.
|
|
222
|
+
|
|
223
|
+
## [0.7.2] - 2026-05-12
|
|
224
|
+
|
|
225
|
+
### Fixed
|
|
226
|
+
- **CI publish pipeline** - Switched the publish job to `node:24` (ships with npm 11+) and removed the brittle `npm install -g npm@latest` self-upgrade step that failed for v0.7.0 and v0.7.1 with `MODULE_NOT_FOUND: promise-retry`. The test job remains on `node:22` to match the realistic consumer runtime.
|
|
227
|
+
- **Release plumbing** - Re-cuts the v0.7.0 release. No code changes from v0.7.0; CHANGELOG is brought up to date with the 0.4.0–0.7.0 release line.
|
|
228
|
+
|
|
229
|
+
## [0.7.1] - 2026-05-12
|
|
230
|
+
|
|
231
|
+
Skipped — tag pushed but publish failed in CI before reaching npm. Superseded by 0.7.2.
|
|
232
|
+
|
|
233
|
+
## [0.7.0] - 2026-05-12
|
|
234
|
+
|
|
235
|
+
### Added
|
|
236
|
+
- **Realm-Based Identity Isolation** - Realms partition the user identity space so users in one realm are invisible to another. The same email address can exist independently in different realms, each with its own password, tokens, and verification state. Designed for white-label platforms, multi-brand businesses, and franchise systems.
|
|
237
|
+
- `IRealmExtractor` interface — extract a realm identifier from each request (subdomain, header, JWT claim, etc.)
|
|
238
|
+
- `RealmMiddleware` — registered automatically by `AuthModule` when an extractor is provided; runs before guards and rejects requests with `400 Bad Request` when the extractor returns `null` (strict mode)
|
|
239
|
+
- `@CurrentRealm()` decorator — pulls the resolved realm out of the GraphQL context
|
|
240
|
+
- `realm` claim embedded in JWT access tokens; `JwtStrategy` validates the claim against the request realm (mismatch → `401 Unauthorized`)
|
|
241
|
+
- Identity-resolution repository methods (`findByEmail`, `findByPhoneNumber`, `findByOAuthProvider`, `create`) accept an optional `realm` parameter so consumer implementations can scope queries
|
|
242
|
+
- `BruteForceProtectionService` and `VerificationService` accept an optional `realm` parameter; in-memory rate limiter keys are prefixed with the realm to prevent cross-realm interference
|
|
243
|
+
- `CreateUserData.realm` field on user creation payloads
|
|
244
|
+
- Opt-in: provide `realmExtractorInstance` in `AuthModuleOptions`. No separate feature flag. Existing single-realm consumers are unaffected.
|
|
245
|
+
- Files: `src/interfaces/realm-extractor.interface.ts`, `src/middleware/realm.middleware.ts`, `src/decorators/current-realm.decorator.ts`, plus updates across `auth.service.ts`, `brute-force-protection.service.ts`, `verification.service.ts`, `jwt.strategy.ts`, `base-auth.resolver.ts`
|
|
246
|
+
|
|
247
|
+
### Fixed
|
|
248
|
+
- Documentation accuracy pass — corrected several inaccuracies in the README and CLAUDE.md around realm wiring and middleware registration
|
|
249
|
+
|
|
250
|
+
### Notes
|
|
251
|
+
- The legacy OAuth controller callback methods (deprecated) are intentionally **not** realm-aware. Use the GraphQL mutations and token-based OAuth endpoints for realm-scoped flows.
|
|
252
|
+
|
|
253
|
+
## [0.6.7] - 2026-05-01
|
|
254
|
+
|
|
255
|
+
### Added
|
|
256
|
+
- **Code Quality Audit (Phases 3–7)** - Comprehensive deduplication, type-safety, dead-code, and guard-cleanup pass across the codebase
|
|
257
|
+
- Extracted `generateTokenPairAndStore()` to deduplicate token generation logic
|
|
258
|
+
- Extracted `mapToRepositoryType()` and `resolveUser()` helpers in `VerificationService`
|
|
259
|
+
- Removed dead code paths and tightened types in several services
|
|
260
|
+
|
|
261
|
+
### Fixed
|
|
262
|
+
- **`ApiKeyStrategy`** - Returns `null` instead of throwing when the auth header is missing, allowing strategy chains (JWT → API key → anonymous) to fall through cleanly
|
|
263
|
+
|
|
264
|
+
### Changed
|
|
265
|
+
- **README rewrite** - Documentation overhauled for accuracy against the current feature set (cookie auth, CSRF, API keys, multi-tenancy, magic links, biometrics)
|
|
266
|
+
- **Publishing checklist** - Added a required "review and update README before tagging" step to `CLAUDE.md`
|
|
267
|
+
|
|
268
|
+
## [0.6.5] - 2026-04 (CI tracking releases)
|
|
269
|
+
|
|
270
|
+
### Fixed
|
|
271
|
+
- CI publishing pipeline: corrected OIDC audience, upgraded npm to 11.5.1+, added `SIGSTORE_ID_TOKEN` for provenance, switched to `NPM_ID_TOKEN` for trusted publishing
|
|
272
|
+
- Multiple iterative CI fixes published as 0.6.1 – 0.6.5 while npm Trusted Publishing was being wired up
|
|
273
|
+
|
|
274
|
+
## [0.6.0]
|
|
275
|
+
|
|
276
|
+
### Added
|
|
277
|
+
- **Email Template Architecture (P4)** - Branded email templates with token verification support, customizable per-consumer
|
|
278
|
+
- **Security Hardening (P5)** - `__Host-` cookie prefix, `Referrer-Policy` header, additional cookie security defaults, and supporting documentation
|
|
279
|
+
- GitLab CI with npm Trusted Publishing (OIDC) — eliminates need for long-lived npm tokens
|
|
280
|
+
|
|
281
|
+
### Fixed
|
|
282
|
+
- `JwtModule.expiresIn` type compatibility issue
|
|
283
|
+
|
|
284
|
+
## [0.5.0]
|
|
285
|
+
|
|
286
|
+
### Added
|
|
287
|
+
- **Cookie-Based Authentication (P2)** - HTTP-only access/refresh token cookies as an alternative to bearer tokens
|
|
288
|
+
- **CSRF Guard** - Double-submit cookie CSRF protection that pairs with cookie auth
|
|
289
|
+
- **JWT Payload Factory (P3)** - `IJwtPayloadFactory` interface for customizing JWT claims; default factory provided; wired through `AuthService` and `JwtStrategy`
|
|
290
|
+
- **API Key Strategy** - `ApiKeyStrategy` + `IApiKeyRepository` for service-account / non-interactive authentication
|
|
291
|
+
- **Tenancy Layer (P1b)** - `TenantGuard`, `PermissionGuard`, `createAuthGuard()` factory, `HeaderTenantExtractor`, NoOp tenant implementations, `ITenantRepository`, `IResourcePermissionRepository`
|
|
292
|
+
- **Auth Decorators** - `@Public()`, `@SkipTenant()`, `@RequirePermissions()`, `@ResourceScope()`, plus supporting metadata keys
|
|
293
|
+
- Module options expansion to wire all of the above through `AuthModule.forRootAsync()`
|
|
294
|
+
|
|
295
|
+
## [0.4.0]
|
|
296
|
+
|
|
297
|
+
### Added
|
|
298
|
+
- **Unified OAuth Methods on `IUserRepository`** - Consolidated `findByGoogleId`/`findByFacebookId` and friends into provider-agnostic `findByOAuthProvider(provider, providerId, realm?)` and related methods
|
|
299
|
+
- **Modular Interface Split** - `IUserRepository` decomposed into `IUserRepositoryCore`, `IUserRepositoryOAuth`, `IUserRepositoryBiometric`, `IUserRepositoryPhone`. `IAuthUser` similarly split into sub-interfaces. Consumers can implement only the sub-interfaces relevant to the features they enable.
|
|
300
|
+
- Widened peer dependency range for NestJS 11 compatibility; added `passport-custom` to peer deps
|
|
301
|
+
|
|
302
|
+
## [0.3.4] - 2025-11-16
|
|
303
|
+
|
|
304
|
+
### Fixed
|
|
305
|
+
- **Phone Verification for OAuth Users** - Fixed "User not found" error when OAuth users (Google/Facebook) attempt to add phone verification
|
|
306
|
+
- Added `generateCodeForUserId()`, `validateCodeForUserId()`, `hasActiveCodeForUserId()` to `VerificationService`
|
|
307
|
+
- Updated `sendPhoneVerification()`, `verifyPhone()`, and `resendPhoneVerification()` to use user ID-based methods
|
|
308
|
+
- Previous implementation required phone number to exist in database before verification could be initiated
|
|
309
|
+
- New implementation uses user ID for code generation/validation, allowing OAuth users to add phone numbers
|
|
310
|
+
- Files: `src/services/verification.service.ts`, `src/services/auth.service.ts`
|
|
311
|
+
|
|
312
|
+
## [0.3.0] - 2025-11-16
|
|
313
|
+
|
|
314
|
+
### Added
|
|
315
|
+
|
|
316
|
+
#### Enhancement #1: Configurable Password Policy
|
|
317
|
+
- **PasswordPolicyConfig Interface** - Customize password strength requirements for signup and password reset
|
|
318
|
+
- Configurable: `minLength`, `maxLength`, `requireUppercase`, `requireLowercase`, `requireNumber`, `requireSpecialChar`
|
|
319
|
+
- Custom validator function support for advanced requirements (e.g., leaked password detection, dictionary checks)
|
|
320
|
+
- Default policy: min 8 chars, uppercase, lowercase, number (backward compatible with v0.2.x)
|
|
321
|
+
- Files: `src/interfaces/password-policy-config.interface.ts`
|
|
322
|
+
|
|
323
|
+
- **PasswordValidationService** - Centralized password validation with configurable rules
|
|
324
|
+
- Fail-fast validation (returns all errors at once for better UX)
|
|
325
|
+
- Support for async custom validators
|
|
326
|
+
- Immutable policy configuration via `getPolicy()`
|
|
327
|
+
- 26 comprehensive unit tests
|
|
328
|
+
- Files: `src/services/password-validation.service.ts`, `src/services/password-validation.service.spec.ts`
|
|
329
|
+
|
|
330
|
+
#### Enhancement #2: IP-Based Rate Limiting
|
|
331
|
+
- **IRateLimiter Interface** - IP-based rate limiting to prevent abuse and email enumeration
|
|
332
|
+
- Methods: `checkRateLimit()`, `recordAttempt()`
|
|
333
|
+
- Sliding window algorithm for accurate rate limiting
|
|
334
|
+
- Files: `src/interfaces/rate-limiter.interface.ts`
|
|
335
|
+
|
|
336
|
+
- **InMemoryRateLimiterService** - Production-ready rate limiter for single-instance deployments
|
|
337
|
+
- Sliding window algorithm
|
|
338
|
+
- Automatic cleanup of expired entries (prevents memory leaks)
|
|
339
|
+
- Default: 5 password reset requests per hour per IP
|
|
340
|
+
- 23 comprehensive unit tests (including edge cases, cleanup, concurrency)
|
|
341
|
+
- Files: `src/services/in-memory-rate-limiter.service.ts`, `src/services/in-memory-rate-limiter.service.spec.ts`
|
|
342
|
+
|
|
343
|
+
- **NoOpRateLimiter** - Fallback implementation for development/testing
|
|
344
|
+
- Logs rate limit checks without enforcing limits
|
|
345
|
+
- Useful for testing environments
|
|
346
|
+
- Files: `src/repositories/noop-rate-limiter.ts`
|
|
347
|
+
|
|
348
|
+
- **IP Rate Limiting in Password Reset** - Enhanced security for `requestPasswordReset()`
|
|
349
|
+
- Checks IP rate limit BEFORE user lookup (prevents enumeration)
|
|
350
|
+
- Throws `PasswordResetRateLimitException` with `retryAfterSeconds` field
|
|
351
|
+
- Configurable limits via `AuthModuleOptions.rateLimiterInstance`
|
|
352
|
+
- Security logging for rate limit violations
|
|
353
|
+
|
|
354
|
+
#### Enhancement #3: Configurable Reset Strategy (Partial Implementation)
|
|
355
|
+
- **IPasswordResetStrategy Interface** - Strategy pattern for password reset methods
|
|
356
|
+
- Methods: `generateResetToken()`, `validateResetToken()`, `getExpiryDuration()`, `deleteResetToken()`
|
|
357
|
+
- Enables flexible password reset implementations (codes, magic links, TOTP, etc.)
|
|
358
|
+
- Files: `src/interfaces/password-reset-strategy.interface.ts`
|
|
359
|
+
|
|
360
|
+
- **VerificationCodeStrategy** - Current 6-digit code implementation (default, backward compatible)
|
|
361
|
+
- Wraps existing `VerificationService`
|
|
362
|
+
- 15-minute expiry, 3 attempts max, HMAC-SHA256 hashing
|
|
363
|
+
- Mobile-friendly (easy to copy-paste)
|
|
364
|
+
- Files: `src/strategies/verification-code.strategy.ts`
|
|
365
|
+
|
|
366
|
+
- **MagicLinkStrategy** - JWT-based magic link implementation (ready for future use)
|
|
367
|
+
- JWT-signed tokens with 1-hour expiry
|
|
368
|
+
- HMAC-SHA256 storage in repository
|
|
369
|
+
- Web-friendly (click link, auto-fill form)
|
|
370
|
+
- Requires `IMagicLinkRepository` implementation by consumers
|
|
371
|
+
- Files: `src/strategies/magic-link.strategy.ts`
|
|
372
|
+
|
|
373
|
+
- **IMagicLinkRepository Interface** - Storage contract for magic link tokens
|
|
374
|
+
- Methods: `storeMagicLink()`, `validateMagicLink()`, `deleteMagicLink()`
|
|
375
|
+
- Consumers implement with Prisma/TypeORM/etc.
|
|
376
|
+
- Files: `src/interfaces/magic-link-repository.interface.ts`
|
|
377
|
+
|
|
378
|
+
- **NoOpMagicLinkRepository** - Fallback implementation
|
|
379
|
+
- Logs operations without storage (magic links won't work)
|
|
380
|
+
- Files: `src/repositories/noop-magic-link.repository.ts`
|
|
381
|
+
|
|
382
|
+
### Changed
|
|
383
|
+
|
|
384
|
+
- **AuthModuleOptions Extended** - Added optional configuration fields:
|
|
385
|
+
- `passwordPolicy?: PasswordPolicyConfig` - Customize password strength requirements
|
|
386
|
+
- `rateLimiterInstance?: IRateLimiter` - Provide custom rate limiter (default: `InMemoryRateLimiterService`)
|
|
387
|
+
|
|
388
|
+
- **AuthService Updated** - Integrated new services:
|
|
389
|
+
- Now uses `PasswordValidationService` for password strength validation (was hardcoded)
|
|
390
|
+
- Now uses `IRateLimiter` for IP-based rate limiting on password reset
|
|
391
|
+
- Injects `AUTH_MODULE_OPTIONS` for password policy configuration
|
|
392
|
+
- Injects `RATE_LIMITER` for IP rate limiting
|
|
393
|
+
|
|
394
|
+
- **Injection Tokens** - Added new constants:
|
|
395
|
+
- `RATE_LIMITER` - Rate limiter service token
|
|
396
|
+
- Exported from `src/constants.ts` and `src/auth.module.ts`
|
|
397
|
+
|
|
398
|
+
### Tests
|
|
399
|
+
|
|
400
|
+
- **Total Tests: 467 → 516 (+49 new tests, +10.5% coverage)**
|
|
401
|
+
- Password Validation: 26 tests (default policy, custom policies, custom validators, edge cases)
|
|
402
|
+
- IP Rate Limiting: 23 tests (sliding window, cleanup, concurrency, edge cases)
|
|
403
|
+
- All existing tests pass (backward compatibility verified)
|
|
404
|
+
|
|
405
|
+
### Backward Compatibility
|
|
406
|
+
|
|
407
|
+
- **100% Backward Compatible** - No breaking changes
|
|
408
|
+
- Default password policy matches v0.2.x hardcoded behavior
|
|
409
|
+
- Rate limiter defaults to `InMemoryRateLimiterService` (automatically registered)
|
|
410
|
+
- Password reset still uses verification codes by default
|
|
411
|
+
- Existing consumers require no code changes to upgrade
|
|
412
|
+
|
|
413
|
+
### Security Improvements
|
|
414
|
+
|
|
415
|
+
- **IP-based rate limiting** prevents email enumeration attacks on password reset
|
|
416
|
+
- **Configurable password policies** enable stricter security requirements
|
|
417
|
+
- **Custom validators** support leaked password detection (HaveIBeenPwned, etc.)
|
|
418
|
+
- **Strategy pattern** enables future password reset methods with consistent security
|
|
419
|
+
|
|
420
|
+
### Documentation
|
|
421
|
+
|
|
422
|
+
- CLAUDE.md: Added "Optional Password Reset Enhancements" section (pending)
|
|
423
|
+
- README.md: Added "Advanced Password Reset Configuration" section (pending)
|
|
424
|
+
- All new interfaces/classes have comprehensive JSDoc documentation
|
|
425
|
+
- Examples provided for all three enhancements
|
|
426
|
+
|
|
427
|
+
### Migration Guide
|
|
428
|
+
|
|
429
|
+
No migration required! All enhancements are optional and backward compatible.
|
|
430
|
+
|
|
431
|
+
To use new features:
|
|
432
|
+
|
|
433
|
+
```typescript
|
|
434
|
+
import {
|
|
435
|
+
InMemoryRateLimiterService,
|
|
436
|
+
PasswordPolicyConfig
|
|
437
|
+
} from '@ambushsoftworks/nestjs-auth-graphql';
|
|
438
|
+
|
|
439
|
+
AuthModule.forRootAsync({
|
|
440
|
+
useFactory: () => ({
|
|
441
|
+
// Enhancement #1: Custom password policy
|
|
442
|
+
passwordPolicy: {
|
|
443
|
+
minLength: 12,
|
|
444
|
+
requireSpecialChar: true,
|
|
445
|
+
customValidator: async (password) => {
|
|
446
|
+
// Check against leaked passwords
|
|
447
|
+
const isLeaked = await checkLeakedPasswords(password);
|
|
448
|
+
return {
|
|
449
|
+
isValid: !isLeaked,
|
|
450
|
+
errors: isLeaked ? ['Password found in data breaches'] : []
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
},
|
|
454
|
+
|
|
455
|
+
// Enhancement #2: Rate limiting (default InMemoryRateLimiterService)
|
|
456
|
+
rateLimiterInstance: new InMemoryRateLimiterService(),
|
|
457
|
+
|
|
458
|
+
// ... other options
|
|
459
|
+
}),
|
|
460
|
+
})
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
### Known Limitations
|
|
464
|
+
|
|
465
|
+
- **Enhancement #3 (Reset Strategy)** - Partially implemented:
|
|
466
|
+
- Interfaces and strategies created but not integrated into AuthService
|
|
467
|
+
- Current implementation still uses VerificationService directly
|
|
468
|
+
- Full strategy pattern integration deferred to future release
|
|
469
|
+
- Magic link strategy requires consumer-implemented repository
|
|
470
|
+
|
|
471
|
+
- **InMemoryRateLimiterService** - Not suitable for multi-instance deployments:
|
|
472
|
+
- Rate limit state not shared between instances
|
|
473
|
+
- For production load-balanced deployments, implement Redis-backed IRateLimiter
|
|
474
|
+
- Single-instance deployments work perfectly
|
|
475
|
+
|
|
476
|
+
## [0.2.2] - 2025-11-16
|
|
477
|
+
|
|
478
|
+
### Fixed
|
|
479
|
+
|
|
480
|
+
- **Critical: Password Reset Email Template** - Fixed broken email template that was showing URL links instead of 6-digit verification codes
|
|
481
|
+
- Email service now properly displays 6-digit codes in mobile-responsive format
|
|
482
|
+
- Removed unused `_resetToken` parameter prefix from `sendPasswordResetEmail()` signature
|
|
483
|
+
- Updated HTML and plain text templates to match email/SMS verification pattern
|
|
484
|
+
- Users can now successfully complete password reset flow
|
|
485
|
+
- Template design: Mobile-responsive, centered code display, 15-minute expiry notice, security warning
|
|
486
|
+
- Files affected: `src/services/sendgrid-email.service.ts`
|
|
487
|
+
|
|
488
|
+
### Added
|
|
489
|
+
|
|
490
|
+
- **Comprehensive Password Reset Test Suite** - Added 17 unit tests covering all password reset scenarios
|
|
491
|
+
- `requestPasswordReset()` tests: rate limiting, enumeration protection, OAuth rejection, account lock detection
|
|
492
|
+
- `resetPassword()` tests: code validation, password strength, token revocation, lifecycle hooks
|
|
493
|
+
- Edge case tests: concurrent requests, email failures, invalid codes
|
|
494
|
+
- Achieved 95%+ line coverage for password reset code paths
|
|
495
|
+
- Total package test count: 246 → 263 tests
|
|
496
|
+
- Files affected: `src/services/auth.service.spec.ts`
|
|
497
|
+
|
|
498
|
+
### Changed
|
|
499
|
+
|
|
500
|
+
- **Documentation Improvements**
|
|
501
|
+
- Updated CLAUDE.md to clarify password reset implementation status and remove misleading "not yet supported" example
|
|
502
|
+
- Added comprehensive "Password Reset Flow" section to CLAUDE.md with security features documentation
|
|
503
|
+
- Added complete password reset section to README.md with step-by-step consumer setup guide
|
|
504
|
+
- Added consumer examples for GraphQL DTOs, resolver mutations, and error handling
|
|
505
|
+
- Documented 6-digit code approach (vs. magic link strategy) and security considerations
|
|
506
|
+
- Added lifecycle hooks example for `onPasswordReset()` event
|
|
507
|
+
|
|
508
|
+
### Technical Details
|
|
509
|
+
|
|
510
|
+
**Email Template Fix:**
|
|
511
|
+
- Changed: `renderPasswordResetHtmlTemplate()` now accepts `code: string` parameter (was `_resetToken`)
|
|
512
|
+
- Changed: `renderPasswordResetTextTemplate()` now accepts `code: string` parameter (was `_resetToken`)
|
|
513
|
+
- Both templates now display 6-digit code prominently instead of URL links
|
|
514
|
+
- Template design: Mobile-responsive, centered code display, 15-minute expiry notice, security warning
|
|
515
|
+
|
|
516
|
+
**Test Suite:**
|
|
517
|
+
- Added: 7 tests for `requestPasswordReset()` method
|
|
518
|
+
- Added: 10 tests for `resetPassword()` method
|
|
519
|
+
- Coverage: 95%+ lines, 100% branches, 100% functions for password reset code
|
|
520
|
+
|
|
521
|
+
**Migration Notes:**
|
|
522
|
+
- No breaking changes
|
|
523
|
+
- No consumer action required (backward compatible)
|
|
524
|
+
- Consumers implementing password reset for the first time should follow README.md setup guide
|
|
525
|
+
- Existing implementations (if any) will automatically benefit from email template fix
|
|
526
|
+
|
|
527
|
+
### Security
|
|
528
|
+
|
|
529
|
+
- Email enumeration protection maintained (generic success messages)
|
|
530
|
+
- Rate limiting enforced (60-second cooldown per user via `passwordResetSentAt` field)
|
|
531
|
+
- HMAC-SHA256 code hashing with constant-time comparison
|
|
532
|
+
- Token revocation on password change (all refresh tokens invalidated)
|
|
533
|
+
- OAuth user protection (users without passwords cannot reset)
|
|
534
|
+
- 15-minute code expiry with max 3 validation attempts
|
|
535
|
+
- Account lock integration (brute force protection applies to reset flow)
|
|
536
|
+
|
|
537
|
+
---
|
|
538
|
+
|
|
539
|
+
## [0.2.1] - 2025-01-16
|
|
540
|
+
|
|
541
|
+
### Fixed
|
|
542
|
+
|
|
543
|
+
- **Facebook OAuth Email Fallback GraphQL Schema**: Added missing `performCompleteFacebookSignUp()` method to `BaseAuthResolver` for consumers using inheritance pattern
|
|
544
|
+
- Consumers extending `BaseAuthResolver` can now expose `completeFacebookSignUp` mutation in their GraphQL schema
|
|
545
|
+
- Fixes production issue where Facebook sign-in fails for users without public email (business accounts, privacy settings)
|
|
546
|
+
- Backend service logic already existed in `AuthService.completeFacebookSignUp()`, but was not accessible via `BaseAuthResolver`
|
|
547
|
+
- No breaking changes - additive only (new protected method + interface export)
|
|
548
|
+
|
|
549
|
+
### Added
|
|
550
|
+
|
|
551
|
+
- `IAuthCompleteFacebookSignUpInput` interface export for type safety in consumer resolvers
|
|
552
|
+
- `BaseAuthResolver.performCompleteFacebookSignUp()` protected method following established pattern
|
|
553
|
+
|
|
554
|
+
### Technical Details
|
|
555
|
+
|
|
556
|
+
**Root Cause**: Facebook OAuth email fallback logic was implemented in `AuthService` (v0.2.0) but the corresponding `performCompleteFacebookSignUp()` protected method was never added to `BaseAuthResolver`. This broke the inheritance pattern for consumers.
|
|
557
|
+
|
|
558
|
+
**Impact**: Consumers using recommended `BaseAuthResolver` pattern (e.g., Lift app) could not complete Facebook sign-in for users without public email (business accounts, privacy settings). This affected ~30% of Facebook OAuth users.
|
|
559
|
+
|
|
560
|
+
**Solution**: Added protected `performCompleteFacebookSignUp()` method to `BaseAuthResolver`, following the same pattern as all other auth operations (signup, login, verifyEmail, verifyPhone, etc.).
|
|
561
|
+
|
|
562
|
+
**Migration**: None required for existing consumers not using Facebook OAuth. Consumers needing Facebook email fallback should:
|
|
563
|
+
1. Upgrade to v0.2.1
|
|
564
|
+
2. Add `completeFacebookSignUp` mutation to their custom resolver (see example below)
|
|
565
|
+
3. No configuration changes needed
|
|
566
|
+
|
|
567
|
+
### Example Consumer Implementation
|
|
568
|
+
|
|
569
|
+
```typescript
|
|
570
|
+
import { Mutation, Args } from '@nestjs/graphql';
|
|
571
|
+
import { Throttle } from '@nestjs/throttler';
|
|
572
|
+
import { AuthCompleteFacebookSignUpInput } from './dto/complete-facebook-signup.input';
|
|
573
|
+
import { AuthResponse } from './dto/auth-response.dto';
|
|
574
|
+
|
|
575
|
+
@Mutation(() => AuthResponse, {
|
|
576
|
+
name: 'completeFacebookSignUp',
|
|
577
|
+
description: 'Complete Facebook sign-up when email not provided by Facebook',
|
|
578
|
+
})
|
|
579
|
+
@Throttle({ default: { limit: 5, ttl: 60000 } })
|
|
580
|
+
async completeFacebookSignUp(
|
|
581
|
+
@Args('input') input: AuthCompleteFacebookSignUpInput,
|
|
582
|
+
): Promise<AuthResponse> {
|
|
583
|
+
return this.performCompleteFacebookSignUp(input) as Promise<AuthResponse>;
|
|
584
|
+
}
|
|
585
|
+
```
|
|
586
|
+
|
|
587
|
+
---
|
|
588
|
+
|
|
589
|
+
## [0.2.0] - 2025-01-15
|
|
590
|
+
|
|
591
|
+
### BREAKING CHANGES
|
|
592
|
+
|
|
593
|
+
This version introduces a complete architectural refactor for maximum portability across diverse NestJS projects. The package now uses an interface-based architecture with abstract base classes instead of concrete decorated resolvers.
|
|
594
|
+
|
|
595
|
+
**Migration Required**: See [MIGRATION.md](./MIGRATION.md) for step-by-step upgrade instructions.
|
|
596
|
+
|
|
597
|
+
#### Removed
|
|
598
|
+
|
|
599
|
+
- **Concrete `AuthResolver` class** - Package no longer exports a decorated GraphQL resolver
|
|
600
|
+
- Consumers must create their own resolver extending `BaseAuthResolver<T>`
|
|
601
|
+
- GraphQL decorators (@Mutation, @Query, etc.) must be added by consumer
|
|
602
|
+
|
|
603
|
+
- **GraphQL decorators on package entities and DTOs** - Package no longer provides GraphQL types
|
|
604
|
+
- All package entities converted to interfaces (IAuthUser, IAuthSignupInput, etc.)
|
|
605
|
+
- Consumers define their own @ObjectType and @InputType classes implementing package interfaces
|
|
606
|
+
- This eliminates GraphQL schema conflicts when integrating into existing projects
|
|
607
|
+
|
|
608
|
+
- **Direct Prisma/TypeORM coupling** - Package no longer assumes specific ORM
|
|
609
|
+
- Repository interfaces require manual implementation by consumer
|
|
610
|
+
- Full control over database schema and queries
|
|
611
|
+
|
|
612
|
+
#### Added
|
|
613
|
+
|
|
614
|
+
- **`BaseAuthResolver<T extends IAuthUser>` abstract class**
|
|
615
|
+
- Provides protected helper methods for all auth operations
|
|
616
|
+
- `performSignup()`, `performLogin()`, `performRefreshToken()`, etc.
|
|
617
|
+
- Type-safe generic parameter allows using any User model implementing IAuthUser
|
|
618
|
+
|
|
619
|
+
- **Interface-based architecture for complete portability**
|
|
620
|
+
- `IAuthUser` - User model contract (replaces concrete User entity)
|
|
621
|
+
- `IAuthSignupInput`, `IAuthLoginInput`, etc. - Input DTOs as interfaces
|
|
622
|
+
- `IAuthResponse`, `ILogoutResponse`, etc. - Response DTOs as interfaces
|
|
623
|
+
- Works with any database, ORM, or GraphQL schema structure
|
|
624
|
+
|
|
625
|
+
- **Comprehensive documentation**
|
|
626
|
+
- [QUICK-START.md](./QUICK-START.md) - Step-by-step setup guide
|
|
627
|
+
- [MIGRATION.md](./MIGRATION.md) - Upgrade guide from v0.1.x
|
|
628
|
+
- [docs/IMPLEMENTATION-ISSUES.md](./docs/IMPLEMENTATION-ISSUES.md) - Common issues and solutions
|
|
629
|
+
- [docs/IMPLEMENTATION-PLAN-V2.md](./docs/IMPLEMENTATION-PLAN-V2.md) - Architecture deep dive
|
|
630
|
+
|
|
631
|
+
- **Canonical enum exports**
|
|
632
|
+
- Package owns `AuthProvider` enum (EMAIL, GOOGLE, FACEBOOK, APPLE)
|
|
633
|
+
- Package owns `UserStatus` enum (ACTIVE, SUSPENDED, DELETED)
|
|
634
|
+
- Single source of truth prevents enum compatibility issues
|
|
635
|
+
|
|
636
|
+
#### Changed
|
|
637
|
+
|
|
638
|
+
- **User entity**: `User` class → `IAuthUser` interface
|
|
639
|
+
- Consumers implement interface with their own GraphQL @ObjectType class
|
|
640
|
+
- Full control over additional custom fields and relationships
|
|
641
|
+
|
|
642
|
+
- **All DTOs converted to interfaces with I* prefix**
|
|
643
|
+
- Example: `AuthSignupInput` → `IAuthSignupInput`
|
|
644
|
+
- Consumers create @InputType classes implementing these interfaces
|
|
645
|
+
|
|
646
|
+
- **Repository interfaces use generic type constraints**
|
|
647
|
+
- More flexible type system allows adaptation to various ORMs
|
|
648
|
+
- Type assertions at repository boundary for Prisma/TypeORM compatibility
|
|
649
|
+
|
|
650
|
+
- **Package architecture follows Layer 0 principles**
|
|
651
|
+
- Complete abstraction over database, ORM, and GraphQL implementation
|
|
652
|
+
- Zero coupling to Prisma, TypeORM, or specific GraphQL libraries
|
|
653
|
+
- Dependency injection via instance-based pattern (not class references)
|
|
654
|
+
|
|
655
|
+
### Features Preserved
|
|
656
|
+
|
|
657
|
+
All authentication features from v0.1.x remain fully functional:
|
|
658
|
+
|
|
659
|
+
- JWT authentication with automatic refresh token rotation
|
|
660
|
+
- OAuth 2.0 (Google, Facebook, Apple)
|
|
661
|
+
- Email verification with 6-digit PIN codes
|
|
662
|
+
- SMS verification via Twilio
|
|
663
|
+
- Brute force protection with account lockout
|
|
664
|
+
- Biometric authentication support
|
|
665
|
+
- Account linking (multiple OAuth providers per account)
|
|
666
|
+
- Security: HMAC-SHA256 token hashing, AES-256-GCM encryption, constant-time comparison
|
|
667
|
+
|
|
668
|
+
### Migration Summary
|
|
669
|
+
|
|
670
|
+
**Before (v0.1.x)**:
|
|
671
|
+
```typescript
|
|
672
|
+
import { AuthModule, AuthResolver } from '@ambushsoftworks/nestjs-auth-graphql';
|
|
673
|
+
|
|
674
|
+
@Module({
|
|
675
|
+
imports: [AuthModule.forRoot(...)],
|
|
676
|
+
providers: [AuthResolver] // Use package resolver directly
|
|
677
|
+
})
|
|
678
|
+
```
|
|
679
|
+
|
|
680
|
+
**After (v0.2.0)**:
|
|
681
|
+
```typescript
|
|
682
|
+
import { AuthModule, BaseAuthResolver } from '@ambushsoftworks/nestjs-auth-graphql';
|
|
683
|
+
|
|
684
|
+
// 1. Create your resolver
|
|
685
|
+
@Resolver()
|
|
686
|
+
export class MyAuthResolver extends BaseAuthResolver<User> {
|
|
687
|
+
@Mutation(() => AuthResponse)
|
|
688
|
+
async signup(@Args('input') input: SignupInput): Promise<AuthResponse> {
|
|
689
|
+
return this.performSignup(input) as Promise<AuthResponse>;
|
|
690
|
+
}
|
|
691
|
+
// ... override all 11 mutations + 3 queries
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
@Module({
|
|
695
|
+
imports: [AuthModule.forRoot(...)],
|
|
696
|
+
providers: [MyAuthResolver] // Use your custom resolver
|
|
697
|
+
})
|
|
698
|
+
```
|
|
699
|
+
|
|
700
|
+
**Benefits**:
|
|
701
|
+
- Zero GraphQL schema conflicts with existing projects
|
|
702
|
+
- Full control over GraphQL types, fields, and descriptions
|
|
703
|
+
- Works with any database schema (not just Prisma)
|
|
704
|
+
- Type-safe integration through interfaces
|
|
705
|
+
- Easier to customize and extend
|
|
706
|
+
|
|
707
|
+
### Testing
|
|
708
|
+
|
|
709
|
+
- **553+ tests passing** in Lift integration (production validation)
|
|
710
|
+
- **0 TypeScript errors** in package build
|
|
711
|
+
- **0 runtime regressions** from architectural changes
|
|
712
|
+
- **100% backward compatibility** of authentication features
|
|
713
|
+
|
|
714
|
+
### Known Issues
|
|
715
|
+
|
|
716
|
+
- **Pre-existing test failures**: BruteForceProtectionService and MuscleGroupRecoveryService tests have DI mock issues (not caused by v0.2.0 refactor, existed in v0.1.x)
|
|
717
|
+
- **Migration effort**: Upgrading from v0.1.x requires 2-4 hours to create resolver and GraphQL types
|
|
718
|
+
|
|
719
|
+
---
|
|
720
|
+
|
|
721
|
+
## [0.1.10] - 2025-01-14
|
|
722
|
+
|
|
723
|
+
### Fixed
|
|
724
|
+
- Fixed instance-based DI pattern (removed ModuleRef complexity)
|
|
725
|
+
- Cleaned up repository injection in AuthModule.forRootAsync()
|
|
726
|
+
|
|
727
|
+
### Changed
|
|
728
|
+
- Simplified dependency injection to use direct instance passing
|
|
729
|
+
- Improved documentation for repository configuration
|
|
730
|
+
|
|
731
|
+
---
|
|
732
|
+
|
|
733
|
+
## [0.1.9] - 2025-01-13
|
|
734
|
+
|
|
735
|
+
### Added
|
|
736
|
+
- Published package to npm registry as `@ambushsoftworks/nestjs-auth-graphql`
|
|
737
|
+
- Standalone repository on GitLab
|
|
738
|
+
|
|
739
|
+
### Changed
|
|
740
|
+
- Migrated Lift backend to use published npm package
|
|
741
|
+
- Removed local package directory from monorepo
|
|
742
|
+
|
|
743
|
+
---
|
|
744
|
+
|
|
745
|
+
## [0.1.8] - 2025-01-12
|
|
746
|
+
|
|
747
|
+
### Changed
|
|
748
|
+
- Backend package cleanup and optimization
|
|
749
|
+
- Updated authentication service interfaces
|
|
750
|
+
|
|
751
|
+
---
|
|
752
|
+
|
|
753
|
+
## [0.1.7] - 2025-01-11
|
|
754
|
+
|
|
755
|
+
### Added
|
|
756
|
+
- SMS verification via Twilio integration
|
|
757
|
+
- Phone number validation using libphonenumber-js
|
|
758
|
+
- `verifyPhone`, `resendPhoneVerification`, `removePhoneNumber` mutations
|
|
759
|
+
|
|
760
|
+
### Changed
|
|
761
|
+
- Enhanced verification service with SMS support
|
|
762
|
+
- Router fixes for phone verification flow
|
|
763
|
+
|
|
764
|
+
---
|
|
765
|
+
|
|
766
|
+
## [0.1.0] - 2024-11-01
|
|
767
|
+
|
|
768
|
+
### Added
|
|
769
|
+
- Initial package extraction from Lift app
|
|
770
|
+
- JWT authentication with refresh tokens
|
|
771
|
+
- Email verification via SendGrid
|
|
772
|
+
- Brute force protection
|
|
773
|
+
- Google and Facebook OAuth
|
|
774
|
+
- Biometric authentication support
|
|
775
|
+
- 246+ tests from production codebase
|
|
776
|
+
|
|
777
|
+
---
|
|
778
|
+
|
|
779
|
+
[0.8.0]: https://gitlab.com/ambushworks/nestjs-auth-graphql/-/compare/v0.7.2...v0.8.0
|
|
780
|
+
[0.2.2]: https://gitlab.com/ambushworks/nestjs-auth-graphql/-/compare/v0.2.1...v0.2.2
|
|
781
|
+
[0.2.1]: https://gitlab.com/ambushworks/nestjs-auth-graphql/-/compare/v0.2.0...v0.2.1
|
|
782
|
+
[0.2.0]: https://gitlab.com/ambushworks/nestjs-auth-graphql/-/compare/v0.1.10...v0.2.0
|
|
783
|
+
[0.1.10]: https://gitlab.com/ambushworks/nestjs-auth-graphql/-/compare/v0.1.9...v0.1.10
|
|
784
|
+
[0.1.9]: https://gitlab.com/ambushworks/nestjs-auth-graphql/-/compare/v0.1.8...v0.1.9
|
|
785
|
+
[0.1.8]: https://gitlab.com/ambushworks/nestjs-auth-graphql/-/compare/v0.1.7...v0.1.8
|
|
786
|
+
[0.1.7]: https://gitlab.com/ambushworks/nestjs-auth-graphql/-/compare/v0.1.0...v0.1.7
|
|
787
|
+
[0.1.0]: https://gitlab.com/ambushworks/nestjs-auth-graphql/-/releases/v0.1.0
|