@ambushsoftworks/nestjs-auth-graphql 0.15.1 → 0.16.0-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +101 -0
- package/README.md +311 -3
- package/dist/auth.module.d.ts +17 -0
- package/dist/auth.module.d.ts.map +1 -1
- package/dist/auth.module.js +91 -0
- package/dist/auth.module.js.map +1 -1
- package/dist/constants.d.ts +2 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +3 -1
- package/dist/constants.js.map +1 -1
- package/dist/dto/passkey.dto.d.ts +43 -0
- package/dist/dto/passkey.dto.d.ts.map +1 -0
- package/dist/dto/passkey.dto.js +5 -0
- package/dist/dto/passkey.dto.js.map +1 -0
- package/dist/enums/verification-type.enum.d.ts +3 -2
- package/dist/enums/verification-type.enum.d.ts.map +1 -1
- package/dist/enums/verification-type.enum.js +1 -0
- package/dist/enums/verification-type.enum.js.map +1 -1
- package/dist/exceptions/step-up.exceptions.d.ts +11 -0
- package/dist/exceptions/step-up.exceptions.d.ts.map +1 -0
- package/dist/exceptions/step-up.exceptions.js +33 -0
- package/dist/exceptions/step-up.exceptions.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -1
- package/dist/interfaces/auth-lifecycle-hooks.interface.d.ts +2 -0
- package/dist/interfaces/auth-lifecycle-hooks.interface.d.ts.map +1 -1
- package/dist/interfaces/auth-logger.interface.d.ts +10 -1
- package/dist/interfaces/auth-logger.interface.d.ts.map +1 -1
- package/dist/interfaces/auth-logger.interface.js +9 -0
- package/dist/interfaces/auth-logger.interface.js.map +1 -1
- package/dist/interfaces/biometric-verifier.interface.d.ts.map +1 -1
- package/dist/interfaces/email-service.interface.d.ts +1 -0
- package/dist/interfaces/email-service.interface.d.ts.map +1 -1
- package/dist/interfaces/email-template-renderer.interface.d.ts +5 -0
- package/dist/interfaces/email-template-renderer.interface.d.ts.map +1 -1
- 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/passkey-repository.interface.d.ts +48 -0
- package/dist/interfaces/passkey-repository.interface.d.ts.map +1 -0
- package/dist/interfaces/passkey-repository.interface.js +13 -0
- package/dist/interfaces/passkey-repository.interface.js.map +1 -0
- package/dist/passkey/passkey-json.types.d.ts +44 -0
- package/dist/passkey/passkey-json.types.d.ts.map +1 -0
- package/dist/passkey/passkey-json.types.js +3 -0
- package/dist/passkey/passkey-json.types.js.map +1 -0
- package/dist/passkey/webauthn-adapter.d.ts +104 -0
- package/dist/passkey/webauthn-adapter.d.ts.map +1 -0
- package/dist/passkey/webauthn-adapter.js +207 -0
- package/dist/passkey/webauthn-adapter.js.map +1 -0
- package/dist/repositories/in-memory-passkey.repository.d.ts +21 -0
- package/dist/repositories/in-memory-passkey.repository.d.ts.map +1 -0
- package/dist/repositories/in-memory-passkey.repository.js +72 -0
- package/dist/repositories/in-memory-passkey.repository.js.map +1 -0
- package/dist/resolvers/base-auth.resolver.d.ts +20 -0
- package/dist/resolvers/base-auth.resolver.d.ts.map +1 -1
- package/dist/resolvers/base-auth.resolver.js +103 -0
- package/dist/resolvers/base-auth.resolver.js.map +1 -1
- package/dist/services/auth.service.d.ts +5 -0
- package/dist/services/auth.service.d.ts.map +1 -1
- package/dist/services/auth.service.js +24 -0
- package/dist/services/auth.service.js.map +1 -1
- package/dist/services/biometric-challenge.service.d.ts.map +1 -1
- package/dist/services/biometric-challenge.service.js.map +1 -1
- package/dist/services/configurable-email.service.d.ts +1 -0
- package/dist/services/configurable-email.service.d.ts.map +1 -1
- package/dist/services/configurable-email.service.js +18 -0
- package/dist/services/configurable-email.service.js.map +1 -1
- package/dist/services/default-email-template-renderer.d.ts +5 -0
- package/dist/services/default-email-template-renderer.d.ts.map +1 -1
- package/dist/services/default-email-template-renderer.js +31 -0
- package/dist/services/default-email-template-renderer.js.map +1 -1
- package/dist/services/passkey.service.d.ts +101 -0
- package/dist/services/passkey.service.d.ts.map +1 -0
- package/dist/services/passkey.service.js +498 -0
- package/dist/services/passkey.service.js.map +1 -0
- package/dist/services/step-up-token.service.d.ts +15 -0
- package/dist/services/step-up-token.service.d.ts.map +1 -0
- package/dist/services/step-up-token.service.js +71 -0
- package/dist/services/step-up-token.service.js.map +1 -0
- package/dist/services/step-up.service.d.ts +50 -0
- package/dist/services/step-up.service.d.ts.map +1 -0
- package/dist/services/step-up.service.js +142 -0
- package/dist/services/step-up.service.js.map +1 -0
- package/dist/services/verification.service.d.ts +2 -2
- package/dist/services/verification.service.d.ts.map +1 -1
- package/dist/services/verification.service.js +5 -4
- package/dist/services/verification.service.js.map +1 -1
- package/dist/test-utils/software-authenticator.d.ts +37 -0
- package/dist/test-utils/software-authenticator.d.ts.map +1 -0
- package/dist/test-utils/software-authenticator.js +212 -0
- package/dist/test-utils/software-authenticator.js.map +1 -0
- package/dist/utils/passkey-rp.d.ts +13 -0
- package/dist/utils/passkey-rp.d.ts.map +1 -0
- package/dist/utils/passkey-rp.js +89 -0
- package/dist/utils/passkey-rp.js.map +1 -0
- package/package.json +6 -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.16.0-rc.1] - 2026-10-01
|
|
31
|
+
|
|
32
|
+
Passkeys (WebAuthn), and step-up re-authentication to protect adding one.
|
|
33
|
+
|
|
34
|
+
**Release candidate**, published to the `next` dist-tag for a pilot on real
|
|
35
|
+
devices; `latest` stays on 0.15.x. Install with
|
|
36
|
+
`npm install @ambushsoftworks/nestjs-auth-graphql@next`.
|
|
37
|
+
|
|
38
|
+
### ⚠ External configuration required
|
|
39
|
+
|
|
40
|
+
Only if you enable passkeys. None of these is visible to a compiler or a test
|
|
41
|
+
suite; each fails as an opaque error on the device. Details in the README,
|
|
42
|
+
"Passkeys → Platform setup".
|
|
43
|
+
|
|
44
|
+
- **Install the optional peer** `@simplewebauthn/server@^14`. It needs
|
|
45
|
+
**Node.js 20+**; the package itself still supports 18 without passkeys. Boot
|
|
46
|
+
fails with the install command if it is missing.
|
|
47
|
+
- **Web:** serve the frontend over HTTPS on `passkey.rp.id` or a subdomain of it,
|
|
48
|
+
and list its exact origin in `passkey.rp.origins`.
|
|
49
|
+
- **Android:** publish `https://<rp.id>/.well-known/assetlinks.json` with
|
|
50
|
+
`delegate_permission/common.get_login_creds` for the app, and add
|
|
51
|
+
`android:apk-key-hash:<base64url SHA-256 of the signing certificate>` to
|
|
52
|
+
`passkey.rp.origins`. With Play App Signing, that is Google's app signing key.
|
|
53
|
+
- **iOS:** publish `https://<rp.id>/.well-known/apple-app-site-association` with
|
|
54
|
+
the app under `webcredentials.apps`, and add `webcredentials:<rp.id>` to the
|
|
55
|
+
app's Associated Domains entitlement.
|
|
56
|
+
- **Database:** tables for passkey credentials and challenges (reference Prisma
|
|
57
|
+
schema in the README), and, if your verification repository stores the type
|
|
58
|
+
in a database enum, the new value `step-up`.
|
|
59
|
+
|
|
60
|
+
### Added
|
|
61
|
+
|
|
62
|
+
- **Passkeys.** `PasskeyService` and `IPasskeyRepository`, enabled by
|
|
63
|
+
`passkeyRepositoryInstance` and `passkey.rp`:
|
|
64
|
+
```
|
|
65
|
+
registrationOptions(userId, { stepUpToken }) -> { challengeId, options }
|
|
66
|
+
verifyRegistration(userId, { challengeId, responseJson, name? })
|
|
67
|
+
authenticationOptions() -> { challengeId, options } // no username
|
|
68
|
+
verifyAuthentication({ challengeId, responseJson }) -> session
|
|
69
|
+
listPasskeys / renamePasskey / removePasskey
|
|
70
|
+
```
|
|
71
|
+
Discoverable credentials (usernameless sign-in, browser autofill),
|
|
72
|
+
`userVerification` configurable and `'required'` by default, EdDSA / ES256 /
|
|
73
|
+
RS256. Sessions come from `issueAuthSession`, so account status, cookie auth,
|
|
74
|
+
realm claims and `SESSION_ISSUED` apply unchanged. Verification is by
|
|
75
|
+
`@simplewebauthn/server`, loaded only when passkeys are configured and kept
|
|
76
|
+
out of the published type declarations.
|
|
77
|
+
- **What the package enforces on top of the library:** single-use challenges
|
|
78
|
+
bound to purpose, user and realm and consumed before anything is checked; the
|
|
79
|
+
realm on the stored credential; the `userHandle` (the library does not check
|
|
80
|
+
it); the signature counter, with a compare-and-set and a
|
|
81
|
+
`PASSKEY_COUNTER_REGRESSION` event; global uniqueness of credential ids
|
|
82
|
+
(`PasskeyCredentialExistsError`); refusing to remove an account's last way in.
|
|
83
|
+
- **Step-up re-authentication.** `StepUpService` and `StepUpTokenService`: a
|
|
84
|
+
signed-in user re-authenticates with a password, a passkey or an emailed
|
|
85
|
+
6-digit code and receives a five-minute token bound to user and realm.
|
|
86
|
+
**Adding a passkey requires one**: a passkey survives a password change and
|
|
87
|
+
logout-all, so a stolen session must not be able to add one. Wrong passwords
|
|
88
|
+
count toward brute-force lockout. The token is signed with an HKDF-derived
|
|
89
|
+
key, so it never passes as an access token.
|
|
90
|
+
- `BaseAuthResolver` methods: `performGetStepUpMethods`,
|
|
91
|
+
`performRequestStepUpEmailCode`, `performStepUpPasskeyOptions`,
|
|
92
|
+
`performReauthenticate`, `performPasskeyRegistrationOptions`,
|
|
93
|
+
`performVerifyPasskeyRegistration`, `performPasskeyAuthenticationOptions`,
|
|
94
|
+
`performVerifyPasskeyAuthentication`, `performListPasskeys`,
|
|
95
|
+
`performRenamePasskey`, `performRemovePasskey`. WebAuthn JSON crosses GraphQL
|
|
96
|
+
as a `String`.
|
|
97
|
+
- Options `passkey` (`rp`, `userVerification`, timeouts, `rateLimit`,
|
|
98
|
+
`userDisplayName`) and `stepUp.ttlSeconds`, validated at boot.
|
|
99
|
+
- Lifecycle hooks `onPasskeyRegistered` and `onPasskeyRemoved`, not awaited.
|
|
100
|
+
Wire the first to a notification email.
|
|
101
|
+
- Security events `PASSKEY_REGISTERED`, `PASSKEY_REGISTRATION_FAILURE`,
|
|
102
|
+
`PASSKEY_LOGIN_SUCCESS`, `PASSKEY_LOGIN_FAILURE`, `PASSKEY_COUNTER_REGRESSION`,
|
|
103
|
+
`PASSKEY_RENAMED`, `PASSKEY_REMOVED`, `STEP_UP_SUCCESS`, `STEP_UP_FAILURE`.
|
|
104
|
+
- Exceptions `StepUpRequiredException` (`STEP_UP_REQUIRED`),
|
|
105
|
+
`StepUpUnavailableException` (`STEP_UP_UNAVAILABLE`), `AuthRateLimitException`
|
|
106
|
+
(`RATE_LIMIT_EXCEEDED`), as `GraphQLError`s so the code reaches clients.
|
|
107
|
+
- `IEmailService.sendStepUpCodeEmail` and
|
|
108
|
+
`IEmailTemplateRenderer.renderStepUpCodeEmail`, both optional; implemented by
|
|
109
|
+
`ConfigurableEmailService` and `DefaultEmailTemplateRenderer`.
|
|
110
|
+
- `VerificationType.STEP_UP`, and `generateCodeForUserId` /
|
|
111
|
+
`validateCodeForUserId` accept `'step-up'`.
|
|
112
|
+
- `AuthService.requireActiveUser` and `AuthService.verifyPasswordForStepUp`.
|
|
113
|
+
- `InMemoryPasskeyRepository`, for tests and a single instance.
|
|
114
|
+
|
|
115
|
+
### Changed
|
|
116
|
+
|
|
117
|
+
- `RepositoryVerificationType` includes `VerificationType.STEP_UP`. An adapter
|
|
118
|
+
with an exhaustive `switch` over it needs the new case.
|
|
119
|
+
- `BaseAuthResolver` gains the protected properties `authPasskeys` and
|
|
120
|
+
`authStepUp`, injected by property so its constructor is unchanged. A
|
|
121
|
+
subclass that declares members with those names must rename them.
|
|
122
|
+
|
|
123
|
+
### Fixed
|
|
124
|
+
|
|
125
|
+
- The 0.14.0 notes and code comments said WebAuthn could be added as a second
|
|
126
|
+
`IBiometricVerifier` "without reshaping the port". It could not: a passkey
|
|
127
|
+
needs authenticator data and a user handle, a counter write, a registration
|
|
128
|
+
ceremony and challenges issued before any credential is known. Passkeys have
|
|
129
|
+
their own port, and the comments now say so.
|
|
130
|
+
|
|
30
131
|
## [0.15.1] - 2026-10-01
|
|
31
132
|
|
|
32
133
|
### Fixed
|
package/README.md
CHANGED
|
@@ -19,6 +19,8 @@ Production-grade authentication module for NestJS GraphQL + REST APIs. JWT, cook
|
|
|
19
19
|
- [Phone \& SMS Verification](#phone--sms-verification)
|
|
20
20
|
- [OAuth](#oauth)
|
|
21
21
|
- [Biometric Authentication](#biometric-authentication)
|
|
22
|
+
- [Passkeys](#passkeys)
|
|
23
|
+
- [Step-Up Re-authentication](#step-up-re-authentication)
|
|
22
24
|
- [Brute Force Protection](#brute-force-protection)
|
|
23
25
|
- [Password Reset](#password-reset)
|
|
24
26
|
- [Password Policy](#password-policy)
|
|
@@ -38,6 +40,8 @@ Production-grade authentication module for NestJS GraphQL + REST APIs. JWT, cook
|
|
|
38
40
|
- [Composable Email](#composable-email-email)
|
|
39
41
|
- [Verification Options](#verification-options-verification)
|
|
40
42
|
- [Biometric Options (`biometric`)](#biometric-options-biometric)
|
|
43
|
+
- [Passkey Options (`passkey`)](#passkey-options-passkey)
|
|
44
|
+
- [Step-Up Options (`stepUp`)](#step-up-options-stepup)
|
|
41
45
|
- [SendGrid Options (`sendgrid`)](#sendgrid-options-sendgrid)
|
|
42
46
|
- [Twilio Options (`twilio`)](#twilio-options-twilio)
|
|
43
47
|
- [Optional Instance Options](#optional-instance-options)
|
|
@@ -79,6 +83,8 @@ npm install @nestjs/common @nestjs/core @nestjs/graphql @nestjs/jwt @nestjs/pass
|
|
|
79
83
|
|
|
80
84
|
`resend` is an optional peer dependency -- install it only if using the Resend email sender.
|
|
81
85
|
|
|
86
|
+
`@simplewebauthn/server` (^14, Node.js 20+) is an optional peer dependency -- install it only if using [passkeys](#passkeys). Boot fails with the install command if passkeys are configured without it.
|
|
87
|
+
|
|
82
88
|
Supports NestJS 10 and NestJS 11. CI runs the unit and end-to-end suites on both: NestJS 10 with `@nestjs/graphql` 12, Express 4 and Apollo Server 4, and NestJS 11 with `@nestjs/graphql` 13, Express 5 and Apollo Server 5.
|
|
83
89
|
|
|
84
90
|
## Quick Start
|
|
@@ -266,6 +272,22 @@ When cookie auth is enabled, pass the GraphQL `context` so tokens are set as Htt
|
|
|
266
272
|
| `performResetPassword(input, context?)` | Reset password |
|
|
267
273
|
| `performChangePassword(userId, current, new)` | Change password |
|
|
268
274
|
|
|
275
|
+
**Passkeys and step-up** (see [Passkeys](#passkeys)):
|
|
276
|
+
|
|
277
|
+
| Method | Purpose |
|
|
278
|
+
|--------|---------|
|
|
279
|
+
| `performGetStepUpMethods(user, context?)` | Which re-authentication methods this account has |
|
|
280
|
+
| `performRequestStepUpEmailCode(user, context?)` | Email a 6-digit re-authentication code |
|
|
281
|
+
| `performStepUpPasskeyOptions(user, context?)` | Options for re-authenticating with a passkey |
|
|
282
|
+
| `performReauthenticate(user, input, context?)` | Prove identity again; returns a step-up token |
|
|
283
|
+
| `performPasskeyRegistrationOptions(user, input, context?)` | Options for adding a passkey (needs a step-up token) |
|
|
284
|
+
| `performVerifyPasskeyRegistration(user, input, context?)` | Store the new passkey |
|
|
285
|
+
| `performPasskeyAuthenticationOptions(context?)` | Options for signing in (public) |
|
|
286
|
+
| `performVerifyPasskeyAuthentication(input, context?)` | Sign in; same response as `performLogin` |
|
|
287
|
+
| `performListPasskeys(user, context?)` | The user's passkeys |
|
|
288
|
+
| `performRenamePasskey(user, input, context?)` | Rename one |
|
|
289
|
+
| `performRemovePasskey(user, input, context?)` | Remove one (refused for the last way in) |
|
|
290
|
+
|
|
269
291
|
**Other:**
|
|
270
292
|
|
|
271
293
|
| Method | Purpose |
|
|
@@ -929,7 +951,7 @@ Treat this as *possession of a device-bound key that the client promises to
|
|
|
929
951
|
gate*. It is a good second factor and a good convenience login. Before making it
|
|
930
952
|
the sole factor for something sensitive, be clear that you are trusting the app,
|
|
931
953
|
not the biometric. WebAuthn's `userVerification` flag is what actually attests to
|
|
932
|
-
user verification, and is not this.
|
|
954
|
+
user verification, and is not this: for that, use [passkeys](#passkeys).
|
|
933
955
|
|
|
934
956
|
#### The flow
|
|
935
957
|
|
|
@@ -1012,6 +1034,267 @@ same answer.
|
|
|
1012
1034
|
biometric services are not registered at all, rather than silently accepting
|
|
1013
1035
|
enrolments that can never authenticate.
|
|
1014
1036
|
|
|
1037
|
+
### Passkeys
|
|
1038
|
+
|
|
1039
|
+
WebAuthn passkeys (since 0.16.0): registration for a signed-in user, then
|
|
1040
|
+
sign-in with no username at all. Enabled by `passkeyRepositoryInstance` plus
|
|
1041
|
+
`passkey.rp`, and needs the optional peer `@simplewebauthn/server` (^14,
|
|
1042
|
+
Node.js 20+).
|
|
1043
|
+
|
|
1044
|
+
```typescript
|
|
1045
|
+
passkeyRepositoryInstance: myPasskeyRepo, // enables the capability
|
|
1046
|
+
passkey: {
|
|
1047
|
+
rp: {
|
|
1048
|
+
id: 'example.com', // the domain passkeys are bound to
|
|
1049
|
+
name: 'Example',
|
|
1050
|
+
origins: [
|
|
1051
|
+
'https://app.example.com',
|
|
1052
|
+
'android:apk-key-hash:<base64url SHA-256 of your signing certificate>',
|
|
1053
|
+
],
|
|
1054
|
+
},
|
|
1055
|
+
// userVerification: 'required', // default; see below
|
|
1056
|
+
},
|
|
1057
|
+
```
|
|
1058
|
+
|
|
1059
|
+
#### What a passkey sign-in proves
|
|
1060
|
+
|
|
1061
|
+
With `userVerification: 'required'` (the default): the user unlocked an
|
|
1062
|
+
authenticator **bound to this domain** that holds the registered key. Unlike
|
|
1063
|
+
the [biometric device key](#biometric-authentication), the authenticator itself
|
|
1064
|
+
attests that it verified the user, and a lookalike domain cannot use the
|
|
1065
|
+
credential. It does not prove *which* person unlocked the device, and a synced
|
|
1066
|
+
passkey is only as safe as the iCloud or Google account it syncs through.
|
|
1067
|
+
|
|
1068
|
+
**`'required'` adds no prompt for users.** On phones and laptops the Face ID,
|
|
1069
|
+
fingerprint or device PIN prompt *is* the passkey prompt, once per sign-in;
|
|
1070
|
+
sessions then continue through refresh tokens as usual. What `'required'`
|
|
1071
|
+
changes is that the server refuses an authenticator reporting that it did not
|
|
1072
|
+
verify the user, which in practice means a presence-only hardware key.
|
|
1073
|
+
`'preferred'` and `'discouraged'` accept those, and give up that guarantee.
|
|
1074
|
+
|
|
1075
|
+
#### The flow
|
|
1076
|
+
|
|
1077
|
+
```
|
|
1078
|
+
Add a passkey (signed in)
|
|
1079
|
+
performReauthenticate({ password }) -> { stepUpToken } (see Step-Up)
|
|
1080
|
+
performPasskeyRegistrationOptions({ stepUpToken }) -> { challengeId, optionsJson }
|
|
1081
|
+
client: navigator.credentials.create(...)
|
|
1082
|
+
performVerifyPasskeyRegistration({ challengeId, responseJson, name? })
|
|
1083
|
+
|
|
1084
|
+
Sign in (signed out)
|
|
1085
|
+
performPasskeyAuthenticationOptions() -> { challengeId, optionsJson }
|
|
1086
|
+
client: navigator.credentials.get(...)
|
|
1087
|
+
performVerifyPasskeyAuthentication({ challengeId, responseJson }) -> AuthResponse
|
|
1088
|
+
```
|
|
1089
|
+
|
|
1090
|
+
WebAuthn JSON crosses GraphQL as a `String`: send `optionsJson` to the platform
|
|
1091
|
+
API unchanged, and send back `JSON.stringify` of what it returns. In a browser:
|
|
1092
|
+
|
|
1093
|
+
```typescript
|
|
1094
|
+
const { challengeId, optionsJson } = await passkeyAuthenticationOptions();
|
|
1095
|
+
const credential = await navigator.credentials.get({
|
|
1096
|
+
publicKey: PublicKeyCredential.parseRequestOptionsFromJSON(JSON.parse(optionsJson)),
|
|
1097
|
+
});
|
|
1098
|
+
await passkeyLogin({ challengeId, responseJson: JSON.stringify(credential.toJSON()) });
|
|
1099
|
+
```
|
|
1100
|
+
|
|
1101
|
+
(`@simplewebauthn/browser`'s `startRegistration({ optionsJSON })` /
|
|
1102
|
+
`startAuthentication({ optionsJSON })` do the same on browsers without the
|
|
1103
|
+
`parse…FromJSON` helpers.) The sign-in options also serve browser autofill:
|
|
1104
|
+
call `navigator.credentials.get` with `mediation: 'conditional'`, and fetch
|
|
1105
|
+
fresh options when they expire (120 s by default). On Flutter, use a plugin that
|
|
1106
|
+
exchanges WebAuthn JSON with Android Credential Manager and iOS
|
|
1107
|
+
`ASAuthorization`, and pass the same two strings through.
|
|
1108
|
+
|
|
1109
|
+
A consumer resolver, in the same shape as every other operation:
|
|
1110
|
+
|
|
1111
|
+
```typescript
|
|
1112
|
+
@ObjectType() class PasskeyOptions { @Field() challengeId!: string; @Field() optionsJson!: string; }
|
|
1113
|
+
@ObjectType() class Passkey {
|
|
1114
|
+
@Field() credentialId!: string; @Field() name!: string;
|
|
1115
|
+
@Field() deviceType!: string; @Field() backedUp!: boolean;
|
|
1116
|
+
@Field() createdAt!: Date; @Field({ nullable: true }) lastUsedAt?: Date;
|
|
1117
|
+
}
|
|
1118
|
+
@ObjectType() class StepUpToken { @Field() stepUpToken!: string; @Field() expiresAt!: Date; }
|
|
1119
|
+
@InputType() class ReauthenticateInput {
|
|
1120
|
+
@Field({ nullable: true }) password?: string;
|
|
1121
|
+
@Field({ nullable: true }) emailCode?: string;
|
|
1122
|
+
@Field({ nullable: true }) passkeyChallengeId?: string;
|
|
1123
|
+
@Field({ nullable: true }) @MaxLength(16384) passkeyResponseJson?: string;
|
|
1124
|
+
}
|
|
1125
|
+
@InputType() class PasskeyRegistrationOptionsInput { @Field() stepUpToken!: string; }
|
|
1126
|
+
@InputType() class VerifyPasskeyInput {
|
|
1127
|
+
@Field() challengeId!: string;
|
|
1128
|
+
@Field() @MaxLength(16384) responseJson!: string;
|
|
1129
|
+
@Field({ nullable: true }) @MaxLength(64) name?: string;
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
@Resolver()
|
|
1133
|
+
export class AppAuthResolver extends BaseAuthResolver<User> {
|
|
1134
|
+
@Mutation(() => StepUpToken) @UseGuards(JwtAuthGuard)
|
|
1135
|
+
reauthenticate(@Args('input') input: ReauthenticateInput, @CurrentUser() user: User, @Context() ctx: any) {
|
|
1136
|
+
return this.performReauthenticate(user, input, ctx);
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
@Mutation(() => PasskeyOptions) @UseGuards(JwtAuthGuard)
|
|
1140
|
+
passkeyRegistrationOptions(@Args('input') input: PasskeyRegistrationOptionsInput, @CurrentUser() user: User, @Context() ctx: any) {
|
|
1141
|
+
return this.performPasskeyRegistrationOptions(user, input, ctx);
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
@Mutation(() => Passkey) @UseGuards(JwtAuthGuard)
|
|
1145
|
+
verifyPasskeyRegistration(@Args('input') input: VerifyPasskeyInput, @CurrentUser() user: User, @Context() ctx: any) {
|
|
1146
|
+
return this.performVerifyPasskeyRegistration(user, input, ctx);
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
@Mutation(() => PasskeyOptions)
|
|
1150
|
+
passkeyAuthenticationOptions(@Context() ctx: any) {
|
|
1151
|
+
return this.performPasskeyAuthenticationOptions(ctx);
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
@Mutation(() => AuthResponse)
|
|
1155
|
+
passkeyLogin(@Args('input') input: VerifyPasskeyInput, @Context() ctx: any) {
|
|
1156
|
+
return this.performVerifyPasskeyAuthentication(input, ctx) as Promise<AuthResponse>;
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
```
|
|
1160
|
+
|
|
1161
|
+
`PasskeyService` (exported) has the same operations if you need them outside a
|
|
1162
|
+
resolver.
|
|
1163
|
+
|
|
1164
|
+
#### What the package guarantees
|
|
1165
|
+
|
|
1166
|
+
| | |
|
|
1167
|
+
|---|---|
|
|
1168
|
+
| Phishing | Origin and rpId are verified against `passkey.rp`; an assertion made for another site fails |
|
|
1169
|
+
| Replay | Every challenge is single use, consumed atomically **before** anything is checked, and burned by a failed attempt. It carries its purpose, user and realm, so a sign-in challenge cannot be spent on registration or step-up |
|
|
1170
|
+
| Adding a passkey | Needs a [step-up token](#step-up-re-authentication). A passkey survives a password change and logout-all, so a stolen session must not be able to add one |
|
|
1171
|
+
| Realms | A passkey signs in only to the realm it was registered in, checked on the stored record whatever the repository does |
|
|
1172
|
+
| User handle | Must match the stored one on sign-in (the library does not check it) |
|
|
1173
|
+
| Cloned authenticators | A counting authenticator's counter must increase, enforced by the package with a compare-and-set; a regression is refused and logged as `PASSKEY_COUNTER_REGRESSION`. Synced passkeys always report 0, which is accepted |
|
|
1174
|
+
| Account status | Sessions come from `issueAuthSession`, so a suspended user is refused after a valid assertion, with `ACCOUNT_INACTIVE` rather than the generic failure |
|
|
1175
|
+
| Enumeration | Sign-in options take no input; every sign-in failure is the same `401 Passkey authentication failed` |
|
|
1176
|
+
| Duplicates | A credential id already registered to anyone is refused (409), leaving the existing one untouched |
|
|
1177
|
+
| Last way in | Removing the only passkey of an account with no password and no social identity is refused (409) |
|
|
1178
|
+
| Rate limiting | Options are limited per user (registration, step-up) and per IP (sign-in); each verification consumes a challenge, so it is bounded by them |
|
|
1179
|
+
|
|
1180
|
+
#### Platform setup
|
|
1181
|
+
|
|
1182
|
+
None of this is visible to a compiler or a test suite; each item fails as an
|
|
1183
|
+
opaque error on the device.
|
|
1184
|
+
|
|
1185
|
+
| Platform | Needs |
|
|
1186
|
+
|---|---|
|
|
1187
|
+
| Web | The frontend on HTTPS, on a host equal to `rp.id` or under it, listed exactly in `rp.origins` (scheme, host, port; no path, no trailing slash) |
|
|
1188
|
+
| Android | `https://<rp.id>/.well-known/assetlinks.json` granting `delegate_permission/common.get_login_creds` to your package name and SHA-256 certificate fingerprint, **and** `android:apk-key-hash:<hash>` in `rp.origins` |
|
|
1189
|
+
| iOS | `https://<rp.id>/.well-known/apple-app-site-association` listing your app under `webcredentials.apps`, and `webcredentials:<rp.id>` in the app's Associated Domains entitlement. iOS reports the web origin `https://<rp.id>`, which must be in `rp.origins` |
|
|
1190
|
+
|
|
1191
|
+
The Android origin is the **base64url** SHA-256 of the signing certificate,
|
|
1192
|
+
not the colon-separated hex `keytool` prints. Convert it:
|
|
1193
|
+
|
|
1194
|
+
```bash
|
|
1195
|
+
echo 'AB:CD:...:EF' | tr -d ':' | xxd -r -p | base64 | tr '+/' '-_' | tr -d '='
|
|
1196
|
+
```
|
|
1197
|
+
|
|
1198
|
+
With Play App Signing, use the fingerprint of Google's app signing key (Play
|
|
1199
|
+
Console → App integrity), not your upload key. Boot refuses an Android origin
|
|
1200
|
+
that is not 43 base64url characters.
|
|
1201
|
+
|
|
1202
|
+
**Several brands, one API:** every `rp` field can be a resolver taking the
|
|
1203
|
+
request's realm, e.g. `id: (realm) => brands[realm].domain`. Resolved values are
|
|
1204
|
+
checked per call, since boot cannot know every realm.
|
|
1205
|
+
|
|
1206
|
+
#### Implementing `IPasskeyRepository`
|
|
1207
|
+
|
|
1208
|
+
Two methods must be one conditional statement each, never a read followed by a
|
|
1209
|
+
write:
|
|
1210
|
+
|
|
1211
|
+
```typescript
|
|
1212
|
+
async consumeChallenge(challengeId: string) {
|
|
1213
|
+
const { count } = await prisma.passkeyChallenge.updateMany({
|
|
1214
|
+
where: { id: challengeId, used: false, expiresAt: { gt: new Date() } },
|
|
1215
|
+
data: { used: true, usedAt: new Date() },
|
|
1216
|
+
});
|
|
1217
|
+
if (count !== 1) return null;
|
|
1218
|
+
return this.loadChallenge(challengeId);
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
async updateCounter({ credentialId, expectedCounter, newCounter, backedUp, lastUsedAt }) {
|
|
1222
|
+
const { count } = await prisma.passkeyCredential.updateMany({
|
|
1223
|
+
where: { credentialId, counter: expectedCounter },
|
|
1224
|
+
data: { counter: newCounter, backedUp, lastUsedAt },
|
|
1225
|
+
});
|
|
1226
|
+
return count === 1;
|
|
1227
|
+
}
|
|
1228
|
+
```
|
|
1229
|
+
|
|
1230
|
+
And `createCredential` must throw `PasskeyCredentialExistsError` on a duplicate
|
|
1231
|
+
`credentialId` **for any user**, from a unique constraint (map Prisma's
|
|
1232
|
+
`P2002`). A reference schema:
|
|
1233
|
+
|
|
1234
|
+
```prisma
|
|
1235
|
+
model PasskeyCredential {
|
|
1236
|
+
credentialId String @id // base64url, unique across all users
|
|
1237
|
+
userId String
|
|
1238
|
+
realm String? // required when realms are enabled
|
|
1239
|
+
publicKey String // base64url COSE key
|
|
1240
|
+
counter BigInt @default(0) // uint32 on the wire; Int is too small
|
|
1241
|
+
userHandle String
|
|
1242
|
+
transports String[]
|
|
1243
|
+
deviceType String
|
|
1244
|
+
backedUp Boolean
|
|
1245
|
+
aaguid String?
|
|
1246
|
+
name String
|
|
1247
|
+
createdAt DateTime @default(now())
|
|
1248
|
+
lastUsedAt DateTime?
|
|
1249
|
+
isActive Boolean @default(true)
|
|
1250
|
+
metadata Json?
|
|
1251
|
+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
1252
|
+
@@index([userId, realm])
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
model PasskeyChallenge {
|
|
1256
|
+
id String @id @default(uuid())
|
|
1257
|
+
challenge String
|
|
1258
|
+
purpose String // registration | authentication | step-up
|
|
1259
|
+
userId String? // not a foreign key
|
|
1260
|
+
realm String?
|
|
1261
|
+
userHandle String? // registration only
|
|
1262
|
+
expiresAt DateTime
|
|
1263
|
+
used Boolean @default(false)
|
|
1264
|
+
usedAt DateTime?
|
|
1265
|
+
@@index([expiresAt])
|
|
1266
|
+
}
|
|
1267
|
+
```
|
|
1268
|
+
|
|
1269
|
+
`getCredential` returns deactivated credentials too; the package decides. Run
|
|
1270
|
+
the concurrency tests in `src/repositories/in-memory-passkey.repository.spec.ts`
|
|
1271
|
+
against your adapter. `InMemoryPasskeyRepository` is exported for tests and
|
|
1272
|
+
single-instance development.
|
|
1273
|
+
|
|
1274
|
+
### Step-Up Re-authentication
|
|
1275
|
+
|
|
1276
|
+
A signed-in user proves again who they are and receives a short-lived **step-up
|
|
1277
|
+
token** (5 minutes by default) for a protected action. Adding a passkey requires
|
|
1278
|
+
one.
|
|
1279
|
+
|
|
1280
|
+
| Proof | Available when |
|
|
1281
|
+
|---|---|
|
|
1282
|
+
| `{ password }` | The account has a password. **Failures count toward [lockout](#brute-force-protection)**, so a stolen session cannot guess it freely |
|
|
1283
|
+
| `{ passkeyChallengeId, passkeyResponseJson }` | Passkeys are configured and the user has one (options from `performStepUpPasskeyOptions`) |
|
|
1284
|
+
| `{ emailCode }` | `verificationRepositoryInstance` is set and the email service implements `sendStepUpCodeEmail` (`ConfigurableEmailService` does). Always a 6-digit code, never a link; one email a minute |
|
|
1285
|
+
|
|
1286
|
+
`performGetStepUpMethods` says which apply, so the client shows only working
|
|
1287
|
+
buttons. Failures are `401 Re-authentication failed`; an action that needs a
|
|
1288
|
+
token answers `STEP_UP_REQUIRED` (403), and a method the account lacks
|
|
1289
|
+
`STEP_UP_UNAVAILABLE` (403).
|
|
1290
|
+
|
|
1291
|
+
The token is signed with a key derived from `jwtSecret` (HKDF), never with
|
|
1292
|
+
`jwtSecret` itself, so it can never pass as an access token. It is bound to the
|
|
1293
|
+
user and realm.
|
|
1294
|
+
|
|
1295
|
+
**Email codes use a new verification type, `step-up`.** If your verification
|
|
1296
|
+
repository stores the type in a database enum, add the value.
|
|
1297
|
+
|
|
1015
1298
|
### Brute Force Protection
|
|
1016
1299
|
|
|
1017
1300
|
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.
|
|
@@ -1228,10 +1511,12 @@ lifecycleHooksInstance: {
|
|
|
1228
1511
|
onAccountLocked(userId, email, ip, lockSeconds, realm) {
|
|
1229
1512
|
/* send account-locked notification email, alert security tooling */
|
|
1230
1513
|
},
|
|
1514
|
+
async onPasskeyRegistered(user, passkey, realm) { /* email "a passkey was added" */ },
|
|
1515
|
+
async onPasskeyRemoved(user, passkey, realm) { /* email "a passkey was removed" */ },
|
|
1231
1516
|
}
|
|
1232
1517
|
```
|
|
1233
1518
|
|
|
1234
|
-
All hooks are optional. Async hooks are awaited but failures are logged and do not block the auth operation. `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.
|
|
1519
|
+
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, so a slow mail send does not hold up the response. **Wire `onPasskeyRegistered` to a notification email**: it is how a user learns of a passkey they did not add. `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.
|
|
1235
1520
|
|
|
1236
1521
|
`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.
|
|
1237
1522
|
|
|
@@ -1460,6 +1745,28 @@ without it is reported at boot, not refused.
|
|
|
1460
1745
|
| `challengeExpirySeconds` | `number` | `60` | Short on purpose: it bounds replay |
|
|
1461
1746
|
| `challengeRateLimit` | `{ maxAttempts, windowMs }` | `5 / 60s` | Per credential. **Requires `rateLimiterInstance`** — the same guardrail as `bruteForce.ipRateLimit`, because per-replica counters would make the limit silently looser. A per-IP limit at ten times this bound also applies when you pass `ipAddress` |
|
|
1462
1747
|
|
|
1748
|
+
### Passkey Options (`passkey`)
|
|
1749
|
+
|
|
1750
|
+
Read only when `passkeyRepositoryInstance` is provided; without it this block is
|
|
1751
|
+
reported at boot as ignored. With it, `rp` is required.
|
|
1752
|
+
|
|
1753
|
+
| Option | Type | Default | Description |
|
|
1754
|
+
|--------|------|---------|-------------|
|
|
1755
|
+
| `rp.id` | `string \| (realm?) => string` | — | The registrable domain passkeys are bound to. No scheme, port or path; not an IP |
|
|
1756
|
+
| `rp.name` | `string \| (realm?) => string` | — | Shown by some authenticators |
|
|
1757
|
+
| `rp.origins` | `string[] \| (realm?) => string[]` | — | Exact origins accepted, each `rp.id` or a subdomain of it, https except localhost; Android `android:apk-key-hash:` origins |
|
|
1758
|
+
| `userVerification` | `'required' \| 'preferred' \| 'discouraged'` | `'required'` | See [What a passkey sign-in proves](#what-a-passkey-sign-in-proves) |
|
|
1759
|
+
| `registrationTimeoutSeconds` | `number` | `300` | Registration ceremony lifetime |
|
|
1760
|
+
| `authenticationTimeoutSeconds` | `number` | `120` | Sign-in and step-up ceremony lifetime |
|
|
1761
|
+
| `rateLimit` | `{ maxAttempts, windowMs }` | `10 / 60s` | Options per user (registration, step-up) and per IP (sign-in). **Requires `rateLimiterInstance`** when set |
|
|
1762
|
+
| `userDisplayName` | `(user) => string` | the email | What authenticators show for the account |
|
|
1763
|
+
|
|
1764
|
+
### Step-Up Options (`stepUp`)
|
|
1765
|
+
|
|
1766
|
+
| Option | Type | Default | Description |
|
|
1767
|
+
|--------|------|---------|-------------|
|
|
1768
|
+
| `ttlSeconds` | `number` | `300` | Step-up token lifetime, 1 to 3600 |
|
|
1769
|
+
|
|
1463
1770
|
### Optional Instance Options
|
|
1464
1771
|
|
|
1465
1772
|
| Option | Interface | Fallback |
|
|
@@ -1469,7 +1776,8 @@ without it is reported at boot, not refused.
|
|
|
1469
1776
|
| `lifecycleHooksInstance` | `IAuthLifecycleHooks` | `{}` (no-op) |
|
|
1470
1777
|
| `verificationRepositoryInstance` | `IVerificationRepository` | `NoOpVerificationRepository` |
|
|
1471
1778
|
| `bruteForceRepositoryInstance` | `IBruteForceRepository` | `NoOpBruteForceRepository` |
|
|
1472
|
-
| `biometricRepositoryInstance` | `IBiometricRepository` |
|
|
1779
|
+
| `biometricRepositoryInstance` | `IBiometricRepository` | none: biometrics disabled |
|
|
1780
|
+
| `passkeyRepositoryInstance` | `IPasskeyRepository` | none: passkeys disabled |
|
|
1473
1781
|
| `authLoggerInstance` | `IAuthLogger` | `ConsoleAuthLogger` |
|
|
1474
1782
|
| `tenantRepositoryInstance` | `ITenantRepository` | `null` |
|
|
1475
1783
|
| `tenantExtractorInstance` | `ITenantExtractor` | `null` |
|
package/dist/auth.module.d.ts
CHANGED
|
@@ -9,6 +9,8 @@ import { IRefreshTokenRepository } from './interfaces/refresh-token-repository.i
|
|
|
9
9
|
import { IVerificationRepository } from './interfaces/verification-repository.interface';
|
|
10
10
|
import { IBruteForceRepository } from './interfaces/brute-force-repository.interface';
|
|
11
11
|
import { IBiometricRepository } from './interfaces/biometric-repository.interface';
|
|
12
|
+
import { IPasskeyRepository } from './interfaces/passkey-repository.interface';
|
|
13
|
+
import { PasskeyRelyingPartyConfig } from './utils/passkey-rp';
|
|
12
14
|
import { IAuthLogger } from './interfaces/auth-logger.interface';
|
|
13
15
|
import { ITenantRepository } from './interfaces/tenant-repository.interface';
|
|
14
16
|
import { ITenantExtractor } from './interfaces/tenant-extractor.interface';
|
|
@@ -32,6 +34,7 @@ export interface AuthModuleOptions {
|
|
|
32
34
|
verificationRepositoryInstance?: IVerificationRepository;
|
|
33
35
|
bruteForceRepositoryInstance?: IBruteForceRepository;
|
|
34
36
|
biometricRepositoryInstance?: IBiometricRepository;
|
|
37
|
+
passkeyRepositoryInstance?: IPasskeyRepository;
|
|
35
38
|
authLoggerInstance?: IAuthLogger;
|
|
36
39
|
refreshTokenSecret?: string;
|
|
37
40
|
verificationCodeSecret?: string;
|
|
@@ -130,6 +133,20 @@ export interface AuthModuleOptions {
|
|
|
130
133
|
windowMs: number;
|
|
131
134
|
};
|
|
132
135
|
};
|
|
136
|
+
passkey?: {
|
|
137
|
+
rp: PasskeyRelyingPartyConfig;
|
|
138
|
+
userVerification?: 'required' | 'preferred' | 'discouraged';
|
|
139
|
+
registrationTimeoutSeconds?: number;
|
|
140
|
+
authenticationTimeoutSeconds?: number;
|
|
141
|
+
rateLimit?: {
|
|
142
|
+
maxAttempts: number;
|
|
143
|
+
windowMs: number;
|
|
144
|
+
};
|
|
145
|
+
userDisplayName?: (user: IAuthUser) => string;
|
|
146
|
+
};
|
|
147
|
+
stepUp?: {
|
|
148
|
+
ttlSeconds?: number;
|
|
149
|
+
};
|
|
133
150
|
verification?: {
|
|
134
151
|
tokenLength?: number;
|
|
135
152
|
tokenExpiresInMinutes?: number;
|
|
@@ -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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"}
|