@owlmeans/server-auth-token 0.1.18-rc.8 → 0.1.18
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 +177 -1
- package/agent-meta/manifest.json +3 -3
- package/agent-meta/skills/server-auth-token/SKILL.md +33 -10
- package/build/guard.d.ts +1 -3
- package/build/guard.d.ts.map +1 -1
- package/build/guard.js +37 -14
- package/build/guard.js.map +1 -1
- package/build/handlers/create.d.ts +5 -0
- package/build/handlers/create.d.ts.map +1 -0
- package/build/handlers/create.js +18 -0
- package/build/handlers/create.js.map +1 -0
- package/build/handlers/index.d.ts +11 -11
- package/build/handlers/index.d.ts.map +1 -1
- package/build/handlers/index.js +8 -93
- package/build/handlers/index.js.map +1 -1
- package/build/handlers/issue.d.ts +6 -0
- package/build/handlers/issue.d.ts.map +1 -0
- package/build/handlers/issue.js +42 -0
- package/build/handlers/issue.js.map +1 -0
- package/build/handlers/list.d.ts +3 -0
- package/build/handlers/list.d.ts.map +1 -0
- package/build/handlers/list.js +18 -0
- package/build/handlers/list.js.map +1 -0
- package/build/handlers/refuse.d.ts +19 -0
- package/build/handlers/refuse.d.ts.map +1 -0
- package/build/handlers/refuse.js +20 -0
- package/build/handlers/refuse.js.map +1 -0
- package/build/handlers/revoke.d.ts +17 -0
- package/build/handlers/revoke.d.ts.map +1 -0
- package/build/handlers/revoke.js +35 -0
- package/build/handlers/revoke.js.map +1 -0
- package/build/handlers/types.d.ts +37 -0
- package/build/handlers/types.d.ts.map +1 -0
- package/build/handlers/types.js +2 -0
- package/build/handlers/types.js.map +1 -0
- package/build/handlers/utils/types.d.ts +10 -0
- package/build/handlers/utils/types.d.ts.map +1 -0
- package/build/handlers/utils/types.js +2 -0
- package/build/handlers/utils/types.js.map +1 -0
- package/build/handlers/utils.d.ts +4 -0
- package/build/handlers/utils.d.ts.map +1 -0
- package/build/handlers/utils.js +11 -0
- package/build/handlers/utils.js.map +1 -0
- package/build/hash/types.d.ts +16 -0
- package/build/hash/types.d.ts.map +1 -0
- package/build/hash/types.js +2 -0
- package/build/hash/types.js.map +1 -0
- package/build/hash.d.ts +4 -15
- package/build/hash.d.ts.map +1 -1
- package/build/hash.js +13 -16
- package/build/hash.js.map +1 -1
- package/build/index.d.ts +3 -0
- package/build/index.d.ts.map +1 -1
- package/build/index.js +1 -0
- package/build/index.js.map +1 -1
- package/build/prefix.d.ts +4 -0
- package/build/prefix.d.ts.map +1 -0
- package/build/prefix.js +4 -0
- package/build/prefix.js.map +1 -0
- package/build/types.d.ts +20 -0
- package/build/types.d.ts.map +1 -1
- package/package.json +17 -16
- package/src/guard.ts +39 -16
- package/src/handlers/create.ts +21 -0
- package/src/handlers/index.ts +17 -115
- package/src/handlers/issue.ts +50 -0
- package/src/handlers/list.ts +21 -0
- package/src/handlers/refuse.ts +20 -0
- package/src/handlers/revoke.ts +42 -0
- package/src/handlers/types.ts +40 -0
- package/src/handlers/utils/types.ts +10 -0
- package/src/handlers/utils.ts +18 -0
- package/src/hash/types.ts +16 -0
- package/src/hash.ts +17 -21
- package/src/index.ts +3 -0
- package/src/prefix.ts +6 -0
- package/src/types.ts +21 -0
- package/tests/guard.spec.ts +54 -10
- package/tests/handlers.spec.ts +56 -8
package/README.md
CHANGED
|
@@ -1,3 +1,179 @@
|
|
|
1
|
+
# @owlmeans/server-auth-token
|
|
2
|
+
|
|
3
|
+
The server half of long-lived access tokens: a MongoDB token store, a guard that verifies a
|
|
4
|
+
presented token and turns it into the same `Auth` payload a browser session produces, the
|
|
5
|
+
list / create / revoke handlers, a shared issuer for other minting paths (an OAuth token endpoint),
|
|
6
|
+
and a coguard helper that admits a token on every already-guarded route. A backend uses it when API
|
|
7
|
+
clients, CLIs or coding agents should call the same routes a signed-in person does. The token
|
|
8
|
+
format, routes and record types live in `@owlmeans/auth-token`; the browser management panel is
|
|
9
|
+
`@owlmeans/web-auth-token`; issuing tokens through an OAuth grant is `@owlmeans/server-oauth`, which
|
|
10
|
+
calls this package's issuer.
|
|
11
|
+
|
|
12
|
+
## Installation
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
bun add @owlmeans/server-auth-token@^0.1.18
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Concepts
|
|
19
|
+
|
|
20
|
+
- **Access token** — `<prefix><base58 of 24 random bytes>`; the prefix defaults to `owl_`. The
|
|
21
|
+
plaintext is returned once at creation; the store keeps only its SHA-256 `hash` and a short
|
|
22
|
+
`display` form.
|
|
23
|
+
- **Guard, not authentication** — the guard verifies what arrives and resolves an `Auth` with
|
|
24
|
+
`type: AuthroizationType.AuthToken`; `authenticated()` always returns `null`, nothing is exchanged.
|
|
25
|
+
- **Prefix claim** — the guard matches only an `Authorization` value with the `AUTH-TOKEN` or
|
|
26
|
+
`Bearer` scheme that carries the deployment's prefix, so it coexists with other bearer guards.
|
|
27
|
+
- **Scope intersection** — on every request the token's scopes are intersected with its identity
|
|
28
|
+
profile's; a missing or expired profile refuses the token.
|
|
29
|
+
- **Audience** — a token minted through OAuth carries `audience`; a guard configured with
|
|
30
|
+
`resources` refuses a token whose audience does not intersect them. Without `resources`, every
|
|
31
|
+
token is admitted.
|
|
32
|
+
- **Deny list** — `denyAliases` names routes the guard never claims, so an interactive-only route
|
|
33
|
+
stays a 401 for a token even when the guard is inherited.
|
|
34
|
+
|
|
35
|
+
## Usage
|
|
36
|
+
|
|
37
|
+
### Register the store and the guard
|
|
38
|
+
|
|
39
|
+
```typescript
|
|
40
|
+
import { GUARD_AUTH_TOKEN } from '@owlmeans/auth-token'
|
|
41
|
+
import { AUTH_IDENTITY_DB_ALIAS } from '@owlmeans/server-auth-identity'
|
|
42
|
+
import { appendAuthTokenGuard, appendAuthTokenResources } from '@owlmeans/server-auth-token'
|
|
43
|
+
|
|
44
|
+
appendAuthTokenResources(context, AUTH_IDENTITY_DB_ALIAS) // same dbAlias as the identity resources
|
|
45
|
+
appendAuthTokenGuard(context, GUARD_AUTH_TOKEN, {
|
|
46
|
+
prefix: 'vib_', // also written to cfg.authToken.prefix
|
|
47
|
+
denyAliases: [protocols.tokens.create.alias, protocols.tokens.revoke.alias, checkoutProtocol.alias],
|
|
48
|
+
})
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Bind the token routes
|
|
52
|
+
|
|
53
|
+
```typescript
|
|
54
|
+
import { makeAuthTokenEntrypoints } from '@owlmeans/auth-token'
|
|
55
|
+
import { bind } from '@owlmeans/server-entrypoint'
|
|
56
|
+
import { createAccessToken, listAccessTokens, revokeAccessToken } from '@owlmeans/server-auth-token'
|
|
57
|
+
|
|
58
|
+
const tokens = makeAuthTokenEntrypoints({ parent: account.base })
|
|
59
|
+
|
|
60
|
+
context.registerEntrypoints([
|
|
61
|
+
bind(tokens.list, listAccessTokens(tokens.list)),
|
|
62
|
+
bind(tokens.create, createAccessToken(tokens.create)),
|
|
63
|
+
bind(tokens.revoke, revokeAccessToken(tokens.revoke)),
|
|
64
|
+
])
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
`revokeAccessToken(protocol, { allowAccessTokens: true })` lets a token revoke the same person's
|
|
68
|
+
tokens (for an agent-facing surface); ownership is still checked and minting stays refused.
|
|
69
|
+
|
|
70
|
+
### Admit tokens on every guarded route
|
|
71
|
+
|
|
72
|
+
```typescript
|
|
73
|
+
import { withAuthTokenCoguard } from '@owlmeans/server-auth-token'
|
|
74
|
+
|
|
75
|
+
export const guardedProtocols = withAuthTokenCoguard(appProtocols) // before creating bindings
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
The coguard is appended after each protocol's existing guards; protocols without a guard are left
|
|
79
|
+
alone, and running it twice changes nothing.
|
|
80
|
+
|
|
81
|
+
### Restrict a surface to tokens issued for it
|
|
82
|
+
|
|
83
|
+
```typescript
|
|
84
|
+
appendAuthTokenGuard(context, 'guard:mcp-token', {
|
|
85
|
+
resources: ctx => [mcpResourceUri(ctx)], // app function, resolved per request
|
|
86
|
+
})
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Mint from another flow
|
|
90
|
+
|
|
91
|
+
```typescript
|
|
92
|
+
import { accessTokenIssuerOf, refuseTokenAuth } from '@owlmeans/server-auth-token'
|
|
93
|
+
|
|
94
|
+
refuseTokenAuth(req, 'token-mint') // a token may never mint a token
|
|
95
|
+
const issued = await accessTokenIssuerOf(context).issueAccessToken(
|
|
96
|
+
{ entityId, userId, profileId, role, scopes: ownScopes },
|
|
97
|
+
{ name: 'MCP connector', scopes: ['*'], expiresIn: 3600, audience: [resourceUri] },
|
|
98
|
+
)
|
|
99
|
+
// issued.token is the plaintext, shown once; issued.record is the hash-free view
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## API
|
|
103
|
+
|
|
104
|
+
### Registration and guard
|
|
105
|
+
|
|
106
|
+
| Symbol | Purpose |
|
|
107
|
+
|---|---|
|
|
108
|
+
| `appendAuthTokenResources(ctx, dbAlias?)` | Registers `makeAccessTokenResource(dbAlias)` |
|
|
109
|
+
| `appendAuthTokenGuard(ctx, alias?, opts?)` | Registers the guard (default alias `GUARD_AUTH_TOKEN`); writes `opts.prefix` into `cfg.authToken.prefix` |
|
|
110
|
+
| `makeAuthTokenGuard(alias?, opts?)` | The `GuardService` |
|
|
111
|
+
| `makeAccessTokenResource(dbAlias?)` | Mongo resource at `AUTH_TOKEN_RESOURCE`, collection `access-token`; indexes `hash` (unique), `owner` (`entityId`, `profileId`), `expiresAt` (sparse) |
|
|
112
|
+
| `withAuthTokenCoguard(tree, guard?)` | Same-shaped protocol tree with the token guard appended to every guarded protocol |
|
|
113
|
+
| `prefixOf(context, opts?)` | Prefix from guard options, then `cfg.authToken.prefix`, then `owl_` |
|
|
114
|
+
|
|
115
|
+
### Handlers and issuance
|
|
116
|
+
|
|
117
|
+
| Symbol | Purpose |
|
|
118
|
+
|---|---|
|
|
119
|
+
| `listAccessTokens(protocol)` | The caller's tokens in the caller's organization entity, newest first, without hashes |
|
|
120
|
+
| `createAccessToken(protocol)` | Mints a token from the session; refuses token auth; scopes may only narrow |
|
|
121
|
+
| `revokeAccessToken(protocol, opts?)` | Sets `revokedAt` on the caller's own token; idempotent; a foreign id answers like an unknown one |
|
|
122
|
+
| `accessTokenIssuerOf(ctx).issueAccessToken(subject, request)` | The shared minting path; `expiresIn` is clamped to `AUTH_TOKEN_MAX_TTL` |
|
|
123
|
+
| `makeAccessTokenIssuer(ctx)` | Builds an `AccessTokenIssuer` (unmemoized) |
|
|
124
|
+
| `refuseTokenAuth(req, what)` | Throws `AuthForbidden(what)` when `req.auth.type` is an access token |
|
|
125
|
+
| `tokenHashHelper.hashAccessToken(token)` | SHA-256 hex — the stored form |
|
|
126
|
+
| `tokenHashHelper.mintAccessToken(prefix)` | `MintedToken` `{ token, hash, display }` |
|
|
127
|
+
| `createTokenHashHelper()` | Builds a `TokenHashHelper` |
|
|
128
|
+
| `issueAccessToken(ctx, subject, request)`, `hashAccessToken(token)` | Deprecated wrappers |
|
|
129
|
+
| `SERVER_AUTH_TOKEN` | `'server-auth-token'` — module alias for error locations |
|
|
130
|
+
|
|
131
|
+
### Types
|
|
132
|
+
|
|
133
|
+
| Symbol | Purpose |
|
|
134
|
+
|---|---|
|
|
135
|
+
| `AuthTokenGuardOptions` | `prefix?`, `denyAliases?`, `touchInterval?` (default 5 min), `resourceAlias?`, `profileAlias?`, `resources?` |
|
|
136
|
+
| `AuthTokenConfig` | `ServerConfig` with `authToken?: { prefix? }` |
|
|
137
|
+
| `AuthTokenContext` | `ServerContext<AuthTokenConfig>` |
|
|
138
|
+
| `AccessTokenResource` | `MongoResource<AccessTokenRecord>` |
|
|
139
|
+
| `IssueAccessTokenSubject` | `{ entityId, userId, profileId, role, scopes }` |
|
|
140
|
+
| `IssueAccessTokenRequest` | `CreateAccessToken` (`name`, `scopes?`, `expiresIn?`) plus `audience?` |
|
|
141
|
+
| `AccessTokenIssuer`, `RevokeAccessTokenOptions`, `MintedToken`, `TokenHashHelper` | Supporting types |
|
|
142
|
+
|
|
143
|
+
## Guard behaviour
|
|
144
|
+
|
|
145
|
+
A presented token is refused when it is unknown, revoked, expired, outside the configured
|
|
146
|
+
`resources`, or when its identity profile is missing, unreadable or expired. On success the payload
|
|
147
|
+
carries the token's **display** form (never the secret), the profile's `role`, the intersected
|
|
148
|
+
scopes, and an `entitySlug` resolved through `ENTITY_RESOLVER` when one is registered. `lastUsedAt`
|
|
149
|
+
is written fire-and-forget at most once per `touchInterval`. Refusals are logged once a minute per
|
|
150
|
+
token and reason, with the record id only.
|
|
151
|
+
|
|
152
|
+
## Common pitfalls
|
|
153
|
+
|
|
154
|
+
- Passing `prefix` only to a guard you build with `makeAuthTokenGuard` — the minting handler reads the
|
|
155
|
+
prefix from config, so the two disagree and every new token is a 401. Use `appendAuthTokenGuard`.
|
|
156
|
+
- Registering the token store on a different `dbAlias` than the identity resources.
|
|
157
|
+
- Forgetting the deny list — a coguard-admitted token then reaches routes that must stay
|
|
158
|
+
session-only (minting, checkout, starting an OAuth flow). A child route cannot drop an inherited
|
|
159
|
+
guard; the refusal belongs in `denyAliases`.
|
|
160
|
+
- Expecting a requested scope the caller lacks to be dropped — it is refused with
|
|
161
|
+
`AuthForbidden('scope')`.
|
|
162
|
+
- Treating `lastUsedAt` as an access log — it is throttled and best-effort.
|
|
163
|
+
- Decorating protocols with `withAuthTokenCoguard` after bindings were created.
|
|
164
|
+
|
|
165
|
+
The `server-auth-token` skill covers the guard, the store and the deny-list rule in depth; the
|
|
166
|
+
`auth-token` skill covers the format and routes.
|
|
167
|
+
|
|
168
|
+
## Related packages
|
|
169
|
+
|
|
170
|
+
- [`@owlmeans/auth-token`](../auth-token) — token format, routes, record types, constants
|
|
171
|
+
- [`@owlmeans/web-auth-token`](../web-auth-token) — the browser token management panel
|
|
172
|
+
- [`@owlmeans/server-oauth`](../server-oauth) — OAuth authorization server that mints audience-scoped tokens
|
|
173
|
+
- [`@owlmeans/server-auth-identity`](../server-auth-identity) — the identity profiles a token is bound to
|
|
174
|
+
- [`@owlmeans/mongo-resource`](../mongo-resource) — the token store's backend
|
|
175
|
+
- [`@owlmeans/server-api`](../server-api) — the `handlers()` builder the handlers use
|
|
176
|
+
|
|
1
177
|
<!-- owlmeans:agent-guidance:start -->
|
|
2
178
|
## Agent guidance
|
|
3
179
|
|
|
@@ -6,7 +182,7 @@ This package ships embedded agent skills under `agent-meta/`. After installing y
|
|
|
6
182
|
your project's skill store (`.agents/skills/`):
|
|
7
183
|
|
|
8
184
|
```sh
|
|
9
|
-
npx @owlmeans/agent-skills@^0.1.18
|
|
185
|
+
npx @owlmeans/agent-skills@^0.1.18
|
|
10
186
|
```
|
|
11
187
|
|
|
12
188
|
The embedded files are version-matched to this package release. Do not edit them
|
package/agent-meta/manifest.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
|
-
"schemaVersion":
|
|
2
|
+
"schemaVersion": 3,
|
|
3
3
|
"package": "@owlmeans/server-auth-token",
|
|
4
|
-
"version": "0.1.18
|
|
5
|
-
"generatedAt": "2026-
|
|
4
|
+
"version": "0.1.18",
|
|
5
|
+
"generatedAt": "2026-10-28T00:31:34.770Z",
|
|
6
6
|
"canonicalRepo": "https://github.com/owlmeans/common",
|
|
7
7
|
"entries": [
|
|
8
8
|
{
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: server-auth-token
|
|
3
|
-
description: How to use @owlmeans/server-auth-token — the server half of long-lived access tokens — the Mongo store, the guard that verifies a presented token and intersects its scopes with the profile's, the mint/list/revoke handlers, and the coguard that admits a token on every already-guarded route. Auto-invoked when registering the token guard or resources, mounting the token handlers, or diagnosing a 401 on a route an access token should reach.
|
|
3
|
+
description: How to use @owlmeans/server-auth-token — the server half of long-lived access tokens — the Mongo store, the guard that verifies a presented token and intersects its scopes with the profile's, the mint/list/revoke handlers, the shared `accessTokenIssuerOf(ctx).issueAccessToken`, audience admission for OAuth-issued tokens, and the coguard that admits a token on every already-guarded route. Auto-invoked when registering the token guard or resources, mounting the token handlers, or diagnosing a 401 on a route an access token should reach.
|
|
4
4
|
user-invocable: false
|
|
5
5
|
---
|
|
6
6
|
<!-- AUTO-GENERATED — do not edit. Regenerate via sync-agent-meta. -->
|
|
@@ -8,7 +8,7 @@ user-invocable: false
|
|
|
8
8
|
# @owlmeans/server-auth-token
|
|
9
9
|
|
|
10
10
|
**Layer:** Server
|
|
11
|
-
**Install:** `"@owlmeans/server-auth-token": "^0.1.18
|
|
11
|
+
**Install:** `"@owlmeans/server-auth-token": "^0.1.18"` in `dependencies`
|
|
12
12
|
**Contracts:** `@owlmeans/auth-token` — the record, the routes, the format helpers
|
|
13
13
|
|
|
14
14
|
## Key Exports
|
|
@@ -20,10 +20,12 @@ user-invocable: false
|
|
|
20
20
|
| `makeAuthTokenGuard(alias?, opts?)` | The guard itself |
|
|
21
21
|
| `makeAccessTokenResource(dbAlias?)` | The Mongo resource, with its three indexes |
|
|
22
22
|
| `prefixOf(context, opts?)` | The deployment's prefix: guard options → config → default |
|
|
23
|
-
| `hashAccessToken(token)` ·
|
|
23
|
+
| `tokenHashHelper` — `.hashAccessToken(token)` · `.mintAccessToken(prefix)` | The stored form; one minted `{ token, hash, display }` |
|
|
24
24
|
| `withAuthTokenCoguard(protocolTree, guard?)` | Return the same-shaped immutable tree with the guard appended to every already-guarded protocol |
|
|
25
|
-
| `listAccessTokens` · `createAccessToken` · `revokeAccessToken` | The three handlers |
|
|
26
|
-
| `
|
|
25
|
+
| `listAccessTokens(protocol)` · `createAccessToken(protocol)` · `revokeAccessToken(protocol, { allowAccessTokens? })` | The three handlers; only revoke takes an option (`RevokeAccessTokenOptions`) |
|
|
26
|
+
| `accessTokenIssuerOf(ctx).issueAccessToken(subject, request)` | Mint one token for `{ entityId, userId, profileId, role, scopes }` — what `createAccessToken` does over an HTTP body, and what an OAuth token endpoint calls over a session it verified itself. `request` is `CreateAccessToken` plus `audience?: string[]` |
|
|
27
|
+
| `refuseTokenAuth(req, what)` | The one "interactive session only" check — throws `AuthForbidden` when `req.auth.type` is an access token |
|
|
28
|
+
| `AuthTokenGuardOptions` (`prefix`, `denyAliases`, `touchInterval`, `resourceAlias`, `profileAlias`, `resources`) · `IssueAccessTokenSubject` · `IssueAccessTokenRequest` · `RevokeAccessTokenOptions` · `AuthTokenConfig` · `AccessTokenResource` | Types |
|
|
27
29
|
|
|
28
30
|
## Wiring
|
|
29
31
|
|
|
@@ -77,19 +79,39 @@ Three properties keep it from colliding with the guards beside it:
|
|
|
77
79
|
- `entitySlug` is resolved through `ENTITY_RESOLVER` when one is registered, so the payload is
|
|
78
80
|
identical to a browser session's rather than carrying a raw id.
|
|
79
81
|
|
|
82
|
+
**Audience admission is opt-in.** A token minted through an OAuth grant carries `audience` (the resources
|
|
83
|
+
it is FOR, RFC 8707). A guard configured with `resources` (`string[]` or `(ctx) => string[]`, resolved
|
|
84
|
+
per request, because `appendAuthTokenGuard` runs from `makeContext` before a secret-mounted hostname is
|
|
85
|
+
necessarily readable) refuses — as an ordinary non-match, a 401 — a token whose non-empty `audience`
|
|
86
|
+
does not intersect it. A token with **no** audience (every hand-minted one) is admitted everywhere its
|
|
87
|
+
scopes reach, and a guard with no `resources` admits every token whatever its audience, so a deployment
|
|
88
|
+
that never mints an audience-restricted token sees no change. Two guards over different surfaces are
|
|
89
|
+
how a token bound to `/mcp` stays out of the ordinary REST API while a REST-scoped token still works
|
|
90
|
+
through `/mcp`.
|
|
91
|
+
|
|
80
92
|
`lastUsedAt` is a **throttled, fire-and-forget** write (`touchInterval`, default 5 minutes). A usage
|
|
81
93
|
timestamp is never a reason to fail a request, and awaiting it would put a database write on the
|
|
82
94
|
critical path of every authenticated call. It answers one question — "is this token still in use?" —
|
|
83
95
|
and is not an access log.
|
|
84
96
|
|
|
85
|
-
## A token may never mint
|
|
97
|
+
## A token may never mint a token; it revokes one only where the deployment opts in
|
|
86
98
|
|
|
87
99
|
Minting is the one operation that turns a stolen credential into a permanent one: a token that can
|
|
88
|
-
create tokens survives the revocation of the token that leaked. `createAccessToken` and
|
|
89
|
-
|
|
100
|
+
create tokens survives the revocation of the token that leaked. `createAccessToken` and any other
|
|
101
|
+
minting path (an OAuth consent approval calls `refuseTokenAuth` before it snapshots the subject)
|
|
102
|
+
refuse a request whose `auth.type` is `AuthroizationType.AuthToken` — always, with no option — and a
|
|
90
103
|
deployment additionally names those routes in the guard's **deny list**, so the refusal is a 401 at
|
|
91
104
|
the boundary rather than a check every future handler has to remember.
|
|
92
105
|
|
|
106
|
+
`revokeAccessToken(protocol)` refuses a token the same way by default. `revokeAccessToken(protocol,
|
|
107
|
+
{ allowAccessTokens: true })` admits one — for a deployment's coding-agent surface that lets a person
|
|
108
|
+
clean up their own tokens (Viable's connector binds it on `connect.account.tokens.revoke`, while the
|
|
109
|
+
browser's own `/tokens/:id` route stays session-only and on the deny list). The option widens the
|
|
110
|
+
CREDENTIAL only: the token revoked must still be the caller's own profile in its own organization
|
|
111
|
+
(`makeEntityScope(req).requireEntityKey()`), a foreign or unknown id still answers `AuthForbidden`
|
|
112
|
+
alike, and a token may revoke itself. Revoking never creates a credential, so it cannot make a
|
|
113
|
+
stolen token permanent; at worst it signs the person's other agents out.
|
|
114
|
+
|
|
93
115
|
Minting only ever narrows: asking for a scope the caller does not hold is **refused**, not silently
|
|
94
116
|
dropped — a token that quietly grants less than it was asked for fails later, somewhere else, with
|
|
95
117
|
an error about the wrong thing. Revoke is idempotent (a retry looks like a second revoke), and
|
|
@@ -121,7 +143,7 @@ works.
|
|
|
121
143
|
## Tests
|
|
122
144
|
|
|
123
145
|
`bun test ./tests` in the package — what the guard claims, what it resolves (including that a token
|
|
124
|
-
can never outrank its profile), the mint/list/revoke rules, and the coguard's four properties.
|
|
146
|
+
can never outrank its profile), the mint/list/revoke rules (the revoke option: own profile and organization only, minting still refused), `accessTokenIssuerOf(ctx).issueAccessToken` parity with the route, audience admission, and the coguard's four properties.
|
|
125
147
|
|
|
126
148
|
## Depends On
|
|
127
149
|
|
|
@@ -132,5 +154,6 @@ can never outrank its profile), the mint/list/revoke rules, and the coguard's fo
|
|
|
132
154
|
|
|
133
155
|
## Related
|
|
134
156
|
|
|
135
|
-
- [[auth-token]] — the format, the routes and the client carrier guard
|
|
157
|
+
- [[auth-token]] — the format, the routes, `audience` and the client carrier guard
|
|
158
|
+
- [[server-oauth]] — the authorization server that mints audience-scoped tokens through `accessTokenIssuerOf(ctx).issueAccessToken`
|
|
136
159
|
- [[web-auth-token]] — the management panel · [[auth-protocol]] · [[server-auth]]
|
package/build/guard.d.ts
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import type { GuardService } from '@owlmeans/entrypoint';
|
|
2
|
-
import type {
|
|
3
|
-
/** Where the deployment's prefix comes from: the guard's own options, then config, then default. */
|
|
4
|
-
export declare const prefixOf: (context: AuthTokenContext, opts?: AuthTokenGuardOptions) => string;
|
|
2
|
+
import type { AuthTokenGuardOptions } from './types.js';
|
|
5
3
|
/**
|
|
6
4
|
* The guard that verifies a long-lived access token.
|
|
7
5
|
*
|
package/build/guard.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"guard.d.ts","sourceRoot":"","sources":["../src/guard.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAqC,YAAY,EAAE,MAAM,sBAAsB,CAAA;
|
|
1
|
+
{"version":3,"file":"guard.d.ts","sourceRoot":"","sources":["../src/guard.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAqC,YAAY,EAAE,MAAM,sBAAsB,CAAA;AAK3F,OAAO,KAAK,EAA0D,qBAAqB,EAAE,MAAM,YAAY,CAAA;AAK/G;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,kBAAkB,WACtB,MAAM,SAA2B,qBAAqB,KAC5D,YA6HF,CAAA"}
|
package/build/guard.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { ALL_SCOPES, AuthroizationType } from '@owlmeans/auth';
|
|
2
|
-
import {
|
|
2
|
+
import { AUTH_TOKEN_SCHEME, AUTH_TOKEN_RESOURCE, AUTH_TOKEN_TOUCH_INTERVAL, BEARER_SCHEME, GUARD_AUTH_TOKEN, tokenFormatHelper } from '@owlmeans/auth-token';
|
|
3
3
|
import { AUTH_HEADER } from '@owlmeans/auth';
|
|
4
4
|
import { ENTITY_RESOLVER } from '@owlmeans/auth-common';
|
|
5
5
|
import { createService } from '@owlmeans/context';
|
|
6
6
|
import { AUTH_IDENTITY_PROFILE } from '@owlmeans/server-auth-identity';
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
import { tokenHashHelper } from './hash.js';
|
|
8
|
+
import { prefixOf } from './prefix.js';
|
|
9
|
+
import { logger, logThrottle } from '@owlmeans/log';
|
|
10
|
+
const log = logger('server-auth-token');
|
|
10
11
|
/**
|
|
11
12
|
* The guard that verifies a long-lived access token.
|
|
12
13
|
*
|
|
@@ -26,14 +27,26 @@ export const prefixOf = (context, opts) => opts?.prefix ?? context.cfg.authToken
|
|
|
26
27
|
* outlive or outrank the person who minted it.
|
|
27
28
|
*/
|
|
28
29
|
export const makeAuthTokenGuard = (alias = GUARD_AUTH_TOKEN, opts = {}) => {
|
|
30
|
+
/**
|
|
31
|
+
* A presented access token the guard did not accept. Never the token, not even its display form:
|
|
32
|
+
* its record id only. Once a minute per token and reason — a client retrying a revoked token calls every second.
|
|
33
|
+
*/
|
|
34
|
+
const refuse = (reason, record) => {
|
|
35
|
+
if (logThrottle(`auth-token:${reason}:${record?.id ?? ''}`, 60_000)) {
|
|
36
|
+
log.warn('Access token refused', {
|
|
37
|
+
reason, tokenId: record?.id, entityId: record?.entityId,
|
|
38
|
+
}, { event: 'auth.refused' });
|
|
39
|
+
}
|
|
40
|
+
return false;
|
|
41
|
+
};
|
|
29
42
|
const touchInterval = opts.touchInterval ?? AUTH_TOKEN_TOUCH_INTERVAL;
|
|
30
43
|
const present = (req, prefix) => {
|
|
31
|
-
const parsed = parseAuthorizationHeader(req.headers?.[AUTH_HEADER]);
|
|
44
|
+
const parsed = tokenFormatHelper.parseAuthorizationHeader(req.headers?.[AUTH_HEADER]);
|
|
32
45
|
if (parsed == null)
|
|
33
46
|
return null;
|
|
34
47
|
if (parsed.scheme !== AUTH_TOKEN_SCHEME && parsed.scheme !== BEARER_SCHEME)
|
|
35
48
|
return null;
|
|
36
|
-
return isAccessToken(parsed.value, prefix) ? parsed.value : null;
|
|
49
|
+
return tokenFormatHelper.isAccessToken(parsed.value, prefix) ? parsed.value : null;
|
|
37
50
|
};
|
|
38
51
|
const service = createService(alias, {
|
|
39
52
|
match: async (req) => {
|
|
@@ -56,13 +69,23 @@ export const makeAuthTokenGuard = (alias = GUARD_AUTH_TOKEN, opts = {}) => {
|
|
|
56
69
|
if (presented == null)
|
|
57
70
|
return false;
|
|
58
71
|
const tokens = context.resource(opts.resourceAlias ?? AUTH_TOKEN_RESOURCE);
|
|
59
|
-
const record = await tokens.load({ hash: hashAccessToken(presented) });
|
|
72
|
+
const record = await tokens.load({ hash: tokenHashHelper.hashAccessToken(presented) });
|
|
60
73
|
if (record == null)
|
|
61
|
-
return
|
|
74
|
+
return refuse('unknown');
|
|
62
75
|
if (record.revokedAt != null)
|
|
63
|
-
return
|
|
76
|
+
return refuse('revoked', record);
|
|
64
77
|
if (record.expiresAt != null && new Date(record.expiresAt) < new Date())
|
|
65
|
-
return
|
|
78
|
+
return refuse('expired', record);
|
|
79
|
+
// A token minted through an OAuth grant carries the resource(s) it was issued FOR. A token
|
|
80
|
+
// with no audience is the ordinary hand-minted kind and is admitted everywhere its scopes
|
|
81
|
+
// reach, exactly as before this check existed — the guard has to OPT IN with `resources` to
|
|
82
|
+
// narrow anything, so a deployment that never touches this option sees no change at all.
|
|
83
|
+
const resources = typeof opts.resources === 'function' ? opts.resources(context) : opts.resources;
|
|
84
|
+
if (record.audience != null && record.audience.length > 0
|
|
85
|
+
&& resources != null && resources.length > 0
|
|
86
|
+
&& !record.audience.some(resource => resources.includes(resource))) {
|
|
87
|
+
return refuse('audience', record);
|
|
88
|
+
}
|
|
66
89
|
// A token is only ever as good as the profile behind it. Reading the profile per request is
|
|
67
90
|
// what makes revoking a person's access revoke every token they ever minted, without any
|
|
68
91
|
// token record being touched.
|
|
@@ -72,12 +95,12 @@ export const makeAuthTokenGuard = (alias = GUARD_AUTH_TOKEN, opts = {}) => {
|
|
|
72
95
|
profile = await profiles.load({ entityId: record.entityId, profileId: record.profileId });
|
|
73
96
|
}
|
|
74
97
|
catch {
|
|
75
|
-
return
|
|
98
|
+
return refuse('profile-unreadable', record);
|
|
76
99
|
}
|
|
77
100
|
if (profile == null)
|
|
78
|
-
return
|
|
101
|
+
return refuse('profile', record);
|
|
79
102
|
if (profile.expiresAt != null && new Date(profile.expiresAt) < new Date())
|
|
80
|
-
return
|
|
103
|
+
return refuse('profile-expired', record);
|
|
81
104
|
const profileScopes = profile.scopes ?? [];
|
|
82
105
|
const scopes = profileScopes.includes(ALL_SCOPES)
|
|
83
106
|
? record.scopes
|
|
@@ -114,7 +137,7 @@ export const makeAuthTokenGuard = (alias = GUARD_AUTH_TOKEN, opts = {}) => {
|
|
|
114
137
|
// Fire and forget: a usage timestamp is never a reason to fail a request, and awaiting it
|
|
115
138
|
// would put a database write on the critical path of every authenticated call.
|
|
116
139
|
void tokens.save({ ...record, lastUsedAt: new Date() })
|
|
117
|
-
.catch((e) =>
|
|
140
|
+
.catch((e) => { log.warn('Access token lastUsedAt not saved', { tokenId: record.id, error: e }); });
|
|
118
141
|
}
|
|
119
142
|
return true;
|
|
120
143
|
},
|
package/build/guard.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"guard.js","sourceRoot":"","sources":["../src/guard.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAA;AAC9D,OAAO,EACL,
|
|
1
|
+
{"version":3,"file":"guard.js","sourceRoot":"","sources":["../src/guard.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { AuthTokenEntrypoints } from '@owlmeans/auth-token';
|
|
2
|
+
export declare const createAccessToken: (protocol: AuthTokenEntrypoints['create']) => import("@owlmeans/server-entrypoint").BoundEntrypointHandler<import("@owlmeans/entrypoint").EntrypointProtocol<{
|
|
3
|
+
body: import("@owlmeans/auth-token").CreateAccessToken;
|
|
4
|
+
}, import("@owlmeans/auth-token").IssuedAccessToken>>;
|
|
5
|
+
//# sourceMappingURL=create.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"create.d.ts","sourceRoot":"","sources":["../../src/handlers/create.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAA;AAOhE,eAAO,MAAM,iBAAiB,aAAc,oBAAoB,CAAC,QAAQ,CAAC;;qDAYxE,CAAA"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { AuthForbidden, AuthRole } from '@owlmeans/auth';
|
|
2
|
+
import { makeEntityScope } from '@owlmeans/auth-common';
|
|
3
|
+
import { handlers } from '@owlmeans/server-api';
|
|
4
|
+
import { accessTokenIssuerOf } from './issue.js';
|
|
5
|
+
import { refuseTokenAuth } from './refuse.js';
|
|
6
|
+
export const createAccessToken = (protocol) => handlers().body(protocol, async (payload, context, req) => {
|
|
7
|
+
const ctx = context;
|
|
8
|
+
refuseTokenAuth(req, 'token-mint');
|
|
9
|
+
const entityId = makeEntityScope(req).requireEntityKey();
|
|
10
|
+
const profileId = req.auth?.profileId;
|
|
11
|
+
const userId = req.auth?.userId;
|
|
12
|
+
if (profileId == null || userId == null)
|
|
13
|
+
throw new AuthForbidden('profile');
|
|
14
|
+
return accessTokenIssuerOf(ctx).issueAccessToken({
|
|
15
|
+
entityId, profileId, userId, role: req.auth?.role ?? AuthRole.User, scopes: req.auth?.scopes ?? [],
|
|
16
|
+
}, payload);
|
|
17
|
+
});
|
|
18
|
+
//# sourceMappingURL=create.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"create.js","sourceRoot":"","sources":["../../src/handlers/create.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAA;AAExD,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAA;AACvD,OAAO,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAA;AAE/C,OAAO,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAA;AAChD,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AAE7C,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,QAAwC,EAAE,EAAE,CAAC,QAAQ,EAAoB,CAAC,IAAI,CAAC,QAAQ,EAAE,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,EAAE;IACzJ,MAAM,GAAG,GAAG,OAA2B,CAAA;IACvC,eAAe,CAAC,GAAG,EAAE,YAAY,CAAC,CAAA;IAElC,MAAM,QAAQ,GAAG,eAAe,CAAC,GAAG,CAAC,CAAC,gBAAgB,EAAE,CAAA;IACxD,MAAM,SAAS,GAAG,GAAG,CAAC,IAAI,EAAE,SAAS,CAAA;IACrC,MAAM,MAAM,GAAG,GAAG,CAAC,IAAI,EAAE,MAAM,CAAA;IAC/B,IAAI,SAAS,IAAI,IAAI,IAAI,MAAM,IAAI,IAAI;QAAE,MAAM,IAAI,aAAa,CAAC,SAAS,CAAC,CAAA;IAE3E,OAAO,mBAAmB,CAAC,GAAG,CAAC,CAAC,gBAAgB,CAAC;QAC/C,QAAQ,EAAE,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,CAAC,IAAI,EAAE,IAAI,IAAI,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,CAAC,IAAI,EAAE,MAAM,IAAI,EAAE;KACnG,EAAE,OAAO,CAAC,CAAA;AACb,CAAC,CAAC,CAAA"}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import type {
|
|
3
|
-
|
|
4
|
-
export
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
export
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
1
|
+
import type { IssuedAccessToken } from '@owlmeans/auth-token';
|
|
2
|
+
import type { AuthTokenContext } from '../types.js';
|
|
3
|
+
import type { IssueAccessTokenRequest, IssueAccessTokenSubject } from './types.js';
|
|
4
|
+
export * from './refuse.js';
|
|
5
|
+
export * from './issue.js';
|
|
6
|
+
export * from './list.js';
|
|
7
|
+
export * from './create.js';
|
|
8
|
+
export * from './revoke.js';
|
|
9
|
+
export type { IssueAccessTokenRequest, IssueAccessTokenSubject, RevokeAccessTokenOptions } from './types.js';
|
|
10
|
+
/** @deprecated compat:factory-refactor — use `accessTokenIssuerOf(ctx).issueAccessToken(…)` */
|
|
11
|
+
export declare const issueAccessToken: (ctx: AuthTokenContext, subject: IssueAccessTokenSubject, payload: IssueAccessTokenRequest) => Promise<IssuedAccessToken>;
|
|
12
12
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/handlers/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/handlers/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAA;AAC7D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAA;AAEnD,OAAO,KAAK,EAAE,uBAAuB,EAAE,uBAAuB,EAAE,MAAM,YAAY,CAAA;AAElF,cAAc,aAAa,CAAA;AAC3B,cAAc,YAAY,CAAA;AAC1B,cAAc,WAAW,CAAA;AACzB,cAAc,aAAa,CAAA;AAC3B,cAAc,aAAa,CAAA;AAE3B,YAAY,EAAE,uBAAuB,EAAE,uBAAuB,EAAE,wBAAwB,EAAE,MAAM,YAAY,CAAA;AAE5G,+FAA+F;AAC/F,eAAO,MAAM,gBAAgB,QACtB,gBAAgB,WAAW,uBAAuB,WAAW,uBAAuB,KACxF,OAAO,CAAC,iBAAiB,CAAsE,CAAA"}
|
package/build/handlers/index.js
CHANGED
|
@@ -1,94 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
return rest;
|
|
10
|
-
};
|
|
11
|
-
const tokens = (ctx) => ctx.resource(AUTH_TOKEN_RESOURCE);
|
|
12
|
-
/**
|
|
13
|
-
* A token may never mint another token.
|
|
14
|
-
*
|
|
15
|
-
* Minting is the one operation that turns a stolen credential into a permanent one: a token that
|
|
16
|
-
* can create tokens survives the revocation of the token that leaked. Creating and revoking
|
|
17
|
-
* therefore require a credential a person produced in a browser — which is also why the deployment
|
|
18
|
-
* names these routes in the guard's deny list, so the refusal is a 401 at the boundary rather than
|
|
19
|
-
* a check every future handler has to remember.
|
|
20
|
-
*/
|
|
21
|
-
const refuseTokenAuth = (req, what) => {
|
|
22
|
-
if (req.auth?.type === AuthroizationType.AuthToken) {
|
|
23
|
-
throw new AuthForbidden(what);
|
|
24
|
-
}
|
|
25
|
-
};
|
|
26
|
-
export const listAccessTokens = (protocol) => handlers().request(protocol, async (req, context) => {
|
|
27
|
-
const ctx = context;
|
|
28
|
-
const entityId = requireEntityKey(req);
|
|
29
|
-
const profileId = req.auth?.profileId;
|
|
30
|
-
if (profileId == null)
|
|
31
|
-
throw new AuthForbidden('profile');
|
|
32
|
-
const result = await tokens(ctx).list({ entityId, profileId }, { size: 0 });
|
|
33
|
-
return {
|
|
34
|
-
items: result.items
|
|
35
|
-
.map(view)
|
|
36
|
-
.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()),
|
|
37
|
-
};
|
|
38
|
-
});
|
|
39
|
-
export const createAccessToken = (protocol) => handlers().body(protocol, async (payload, context, req) => {
|
|
40
|
-
const ctx = context;
|
|
41
|
-
refuseTokenAuth(req, 'token-mint');
|
|
42
|
-
const entityId = requireEntityKey(req);
|
|
43
|
-
const profileId = req.auth?.profileId;
|
|
44
|
-
const userId = req.auth?.userId;
|
|
45
|
-
if (profileId == null || userId == null)
|
|
46
|
-
throw new AuthForbidden('profile');
|
|
47
|
-
const own = req.auth?.scopes ?? [];
|
|
48
|
-
const scopes = payload.scopes ?? own;
|
|
49
|
-
// A token is a delegation, so it can only ever narrow. Asking for a scope the caller does not
|
|
50
|
-
// hold is refused rather than silently dropped: a token that quietly grants less than it was
|
|
51
|
-
// asked for fails later, somewhere else, with an error about the wrong thing.
|
|
52
|
-
if (!own.includes(ALL_SCOPES) && scopes.some(scope => !own.includes(scope))) {
|
|
53
|
-
throw new AuthForbidden('scope');
|
|
54
|
-
}
|
|
55
|
-
// The SAME resolver the guard uses, so a minted token is one this deployment's guard claims.
|
|
56
|
-
const minted = mintAccessToken(prefixOf(ctx));
|
|
57
|
-
const now = new Date();
|
|
58
|
-
const record = await tokens(ctx).create({
|
|
59
|
-
hash: minted.hash,
|
|
60
|
-
display: minted.display,
|
|
61
|
-
name: payload.name,
|
|
62
|
-
userId,
|
|
63
|
-
profileId,
|
|
64
|
-
entityId,
|
|
65
|
-
scopes,
|
|
66
|
-
role: req.auth?.role ?? AuthRole.User,
|
|
67
|
-
createdAt: now,
|
|
68
|
-
...(payload.expiresIn != null
|
|
69
|
-
? { expiresAt: new Date(now.getTime() + Math.min(payload.expiresIn * 1000, AUTH_TOKEN_MAX_TTL)) }
|
|
70
|
-
: {}),
|
|
71
|
-
});
|
|
72
|
-
// The only moment the plaintext exists outside the caller's own machine.
|
|
73
|
-
return { token: minted.token, record: view(record) };
|
|
74
|
-
});
|
|
75
|
-
export const revokeAccessToken = (protocol) => handlers().params(protocol, async (payload, context, req) => {
|
|
76
|
-
const ctx = context;
|
|
77
|
-
refuseTokenAuth(req, 'token-revoke');
|
|
78
|
-
const entityId = requireEntityKey(req);
|
|
79
|
-
const profileId = req.auth?.profileId;
|
|
80
|
-
if (profileId == null)
|
|
81
|
-
throw new AuthForbidden('profile');
|
|
82
|
-
const record = await tokens(ctx).load(payload.id);
|
|
83
|
-
// A token of another profile answers exactly as an unknown id does — an owner learns nothing
|
|
84
|
-
// about tokens that are not theirs, not even that one exists.
|
|
85
|
-
if (record == null || record.entityId !== entityId || record.profileId !== profileId) {
|
|
86
|
-
throw new AuthForbidden('token');
|
|
87
|
-
}
|
|
88
|
-
// Idempotent: revoking twice is what a retry looks like.
|
|
89
|
-
if (record.revokedAt == null) {
|
|
90
|
-
await tokens(ctx).save({ ...record, revokedAt: new Date(), updatedAt: new Date() });
|
|
91
|
-
}
|
|
92
|
-
return { id: payload.id };
|
|
93
|
-
});
|
|
1
|
+
import { accessTokenIssuerOf } from './issue.js';
|
|
2
|
+
export * from './refuse.js';
|
|
3
|
+
export * from './issue.js';
|
|
4
|
+
export * from './list.js';
|
|
5
|
+
export * from './create.js';
|
|
6
|
+
export * from './revoke.js';
|
|
7
|
+
/** @deprecated compat:factory-refactor — use `accessTokenIssuerOf(ctx).issueAccessToken(…)` */
|
|
8
|
+
export const issueAccessToken = async (ctx, subject, payload) => await accessTokenIssuerOf(ctx).issueAccessToken(subject, payload);
|
|
94
9
|
//# sourceMappingURL=index.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/handlers/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/handlers/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAA;AAGhD,cAAc,aAAa,CAAA;AAC3B,cAAc,YAAY,CAAA;AAC1B,cAAc,WAAW,CAAA;AACzB,cAAc,aAAa,CAAA;AAC3B,cAAc,aAAa,CAAA;AAI3B,+FAA+F;AAC/F,MAAM,CAAC,MAAM,gBAAgB,GAAG,KAAK,EACnC,GAAqB,EAAE,OAAgC,EAAE,OAAgC,EAC7D,EAAE,CAAC,MAAM,mBAAmB,CAAC,GAAG,CAAC,CAAC,gBAAgB,CAAC,OAAO,EAAE,OAAO,CAAC,CAAA"}
|