You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: README.md
+147-9Lines changed: 147 additions & 9 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -13,7 +13,11 @@ HTTP API providing user/client message handling for an fmsg host. Exposes CRUD o
13
13
|`FMSG_JWT_ISSUER`|*(prod, required with JWKS)*| Expected `iss` claim value (e.g. `https://idp.example.com/`). Tokens with a different issuer are rejected. This must exactly match the token issuer. |
14
14
|`FMSG_JWT_AUDIENCE`|*(prod, required with JWKS)*| Expected `aud` claim value for this application or API. |
15
15
|`FMSG_JWT_ADDRESS_CLAIM`|*(prod, required with JWKS)*| JWT claim name containing the fmsg address in `@user@domain` form, e.g. `fmsg_address` or a namespaced custom claim. |
16
-
|`FMSG_API_JWT_SECRET`|*(dev)*| HMAC secret for HS256 token verification. Used only in dev mode (when `FMSG_JWT_JWKS_URL` is unset). Prefix with `base64:` to supply a base64-encoded key. Either this or `FMSG_JWT_JWKS_URL` must be set. |
16
+
|`FMSG_API_TOKEN_ED25519_PRIVATE_KEY`|*(optional)*| Base64-encoded Ed25519 private key or seed used to mint first-party JWTs from API keys. Required to enable `/fmsg/token` and sub-account routes. |
17
+
|`FMSG_API_TOKEN_ISSUER`|`fmsg-webapi`| Issuer for first-party API-key JWTs. |
18
+
|`FMSG_API_TOKEN_AUDIENCE`|`fmsg-webapi`| Audience for first-party API-key JWTs. |
19
+
|`FMSG_API_TOKEN_TTL`|`12h`| Lifetime of JWTs minted by `POST /fmsg/token`. |
20
+
|`FMSG_TRUSTED_PROXIES`|*(optional)*| Comma-separated trusted proxy CIDRs/IPs for Gin client IP resolution. Leave unset to use direct client addresses for API-key CIDR checks. |
17
21
|`FMSG_TLS_CERT`|*(optional)*| Path to the TLS certificate file (e.g. `/etc/letsencrypt/live/example.com/fullchain.pem`). When set with `FMSG_TLS_KEY`, enables HTTPS. |
18
22
|`FMSG_TLS_KEY`|*(optional)*| Path to the TLS private key file (e.g. `/etc/letsencrypt/live/example.com/privkey.pem`). Must be set together with `FMSG_TLS_CERT`. |
19
23
|`FMSG_API_PORT`|`443` (TLS) / `8000` (plain) | TCP port to listen on. |
@@ -36,8 +40,13 @@ A `.env` file placed in the working directory is loaded automatically at startup
36
40
37
41
## Authentication
38
42
39
-
All `/fmsg/*` routes require an `Authorization: Bearer <token>` header. The API
40
-
operates in one of two verification modes, selected automatically at startup:
43
+
Most `/fmsg/*` routes require an `Authorization: Bearer <token>` header. The
44
+
API can enable either or both authentication methods at startup:
45
+
46
+
- RS256/JWKS tokens from an external identity provider.
47
+
- First-party Ed25519 JWTs minted by `POST /fmsg/token` from opaque API keys.
48
+
49
+
Startup fails unless at least one method is configured.
41
50
42
51
### RS256 (production, JWKS-backed JWTs)
43
52
@@ -68,11 +77,60 @@ includes the configured address claim. Whether that token is an ID token or
68
77
access token is determined by the identity provider configuration for the
69
78
deployment.
70
79
71
-
### HMAC (development)
80
+
### API Keys And First-Party JWTs
81
+
82
+
Active when `FMSG_API_TOKEN_ED25519_PRIVATE_KEY` is set. Programmatic clients
83
+
authenticate with opaque API keys bound to sub-account addresses. The server
84
+
stores only API-key hashes and exchanges valid keys for short-lived Ed25519 JWTs.
85
+
86
+
API keys are sent only to `POST /fmsg/token`:
87
+
88
+
```http
89
+
Authorization: Bearer fmsgk_<key_id>_<secret>
90
+
```
91
+
92
+
The returned JWT contains `sub` (the sub-account address), `owner`, `api_key_id`,
93
+
`iss`, `aud`, `iat`, and `exp`. Protected routes re-check the backing key row on
94
+
each request, so deleting a sub-account or expiring its key invalidates existing
95
+
tokens before their normal expiry.
96
+
97
+
An RS256-authenticated owner can perform normal message routes as one of their
98
+
sub-accounts without changing request bodies:
99
+
100
+
```http
101
+
X-FMSG-Act-As: @user_bot@example.com
102
+
```
103
+
104
+
The requested sub-account must be owned by the authenticated user and must exist
105
+
in fmsgid.
106
+
107
+
Apply [api_keys.sql](api_keys.sql) before enabling API-key auth.
108
+
109
+
To set a custom per-owner sub-account limit, insert an owner config row:
72
110
73
-
Active when `FMSG_JWT_JWKS_URL` is unset. Tokens must be HS256-signed with the
74
-
shared secret in `FMSG_API_JWT_SECRET`. Required claims are `sub` and `exp`;
75
-
`iat`/`nbf` are honoured when present.
111
+
```sql
112
+
INSERT INTO fmsg_api_sub_account (owner_addr, agent, max_sub_accounts)
113
+
VALUES ('@alice@example.com', '', 10)
114
+
ON CONFLICT (owner_addr, agent)
115
+
DO UPDATESET max_sub_accounts =EXCLUDED.max_sub_accounts;
116
+
```
117
+
118
+
Operators can bootstrap or rotate keys without RS256 by using the built-in CLI
119
+
command. It uses the standard `PG*` connection environment variables and prints
@@ -141,7 +201,10 @@ the HTTP server and kept alive by its own ping/pong heartbeat.
141
201
142
202
## API Routes
143
203
144
-
All routes are prefixed with `/fmsg` and require a valid `Authorization: Bearer <token>` header. The one exception is the WebSocket route `/fmsg/ws`, which additionally accepts the token via an `access_token` query parameter (browsers cannot set headers on a WebSocket).
204
+
All routes are prefixed with `/fmsg`. `POST /fmsg/token` accepts an API key and
205
+
returns a JWT. Other routes require a valid `Authorization: Bearer <token>`
206
+
header. The WebSocket route `/fmsg/ws` additionally accepts the token via an
207
+
`access_token` query parameter (browsers cannot set headers on a WebSocket).
145
208
146
209
Rate limiting is enforced at the host level (e.g. `nftables`) rather than in
147
210
the application.
@@ -151,6 +214,11 @@ the application.
151
214
|`GET`|`/fmsg`| List messages for user |
152
215
|`GET`|`/fmsg/sent`| List authored messages (sent + drafts) |
153
216
|`GET`|`/fmsg/ws`| WebSocket for pushed event notifications |
217
+
|`POST`|`/fmsg/token`| Exchange an API key for a JWT |
218
+
|`GET`|`/fmsg/sub-accounts`| List owned sub-accounts |
219
+
|`POST`|`/fmsg/sub-accounts`| Create a sub-account API key |
220
+
|`POST`|`/fmsg/sub-accounts/:agent/rotate-key`| Rotate a sub-account API key |
221
+
|`DELETE`|`/fmsg/sub-accounts/:agent`| Delete a sub-account |
154
222
|`POST`|`/fmsg`| Create a draft message |
155
223
|`GET`|`/fmsg/:id`| Retrieve a message |
156
224
|`PUT`|`/fmsg/:id`| Update a draft message |
@@ -168,6 +236,76 @@ the application.
168
236
The `/fmsg/push/subscribe` routes are registered only when Web Push is
169
237
configured (see [Web Push](#web-push)).
170
238
239
+
The `/fmsg/token` and `/fmsg/sub-accounts*` routes are registered only when
240
+
API-key auth is configured with `FMSG_API_TOKEN_ED25519_PRIVATE_KEY`.
241
+
242
+
### POST `/fmsg/token`
243
+
244
+
Exchanges an opaque API key for a short-lived JWT.
(agent =''AND sub_addr IS NULLAND key_id IS NULLAND key_hash IS NULLAND allowed_cidrs IS NULLAND key_expires_at IS NULL)
18
+
OR
19
+
(agent <>''AND sub_addr IS NOT NULLAND key_id IS NOT NULLAND key_hash IS NOT NULLAND allowed_cidrs IS NOT NULLAND cardinality(allowed_cidrs) >0AND key_expires_at IS NOT NULL)
20
+
),
21
+
CHECK (agent =''OR agent NOT LIKE'%\_%' ESCAPE '\')
22
+
);
23
+
24
+
CREATE INDEX IF NOT EXISTS fmsg_api_sub_account_owner_idx
25
+
ON fmsg_api_sub_account ((lower(owner_addr)));
26
+
27
+
CREATE INDEX IF NOT EXISTS fmsg_api_sub_account_sub_idx
0 commit comments