@warlock.js/auth 5.29.0 → 5.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +23 -0
- package/esm/commands/auth-cleanup-command.d.mts +1 -1
- package/esm/commands/auth-cleanup-command.d.mts.map +1 -1
- package/esm/commands/auth-cleanup-command.mjs +1 -1
- package/esm/commands/auth-cleanup-command.mjs.map +1 -1
- package/esm/commands/auth-purge-never-expiring-command.d.mts +1 -1
- package/esm/commands/auth-purge-never-expiring-command.d.mts.map +1 -1
- package/esm/commands/auth-purge-never-expiring-command.mjs +1 -1
- package/esm/commands/auth-purge-never-expiring-command.mjs.map +1 -1
- package/esm/commands/jwt-secret-generator-command.d.mts +1 -1
- package/esm/commands/jwt-secret-generator-command.d.mts.map +1 -1
- package/esm/commands/jwt-secret-generator-command.mjs +1 -1
- package/esm/commands/jwt-secret-generator-command.mjs.map +1 -1
- package/esm/contracts/auth-contract.d.mts +1 -1
- package/esm/contracts/auth-contract.d.mts.map +1 -1
- package/esm/contracts/providers.d.mts +1 -1
- package/esm/contracts/providers.d.mts.map +1 -1
- package/esm/contracts/request-locals.d.mts +1 -1
- package/esm/contracts/request-locals.d.mts.map +1 -1
- package/esm/contracts/types.d.mts +1 -1
- package/esm/contracts/types.d.mts.map +1 -1
- package/esm/contracts/types.mjs +1 -1
- package/esm/contracts/types.mjs.map +1 -1
- package/esm/errors/auth-provider-sdk-missing.error.d.mts +1 -1
- package/esm/errors/auth-provider-sdk-missing.error.d.mts.map +1 -1
- package/esm/errors/auth-provider-sdk-missing.error.mjs +1 -1
- package/esm/errors/auth-provider-sdk-missing.error.mjs.map +1 -1
- package/esm/errors/email-not-verified.error.d.mts +1 -1
- package/esm/errors/email-not-verified.error.d.mts.map +1 -1
- package/esm/errors/email-not-verified.error.mjs +1 -1
- package/esm/errors/email-not-verified.error.mjs.map +1 -1
- package/esm/errors/invalid-one-time-token.error.d.mts +1 -1
- package/esm/errors/invalid-one-time-token.error.d.mts.map +1 -1
- package/esm/errors/invalid-one-time-token.error.mjs +1 -1
- package/esm/errors/invalid-one-time-token.error.mjs.map +1 -1
- package/esm/errors/invalid-passkey.error.d.mts +1 -1
- package/esm/errors/invalid-passkey.error.d.mts.map +1 -1
- package/esm/errors/invalid-passkey.error.mjs +1 -1
- package/esm/errors/invalid-passkey.error.mjs.map +1 -1
- package/esm/errors/invalid-provider-callback.error.d.mts +1 -1
- package/esm/errors/invalid-provider-callback.error.d.mts.map +1 -1
- package/esm/errors/invalid-provider-callback.error.mjs +1 -1
- package/esm/errors/invalid-provider-callback.error.mjs.map +1 -1
- package/esm/errors/notifications-unavailable.error.d.mts +1 -1
- package/esm/errors/notifications-unavailable.error.d.mts.map +1 -1
- package/esm/errors/notifications-unavailable.error.mjs +1 -1
- package/esm/errors/notifications-unavailable.error.mjs.map +1 -1
- package/esm/errors/provider-email-not-verified.error.d.mts +1 -1
- package/esm/errors/provider-email-not-verified.error.d.mts.map +1 -1
- package/esm/errors/provider-email-not-verified.error.mjs +1 -1
- package/esm/errors/provider-email-not-verified.error.mjs.map +1 -1
- package/esm/index.d.mts +2 -2
- package/esm/index.mjs +2 -2
- package/esm/localization/ar.translations.mjs +1 -1
- package/esm/localization/ar.translations.mjs.map +1 -1
- package/esm/localization/auth-default-translations.d.mts +1 -1
- package/esm/localization/auth-default-translations.d.mts.map +1 -1
- package/esm/localization/auth-default-translations.mjs +1 -1
- package/esm/localization/auth-default-translations.mjs.map +1 -1
- package/esm/localization/auth-translations.type.d.mts +1 -1
- package/esm/localization/auth-translations.type.d.mts.map +1 -1
- package/esm/localization/en.translations.mjs +1 -1
- package/esm/localization/en.translations.mjs.map +1 -1
- package/esm/localization/init.mjs +1 -1
- package/esm/localization/init.mjs.map +1 -1
- package/esm/localization/register-auth-translations.d.mts +1 -1
- package/esm/localization/register-auth-translations.d.mts.map +1 -1
- package/esm/localization/register-auth-translations.mjs +1 -1
- package/esm/localization/register-auth-translations.mjs.map +1 -1
- package/esm/middleware/auth.middleware.d.mts +1 -1
- package/esm/middleware/auth.middleware.d.mts.map +1 -1
- package/esm/middleware/auth.middleware.mjs +3 -3
- package/esm/middleware/auth.middleware.mjs.map +1 -1
- package/esm/middleware/csrf-origin-check.d.mts +1 -1
- package/esm/middleware/csrf-origin-check.d.mts.map +1 -1
- package/esm/middleware/csrf-origin-check.mjs +1 -1
- package/esm/middleware/csrf-origin-check.mjs.map +1 -1
- package/esm/middleware/index.d.mts +1 -1
- package/esm/middleware/index.mjs +1 -1
- package/esm/middleware/localized-login-path.mjs +1 -1
- package/esm/middleware/localized-login-path.mjs.map +1 -1
- package/esm/middleware/login-throttle.middleware.d.mts +17 -2
- package/esm/middleware/login-throttle.middleware.d.mts.map +1 -1
- package/esm/middleware/login-throttle.middleware.mjs +84 -6
- package/esm/middleware/login-throttle.middleware.mjs.map +1 -1
- package/esm/middleware/one-time-token-throttle.middleware.d.mts +1 -1
- package/esm/middleware/one-time-token-throttle.middleware.d.mts.map +1 -1
- package/esm/middleware/one-time-token-throttle.middleware.mjs +1 -1
- package/esm/middleware/one-time-token-throttle.middleware.mjs.map +1 -1
- package/esm/middleware/otp-throttle.middleware.d.mts +1 -1
- package/esm/middleware/otp-throttle.middleware.d.mts.map +1 -1
- package/esm/middleware/otp-throttle.middleware.mjs +1 -1
- package/esm/middleware/otp-throttle.middleware.mjs.map +1 -1
- package/esm/middleware/require-verified-email.middleware.d.mts +1 -1
- package/esm/middleware/require-verified-email.middleware.d.mts.map +1 -1
- package/esm/middleware/require-verified-email.middleware.mjs +1 -1
- package/esm/middleware/require-verified-email.middleware.mjs.map +1 -1
- package/esm/models/access-token/access-token.model.d.mts +3 -35
- package/esm/models/access-token/access-token.model.d.mts.map +1 -1
- package/esm/models/access-token/access-token.model.mjs +1 -1
- package/esm/models/access-token/access-token.model.mjs.map +1 -1
- package/esm/models/access-token/family-association.migration.mjs +1 -1
- package/esm/models/access-token/family-association.migration.mjs.map +1 -1
- package/esm/models/access-token/migration.mjs +1 -1
- package/esm/models/access-token/migration.mjs.map +1 -1
- package/esm/models/auth-token-family/auth-token-family.model.d.mts +3 -43
- package/esm/models/auth-token-family/auth-token-family.model.d.mts.map +1 -1
- package/esm/models/auth-token-family/auth-token-family.model.mjs +1 -1
- package/esm/models/auth-token-family/auth-token-family.model.mjs.map +1 -1
- package/esm/models/auth-token-family/migration.mjs +1 -1
- package/esm/models/auth-token-family/migration.mjs.map +1 -1
- package/esm/models/auth.model.d.mts +1 -1
- package/esm/models/auth.model.d.mts.map +1 -1
- package/esm/models/auth.model.mjs +1 -1
- package/esm/models/auth.model.mjs.map +1 -1
- package/esm/models/index.d.mts +1 -1
- package/esm/models/index.d.mts.map +1 -1
- package/esm/models/index.mjs +1 -1
- package/esm/models/index.mjs.map +1 -1
- package/esm/models/one-time-token/migration.mjs +1 -1
- package/esm/models/one-time-token/migration.mjs.map +1 -1
- package/esm/models/one-time-token/one-time-token.model.d.mts +3 -67
- package/esm/models/one-time-token/one-time-token.model.d.mts.map +1 -1
- package/esm/models/one-time-token/one-time-token.model.mjs +1 -1
- package/esm/models/one-time-token/one-time-token.model.mjs.map +1 -1
- package/esm/models/passkey-credential/migration.mjs +1 -1
- package/esm/models/passkey-credential/migration.mjs.map +1 -1
- package/esm/models/passkey-credential/passkey-credential.model.d.mts +3 -47
- package/esm/models/passkey-credential/passkey-credential.model.d.mts.map +1 -1
- package/esm/models/passkey-credential/passkey-credential.model.mjs +1 -1
- package/esm/models/passkey-credential/passkey-credential.model.mjs.map +1 -1
- package/esm/models/provider-account/migration.mjs +1 -1
- package/esm/models/provider-account/migration.mjs.map +1 -1
- package/esm/models/provider-account/provider-account.model.d.mts +3 -35
- package/esm/models/provider-account/provider-account.model.d.mts.map +1 -1
- package/esm/models/provider-account/provider-account.model.mjs +1 -1
- package/esm/models/provider-account/provider-account.model.mjs.map +1 -1
- package/esm/models/refresh-token/migration.mjs +1 -1
- package/esm/models/refresh-token/migration.mjs.map +1 -1
- package/esm/models/refresh-token/refresh-token.model.d.mts +7 -99
- package/esm/models/refresh-token/refresh-token.model.d.mts.map +1 -1
- package/esm/models/refresh-token/refresh-token.model.mjs +1 -1
- package/esm/models/refresh-token/refresh-token.model.mjs.map +1 -1
- package/esm/models/refresh-token/successor-association.migration.mjs +1 -1
- package/esm/models/refresh-token/successor-association.migration.mjs.map +1 -1
- package/esm/otp/otp.d.mts +1 -1
- package/esm/otp/otp.d.mts.map +1 -1
- package/esm/otp/otp.mjs +1 -1
- package/esm/otp/otp.mjs.map +1 -1
- package/esm/passkeys/passkey-authentication.d.mts +1 -1
- package/esm/passkeys/passkey-authentication.d.mts.map +1 -1
- package/esm/passkeys/passkey-authentication.mjs +1 -1
- package/esm/passkeys/passkey-authentication.mjs.map +1 -1
- package/esm/passkeys/passkey-challenges.mjs +1 -1
- package/esm/passkeys/passkey-challenges.mjs.map +1 -1
- package/esm/passkeys/passkey-registration.d.mts +1 -1
- package/esm/passkeys/passkey-registration.d.mts.map +1 -1
- package/esm/passkeys/passkey-registration.mjs +1 -1
- package/esm/passkeys/passkey-registration.mjs.map +1 -1
- package/esm/passkeys/simplewebauthn.d.mts +1 -1
- package/esm/passkeys/simplewebauthn.d.mts.map +1 -1
- package/esm/passkeys/simplewebauthn.mjs +1 -1
- package/esm/passkeys/simplewebauthn.mjs.map +1 -1
- package/esm/providers/apple-provider.d.mts +1 -1
- package/esm/providers/apple-provider.d.mts.map +1 -1
- package/esm/providers/apple-provider.mjs +1 -1
- package/esm/providers/apple-provider.mjs.map +1 -1
- package/esm/providers/discord-provider.d.mts +1 -1
- package/esm/providers/discord-provider.d.mts.map +1 -1
- package/esm/providers/discord-provider.mjs +1 -1
- package/esm/providers/discord-provider.mjs.map +1 -1
- package/esm/providers/facebook-provider.d.mts +1 -1
- package/esm/providers/facebook-provider.d.mts.map +1 -1
- package/esm/providers/facebook-provider.mjs +1 -1
- package/esm/providers/facebook-provider.mjs.map +1 -1
- package/esm/providers/github-provider.d.mts +1 -1
- package/esm/providers/github-provider.d.mts.map +1 -1
- package/esm/providers/github-provider.mjs +1 -1
- package/esm/providers/github-provider.mjs.map +1 -1
- package/esm/providers/google-provider.d.mts +1 -1
- package/esm/providers/google-provider.d.mts.map +1 -1
- package/esm/providers/google-provider.mjs +1 -1
- package/esm/providers/google-provider.mjs.map +1 -1
- package/esm/providers/linkedin-provider.d.mts +1 -1
- package/esm/providers/linkedin-provider.d.mts.map +1 -1
- package/esm/providers/linkedin-provider.mjs +1 -1
- package/esm/providers/linkedin-provider.mjs.map +1 -1
- package/esm/providers/provider-login.d.mts +1 -1
- package/esm/providers/provider-login.d.mts.map +1 -1
- package/esm/providers/provider-login.mjs +1 -1
- package/esm/providers/provider-login.mjs.map +1 -1
- package/esm/providers/provider-state-cookie.mjs +1 -1
- package/esm/providers/provider-state-cookie.mjs.map +1 -1
- package/esm/providers/resolve-provider-user.mjs +1 -1
- package/esm/providers/resolve-provider-user.mjs.map +1 -1
- package/esm/providers/x-provider.d.mts +1 -1
- package/esm/providers/x-provider.d.mts.map +1 -1
- package/esm/providers/x-provider.mjs +1 -1
- package/esm/providers/x-provider.mjs.map +1 -1
- package/esm/services/auth-config.mjs +1 -1
- package/esm/services/auth-config.mjs.map +1 -1
- package/esm/services/auth-events.d.mts +1 -1
- package/esm/services/auth-events.d.mts.map +1 -1
- package/esm/services/auth-events.mjs +1 -1
- package/esm/services/auth-events.mjs.map +1 -1
- package/esm/services/auth.service.d.mts +1 -1
- package/esm/services/auth.service.d.mts.map +1 -1
- package/esm/services/auth.service.mjs +1 -1
- package/esm/services/auth.service.mjs.map +1 -1
- package/esm/services/current-user.d.mts +1 -1
- package/esm/services/current-user.d.mts.map +1 -1
- package/esm/services/current-user.mjs +1 -1
- package/esm/services/current-user.mjs.map +1 -1
- package/esm/services/email-verification.d.mts +1 -1
- package/esm/services/email-verification.d.mts.map +1 -1
- package/esm/services/email-verification.mjs +1 -1
- package/esm/services/email-verification.mjs.map +1 -1
- package/esm/services/generate-jwt-secret.d.mts +1 -1
- package/esm/services/generate-jwt-secret.d.mts.map +1 -1
- package/esm/services/generate-jwt-secret.mjs +1 -1
- package/esm/services/generate-jwt-secret.mjs.map +1 -1
- package/esm/services/jwt.d.mts +1 -1
- package/esm/services/jwt.d.mts.map +1 -1
- package/esm/services/jwt.mjs +1 -1
- package/esm/services/jwt.mjs.map +1 -1
- package/esm/services/one-time-tokens.d.mts +1 -1
- package/esm/services/one-time-tokens.d.mts.map +1 -1
- package/esm/services/one-time-tokens.mjs +1 -1
- package/esm/services/one-time-tokens.mjs.map +1 -1
- package/esm/services/optional-peer.mjs +1 -1
- package/esm/services/optional-peer.mjs.map +1 -1
- package/esm/services/password-reset.d.mts +1 -1
- package/esm/services/password-reset.d.mts.map +1 -1
- package/esm/services/password-reset.mjs +1 -1
- package/esm/services/password-reset.mjs.map +1 -1
- package/esm/services/resolve-request-user.d.mts +1 -1
- package/esm/services/resolve-request-user.d.mts.map +1 -1
- package/esm/services/resolve-request-user.mjs +1 -1
- package/esm/services/resolve-request-user.mjs.map +1 -1
- package/esm/services/token-family-operation.mjs +1 -1
- package/esm/services/token-family-operation.mjs.map +1 -1
- package/esm/services/token-notifications.mjs +1 -1
- package/esm/services/token-notifications.mjs.map +1 -1
- package/esm/session/page-session.d.mts +1 -1
- package/esm/session/page-session.d.mts.map +1 -1
- package/esm/session/page-session.mjs +1 -1
- package/esm/session/page-session.mjs.map +1 -1
- package/esm/utils/auth-error-codes.d.mts +1 -1
- package/esm/utils/auth-error-codes.d.mts.map +1 -1
- package/esm/utils/auth-error-codes.mjs +1 -1
- package/esm/utils/auth-error-codes.mjs.map +1 -1
- package/esm/utils/safe-equal.d.mts +1 -1
- package/esm/utils/safe-equal.d.mts.map +1 -1
- package/esm/utils/safe-equal.mjs +1 -1
- package/esm/utils/safe-equal.mjs.map +1 -1
- package/esm/utils/token-expiry.d.mts +1 -1
- package/esm/utils/token-expiry.d.mts.map +1 -1
- package/esm/utils/token-expiry.mjs +1 -1
- package/esm/utils/token-expiry.mjs.map +1 -1
- package/llms-full.txt +192 -112
- package/package.json +10 -9
- package/skills/auth-basics/SKILL.md +10 -13
- package/skills/customize-token-storage/SKILL.md +24 -19
- package/skills/customize-user-type/SKILL.md +3 -1
- package/skills/index.md +4 -1
- package/skills/login-with-providers/SKILL.md +54 -29
- package/skills/manage-tokens/SKILL.md +2 -2
- package/skills/overview/SKILL.md +10 -1
- package/skills/protect-routes/SKILL.md +23 -14
- package/skills/register-user/SKILL.md +4 -4
- package/skills/run-auth-commands/SKILL.md +1 -1
- package/skills/throttle-login-attempts/SKILL.md +54 -23
- package/skills/verify-email-and-reset-password/SKILL.md +7 -5
package/package.json
CHANGED
|
@@ -12,13 +12,13 @@
|
|
|
12
12
|
"ms": "^2.1.3"
|
|
13
13
|
},
|
|
14
14
|
"peerDependencies": {
|
|
15
|
-
"@warlock.js/fs": "5.
|
|
16
|
-
"@warlock.js/cache": "5.
|
|
17
|
-
"@warlock.js/cascade": "5.
|
|
18
|
-
"@warlock.js/core": "5.
|
|
19
|
-
"@warlock.js/logger": "5.
|
|
20
|
-
"@warlock.js/seal": "5.
|
|
21
|
-
"@warlock.js/notifications": "5.
|
|
15
|
+
"@warlock.js/fs": "5.31.0",
|
|
16
|
+
"@warlock.js/cache": "5.31.0",
|
|
17
|
+
"@warlock.js/cascade": "5.31.0",
|
|
18
|
+
"@warlock.js/core": "5.31.0",
|
|
19
|
+
"@warlock.js/logger": "5.31.0",
|
|
20
|
+
"@warlock.js/seal": "5.31.0",
|
|
21
|
+
"@warlock.js/notifications": "5.31.0",
|
|
22
22
|
"@simplewebauthn/server": "^13.1.0",
|
|
23
23
|
"jose": "^6.1.0"
|
|
24
24
|
},
|
|
@@ -35,7 +35,8 @@
|
|
|
35
35
|
},
|
|
36
36
|
"repository": {
|
|
37
37
|
"type": "git",
|
|
38
|
-
"url": "https://github.com/warlockjs/
|
|
38
|
+
"url": "git+https://github.com/warlockjs/framework.git",
|
|
39
|
+
"directory": "packages/auth"
|
|
39
40
|
},
|
|
40
41
|
"keywords": [
|
|
41
42
|
"nodejs",
|
|
@@ -47,7 +48,7 @@
|
|
|
47
48
|
],
|
|
48
49
|
"author": "hassanzohdy",
|
|
49
50
|
"license": "MIT",
|
|
50
|
-
"version": "5.
|
|
51
|
+
"version": "5.31.0",
|
|
51
52
|
"type": "module",
|
|
52
53
|
"main": "./esm/index.mjs",
|
|
53
54
|
"module": "./esm/index.mjs",
|
|
@@ -51,10 +51,7 @@ import { defineConfig } from "@warlock.js/core";
|
|
|
51
51
|
|
|
52
52
|
export default defineConfig({
|
|
53
53
|
cli: {
|
|
54
|
-
commands: [
|
|
55
|
-
registerJWTSecretGeneratorCommand(),
|
|
56
|
-
registerAuthCleanupCommand(),
|
|
57
|
-
],
|
|
54
|
+
commands: [registerJWTSecretGeneratorCommand(), registerAuthCleanupCommand()],
|
|
58
55
|
},
|
|
59
56
|
database: {
|
|
60
57
|
migrations: authMigrations,
|
|
@@ -98,19 +95,19 @@ That is deliberate. `ms` answers `undefined` for a string it can't parse (`"30da
|
|
|
98
95
|
|
|
99
96
|
Two values that look right and are not:
|
|
100
97
|
|
|
101
|
-
- `expiresIn: 2592000` — a bare number. `ms`
|
|
98
|
+
- `expiresIn: 2592000` — a bare number. `ms` _formats_ numbers instead of parsing them, so this is rejected; write `"30d"`.
|
|
102
99
|
- `expiresIn: env("JWT_TTL")` where the variable is unset — resolves to `""` and is rejected. Give the env read a default: `env("JWT_TTL") || "1h"`.
|
|
103
100
|
|
|
104
101
|
## Pick a skill
|
|
105
102
|
|
|
106
|
-
| If the task is about…
|
|
107
|
-
|
|
|
108
|
-
| Gating routes with `authMiddleware(allowedUserType)`, any-authenticated vs typed access
|
|
109
|
-
| `authService.login(...)`, `attemptLogin`, full credentials-to-tokens flow + logout
|
|
110
|
-
| Token lifecycle — `generateAccessToken`, `createRefreshToken`, rotation, family revocation, max-per-user | The `manage-tokens` topic
|
|
111
|
-
| Register a new user + issue tokens in one flow
|
|
112
|
-
| Multi-user-type apps (`user`, `admin`, `client`), `config.auth.userType.<name>` mapping
|
|
113
|
-
| `warlock jwt.generate` + `warlock auth.cleanup` CLI commands
|
|
103
|
+
| If the task is about… | Load |
|
|
104
|
+
| -------------------------------------------------------------------------------------------------------- | ----------------------------------- |
|
|
105
|
+
| Gating routes with `authMiddleware(allowedUserType)`, any-authenticated vs typed access | The `protect-routes` topic |
|
|
106
|
+
| `authService.login(...)`, `attemptLogin`, full credentials-to-tokens flow + logout | The `handle-login-and-logout` topic |
|
|
107
|
+
| Token lifecycle — `generateAccessToken`, `createRefreshToken`, rotation, family revocation, max-per-user | The `manage-tokens` topic |
|
|
108
|
+
| Register a new user + issue tokens in one flow | The `register-user` topic |
|
|
109
|
+
| Multi-user-type apps (`user`, `admin`, `client`), `config.auth.userType.<name>` mapping | The `customize-user-type` topic |
|
|
110
|
+
| `warlock jwt.generate` + `warlock auth.cleanup` CLI commands | The `run-auth-commands` topic |
|
|
114
111
|
|
|
115
112
|
## Things NOT to do
|
|
116
113
|
|
|
@@ -5,7 +5,7 @@ description: 'Override the persisted AccessToken / RefreshToken models to add co
|
|
|
5
5
|
|
|
6
6
|
# Customize token storage
|
|
7
7
|
|
|
8
|
-
The package ships `AccessToken` and `RefreshToken` models and runs the whole token flow through their **named statics** — `issue`, `findByToken`, `activeFor`, `revokeAllFor`, etc. The auth service never references a column name directly; it resolves the
|
|
8
|
+
The package ships `AccessToken` and `RefreshToken` models and runs the whole token flow through their **named statics** — `issue`, `findByToken`, `activeFor`, `revokeAllFor`, etc. The auth service never references a column name directly; it resolves the _active_ model from config. So you change token storage by **registering a subclass**, not by forking.
|
|
9
9
|
|
|
10
10
|
Reach for this when you need an extra column on the token tables — the common case is a tenant key (`organization_id`) so tokens are partitioned per organization, or richer device metadata.
|
|
11
11
|
|
|
@@ -56,7 +56,12 @@ export class AppAccessToken extends AccessToken {
|
|
|
56
56
|
Refresh tokens follow the same shape — extend `refreshTokenSchema`, and override `issue(user, token, options)` (`options` is `{ familyId, expiresAt, deviceInfo? }`) the same way, copying the base fields plus your column.
|
|
57
57
|
|
|
58
58
|
```ts title="src/app/auth/models/app-refresh-token.ts"
|
|
59
|
-
import {
|
|
59
|
+
import {
|
|
60
|
+
RefreshToken,
|
|
61
|
+
refreshTokenSchema,
|
|
62
|
+
type Auth,
|
|
63
|
+
type RefreshTokenIssueOptions,
|
|
64
|
+
} from "@warlock.js/auth";
|
|
60
65
|
import { v } from "@warlock.js/seal";
|
|
61
66
|
|
|
62
67
|
export class AppRefreshToken extends RefreshToken {
|
|
@@ -88,12 +93,12 @@ import { AppRefreshToken } from "app/auth/models/app-refresh-token";
|
|
|
88
93
|
export default {
|
|
89
94
|
userType: { user: User },
|
|
90
95
|
accessToken: {
|
|
91
|
-
model: AppAccessToken,
|
|
96
|
+
model: AppAccessToken, // ← the override
|
|
92
97
|
secret: env("JWT_SECRET"),
|
|
93
98
|
expiresIn: "1h",
|
|
94
99
|
},
|
|
95
100
|
refreshToken: {
|
|
96
|
-
model: AppRefreshToken,
|
|
101
|
+
model: AppRefreshToken, // ← the override
|
|
97
102
|
secret: env("JWT_REFRESH_SECRET"),
|
|
98
103
|
expiresIn: "30d",
|
|
99
104
|
},
|
|
@@ -114,7 +119,7 @@ See the `write-migration` topic of the `warlock-js-cascade` skill for the migrat
|
|
|
114
119
|
|
|
115
120
|
## Why all three — the strict-mode trap
|
|
116
121
|
|
|
117
|
-
This is the failure people hit. Cascade's `strictMode: "strip"` **drops any field your schema doesn't declare** before the INSERT. So if you set `organization_id` in `issue()` but don't add it to the schema (step 1), the value is silently stripped, and a NOT-NULL `organization_id` column then **fails the INSERT**. The chain is: `issue()` sets it → the
|
|
122
|
+
This is the failure people hit. Cascade's `strictMode: "strip"` **drops any field your schema doesn't declare** before the INSERT. So if you set `organization_id` in `issue()` but don't add it to the schema (step 1), the value is silently stripped, and a NOT-NULL `organization_id` column then **fails the INSERT**. The chain is: `issue()` sets it → the _schema_ must declare it so it survives → the _migration_ must create the column. All three, or nothing.
|
|
118
123
|
|
|
119
124
|
## You do NOT need read-side scoping
|
|
120
125
|
|
|
@@ -124,23 +129,23 @@ Token lookups go by the unique `token` string (`findByToken`), which already uni
|
|
|
124
129
|
|
|
125
130
|
Your subclass inherits and may override these statics — the service calls them, never raw queries:
|
|
126
131
|
|
|
127
|
-
| Static
|
|
128
|
-
|
|
|
129
|
-
| `issue(user, token, …)`
|
|
130
|
-
| `findByToken(token)`
|
|
131
|
-
| `findForUser(user, token)`
|
|
132
|
-
| `activeFor(user)`
|
|
133
|
-
| `revokeAllFor(user)` / `revokeFamily(id)`
|
|
134
|
-
| `enforceMax(user, max)`
|
|
135
|
-
| `purgeExpired()`
|
|
132
|
+
| Static | Role |
|
|
133
|
+
| ---------------------------------------------- | -------------------------------------------------------- |
|
|
134
|
+
| `issue(user, token, …)` | persist a freshly-signed token (override to add columns) |
|
|
135
|
+
| `findByToken(token)` | look a row up by its token string |
|
|
136
|
+
| `findForUser(user, token)` | a user-scoped lookup (logout) |
|
|
137
|
+
| `activeFor(user)` | active, unexpired sessions, newest-first |
|
|
138
|
+
| `revokeAllFor(user)` / `revokeFamily(id)` | revoke a set, returning the revoked rows |
|
|
139
|
+
| `enforceMax(user, max)` | cap concurrent refresh tokens |
|
|
140
|
+
| `purgeExpired()` | delete expired rows (CLI cleanup) |
|
|
136
141
|
| `findNeverExpiring()` / `purgeNeverExpiring()` | find/delete rows that can never expire (CLI remediation) |
|
|
137
142
|
|
|
138
143
|
Plus two instance getters, both of which the route gate consults on every request:
|
|
139
144
|
|
|
140
|
-
| Getter
|
|
141
|
-
|
|
|
142
|
-
| `isExpired`
|
|
143
|
-
| `neverExpires` | whether nothing can ever retire the row (unusable `expires_at`, or a token with no `exp` claim)
|
|
145
|
+
| Getter | Role |
|
|
146
|
+
| -------------- | ---------------------------------------------------------------------------------------------------------- |
|
|
147
|
+
| `isExpired` | whether the row's persisted expiry has passed — **the middleware rejects the request when this is `true`** |
|
|
148
|
+
| `neverExpires` | whether nothing can ever retire the row (unusable `expires_at`, or a token with no `exp` claim) |
|
|
144
149
|
|
|
145
150
|
If you rename a column, override the statics **and these getters** so they map to your name — the service depends on the method, not the column.
|
|
146
151
|
|
|
@@ -155,6 +160,6 @@ If you rename a column, override the statics **and these getters** so they map t
|
|
|
155
160
|
|
|
156
161
|
## See also
|
|
157
162
|
|
|
158
|
-
- The `customize-user-type` topic — multiple user
|
|
163
|
+
- The `customize-user-type` topic — multiple user _types_ (a different axis from token _storage_).
|
|
159
164
|
- The `manage-tokens` topic — the token lifecycle your statics power.
|
|
160
165
|
- The `define-model` topic of the `warlock-js-cascade` skill — extending models and schemas.
|
|
@@ -99,7 +99,9 @@ const dashboardController: RequestHandler = async ({ response }) => response.suc
|
|
|
99
99
|
|
|
100
100
|
router.get("/account", userAccountController, { middleware: [authMiddleware("user")] });
|
|
101
101
|
router.get("/admin/users", listUsersController, { middleware: [authMiddleware("admin")] });
|
|
102
|
-
router.get("/back-office", backOfficeController, {
|
|
102
|
+
router.get("/back-office", backOfficeController, {
|
|
103
|
+
middleware: [authMiddleware(["admin", "staff"])],
|
|
104
|
+
});
|
|
103
105
|
router.get("/dashboard", dashboardController, { middleware: [authMiddleware([])] }); // any logged-in
|
|
104
106
|
```
|
|
105
107
|
|
package/skills/index.md
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
---
|
|
2
|
-
description:
|
|
2
|
+
description: 'JWT authentication for Warlock apps: login/logout, refresh-token rotation, protected routes, multiple user types. Exports `Auth`, `authService`, `authMiddleware`, `authEvents`, `currentUser`, `loginThrottleMiddleware`, `requireVerifiedEmail`, `GoogleProvider`, `AuthErrorCodes`. Use for: "log a user in and issue tokens", "protect this route", "add an admin user type", "rate-limit login attempts", "verify email / reset password", "sign in with Google". Not this package: general route middleware lives in @warlock.js/core; request-body validation is @warlock.js/seal.'
|
|
3
3
|
---
|
|
4
|
+
|
|
4
5
|
# @warlock.js/auth
|
|
5
6
|
|
|
6
7
|
A user model extends the `Auth` base model and declares a `userType`. `authService.login(Model, credentials)` verifies the password and issues persisted access + refresh tokens; `authMiddleware("user")` gates routes per type. Refresh tokens rotate within a token family, with replay detection. Server-only, and requires `@warlock.js/core`.
|
|
7
8
|
|
|
8
9
|
## The 80% path
|
|
10
|
+
|
|
9
11
|
1. Orient with `overview.md`, then `auth-basics.md` for the shape of config and models.
|
|
10
12
|
2. Extend `Auth` for your user model: `customize-user-type.md`, `register-user.md`.
|
|
11
13
|
3. Log in / out: `handle-login-and-logout.md`; token lifecycle: `manage-tokens.md`.
|
|
@@ -14,6 +16,7 @@ A user model extends the `Auth` base model and declares a `userType`. `authServi
|
|
|
14
16
|
6. Social sign-in: `login-with-providers.md`; CLI (JWT secret, token cleanup): `run-auth-commands.md`.
|
|
15
17
|
|
|
16
18
|
## Conventions and pitfalls
|
|
19
|
+
|
|
17
20
|
- Tokens are persisted rows, not stateless JWTs alone; revoking a family invalidates its refresh and access rows.
|
|
18
21
|
- `authMiddleware()` with no argument resolves the configured default user type. Be explicit when an app has several.
|
|
19
22
|
- Not for session-cookie auth; this is JWT-based.
|
|
@@ -16,13 +16,13 @@ authService.setAuthCookie(response, tokens.accessToken); // cookie session, or r
|
|
|
16
16
|
|
|
17
17
|
## Install
|
|
18
18
|
|
|
19
|
-
| Method
|
|
20
|
-
|
|
|
21
|
-
| Google
|
|
22
|
-
| Apple, LinkedIn
|
|
23
|
-
| GitHub, Discord, Facebook, X | nothing — plain OAuth 2 over `fetch`
|
|
24
|
-
| Passkeys
|
|
25
|
-
| Phone code
|
|
19
|
+
| Method | Command | Installs |
|
|
20
|
+
| ---------------------------- | ------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------ |
|
|
21
|
+
| Google | `warlock add auth-google` | `jose` |
|
|
22
|
+
| Apple, LinkedIn | `warlock add auth-google` (or `npm install jose`) — there is no separate `auth-apple` / `auth-linkedin` feature yet | `jose` |
|
|
23
|
+
| GitHub, Discord, Facebook, X | nothing — plain OAuth 2 over `fetch` | nothing |
|
|
24
|
+
| Passkeys | `warlock add auth-passkeys` | `@simplewebauthn/server` (add `@simplewebauthn/browser` to your client bundle) |
|
|
25
|
+
| Phone code | `warlock add notifications` + your own `sms`/`whatsapp` channel | nothing else. Auth ships no SMS/WhatsApp driver |
|
|
26
26
|
|
|
27
27
|
The SDKs are optional peers, loaded with `import()` only when a method runs. If one is missing, the call throws `AuthProviderSdkMissingError` (500), and the message names the `warlock add` command. Run the migrations: `authMigrations` now includes `provider_accounts` and `passkey_credentials`, and `one_time_tokens` gained an `attempts` column.
|
|
28
28
|
|
|
@@ -200,17 +200,25 @@ passkeys: { rpID: "example.com", rpName: "Example", origin: "https://example.com
|
|
|
200
200
|
|
|
201
201
|
```ts
|
|
202
202
|
// Register (logged in)
|
|
203
|
-
router.post(
|
|
204
|
-
|
|
205
|
-
{
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
203
|
+
router.post(
|
|
204
|
+
"/auth/passkeys/register/options",
|
|
205
|
+
async ({ request, response }) =>
|
|
206
|
+
response.success(await generatePasskeyRegistrationOptions(request.locals.user)),
|
|
207
|
+
{ middleware: [authMiddleware("user")] },
|
|
208
|
+
);
|
|
209
|
+
router.post(
|
|
210
|
+
"/auth/passkeys/register",
|
|
211
|
+
async ({ request, response }) => {
|
|
212
|
+
await verifyPasskeyRegistration(request, request.locals.user, request.input("credential"));
|
|
213
|
+
return response.success({ registered: true });
|
|
214
|
+
},
|
|
215
|
+
{ middleware: [authMiddleware("user")] },
|
|
216
|
+
);
|
|
210
217
|
|
|
211
218
|
// Log in
|
|
212
219
|
router.post("/auth/passkeys/login/options", async ({ response }) =>
|
|
213
|
-
response.success(await generatePasskeyAuthenticationOptions())
|
|
220
|
+
response.success(await generatePasskeyAuthenticationOptions()),
|
|
221
|
+
);
|
|
214
222
|
router.post("/auth/passkeys/login", async ({ request, response }) => {
|
|
215
223
|
const { tokens } = await verifyPasskeyAuthentication(request, request.input("credential"));
|
|
216
224
|
authService.setAuthCookie(response, tokens.accessToken);
|
|
@@ -224,16 +232,23 @@ Browser:
|
|
|
224
232
|
import { startAuthentication, startRegistration } from "@simplewebauthn/browser";
|
|
225
233
|
|
|
226
234
|
const post = (url: string, body?: unknown) =>
|
|
227
|
-
fetch(url, {
|
|
228
|
-
|
|
235
|
+
fetch(url, {
|
|
236
|
+
method: "POST",
|
|
237
|
+
headers: { "content-type": "application/json" },
|
|
238
|
+
body: JSON.stringify(body),
|
|
239
|
+
}).then((r) => r.json());
|
|
229
240
|
|
|
230
241
|
// register
|
|
231
242
|
const regOptions = await post("/auth/passkeys/register/options");
|
|
232
|
-
await post("/auth/passkeys/register", {
|
|
243
|
+
await post("/auth/passkeys/register", {
|
|
244
|
+
credential: await startRegistration({ optionsJSON: regOptions }),
|
|
245
|
+
});
|
|
233
246
|
|
|
234
247
|
// log in
|
|
235
248
|
const authOptions = await post("/auth/passkeys/login/options");
|
|
236
|
-
await post("/auth/passkeys/login", {
|
|
249
|
+
await post("/auth/passkeys/login", {
|
|
250
|
+
credential: await startAuthentication({ optionsJSON: authOptions }),
|
|
251
|
+
});
|
|
237
252
|
```
|
|
238
253
|
|
|
239
254
|
(Unwrap `response.success`'s envelope if your app wraps payloads.)
|
|
@@ -250,16 +265,26 @@ otp: { channel: "sms" /* or "whatsapp" */, phoneField: "phone", expiresIn: "5m",
|
|
|
250
265
|
```
|
|
251
266
|
|
|
252
267
|
```ts
|
|
253
|
-
router.post(
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
268
|
+
router.post(
|
|
269
|
+
"/auth/otp/request",
|
|
270
|
+
async ({ request, response }) => {
|
|
271
|
+
await requestOtp(User, request.input("phone"), {
|
|
272
|
+
channel: request.input("via") === "whatsapp" ? "whatsapp" : "sms",
|
|
273
|
+
});
|
|
274
|
+
return response.success({ message: "If that number is registered, a code is on its way." });
|
|
275
|
+
},
|
|
276
|
+
{ middleware: [otpRequestThrottleMiddleware()] },
|
|
277
|
+
); // 3 / 1h per phone + IP
|
|
278
|
+
|
|
279
|
+
router.post(
|
|
280
|
+
"/auth/otp/verify",
|
|
281
|
+
async ({ request, response }) => {
|
|
282
|
+
const { tokens } = await verifyOtp(User, request.input("phone"), request.input("code"));
|
|
283
|
+
authService.setAuthCookie(response, tokens.accessToken);
|
|
284
|
+
return response.success({ ok: true });
|
|
285
|
+
},
|
|
286
|
+
{ middleware: [otpVerifyThrottleMiddleware()] },
|
|
287
|
+
); // 5 failures / 15m per phone + IP
|
|
263
288
|
```
|
|
264
289
|
|
|
265
290
|
- Codes have 6 digits and are stored in `one_time_tokens` (purpose `otp`) as a salted HMAC keyed from your access-token secret, never as plain SHA-256, which is trivially reversible for 6 digits. A new request invalidates the previous code.
|
|
@@ -15,7 +15,7 @@ type RefreshTokenOutput = { token: string; expiresAt: string };
|
|
|
15
15
|
|
|
16
16
|
type TokenPair = {
|
|
17
17
|
accessToken: AccessTokenOutput;
|
|
18
|
-
refreshToken?: RefreshTokenOutput;
|
|
18
|
+
refreshToken?: RefreshTokenOutput; // omitted if config.auth.refreshToken.enabled = false
|
|
19
19
|
};
|
|
20
20
|
```
|
|
21
21
|
|
|
@@ -90,7 +90,7 @@ The family ties together "successive rotations of one session." In 5.19 it is al
|
|
|
90
90
|
const sessions = await authService.getActiveSessions(user);
|
|
91
91
|
|
|
92
92
|
for (const session of sessions) {
|
|
93
|
-
session.get("device_info");
|
|
93
|
+
session.get("device_info"); // { userAgent, ip, deviceId? } if provided at login
|
|
94
94
|
session.get("created_at");
|
|
95
95
|
session.get("expires_at");
|
|
96
96
|
}
|
package/skills/overview/SKILL.md
CHANGED
|
@@ -30,41 +30,50 @@ Nine task skills. Most apps need `auth-basics` + `protect-routes` + `handle-logi
|
|
|
30
30
|
### Foundations
|
|
31
31
|
|
|
32
32
|
#### `auth-basics`
|
|
33
|
+
|
|
33
34
|
Start here. The `Auth` base model, `authMiddleware` gate, `authService` (login/logout/refresh), AccessToken + RefreshToken persistence, multi-user-type support.
|
|
34
35
|
|
|
35
36
|
### The flows
|
|
36
37
|
|
|
37
38
|
#### `handle-login-and-logout`
|
|
39
|
+
|
|
38
40
|
`authService.login(Model, credentials, deviceInfo?)` — verify password, issue the token pair, fire events. `authService.logout(user, accessToken?, refreshToken?)` — revoke tokens. For your `POST /login` and `POST /logout` controllers.
|
|
39
41
|
|
|
40
42
|
#### `register-user`
|
|
43
|
+
|
|
41
44
|
Sign up a new user and issue the first token pair — `User.create({ ...password: await hashPassword(plain) })` then `authService.createTokenPair(user)`. For `POST /register`.
|
|
42
45
|
|
|
43
46
|
#### `protect-routes`
|
|
47
|
+
|
|
44
48
|
`authMiddleware` object and legacy overloads: typed/default hard gates, optional resolution, credential source, and page-local redirects. Sets `request.locals.user` + `request.decodedAccessToken` when a user resolves.
|
|
45
49
|
|
|
46
50
|
### Going deeper
|
|
47
51
|
|
|
48
52
|
#### `manage-tokens`
|
|
53
|
+
|
|
49
54
|
The token lifecycle — `generateAccessToken`, `createRefreshToken`, `createTokenPair`, `refreshTokens` (rotation + replay detection), `revokeAllTokens`, `revokeTokenFamily`, `cleanupExpiredTokens`, `getActiveSessions`. For custom login/registration, token revocation, "logout everywhere", and scheduled cleanup.
|
|
50
55
|
|
|
51
56
|
#### `customize-user-type`
|
|
57
|
+
|
|
52
58
|
Support multiple user types in one system — each `Auth` subclass overrides `userType`, `config.auth.userType.<slug>` maps the slug to a model class, `authMiddleware("admin")` / `authMiddleware(["admin", "staff"])` gates per type.
|
|
53
59
|
|
|
54
60
|
#### `customize-token-storage`
|
|
61
|
+
|
|
55
62
|
Override the persisted `AccessToken` / `RefreshToken` models to add columns (multi-tenant `organization_id`), rename, or change storage — extend the model + schema and register it under `config.auth.accessToken.model` / `config.auth.refreshToken.model`.
|
|
56
63
|
|
|
57
64
|
#### `throttle-login-attempts`
|
|
65
|
+
|
|
58
66
|
Brute-force / credential-stuffing protection — `loginThrottleMiddleware()` counts only failed logins, resets on success, locks per-account + per-IP after a threshold, and rejects pre-controller with 429. Cache-backed, fails open on a cache outage.
|
|
59
67
|
|
|
60
68
|
#### `run-auth-commands`
|
|
69
|
+
|
|
61
70
|
Two CLI commands — `warlock jwt.generate` (strong JWT secret → `.env`) and `warlock auth.cleanup` (remove expired refresh tokens). Register via `registerJWTSecretGeneratorCommand()` and `registerAuthCleanupCommand()`.
|
|
62
71
|
|
|
63
72
|
## What this package deliberately doesn't do
|
|
64
73
|
|
|
65
74
|
- **Session-cookie auth.** It's JWT/token-based. If you need server-side sessions, this isn't it.
|
|
66
75
|
- **OAuth / social login / SSO.** No provider adapters here — wire those at the controller layer and create the user through this package's models.
|
|
67
|
-
- **Authorization / roles / permissions (RBAC).** It authenticates (who you are) and gates by user
|
|
76
|
+
- **Authorization / roles / permissions (RBAC).** It authenticates (who you are) and gates by user _type_, not fine-grained permissions. Build RBAC on top.
|
|
68
77
|
- **Standalone use.** It depends on `@warlock.js/core` for routing, models (Cascade), and config.
|
|
69
78
|
|
|
70
79
|
## See also
|
|
@@ -64,21 +64,21 @@ The user is loaded via `Model.find(decodedToken.id)` against the `config.auth.us
|
|
|
64
64
|
|
|
65
65
|
A token passes three separate checks, in order — a token can fail any one of them while satisfying the other two:
|
|
66
66
|
|
|
67
|
-
1. **The JWT verifies** — signature, algorithm, `tokenType`, and (since 4.12.0) the presence of an `exp` claim that has not passed. A token with
|
|
67
|
+
1. **The JWT verifies** — signature, algorithm, `tokenType`, and (since 4.12.0) the presence of an `exp` claim that has not passed. A token with _no_ `exp` is rejected: there is no deadline to check, so it would otherwise verify indefinitely.
|
|
68
68
|
2. **The access-token row still exists** — deleting it (logout) invalidates the token immediately, before its JWT expiry.
|
|
69
69
|
3. **The row's own `expires_at` has not passed** (since 4.12.0). The database is the authority on the session: a row whose expiry has elapsed is rejected and deleted, even if the JWT itself is still within its lifetime. A row with a missing or unparseable `expires_at` is treated as expired, not as never-expiring.
|
|
70
70
|
|
|
71
71
|
Missing or invalid credentials return 401; an authenticated disallowed user type returns 403:
|
|
72
72
|
|
|
73
|
-
| Error code
|
|
74
|
-
|
|
|
75
|
-
| `MissingAccessToken` | No credential at the configured source
|
|
73
|
+
| Error code | When |
|
|
74
|
+
| -------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
|
|
75
|
+
| `MissingAccessToken` | No credential at the configured source |
|
|
76
76
|
| `InvalidAccessToken` | Token doesn't verify (signature, missing/passed `exp`, wrong token type), has no DB row, or the row's `expires_at` has passed |
|
|
77
|
-
| `Unauthorized`
|
|
77
|
+
| `Unauthorized` | Token valid but user-type isn't in the allowed list (403) |
|
|
78
78
|
|
|
79
79
|
## Page routes: redirect to login instead of the JSON 401
|
|
80
80
|
|
|
81
|
-
By default every auth failure — API or page — returns the JSON `401` above. That's right for an API client, but a logged-out human who
|
|
81
|
+
By default every auth failure — API or page — returns the JSON `401` above. That's right for an API client, but a logged-out human who _navigates_ to a guarded page route gets a raw JSON error blob instead of a login screen.
|
|
82
82
|
|
|
83
83
|
Opt into a redirect with `auth.pageAuth` (5.8+). Set `loginPath` and a guarded **page** route (`route.isPage`) that a logged-out browser hits redirects to `loginPath?returnUrl=<original path>` instead of returning the JSON 401:
|
|
84
84
|
|
|
@@ -88,7 +88,7 @@ import { User } from "app/users/models/user/user.model";
|
|
|
88
88
|
export default {
|
|
89
89
|
userType: { user: User },
|
|
90
90
|
pageAuth: {
|
|
91
|
-
loginPath: "/login",
|
|
91
|
+
loginPath: "/login", // set this to turn the redirect on
|
|
92
92
|
returnUrlParam: "returnUrl", // optional, this is the default
|
|
93
93
|
},
|
|
94
94
|
};
|
|
@@ -120,7 +120,7 @@ declare module "@warlock.js/auth" {
|
|
|
120
120
|
import { type RequestHandler } from "@warlock.js/core";
|
|
121
121
|
|
|
122
122
|
export const accountController: RequestHandler = async ({ request, response }) => {
|
|
123
|
-
const user = request.locals.user!;
|
|
123
|
+
const user = request.locals.user!; // typed via the RequestUser augmentation above
|
|
124
124
|
return response.success({
|
|
125
125
|
id: user.id,
|
|
126
126
|
email: user.get("email"),
|
|
@@ -146,9 +146,14 @@ Every route inside the group is gated — the group's `middleware` array applies
|
|
|
146
146
|
Use `optional: true` in the object form when a public route should receive a verified user when one is present. Missing or invalid credentials continue anonymously; a hard gate remains the default.
|
|
147
147
|
|
|
148
148
|
```ts
|
|
149
|
-
middleware: [authMiddleware({ source: "header", optional: true })]
|
|
149
|
+
middleware: [authMiddleware({ source: "header", optional: true })];
|
|
150
150
|
```
|
|
151
151
|
|
|
152
|
+
`authMiddleware()` marks itself as an auth guard for `warlock dev`'s
|
|
153
|
+
unguarded-route warning (`isAuthGuardMiddleware()` from `@warlock.js/core`);
|
|
154
|
+
the optional form does not count, so a mutating route behind only optional
|
|
155
|
+
auth still needs `public: true` to stay quiet.
|
|
156
|
+
|
|
152
157
|
## Automatic cookie renewal
|
|
153
158
|
|
|
154
159
|
Add `refresh: { source: "cookie", key: "refresh_token", overlapMs?: 5000 }`
|
|
@@ -158,6 +163,7 @@ through `authService.setAuthCookie`; it never replays a handler. `overlapMs` is
|
|
|
158
163
|
clamped to 0–10,000 ms (default 5,000). Only the exact immediate successor is
|
|
159
164
|
tolerated during that window. The legacy `refreshTokens` API stays strict; a
|
|
160
165
|
later old-token presentation revokes its family.
|
|
166
|
+
|
|
161
167
|
## Custom error responses
|
|
162
168
|
|
|
163
169
|
The middleware uses the framework's `response.unauthorized({...})` shape. To override the response globally, hook the framework's error transformer to remap `AuthErrorCodes.*` codes.
|
|
@@ -182,8 +188,9 @@ import { User } from "app/users/models/user/user.model";
|
|
|
182
188
|
|
|
183
189
|
const articlesController: RequestHandler = async ({ request, response }) => {
|
|
184
190
|
// Who-may-act-on-whom: an ABAC policy on top of the RBAC grant
|
|
185
|
-
definePolicy(
|
|
186
|
-
|
|
191
|
+
definePolicy(
|
|
192
|
+
"users.create",
|
|
193
|
+
(actor, target, ctx) => ctx.hasRole("superAdmin") || (target as User).userType === "teacher",
|
|
187
194
|
);
|
|
188
195
|
|
|
189
196
|
if (await can(request.locals.user!, "users.create", { resource: request.body })) {
|
|
@@ -194,10 +201,12 @@ const articlesController: RequestHandler = async ({ request, response }) => {
|
|
|
194
201
|
};
|
|
195
202
|
|
|
196
203
|
// Permission-based route gate, in place of a user-type gate
|
|
197
|
-
router.post("/articles", articlesController, {
|
|
204
|
+
router.post("/articles", articlesController, {
|
|
205
|
+
middleware: [authMiddleware([]), gate("articles.create")],
|
|
206
|
+
});
|
|
198
207
|
```
|
|
199
208
|
|
|
200
|
-
Use `authMiddleware` to establish
|
|
209
|
+
Use `authMiddleware` to establish _who the caller is_, and `access` to decide _what they may do_. They compose — `access` reads the user `authMiddleware` put on the request.
|
|
201
210
|
|
|
202
211
|
## Cookie credentials
|
|
203
212
|
|
|
@@ -213,7 +222,7 @@ A route is header-only or cookie-only, never both — `tokenFrom` accepts exactl
|
|
|
213
222
|
|
|
214
223
|
### Writing and clearing the cookie: `authService.setAuthCookie` / `clearAuthCookie`
|
|
215
224
|
|
|
216
|
-
Reading a cookie is opt-in per route (above);
|
|
225
|
+
Reading a cookie is opt-in per route (above); _writing_ one is a separate, explicit step your login/logout controller calls — `authService.login`/`logout` never set cookies implicitly, so upgrading never starts emitting `Set-Cookie` for an existing bearer-only app.
|
|
217
226
|
|
|
218
227
|
```ts
|
|
219
228
|
import { type RequestHandler } from "@warlock.js/core";
|
|
@@ -52,7 +52,7 @@ export const registerController: RequestHandler = async ({ request, response })
|
|
|
52
52
|
|
|
53
53
|
// 4. Respond
|
|
54
54
|
return response.successCreate({
|
|
55
|
-
user,
|
|
55
|
+
user, // shape via static toJsonColumns / static resource
|
|
56
56
|
tokens,
|
|
57
57
|
});
|
|
58
58
|
};
|
|
@@ -67,7 +67,7 @@ Always pass `hashPassword(plain)` — never store the plain password. The `hashP
|
|
|
67
67
|
```ts
|
|
68
68
|
import { hashPassword, verifyPassword } from "@warlock.js/core";
|
|
69
69
|
|
|
70
|
-
const hash = await hashPassword("plaintext");
|
|
70
|
+
const hash = await hashPassword("plaintext"); // store this
|
|
71
71
|
const ok = await verifyPassword("plaintext", hash); // compare on login
|
|
72
72
|
```
|
|
73
73
|
|
|
@@ -81,7 +81,7 @@ Define the password as `v.string().strongPassword(12)` (or similar) in your User
|
|
|
81
81
|
const userSchema = v.object({
|
|
82
82
|
email: v.string().email(),
|
|
83
83
|
name: v.string().min(2).max(120),
|
|
84
|
-
password: v.string().strongPassword(12),
|
|
84
|
+
password: v.string().strongPassword(12), // 12+ chars, upper/lower/digit/symbol
|
|
85
85
|
// status, role, etc.
|
|
86
86
|
});
|
|
87
87
|
```
|
|
@@ -93,7 +93,7 @@ But **don't return the password in the public output**:
|
|
|
93
93
|
export class User extends Model<UserSchema> {
|
|
94
94
|
public static table = "users";
|
|
95
95
|
public static schema = userSchema;
|
|
96
|
-
public static toJsonColumns = ["id", "email", "name", "created_at"];
|
|
96
|
+
public static toJsonColumns = ["id", "email", "name", "created_at"]; // omit password
|
|
97
97
|
}
|
|
98
98
|
```
|
|
99
99
|
|
|
@@ -93,7 +93,7 @@ Register with `registerAuthPurgeNeverExpiringCommand()`.
|
|
|
93
93
|
|
|
94
94
|
Finds every token row that **can never retire itself**, on two independent signals: an `expires_at` that is missing or unparseable, and a stored token carrying no `exp` claim. It reports `id` / `user_id` / `user_type` / `expires_at` per row — never the token string, which is a live credential until the row is deleted — and then deletes them. Affected users must log in again.
|
|
95
95
|
|
|
96
|
-
**This is not a substitute for `auth.cleanup`, and `auth.cleanup` is not a substitute for it.** Cleanup selects `expires_at < now`; an `Invalid Date` compares `false` against
|
|
96
|
+
**This is not a substitute for `auth.cleanup`, and `auth.cleanup` is not a substitute for it.** Cleanup selects `expires_at < now`; an `Invalid Date` compares `false` against _every_ date, so such a row satisfies neither `< now` nor `> now` and no date predicate can ever reach it. The "no `exp` claim" signal is not in a column at all — it is inside the JWT.
|
|
97
97
|
|
|
98
98
|
Run it once after upgrading to 4.12.0 **if any deployment ever ran an `expiresIn` that `ms` could not parse** (`"30dayz"`, `"0d"`, `""`, a bare number). Those configurations minted tokens that verify forever. It is safe to run when unaffected — it reports nothing and deletes nothing.
|
|
99
99
|
|