@spinajs/rbac-http-token 2.0.507
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/README.md +152 -0
- package/lib/cjs/actions.d.ts +119 -0
- package/lib/cjs/actions.d.ts.map +1 -0
- package/lib/cjs/actions.js +257 -0
- package/lib/cjs/actions.js.map +1 -0
- package/lib/cjs/cli/CreateToken.d.ts +13 -0
- package/lib/cjs/cli/CreateToken.d.ts.map +1 -0
- package/lib/cjs/cli/CreateToken.js +56 -0
- package/lib/cjs/cli/CreateToken.js.map +1 -0
- package/lib/cjs/cli/DeleteExpiredTokens.d.ts +11 -0
- package/lib/cjs/cli/DeleteExpiredTokens.d.ts.map +1 -0
- package/lib/cjs/cli/DeleteExpiredTokens.js +39 -0
- package/lib/cjs/cli/DeleteExpiredTokens.js.map +1 -0
- package/lib/cjs/cli/DeleteToken.d.ts +7 -0
- package/lib/cjs/cli/DeleteToken.d.ts.map +1 -0
- package/lib/cjs/cli/DeleteToken.js +36 -0
- package/lib/cjs/cli/DeleteToken.js.map +1 -0
- package/lib/cjs/cli/GrantTokenRole.d.ts +7 -0
- package/lib/cjs/cli/GrantTokenRole.d.ts.map +1 -0
- package/lib/cjs/cli/GrantTokenRole.js +37 -0
- package/lib/cjs/cli/GrantTokenRole.js.map +1 -0
- package/lib/cjs/cli/RevokeTokenRole.d.ts +12 -0
- package/lib/cjs/cli/RevokeTokenRole.d.ts.map +1 -0
- package/lib/cjs/cli/RevokeTokenRole.js +42 -0
- package/lib/cjs/cli/RevokeTokenRole.js.map +1 -0
- package/lib/cjs/config/rbac-http-token.d.ts +71 -0
- package/lib/cjs/config/rbac-http-token.d.ts.map +1 -0
- package/lib/cjs/config/rbac-http-token.js +98 -0
- package/lib/cjs/config/rbac-http-token.js.map +1 -0
- package/lib/cjs/controllers/AccessTokenController.d.ts +139 -0
- package/lib/cjs/controllers/AccessTokenController.d.ts.map +1 -0
- package/lib/cjs/controllers/AccessTokenController.js +264 -0
- package/lib/cjs/controllers/AccessTokenController.js.map +1 -0
- package/lib/cjs/dto/create-token-dto.d.ts +34 -0
- package/lib/cjs/dto/create-token-dto.d.ts.map +1 -0
- package/lib/cjs/dto/create-token-dto.js +35 -0
- package/lib/cjs/dto/create-token-dto.js.map +1 -0
- package/lib/cjs/events/AccessTokenCreated.d.ts +4 -0
- package/lib/cjs/events/AccessTokenCreated.d.ts.map +1 -0
- package/lib/cjs/events/AccessTokenCreated.js +18 -0
- package/lib/cjs/events/AccessTokenCreated.js.map +1 -0
- package/lib/cjs/events/AccessTokenDeleted.d.ts +4 -0
- package/lib/cjs/events/AccessTokenDeleted.d.ts.map +1 -0
- package/lib/cjs/events/AccessTokenDeleted.js +18 -0
- package/lib/cjs/events/AccessTokenDeleted.js.map +1 -0
- package/lib/cjs/events/AccessTokenEvent.d.ts +10 -0
- package/lib/cjs/events/AccessTokenEvent.d.ts.map +1 -0
- package/lib/cjs/events/AccessTokenEvent.js +26 -0
- package/lib/cjs/events/AccessTokenEvent.js.map +1 -0
- package/lib/cjs/events/AccessTokenRoleGranted.d.ts +7 -0
- package/lib/cjs/events/AccessTokenRoleGranted.d.ts.map +1 -0
- package/lib/cjs/events/AccessTokenRoleGranted.js +27 -0
- package/lib/cjs/events/AccessTokenRoleGranted.js.map +1 -0
- package/lib/cjs/events/AccessTokenRoleRevoked.d.ts +7 -0
- package/lib/cjs/events/AccessTokenRoleRevoked.d.ts.map +1 -0
- package/lib/cjs/events/AccessTokenRoleRevoked.js +27 -0
- package/lib/cjs/events/AccessTokenRoleRevoked.js.map +1 -0
- package/lib/cjs/events/index.d.ts +6 -0
- package/lib/cjs/events/index.d.ts.map +1 -0
- package/lib/cjs/events/index.js +22 -0
- package/lib/cjs/events/index.js.map +1 -0
- package/lib/cjs/generator.d.ts +12 -0
- package/lib/cjs/generator.d.ts.map +1 -0
- package/lib/cjs/generator.js +42 -0
- package/lib/cjs/generator.js.map +1 -0
- package/lib/cjs/index.d.ts +20 -0
- package/lib/cjs/index.d.ts.map +1 -0
- package/lib/cjs/index.js +39 -0
- package/lib/cjs/index.js.map +1 -0
- package/lib/cjs/interfaces.d.ts +57 -0
- package/lib/cjs/interfaces.d.ts.map +1 -0
- package/lib/cjs/interfaces.js +11 -0
- package/lib/cjs/interfaces.js.map +1 -0
- package/lib/cjs/middlewares.d.ts +76 -0
- package/lib/cjs/middlewares.d.ts.map +1 -0
- package/lib/cjs/middlewares.js +181 -0
- package/lib/cjs/middlewares.js.map +1 -0
- package/lib/cjs/migrations/RbacHttpTokenInitial_2026_08_11_01_00_00.d.ts +6 -0
- package/lib/cjs/migrations/RbacHttpTokenInitial_2026_08_11_01_00_00.d.ts.map +1 -0
- package/lib/cjs/migrations/RbacHttpTokenInitial_2026_08_11_01_00_00.js +38 -0
- package/lib/cjs/migrations/RbacHttpTokenInitial_2026_08_11_01_00_00.js.map +1 -0
- package/lib/cjs/models/AccessToken.d.ts +55 -0
- package/lib/cjs/models/AccessToken.d.ts.map +1 -0
- package/lib/cjs/models/AccessToken.js +73 -0
- package/lib/cjs/models/AccessToken.js.map +1 -0
- package/lib/cjs/package.json +1 -0
- package/lib/cjs/policies/NoImpersonationPolicy.d.ts +28 -0
- package/lib/cjs/policies/NoImpersonationPolicy.d.ts.map +1 -0
- package/lib/cjs/policies/NoImpersonationPolicy.js +45 -0
- package/lib/cjs/policies/NoImpersonationPolicy.js.map +1 -0
- package/lib/cjs/policies/NoTokenAuthPolicy.d.ts +12 -0
- package/lib/cjs/policies/NoTokenAuthPolicy.d.ts.map +1 -0
- package/lib/cjs/policies/NoTokenAuthPolicy.js +24 -0
- package/lib/cjs/policies/NoTokenAuthPolicy.js.map +1 -0
- package/lib/cjs/policies/TokenPolicy.d.ts +49 -0
- package/lib/cjs/policies/TokenPolicy.d.ts.map +1 -0
- package/lib/cjs/policies/TokenPolicy.js +79 -0
- package/lib/cjs/policies/TokenPolicy.js.map +1 -0
- package/lib/mjs/actions.d.ts +119 -0
- package/lib/mjs/actions.d.ts.map +1 -0
- package/lib/mjs/actions.js +242 -0
- package/lib/mjs/actions.js.map +1 -0
- package/lib/mjs/cli/CreateToken.d.ts +13 -0
- package/lib/mjs/cli/CreateToken.d.ts.map +1 -0
- package/lib/mjs/cli/CreateToken.js +53 -0
- package/lib/mjs/cli/CreateToken.js.map +1 -0
- package/lib/mjs/cli/DeleteExpiredTokens.d.ts +11 -0
- package/lib/mjs/cli/DeleteExpiredTokens.d.ts.map +1 -0
- package/lib/mjs/cli/DeleteExpiredTokens.js +36 -0
- package/lib/mjs/cli/DeleteExpiredTokens.js.map +1 -0
- package/lib/mjs/cli/DeleteToken.d.ts +7 -0
- package/lib/mjs/cli/DeleteToken.d.ts.map +1 -0
- package/lib/mjs/cli/DeleteToken.js +33 -0
- package/lib/mjs/cli/DeleteToken.js.map +1 -0
- package/lib/mjs/cli/GrantTokenRole.d.ts +7 -0
- package/lib/mjs/cli/GrantTokenRole.d.ts.map +1 -0
- package/lib/mjs/cli/GrantTokenRole.js +34 -0
- package/lib/mjs/cli/GrantTokenRole.js.map +1 -0
- package/lib/mjs/cli/RevokeTokenRole.d.ts +12 -0
- package/lib/mjs/cli/RevokeTokenRole.d.ts.map +1 -0
- package/lib/mjs/cli/RevokeTokenRole.js +39 -0
- package/lib/mjs/cli/RevokeTokenRole.js.map +1 -0
- package/lib/mjs/config/rbac-http-token.d.ts +71 -0
- package/lib/mjs/config/rbac-http-token.d.ts.map +1 -0
- package/lib/mjs/config/rbac-http-token.js +96 -0
- package/lib/mjs/config/rbac-http-token.js.map +1 -0
- package/lib/mjs/controllers/AccessTokenController.d.ts +139 -0
- package/lib/mjs/controllers/AccessTokenController.d.ts.map +1 -0
- package/lib/mjs/controllers/AccessTokenController.js +261 -0
- package/lib/mjs/controllers/AccessTokenController.js.map +1 -0
- package/lib/mjs/dto/create-token-dto.d.ts +34 -0
- package/lib/mjs/dto/create-token-dto.d.ts.map +1 -0
- package/lib/mjs/dto/create-token-dto.js +32 -0
- package/lib/mjs/dto/create-token-dto.js.map +1 -0
- package/lib/mjs/events/AccessTokenCreated.d.ts +4 -0
- package/lib/mjs/events/AccessTokenCreated.d.ts.map +1 -0
- package/lib/mjs/events/AccessTokenCreated.js +15 -0
- package/lib/mjs/events/AccessTokenCreated.js.map +1 -0
- package/lib/mjs/events/AccessTokenDeleted.d.ts +4 -0
- package/lib/mjs/events/AccessTokenDeleted.d.ts.map +1 -0
- package/lib/mjs/events/AccessTokenDeleted.js +15 -0
- package/lib/mjs/events/AccessTokenDeleted.js.map +1 -0
- package/lib/mjs/events/AccessTokenEvent.d.ts +10 -0
- package/lib/mjs/events/AccessTokenEvent.d.ts.map +1 -0
- package/lib/mjs/events/AccessTokenEvent.js +23 -0
- package/lib/mjs/events/AccessTokenEvent.js.map +1 -0
- package/lib/mjs/events/AccessTokenRoleGranted.d.ts +7 -0
- package/lib/mjs/events/AccessTokenRoleGranted.d.ts.map +1 -0
- package/lib/mjs/events/AccessTokenRoleGranted.js +24 -0
- package/lib/mjs/events/AccessTokenRoleGranted.js.map +1 -0
- package/lib/mjs/events/AccessTokenRoleRevoked.d.ts +7 -0
- package/lib/mjs/events/AccessTokenRoleRevoked.d.ts.map +1 -0
- package/lib/mjs/events/AccessTokenRoleRevoked.js +24 -0
- package/lib/mjs/events/AccessTokenRoleRevoked.js.map +1 -0
- package/lib/mjs/events/index.d.ts +6 -0
- package/lib/mjs/events/index.d.ts.map +1 -0
- package/lib/mjs/events/index.js +6 -0
- package/lib/mjs/events/index.js.map +1 -0
- package/lib/mjs/generator.d.ts +12 -0
- package/lib/mjs/generator.d.ts.map +1 -0
- package/lib/mjs/generator.js +39 -0
- package/lib/mjs/generator.js.map +1 -0
- package/lib/mjs/index.d.ts +20 -0
- package/lib/mjs/index.d.ts.map +1 -0
- package/lib/mjs/index.js +23 -0
- package/lib/mjs/index.js.map +1 -0
- package/lib/mjs/interfaces.d.ts +57 -0
- package/lib/mjs/interfaces.d.ts.map +1 -0
- package/lib/mjs/interfaces.js +7 -0
- package/lib/mjs/interfaces.js.map +1 -0
- package/lib/mjs/middlewares.d.ts +76 -0
- package/lib/mjs/middlewares.d.ts.map +1 -0
- package/lib/mjs/middlewares.js +178 -0
- package/lib/mjs/middlewares.js.map +1 -0
- package/lib/mjs/migrations/RbacHttpTokenInitial_2026_08_11_01_00_00.d.ts +6 -0
- package/lib/mjs/migrations/RbacHttpTokenInitial_2026_08_11_01_00_00.d.ts.map +1 -0
- package/lib/mjs/migrations/RbacHttpTokenInitial_2026_08_11_01_00_00.js +35 -0
- package/lib/mjs/migrations/RbacHttpTokenInitial_2026_08_11_01_00_00.js.map +1 -0
- package/lib/mjs/models/AccessToken.d.ts +55 -0
- package/lib/mjs/models/AccessToken.d.ts.map +1 -0
- package/lib/mjs/models/AccessToken.js +70 -0
- package/lib/mjs/models/AccessToken.js.map +1 -0
- package/lib/mjs/package.json +1 -0
- package/lib/mjs/policies/NoImpersonationPolicy.d.ts +28 -0
- package/lib/mjs/policies/NoImpersonationPolicy.d.ts.map +1 -0
- package/lib/mjs/policies/NoImpersonationPolicy.js +41 -0
- package/lib/mjs/policies/NoImpersonationPolicy.js.map +1 -0
- package/lib/mjs/policies/NoTokenAuthPolicy.d.ts +12 -0
- package/lib/mjs/policies/NoTokenAuthPolicy.d.ts.map +1 -0
- package/lib/mjs/policies/NoTokenAuthPolicy.js +20 -0
- package/lib/mjs/policies/NoTokenAuthPolicy.js.map +1 -0
- package/lib/mjs/policies/TokenPolicy.d.ts +49 -0
- package/lib/mjs/policies/TokenPolicy.d.ts.map +1 -0
- package/lib/mjs/policies/TokenPolicy.js +75 -0
- package/lib/mjs/policies/TokenPolicy.js.map +1 -0
- package/lib/tsconfig.cjs.tsbuildinfo +1 -0
- package/lib/tsconfig.mjs.tsbuildinfo +1 -0
- package/package.json +78 -0
package/README.md
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# @spinajs/rbac-http-token
|
|
2
|
+
|
|
3
|
+
Personal access tokens (PAT) for spinajs HTTP routes. Opaque `spt_...` tokens,
|
|
4
|
+
stored hashed (SHA-256), assigned to users, with rbac role intersection and
|
|
5
|
+
optional expiration.
|
|
6
|
+
|
|
7
|
+
## Securing a route
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import { BaseController, BasePath, Get, Policy } from '@spinajs/http';
|
|
11
|
+
import { Permission, Resource } from '@spinajs/rbac-http';
|
|
12
|
+
import { TokenPolicy } from '@spinajs/rbac-http-token';
|
|
13
|
+
|
|
14
|
+
@BasePath('api')
|
|
15
|
+
@Resource('my.resource')
|
|
16
|
+
export class ApiController extends BaseController {
|
|
17
|
+
@Get('data')
|
|
18
|
+
@Permission(['readOwn']) // permission metadata + the RbacPolicy group
|
|
19
|
+
@Policy(TokenPolicy) // METHOD level, BELOW @Permission
|
|
20
|
+
public async data() { ... }
|
|
21
|
+
}
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
The placement is load bearing (see the docblock on `src/policies/TokenPolicy.ts`):
|
|
25
|
+
|
|
26
|
+
- `@Policy(TokenPolicy)` must sit at the **method** level. A class level
|
|
27
|
+
declaration lands in the CONTROLLER policy scope, and the controller scope is
|
|
28
|
+
ANDed with the route scope - it would then have to hold together with the
|
|
29
|
+
`RbacPolicy` group that `@Permission` pushes on the route, and `RbacPolicy`
|
|
30
|
+
demands an authorized session that a token request never has. Every token
|
|
31
|
+
request would 401. Method level puts it in the same scope as `@Permission`,
|
|
32
|
+
and groups within one scope are ORed: a token request satisfies the
|
|
33
|
+
`TokenPolicy` group, a session request satisfies the `RbacPolicy` one.
|
|
34
|
+
- For the same reason it must not be passed as `@Permission`'s `also` argument -
|
|
35
|
+
`@Permission(['readOwn'], TokenPolicy)` bundles both into a single ANDed group.
|
|
36
|
+
- Writing it **below** `@Permission` is deliberate: decorators apply bottom up,
|
|
37
|
+
so when no group holds, the reported rejection is this policy's 403
|
|
38
|
+
("token role(s) ... do not have permission ...") instead of `RbacPolicy`'s
|
|
39
|
+
misleading "user not logged" 401.
|
|
40
|
+
- Because the two route-scope groups are **ORed**, adding `@Policy(TokenPolicy)`
|
|
41
|
+
*widens* the route: it becomes reachable by a token holder whose effective
|
|
42
|
+
roles carry the grant **and** by a session holder whose active role carries
|
|
43
|
+
it, exactly as before. It is not a "tokens only" switch. Use
|
|
44
|
+
`NoTokenAuthPolicy` (below) for the opposite - a route sessions may use and
|
|
45
|
+
tokens may not.
|
|
46
|
+
- The `@Permission` line stays required - `TokenPolicy` reads the same route
|
|
47
|
+
permission metadata, and a route without it silently inherits the
|
|
48
|
+
controller-level default `['readOwn']`.
|
|
49
|
+
|
|
50
|
+
Clients authenticate with `Authorization: Bearer spt_...` (scheme name is
|
|
51
|
+
case-insensitive) or the configured fallback header (`x-api-key` by default).
|
|
52
|
+
|
|
53
|
+
## Token semantics
|
|
54
|
+
|
|
55
|
+
- Effective roles at request time = token roles ∩ owner's current roles. Roles
|
|
56
|
+
are re-intersected on every request, so a role revoked on the user takes
|
|
57
|
+
effect immediately for all of their tokens.
|
|
58
|
+
- A token whose effective role set becomes empty stops authenticating.
|
|
59
|
+
- Permission checks run against the **whole** effective role set. A session has
|
|
60
|
+
an *active role* it can switch at runtime (`POST /auth/active-role`) and is
|
|
61
|
+
authorized by that one role; a token has no session and no way to switch, so
|
|
62
|
+
it would be stuck with whichever role happened to be first. Instead
|
|
63
|
+
`TokenAuthMiddleware` clears `req.storage.ActiveRole`, and every consumer
|
|
64
|
+
(`checkRoutePermission`, the orm rbac query middleware) falls back to
|
|
65
|
+
`User.Role` - already narrowed to the effective set.
|
|
66
|
+
- Owner deactivated / banned / soft-deleted => all their tokens stop working.
|
|
67
|
+
- `ExpiresAt` null/absent => token never expires.
|
|
68
|
+
- Plaintext shown exactly once at creation; only its SHA-256 hash is stored, and
|
|
69
|
+
the hash itself is `@Hidden()` so it never reaches a response either.
|
|
70
|
+
- Session-authenticated requests ignore tokens: `TokenAuthMiddleware` runs after
|
|
71
|
+
`RbacMiddleware` and bails out when a session is present, so a request never
|
|
72
|
+
mixes both.
|
|
73
|
+
- Token-authenticated responses are sent with `Cache-Control: no-store`.
|
|
74
|
+
- Tokens cannot manage tokens - the management API is guarded by
|
|
75
|
+
`NoTokenAuthPolicy` (no self-replication).
|
|
76
|
+
- Tokens cannot be minted or managed while impersonating another user -
|
|
77
|
+
`NoImpersonationPolicy` (also exported) rejects such requests with 403. An
|
|
78
|
+
impersonated session is supervised and revocable; a token it minted would
|
|
79
|
+
outlive it, carry the victim's roles, and be invisible to them.
|
|
80
|
+
- A token always keeps at least one role - revoking the last one is refused
|
|
81
|
+
(400 / `E_TOKEN_ROLE_NOT_ALLOWED`). Delete the token to revoke it entirely.
|
|
82
|
+
- A token can never carry a role its owner does not hold, at creation or grant.
|
|
83
|
+
- `LastUsedAt` is stamped on use, throttled to one write per
|
|
84
|
+
`rbac.token.lastUsedUpdateInterval` seconds.
|
|
85
|
+
|
|
86
|
+
## HTTP API (session auth required)
|
|
87
|
+
|
|
88
|
+
All routes live under the `user.tokens` resource and operate on the caller's own
|
|
89
|
+
tokens only - a foreign uuid simply reads as 404.
|
|
90
|
+
|
|
91
|
+
| Route | Description |
|
|
92
|
+
|---|---|
|
|
93
|
+
| `GET user/tokens` | list own tokens |
|
|
94
|
+
| `POST user/tokens` | create (`{ Name, Roles, ExpiresAt? }`), returns plaintext once |
|
|
95
|
+
| `DELETE user/tokens/:uuid` | revoke (delete) a token |
|
|
96
|
+
| `PUT user/tokens/:uuid/roles/:role` | grant role |
|
|
97
|
+
| `DELETE user/tokens/:uuid/roles/:role` | revoke role |
|
|
98
|
+
|
|
99
|
+
Responses: `400` for role refusals (role not held by the caller, last role
|
|
100
|
+
revoke), `401` without a valid session, `403` when the request is
|
|
101
|
+
token-authenticated or impersonated, `404` for a token the caller does not own.
|
|
102
|
+
|
|
103
|
+
## CLI
|
|
104
|
+
|
|
105
|
+
```
|
|
106
|
+
rbac:token-create <userIdOrUuid> -n <name> -r <roles> [-e <iso>]
|
|
107
|
+
rbac:token-delete <uuid>
|
|
108
|
+
rbac:token-grant <uuid> <role>
|
|
109
|
+
rbac:token-revoke <uuid> <role>
|
|
110
|
+
rbac:token-delete-expired # run cyclically from a worker
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
- `-r, --roles` is a comma separated list, and must be a subset of the owner's
|
|
114
|
+
roles.
|
|
115
|
+
- `-e, --expires` takes an ISO instant. Omitting the flag entirely means the
|
|
116
|
+
token never expires; an empty or unparsable value is refused rather than
|
|
117
|
+
silently treated as "no expiration".
|
|
118
|
+
- `rbac:token-create` prints the plaintext once - it cannot be retrieved later.
|
|
119
|
+
- `rbac:token-delete-expired` is housekeeping only; expired tokens are already
|
|
120
|
+
refused at authentication time.
|
|
121
|
+
|
|
122
|
+
## Configuration
|
|
123
|
+
|
|
124
|
+
**A consuming app MUST import the package in its bootstrap** -
|
|
125
|
+
`import '@spinajs/rbac-http-token';` - even when it uses nothing from it
|
|
126
|
+
directly. The model, the migration and the auth middleware register themselves
|
|
127
|
+
through decorators when their module is loaded, and the migration in particular
|
|
128
|
+
is *not* found by filesystem scan (see the comment on `system.dirs` in
|
|
129
|
+
`src/config/rbac-http-token.ts` and the note at the bottom of this file). Without
|
|
130
|
+
that import the `rbac_access_tokens` table is never created.
|
|
131
|
+
|
|
132
|
+
See `src/config/rbac-http-token.ts` - `rbac.token.*`:
|
|
133
|
+
|
|
134
|
+
| Key | Default | Meaning |
|
|
135
|
+
|---|---|---|
|
|
136
|
+
| `rbac.token.generation.service` | `SecureRandomTokenProvider` | `AccessTokenGenerationProvider` implementation used to generate/hash tokens |
|
|
137
|
+
| `rbac.token.prefix` | `spt_` | stable plaintext prefix, lets secret scanners recognise leaked tokens |
|
|
138
|
+
| `rbac.token.length` | `32` | random bytes per token (32 = 256 bit entropy) |
|
|
139
|
+
| `rbac.token.headerName` | `x-api-key` | fallback header checked when no `Authorization: Bearer` is present |
|
|
140
|
+
| `rbac.token.lastUsedUpdateInterval` | `60` | seconds between `LastUsedAt` writes for a busy token |
|
|
141
|
+
|
|
142
|
+
The package also ships `rbac.grants` for the `user.tokens` resource and queue
|
|
143
|
+
routing that sends its events (`AccessTokenCreated`, `AccessTokenDeleted`,
|
|
144
|
+
`AccessTokenRoleGranted`, `AccessTokenRoleRevoked`) to rbac's empty connection
|
|
145
|
+
instead of the application's default broker.
|
|
146
|
+
|
|
147
|
+
`system.dirs` ships `cli` and `controllers` only - those are found by filesystem
|
|
148
|
+
scan. `migrations` is deliberately absent: a non-empty `system.dirs.migrations`
|
|
149
|
+
makes the orm replace its build-layout defaults, which would switch off
|
|
150
|
+
migration discovery in every consuming app. The initial migration carries
|
|
151
|
+
`@Migration`, is exported from the package index, and is picked up from the DI
|
|
152
|
+
registry instead.
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { DateTime } from 'luxon';
|
|
2
|
+
import { User } from '@spinajs/rbac';
|
|
3
|
+
import { AccessToken } from './models/AccessToken.js';
|
|
4
|
+
/**
|
|
5
|
+
* Failure codes carried by every {@link ErrorCode} this module throws.
|
|
6
|
+
*
|
|
7
|
+
* Named `E_TOKEN_CODES` rather than the shorter `E_CODES` on purpose:
|
|
8
|
+
* `@spinajs/rbac` exports an enum called `E_CODES` too, and both are re-exported
|
|
9
|
+
* from their package index. A downstream barrel that does
|
|
10
|
+
* `export * from '@spinajs/rbac'; export * from '@spinajs/rbac-http-token';`
|
|
11
|
+
* would collide on the NAME while the members underneath carry different
|
|
12
|
+
* numeric values - the kind of clash that resolves silently in a barrel and
|
|
13
|
+
* makes an `err.code === E_CODES.X` comparison answer the wrong question.
|
|
14
|
+
*/
|
|
15
|
+
export declare enum E_TOKEN_CODES {
|
|
16
|
+
E_TOKEN_NOT_FOUND = 0,
|
|
17
|
+
E_TOKEN_EXPIRED = 1,
|
|
18
|
+
E_TOKEN_OWNER_INVALID = 2,
|
|
19
|
+
E_TOKEN_ROLE_NOT_ALLOWED = 3
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Resolves an AccessToken from an instance or its uuid.
|
|
23
|
+
*/
|
|
24
|
+
export declare function _token(token: AccessToken | string): () => Promise<AccessToken>;
|
|
25
|
+
/**
|
|
26
|
+
* Resolves the owning user by instance, numeric id or uuid, with metadata
|
|
27
|
+
* populated ( needed for IsBanned checks downstream ).
|
|
28
|
+
*/
|
|
29
|
+
export declare function _owner(user: User | number | string): () => Promise<User>;
|
|
30
|
+
/**
|
|
31
|
+
* Creates a new access token for a user.
|
|
32
|
+
*
|
|
33
|
+
* Roles must be a non-empty subset of the owner's current roles. `expiresAt`
|
|
34
|
+
* null means the token never expires. The plaintext is returned once and
|
|
35
|
+
* never persisted - only its hash is stored.
|
|
36
|
+
*
|
|
37
|
+
* @param user - owner: {@link User} instance, numeric id or uuid
|
|
38
|
+
* @param name - human readable label
|
|
39
|
+
* @param roles - roles carried by the token, subset of the owner's roles
|
|
40
|
+
* @param expiresAt - absolute expiration, or null for a token that never expires
|
|
41
|
+
*/
|
|
42
|
+
export declare function createToken(user: User | number | string, name: string, roles: string[], expiresAt: DateTime | null): Promise<{
|
|
43
|
+
Token: AccessToken;
|
|
44
|
+
Plaintext: string;
|
|
45
|
+
}>;
|
|
46
|
+
/**
|
|
47
|
+
* Permanently deletes ( revokes ) a token.
|
|
48
|
+
*
|
|
49
|
+
* @param token - {@link AccessToken} instance or its uuid
|
|
50
|
+
*/
|
|
51
|
+
export declare function deleteToken(token: AccessToken | string): Promise<void>;
|
|
52
|
+
/**
|
|
53
|
+
* Adds a role to a token. The role must be held by the token owner.
|
|
54
|
+
*
|
|
55
|
+
* @param token - {@link AccessToken} instance or its uuid
|
|
56
|
+
* @param role - role name to grant
|
|
57
|
+
*/
|
|
58
|
+
export declare function grantTokenRole(token: AccessToken | string, role: string): Promise<AccessToken>;
|
|
59
|
+
/**
|
|
60
|
+
* Removes a role from a token.
|
|
61
|
+
*
|
|
62
|
+
* Not symmetric with {@link grantTokenRole}: revoking only ever narrows what a
|
|
63
|
+
* token may do, so it needs no owner check.
|
|
64
|
+
*
|
|
65
|
+
* A token must always keep at least one role - revoking the last one is
|
|
66
|
+
* refused, and full revocation means deleting the token. Beyond being
|
|
67
|
+
* meaningless ( a role-less token authorises nothing ), an empty list is
|
|
68
|
+
* actively corrupting: `@Set()` columns round-trip through `SqlSetConverter`,
|
|
69
|
+
* which stores `[]` as `''` and reads `''` back as `['']`. That phantom empty
|
|
70
|
+
* role then survives every later grant, permanently.
|
|
71
|
+
*
|
|
72
|
+
* @param token - {@link AccessToken} instance or its uuid
|
|
73
|
+
* @param role - role name to revoke
|
|
74
|
+
* @throws ErrorCode E_TOKEN_ROLE_NOT_ALLOWED when `role` is the token's last role
|
|
75
|
+
*/
|
|
76
|
+
export declare function revokeTokenRole(token: AccessToken | string, role: string): Promise<AccessToken>;
|
|
77
|
+
export interface ITokenValidationResult {
|
|
78
|
+
User: User;
|
|
79
|
+
Token: AccessToken;
|
|
80
|
+
/**
|
|
81
|
+
* Token roles that the owner still holds. Permission checks run with these.
|
|
82
|
+
*/
|
|
83
|
+
EffectiveRoles: string[];
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Validates a presented plaintext token.
|
|
87
|
+
*
|
|
88
|
+
* Checks, in order: token exists ( by hash ), token not expired, owner active /
|
|
89
|
+
* not soft-deleted / not banned, effective role intersection non-empty.
|
|
90
|
+
* Throws ErrorCode with {@link E_TOKEN_CODES} on every failure path.
|
|
91
|
+
*
|
|
92
|
+
* The owner checks are spelled out instead of reusing the `isActiveUser` query
|
|
93
|
+
* scope on purpose - a single "no such active user" result cannot say WHY, and
|
|
94
|
+
* callers need to tell an expired token from a banned account.
|
|
95
|
+
*
|
|
96
|
+
* @param plaintext - token exactly as presented by the client
|
|
97
|
+
*/
|
|
98
|
+
export declare function validateToken(plaintext: string): Promise<ITokenValidationResult>;
|
|
99
|
+
/**
|
|
100
|
+
* Deletes every token whose expiration has passed. Returns the deleted count.
|
|
101
|
+
*
|
|
102
|
+
* Intended for cyclic execution from a worker ( see `rbac:token-delete-expired` ).
|
|
103
|
+
* Expired tokens are already refused by {@link validateToken}, so this is
|
|
104
|
+
* housekeeping, not enforcement.
|
|
105
|
+
*/
|
|
106
|
+
export declare function deleteExpiredTokens(): Promise<number>;
|
|
107
|
+
/**
|
|
108
|
+
* Updates `LastUsedAt`, throttled: writes only when the stamp is absent or
|
|
109
|
+
* older than `intervalSeconds`.
|
|
110
|
+
*
|
|
111
|
+
* Every authenticated request would otherwise write to the token row; the stamp
|
|
112
|
+
* only needs to be accurate enough to spot unused tokens. Callers may
|
|
113
|
+
* fire-and-forget.
|
|
114
|
+
*
|
|
115
|
+
* @param token - token to stamp
|
|
116
|
+
* @param intervalSeconds - minimum time between two writes
|
|
117
|
+
*/
|
|
118
|
+
export declare function touchToken(token: AccessToken, intervalSeconds: number): Promise<void>;
|
|
119
|
+
//# sourceMappingURL=actions.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"actions.d.ts","sourceRoot":"","sources":["../../src/actions.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AACjC,OAAO,EAAE,IAAI,EAAE,MAAM,eAAe,CAAC;AAOrC,OAAO,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAItD;;;;;;;;;;GAUG;AACH,oBAAY,aAAa;IACvB,iBAAiB,IAAA;IACjB,eAAe,IAAA;IACf,qBAAqB,IAAA;IACrB,wBAAwB,IAAA;CACzB;AAED;;GAEG;AACH,wBAAgB,MAAM,CAAC,KAAK,EAAE,WAAW,GAAG,MAAM,GAAG,MAAM,OAAO,CAAC,WAAW,CAAC,CAM9E;AAED;;;GAGG;AACH,wBAAgB,MAAM,CAAC,IAAI,EAAE,IAAI,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC,CAUxE;AA2BD;;;;;;;;;;;GAWG;AACH,wBAAsB,WAAW,CAAC,IAAI,EAAE,IAAI,GAAG,MAAM,GAAG,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE,SAAS,EAAE,QAAQ,GAAG,IAAI,GAAG,OAAO,CAAC;IAAE,KAAK,EAAE,WAAW,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,CAAC,CA0B7K;AAED;;;;GAIG;AACH,wBAAsB,WAAW,CAAC,KAAK,EAAE,WAAW,GAAG,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAO5E;AAED;;;;;GAKG;AACH,wBAAsB,cAAc,CAAC,KAAK,EAAE,WAAW,GAAG,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CAcpG;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAsB,eAAe,CAAC,KAAK,EAAE,WAAW,GAAG,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CAmBrG;AAED,MAAM,WAAW,sBAAsB;IACrC,IAAI,EAAE,IAAI,CAAC;IACX,KAAK,EAAE,WAAW,CAAC;IAEnB;;OAEG;IACH,cAAc,EAAE,MAAM,EAAE,CAAC;CAC1B;AAED;;;;;;;;;;;;GAYG;AACH,wBAAsB,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,sBAAsB,CAAC,CA4CtF;AAED;;;;;;GAMG;AACH,wBAAsB,mBAAmB,IAAI,OAAO,CAAC,MAAM,CAAC,CAU3D;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,UAAU,CAAC,KAAK,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAS3F"}
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.E_TOKEN_CODES = void 0;
|
|
7
|
+
exports._token = _token;
|
|
8
|
+
exports._owner = _owner;
|
|
9
|
+
exports.createToken = createToken;
|
|
10
|
+
exports.deleteToken = deleteToken;
|
|
11
|
+
exports.grantTokenRole = grantTokenRole;
|
|
12
|
+
exports.revokeTokenRole = revokeTokenRole;
|
|
13
|
+
exports.validateToken = validateToken;
|
|
14
|
+
exports.deleteExpiredTokens = deleteExpiredTokens;
|
|
15
|
+
exports.touchToken = touchToken;
|
|
16
|
+
const lodash_1 = __importDefault(require("lodash"));
|
|
17
|
+
const luxon_1 = require("luxon");
|
|
18
|
+
const rbac_1 = require("@spinajs/rbac");
|
|
19
|
+
const util_1 = require("@spinajs/util");
|
|
20
|
+
const configuration_1 = require("@spinajs/configuration");
|
|
21
|
+
const queue_1 = require("@spinajs/queue");
|
|
22
|
+
const exceptions_1 = require("@spinajs/exceptions");
|
|
23
|
+
const AccessToken_js_1 = require("./models/AccessToken.js");
|
|
24
|
+
const interfaces_js_1 = require("./interfaces.js");
|
|
25
|
+
const index_js_1 = require("./events/index.js");
|
|
26
|
+
/**
|
|
27
|
+
* Failure codes carried by every {@link ErrorCode} this module throws.
|
|
28
|
+
*
|
|
29
|
+
* Named `E_TOKEN_CODES` rather than the shorter `E_CODES` on purpose:
|
|
30
|
+
* `@spinajs/rbac` exports an enum called `E_CODES` too, and both are re-exported
|
|
31
|
+
* from their package index. A downstream barrel that does
|
|
32
|
+
* `export * from '@spinajs/rbac'; export * from '@spinajs/rbac-http-token';`
|
|
33
|
+
* would collide on the NAME while the members underneath carry different
|
|
34
|
+
* numeric values - the kind of clash that resolves silently in a barrel and
|
|
35
|
+
* makes an `err.code === E_CODES.X` comparison answer the wrong question.
|
|
36
|
+
*/
|
|
37
|
+
var E_TOKEN_CODES;
|
|
38
|
+
(function (E_TOKEN_CODES) {
|
|
39
|
+
E_TOKEN_CODES[E_TOKEN_CODES["E_TOKEN_NOT_FOUND"] = 0] = "E_TOKEN_NOT_FOUND";
|
|
40
|
+
E_TOKEN_CODES[E_TOKEN_CODES["E_TOKEN_EXPIRED"] = 1] = "E_TOKEN_EXPIRED";
|
|
41
|
+
E_TOKEN_CODES[E_TOKEN_CODES["E_TOKEN_OWNER_INVALID"] = 2] = "E_TOKEN_OWNER_INVALID";
|
|
42
|
+
E_TOKEN_CODES[E_TOKEN_CODES["E_TOKEN_ROLE_NOT_ALLOWED"] = 3] = "E_TOKEN_ROLE_NOT_ALLOWED";
|
|
43
|
+
})(E_TOKEN_CODES || (exports.E_TOKEN_CODES = E_TOKEN_CODES = {}));
|
|
44
|
+
/**
|
|
45
|
+
* Resolves an AccessToken from an instance or its uuid.
|
|
46
|
+
*/
|
|
47
|
+
function _token(token) {
|
|
48
|
+
if (lodash_1.default.isString(token)) {
|
|
49
|
+
return () => AccessToken_js_1.AccessToken.where('Uuid', token).firstOrFail();
|
|
50
|
+
}
|
|
51
|
+
return () => Promise.resolve(token);
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Resolves the owning user by instance, numeric id or uuid, with metadata
|
|
55
|
+
* populated ( needed for IsBanned checks downstream ).
|
|
56
|
+
*/
|
|
57
|
+
function _owner(user) {
|
|
58
|
+
if (lodash_1.default.isString(user)) {
|
|
59
|
+
return () => rbac_1.User.where('Uuid', user).populate('Metadata').firstOrFail();
|
|
60
|
+
}
|
|
61
|
+
if (lodash_1.default.isNumber(user)) {
|
|
62
|
+
return () => rbac_1.User.where('Id', user).populate('Metadata').firstOrFail();
|
|
63
|
+
}
|
|
64
|
+
return () => Promise.resolve(user);
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Emits a token-related event through the queue service and forwards the token.
|
|
68
|
+
*
|
|
69
|
+
* @param event - constructor of the {@link AccessTokenEvent} subclass to emit
|
|
70
|
+
* @param args - additional arguments forwarded to the event constructor
|
|
71
|
+
*/
|
|
72
|
+
function _token_ev(event, ...args) {
|
|
73
|
+
return async (t) => {
|
|
74
|
+
await (0, queue_1._ev)(new event(t, ...args))();
|
|
75
|
+
return t;
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Ensures every role in `roles` is currently held by `owner`.
|
|
80
|
+
* A token must never carry a role its owner does not have.
|
|
81
|
+
*/
|
|
82
|
+
function _assert_roles_subset(owner, roles) {
|
|
83
|
+
const missing = roles.filter((r) => !owner.Role.includes(r));
|
|
84
|
+
if (missing.length !== 0) {
|
|
85
|
+
throw new exceptions_1.ErrorCode(E_TOKEN_CODES.E_TOKEN_ROLE_NOT_ALLOWED, `Owner does not hold role(s): ${missing.join(', ')}`, { roles: missing });
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Creates a new access token for a user.
|
|
90
|
+
*
|
|
91
|
+
* Roles must be a non-empty subset of the owner's current roles. `expiresAt`
|
|
92
|
+
* null means the token never expires. The plaintext is returned once and
|
|
93
|
+
* never persisted - only its hash is stored.
|
|
94
|
+
*
|
|
95
|
+
* @param user - owner: {@link User} instance, numeric id or uuid
|
|
96
|
+
* @param name - human readable label
|
|
97
|
+
* @param roles - roles carried by the token, subset of the owner's roles
|
|
98
|
+
* @param expiresAt - absolute expiration, or null for a token that never expires
|
|
99
|
+
*/
|
|
100
|
+
async function createToken(user, name, roles, expiresAt) {
|
|
101
|
+
name = (0, util_1._check_arg)((0, util_1._trim)(), (0, util_1._non_empty)(), (0, util_1._max_length)(128))(name, 'name');
|
|
102
|
+
roles = (0, util_1._check_arg)((0, util_1._non_nil)(), (0, util_1._non_empty)())(roles, 'roles');
|
|
103
|
+
const generator = await (0, configuration_1._service)('rbac.token.generation', interfaces_js_1.AccessTokenGenerationProvider)();
|
|
104
|
+
const generated = await generator.generate();
|
|
105
|
+
return (0, util_1._chain)(_owner(user), (0, util_1._tap)(async (u) => _assert_roles_subset(u, roles)), async (u) => {
|
|
106
|
+
const token = new AccessToken_js_1.AccessToken({
|
|
107
|
+
Name: name,
|
|
108
|
+
Token: generated.Hash,
|
|
109
|
+
Roles: lodash_1.default.uniq(roles),
|
|
110
|
+
// `ExpiresAt` is optional rather than nullable ( see the model ), so a
|
|
111
|
+
// "never expires" token leaves the property unset instead of holding null.
|
|
112
|
+
ExpiresAt: expiresAt ?? undefined,
|
|
113
|
+
user_id: u.Id,
|
|
114
|
+
});
|
|
115
|
+
await token.insert();
|
|
116
|
+
return token;
|
|
117
|
+
}, _token_ev(index_js_1.AccessTokenCreated), (t) => ({ Token: t, Plaintext: generated.Plaintext }));
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Permanently deletes ( revokes ) a token.
|
|
121
|
+
*
|
|
122
|
+
* @param token - {@link AccessToken} instance or its uuid
|
|
123
|
+
*/
|
|
124
|
+
async function deleteToken(token) {
|
|
125
|
+
return (0, util_1._chain)(_token(token), (0, util_1._tap)((t) => t.destroy()), _token_ev(index_js_1.AccessTokenDeleted), () => undefined);
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Adds a role to a token. The role must be held by the token owner.
|
|
129
|
+
*
|
|
130
|
+
* @param token - {@link AccessToken} instance or its uuid
|
|
131
|
+
* @param role - role name to grant
|
|
132
|
+
*/
|
|
133
|
+
async function grantTokenRole(token, role) {
|
|
134
|
+
role = (0, util_1._check_arg)((0, util_1._trim)(), (0, util_1._non_empty)())(role, 'role');
|
|
135
|
+
return (0, util_1._chain)(_token(token), (0, util_1._tap)(async (t) => {
|
|
136
|
+
const owner = await _owner(t.user_id)();
|
|
137
|
+
_assert_roles_subset(owner, [role]);
|
|
138
|
+
t.Roles = lodash_1.default.uniq([...t.Roles, role]);
|
|
139
|
+
await t.update();
|
|
140
|
+
}), _token_ev(index_js_1.AccessTokenRoleGranted, role));
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Removes a role from a token.
|
|
144
|
+
*
|
|
145
|
+
* Not symmetric with {@link grantTokenRole}: revoking only ever narrows what a
|
|
146
|
+
* token may do, so it needs no owner check.
|
|
147
|
+
*
|
|
148
|
+
* A token must always keep at least one role - revoking the last one is
|
|
149
|
+
* refused, and full revocation means deleting the token. Beyond being
|
|
150
|
+
* meaningless ( a role-less token authorises nothing ), an empty list is
|
|
151
|
+
* actively corrupting: `@Set()` columns round-trip through `SqlSetConverter`,
|
|
152
|
+
* which stores `[]` as `''` and reads `''` back as `['']`. That phantom empty
|
|
153
|
+
* role then survives every later grant, permanently.
|
|
154
|
+
*
|
|
155
|
+
* @param token - {@link AccessToken} instance or its uuid
|
|
156
|
+
* @param role - role name to revoke
|
|
157
|
+
* @throws ErrorCode E_TOKEN_ROLE_NOT_ALLOWED when `role` is the token's last role
|
|
158
|
+
*/
|
|
159
|
+
async function revokeTokenRole(token, role) {
|
|
160
|
+
role = (0, util_1._check_arg)((0, util_1._trim)(), (0, util_1._non_empty)())(role, 'role');
|
|
161
|
+
return (0, util_1._chain)(_token(token), (0, util_1._tap)(async (t) => {
|
|
162
|
+
const remaining = t.Roles.filter((r) => r !== role);
|
|
163
|
+
// checked BEFORE mutating, so a refused revoke leaves the instance and
|
|
164
|
+
// the row exactly as they were
|
|
165
|
+
if (remaining.length === 0) {
|
|
166
|
+
throw new exceptions_1.ErrorCode(E_TOKEN_CODES.E_TOKEN_ROLE_NOT_ALLOWED, 'Cannot revoke the last role from a token - delete the token instead', { token: t.Uuid });
|
|
167
|
+
}
|
|
168
|
+
t.Roles = remaining;
|
|
169
|
+
await t.update();
|
|
170
|
+
}), _token_ev(index_js_1.AccessTokenRoleRevoked, role));
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Validates a presented plaintext token.
|
|
174
|
+
*
|
|
175
|
+
* Checks, in order: token exists ( by hash ), token not expired, owner active /
|
|
176
|
+
* not soft-deleted / not banned, effective role intersection non-empty.
|
|
177
|
+
* Throws ErrorCode with {@link E_TOKEN_CODES} on every failure path.
|
|
178
|
+
*
|
|
179
|
+
* The owner checks are spelled out instead of reusing the `isActiveUser` query
|
|
180
|
+
* scope on purpose - a single "no such active user" result cannot say WHY, and
|
|
181
|
+
* callers need to tell an expired token from a banned account.
|
|
182
|
+
*
|
|
183
|
+
* @param plaintext - token exactly as presented by the client
|
|
184
|
+
*/
|
|
185
|
+
async function validateToken(plaintext) {
|
|
186
|
+
// Degenerate input is a failed authentication like any other, so it has to
|
|
187
|
+
// leave through the same door. `_check_arg(_trim(), _non_empty())` would
|
|
188
|
+
// throw a raw InvalidArgument here - and a bare TypeError for nil, since
|
|
189
|
+
// `_non_empty` dereferences `.length` - breaking the "always an ErrorCode"
|
|
190
|
+
// contract this function is called under. Reported as E_TOKEN_NOT_FOUND with
|
|
191
|
+
// the unknown-token message on purpose: a caller must not learn WHY.
|
|
192
|
+
if (!lodash_1.default.isString(plaintext) || plaintext.trim().length === 0) {
|
|
193
|
+
throw new exceptions_1.ErrorCode(E_TOKEN_CODES.E_TOKEN_NOT_FOUND, 'Access token not found');
|
|
194
|
+
}
|
|
195
|
+
plaintext = plaintext.trim();
|
|
196
|
+
const generator = await (0, configuration_1._service)('rbac.token.generation', interfaces_js_1.AccessTokenGenerationProvider)();
|
|
197
|
+
const hash = generator.hash(plaintext);
|
|
198
|
+
const token = await AccessToken_js_1.AccessToken.where('Token', hash).first();
|
|
199
|
+
if (!token) {
|
|
200
|
+
throw new exceptions_1.ErrorCode(E_TOKEN_CODES.E_TOKEN_NOT_FOUND, 'Access token not found');
|
|
201
|
+
}
|
|
202
|
+
if (token.IsExpired) {
|
|
203
|
+
throw new exceptions_1.ErrorCode(E_TOKEN_CODES.E_TOKEN_EXPIRED, 'Access token expired', { token: token.Uuid });
|
|
204
|
+
}
|
|
205
|
+
// Metadata carries the ban flag, so it must be populated - without it
|
|
206
|
+
// `IsBanned` silently answers false and every banned owner authenticates.
|
|
207
|
+
const owner = await rbac_1.User.where('Id', token.user_id).populate('Metadata').first();
|
|
208
|
+
// `DeletedAt` is defence in depth: the orm's soft-delete scope already appends
|
|
209
|
+
// `DeletedAt IS NULL` to User selects, so a deleted owner arrives as undefined.
|
|
210
|
+
if (!owner || !owner.IsActive || owner.DeletedAt || owner.IsBanned) {
|
|
211
|
+
throw new exceptions_1.ErrorCode(E_TOKEN_CODES.E_TOKEN_OWNER_INVALID, 'Access token owner is not allowed to authenticate', { token: token.Uuid });
|
|
212
|
+
}
|
|
213
|
+
// roles are re-intersected on every request rather than trusted from the row:
|
|
214
|
+
// a role revoked on the user must take effect immediately, without having to
|
|
215
|
+
// hunt down every token that still carries it
|
|
216
|
+
const effective = token.Roles.filter((r) => owner.Role.includes(r));
|
|
217
|
+
if (effective.length === 0) {
|
|
218
|
+
throw new exceptions_1.ErrorCode(E_TOKEN_CODES.E_TOKEN_ROLE_NOT_ALLOWED, 'Access token has no effective roles', { token: token.Uuid });
|
|
219
|
+
}
|
|
220
|
+
return { User: owner, Token: token, EffectiveRoles: effective };
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Deletes every token whose expiration has passed. Returns the deleted count.
|
|
224
|
+
*
|
|
225
|
+
* Intended for cyclic execution from a worker ( see `rbac:token-delete-expired` ).
|
|
226
|
+
* Expired tokens are already refused by {@link validateToken}, so this is
|
|
227
|
+
* housekeeping, not enforcement.
|
|
228
|
+
*/
|
|
229
|
+
async function deleteExpiredTokens() {
|
|
230
|
+
// `whereNotNull` is not redundant next to `<=`: a null expiry means "never
|
|
231
|
+
// expires", and null comparisons must not sweep those rows away.
|
|
232
|
+
const expired = await AccessToken_js_1.AccessToken.where('ExpiresAt', '<=', luxon_1.DateTime.now()).whereNotNull('ExpiresAt');
|
|
233
|
+
for (const t of expired) {
|
|
234
|
+
await t.destroy();
|
|
235
|
+
}
|
|
236
|
+
return expired.length;
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Updates `LastUsedAt`, throttled: writes only when the stamp is absent or
|
|
240
|
+
* older than `intervalSeconds`.
|
|
241
|
+
*
|
|
242
|
+
* Every authenticated request would otherwise write to the token row; the stamp
|
|
243
|
+
* only needs to be accurate enough to spot unused tokens. Callers may
|
|
244
|
+
* fire-and-forget.
|
|
245
|
+
*
|
|
246
|
+
* @param token - token to stamp
|
|
247
|
+
* @param intervalSeconds - minimum time between two writes
|
|
248
|
+
*/
|
|
249
|
+
async function touchToken(token, intervalSeconds) {
|
|
250
|
+
const now = luxon_1.DateTime.now();
|
|
251
|
+
if (token.LastUsedAt && token.LastUsedAt > now.minus({ seconds: intervalSeconds })) {
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
token.LastUsedAt = now;
|
|
255
|
+
await token.update();
|
|
256
|
+
}
|
|
257
|
+
//# sourceMappingURL=actions.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"actions.js","sourceRoot":"","sources":["../../src/actions.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Log } from '@spinajs/log';
|
|
2
|
+
import { CliCommand } from '@spinajs/cli';
|
|
3
|
+
interface ICreateTokenOptions {
|
|
4
|
+
name: string;
|
|
5
|
+
roles: string;
|
|
6
|
+
expires?: string;
|
|
7
|
+
}
|
|
8
|
+
export declare class CreateToken extends CliCommand {
|
|
9
|
+
protected Log: Log;
|
|
10
|
+
execute(userIdOrUuid: string, options: ICreateTokenOptions): Promise<void>;
|
|
11
|
+
}
|
|
12
|
+
export {};
|
|
13
|
+
//# sourceMappingURL=CreateToken.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CreateToken.d.ts","sourceRoot":"","sources":["../../../src/cli/CreateToken.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAU,MAAM,cAAc,CAAC;AAC3C,OAAO,EAAY,UAAU,EAAmB,MAAM,cAAc,CAAC;AAKrE,UAAU,mBAAmB;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,qBAKa,WAAY,SAAQ,UAAU;IAEzC,SAAS,CAAC,GAAG,EAAE,GAAG,CAAC;IAEN,OAAO,CAAC,YAAY,EAAE,MAAM,EAAE,OAAO,EAAE,mBAAmB,GAAG,OAAO,CAAC,IAAI,CAAC,CA0BtF;CACF"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
9
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.CreateToken = void 0;
|
|
13
|
+
const log_1 = require("@spinajs/log");
|
|
14
|
+
const cli_1 = require("@spinajs/cli");
|
|
15
|
+
const luxon_1 = require("luxon");
|
|
16
|
+
const actions_js_1 = require("../actions.js");
|
|
17
|
+
let CreateToken = class CreateToken extends cli_1.CliCommand {
|
|
18
|
+
async execute(userIdOrUuid, options) {
|
|
19
|
+
try {
|
|
20
|
+
// Nil means the flag was not given at all - a token that never expires.
|
|
21
|
+
// Anything else, empty string included, is a value the user meant to be a
|
|
22
|
+
// date and must be validated: a truthiness check here would quietly turn
|
|
23
|
+
// `--expires ""` into an infinite token.
|
|
24
|
+
const expiresAt = options.expires === undefined || options.expires === null ? null : luxon_1.DateTime.fromISO(options.expires);
|
|
25
|
+
if (expiresAt && !expiresAt.isValid) {
|
|
26
|
+
this.Log.error(`Invalid --expires value: ${options.expires}`);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
const roles = options.roles
|
|
30
|
+
.split(',')
|
|
31
|
+
.map((r) => r.trim())
|
|
32
|
+
.filter((r) => r.length > 0);
|
|
33
|
+
const owner = /^\d+$/.test(userIdOrUuid) ? parseInt(userIdOrUuid, 10) : userIdOrUuid;
|
|
34
|
+
const { Token, Plaintext } = await (0, actions_js_1.createToken)(owner, options.name, roles, expiresAt);
|
|
35
|
+
this.Log.success(`Token created: ${Token.Uuid}`);
|
|
36
|
+
// The single moment the plaintext exists outside the caller's hands.
|
|
37
|
+
this.Log.success(`Token ( copy now, it will not be shown again ): ${Plaintext}`);
|
|
38
|
+
}
|
|
39
|
+
catch (e) {
|
|
40
|
+
this.Log.error(`Error while creating token: ${e.message}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
exports.CreateToken = CreateToken;
|
|
45
|
+
__decorate([
|
|
46
|
+
(0, log_1.Logger)('rbac-http-token'),
|
|
47
|
+
__metadata("design:type", log_1.Log)
|
|
48
|
+
], CreateToken.prototype, "Log", void 0);
|
|
49
|
+
exports.CreateToken = CreateToken = __decorate([
|
|
50
|
+
(0, cli_1.Command)('rbac:token-create', 'Creates an access token for a user'),
|
|
51
|
+
(0, cli_1.Argument)('userIdOrUuid', true, 'numeric id or uuid of the owner'),
|
|
52
|
+
(0, cli_1.Option)('-n, --name <name>', true, 'token label'),
|
|
53
|
+
(0, cli_1.Option)('-r, --roles <roles>', true, 'token roles, comma separated, must be subset of owner roles'),
|
|
54
|
+
(0, cli_1.Option)('-e, --expires <expires>', false, 'ISO expiration instant; omit for a token that never expires')
|
|
55
|
+
], CreateToken);
|
|
56
|
+
//# sourceMappingURL=CreateToken.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CreateToken.js","sourceRoot":"","sources":["../../../src/cli/CreateToken.ts"],"names":[],"mappings":";;;;;;;;;;;;AAAA,sCAA2C;AAC3C,sCAAqE;AACrE,iCAAiC;AAEjC,8CAA4C;AAarC,IAAM,WAAW,GAAjB,MAAM,WAAY,SAAQ,gBAAU;IAIlC,KAAK,CAAC,OAAO,CAAC,YAAoB,EAAE,OAA4B;QACrE,IAAI,CAAC;YACH,wEAAwE;YACxE,0EAA0E;YAC1E,yEAAyE;YACzE,yCAAyC;YACzC,MAAM,SAAS,GAAG,OAAO,CAAC,OAAO,KAAK,SAAS,IAAI,OAAO,CAAC,OAAO,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,gBAAQ,CAAC,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;YACvH,IAAI,SAAS,IAAI,CAAC,SAAS,CAAC,OAAO,EAAE,CAAC;gBACpC,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,4BAA4B,OAAO,CAAC,OAAO,EAAE,CAAC,CAAC;gBAC9D,OAAO;YACT,CAAC;YAED,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK;iBACxB,KAAK,CAAC,GAAG,CAAC;iBACV,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;iBACpB,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;YAC/B,MAAM,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,YAAY,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC;YAErF,MAAM,EAAE,KAAK,EAAE,SAAS,EAAE,GAAG,MAAM,IAAA,wBAAW,EAAC,KAAK,EAAE,OAAO,CAAC,IAAI,EAAE,KAAK,EAAE,SAAS,CAAC,CAAC;YAEtF,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,kBAAkB,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC;YACjD,qEAAqE;YACrE,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,mDAAmD,SAAS,EAAE,CAAC,CAAC;QACnF,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACX,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,+BAAgC,CAAW,CAAC,OAAO,EAAE,CAAC,CAAC;QACxE,CAAC;IACH,CAAC;CACF,CAAA;;AA7BW;IADT,IAAA,YAAM,EAAC,iBAAiB,CAAC;8BACX,SAAG;wCAAC;sBAFR,WAAW;IALvB,IAAA,aAAO,EAAC,mBAAmB,EAAE,oCAAoC,CAAC;IAClE,IAAA,cAAQ,EAAC,cAAc,EAAE,IAAI,EAAE,iCAAiC,CAAC;IACjE,IAAA,YAAM,EAAC,mBAAmB,EAAE,IAAI,EAAE,aAAa,CAAC;IAChD,IAAA,YAAM,EAAC,qBAAqB,EAAE,IAAI,EAAE,6DAA6D,CAAC;IAClG,IAAA,YAAM,EAAC,yBAAyB,EAAE,KAAK,EAAE,6DAA6D,CAAC;GAC3F,WAAW,CA+BvB"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Log } from '@spinajs/log';
|
|
2
|
+
import { CliCommand } from '@spinajs/cli';
|
|
3
|
+
/**
|
|
4
|
+
* Intended for cyclic execution from a worker process
|
|
5
|
+
* ( eg. cron / task scheduler ) to keep the token table clean.
|
|
6
|
+
*/
|
|
7
|
+
export declare class DeleteExpiredTokens extends CliCommand {
|
|
8
|
+
protected Log: Log;
|
|
9
|
+
execute(): Promise<void>;
|
|
10
|
+
}
|
|
11
|
+
//# sourceMappingURL=DeleteExpiredTokens.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"DeleteExpiredTokens.d.ts","sourceRoot":"","sources":["../../../src/cli/DeleteExpiredTokens.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAU,MAAM,cAAc,CAAC;AAC3C,OAAO,EAAE,UAAU,EAAW,MAAM,cAAc,CAAC;AAInD;;;GAGG;AACH,qBACa,mBAAoB,SAAQ,UAAU;IAEjD,SAAS,CAAC,GAAG,EAAE,GAAG,CAAC;IAEN,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAOpC;CACF"}
|