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
Updates docs/client-api.md to reflect the new client-facing surface:
- §2.2 POST /auth — request body now documents the authToken field
alongside ssoToken (auto-routed by which is present). New error
reasons: ambiguous_token, missing_token, invalid_token,
upstream_unavailable.
- §2.3 GET /api/userInfo — response shape branches on roles. The
existing rich employee shape is documented as the SSO default; a
new "minimal shape (bot/admin)" subsection documents the 5-field
URL bundle returned when users.roles contains bot or admin. Notes
the dotted-account validator constraint and points bot SDKs at
the direct botplatform endpoint instead.
- §2.5 POST /v1/login (portal-service, NEW) — full reference for
the bot/admin password-login forwarder including the uniform-401
enumeration guard, requirePasswordChange flag, and all 4xx/5xx
reasons (site_unknown, upstream_unavailable).
- §2.6 POST /v1/login (botplatform-service, NEW) — direct bot SDK
endpoint, legacy {userId, authToken, me} response shape with the
me block (requirePasswordChange included), /api/v1/login alias
noted for legacy Rocket.Chat wire compat, 403 wrong_site
documented.
- §2.6b POST /v1/password/change (botplatform-service, NEW) —
authenticated rotation, both Authorization: Bearer and
X-Auth-Token header shapes, side-effects (password updated,
requirePasswordChange unset, other sessions revoked), all error
reasons.
- §2.7 POST /v1/auth/validate (botplatform-service, NEW) — full
principal-shape reference for the server-to-server validate
endpoint consumed by auth-service and the gateway.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VrbwirKCHJ5Qz7xkYYfzxL
Copy file name to clipboardExpand all lines: docs/client-api.md
+314-5Lines changed: 314 additions & 5 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -158,15 +158,18 @@ The exact event subjects a client may receive as a result of an RPC are listed u
158
158
**Endpoint:**`POST /auth`
159
159
**Reply:** synchronous HTTP response
160
160
161
-
Exchanges an SSO token for a signed NATS user JWT. The returned JWT is what the client uses to connect to NATS (see §2.1).
161
+
Exchanges either an OIDC SSO token (humans) or a botplatform session token (bots/admins) for a signed NATS user JWT. The returned JWT is what the client uses to connect to NATS (see §2.1). The server auto-routes by which token field is present — exactly one of `ssoToken` / `authToken` must be supplied.
|`ssoToken`| string | one-of | OIDC-issued SSO token. Set this for the SSO (human) path; leave `authToken` empty. |
168
+
|`authToken`| string | one-of | Botplatform session token from §2.6 / §2.7. Set this for the bot/admin path; leave `ssoToken` empty. |
168
169
|`natsPublicKey`| string | yes | The client's NATS user public NKey (must pass `nkeys.IsValidPublicUserKey`). |
169
170
171
+
Exactly one of `ssoToken` / `authToken` must be set: both → `400 ambiguous_token`; neither → `400 missing_token`. The scope of the returned JWT is derived server-side from the principal's roles (admin > bot > user); the client never declares a role.
172
+
170
173
```json
171
174
{
172
175
"ssoToken": "<sso-token>",
@@ -210,11 +213,15 @@ See [Error envelope](#6-error-envelope-reference). HTTP statuses:
210
213
211
214
| Status |`code`|`reason`| Example body |
212
215
|---|---|---|---|
213
-
| 400 |`bad_request`| — |`{ "code": "bad_request", "error": "ssoToken and natsPublicKey are required" }`|
| 400 |`bad_request`|`ambiguous_token`|`{ "code": "bad_request", "reason": "ambiguous_token", "error": "set exactly one of ssoToken / authToken" }`|
219
+
| 400 |`bad_request`|`missing_token`|`{ "code": "bad_request", "reason": "missing_token", "error": "set exactly one of ssoToken / authToken" }`|
215
220
| 400 |`bad_request`| — |`{ "code": "bad_request", "error": "account must be a single NATS subject token (no '.', '*', '>' or whitespace)" }` — the account becomes a NATS subject token, so separator/wildcard/whitespace characters are refused. |
Note: bot account names that contain `.` (e.g. `name.shortcode.bot`) cannot be served through this endpoint — the existing single-NATS-token validator refuses dotted accounts. Bot SDKs do not call this endpoint; they hit botplatform `/v1/login` directly (§2.6).
308
+
281
309
#### Error response
282
310
283
311
See [Error envelope](#6-error-envelope-reference). HTTP statuses:
@@ -299,6 +327,287 @@ See [Error envelope](#6-error-envelope-reference). HTTP statuses:
299
327
300
328
---
301
329
330
+
### 2.5 HTTP — POST /v1/login (portal-service)
331
+
332
+
**Endpoint:**`POST /v1/login`
333
+
**Reply:** synchronous HTTP response
334
+
335
+
Password login for bot/admin accounts via portal-service, called by web / mobile / desktop / admin-UI clients. Portal looks up the user's home site, forwards `{username, password}` east-west to that site's botplatform `/v1/login` (§2.6), and returns a merged 8-field response so the client has both the session token and the home-site URL bundle in one round trip. **Regular SSO users do not use this endpoint** — they use the existing SSO flow (§2.3 + §2.2).
336
+
337
+
Bot SDKs do not call portal — they hit botplatform `/v1/login` directly (§2.6).
338
+
339
+
#### Request body
340
+
341
+
| Field | Type | Required | Notes |
342
+
|---|---|---|---|
343
+
|`username`| string | yes | Account name (matches `users.account`). Must hold the `bot` or `admin` role; SSO-only users get a uniform `401 invalid_credentials`. |
344
+
|`password`| string | yes | Plaintext password. Verified server-side using `bcrypt(sha256_hex(plaintext))` per the legacy recipe. |
345
+
346
+
```json
347
+
{
348
+
"username": "p_admin",
349
+
"password": "<secret>"
350
+
}
351
+
```
352
+
353
+
#### Success response
354
+
355
+
`HTTP 200`
356
+
357
+
| Field | Type | Notes |
358
+
|---|---|---|
359
+
|`userId`| string | Canonical 17-char user identifier from botplatform. |
360
+
|`authToken`| string | 43-char base64url session token. Use as the `authToken` field in `POST /auth` (§2.2) to obtain a NATS JWT. |
361
+
|`account`| string | The `{account}` used in every NATS subject; same as `username`. |
362
+
|`siteId`| string | The user's home site; informational. |
|`requirePasswordChange`| boolean | First-login flag from the user doc. When `true`, the client should route to a password-update page before normal app use. The session token is still valid. |
See [Error envelope](#6-error-envelope-reference). HTTP statuses:
384
+
385
+
| Status |`code`|`reason`| Notes |
386
+
|---|---|---|---|
387
+
| 400 |`bad_request`|`missing_fields`|`username` or `password` empty. |
388
+
| 401 |`unauthenticated`|`invalid_credentials`| Uniform rejection covering unknown account, wrong password, AND SSO-only accounts attempting password login. Body is byte-identical across the three arms to prevent enumeration. |
389
+
| 403 |`forbidden`|`wrong_site`| Upstream botplatform's siteId guard fired (the request reached a non-home-site botplatform). Re-do `/userInfo` to find the correct home site. |
390
+
| 500 |`internal`|`site_unknown`| The user's `siteId` is missing from `PORTAL_SITE_URLS`. Server misconfiguration. |
391
+
| 503 |`unavailable`|`upstream_unavailable`| Portal cannot reach the home-site botplatform. |
392
+
393
+
#### Triggered events — success path
394
+
395
+
`None — HTTP-only.`
396
+
397
+
#### Triggered events — error path
398
+
399
+
`None.`
400
+
401
+
---
402
+
403
+
### 2.6 HTTP — POST /v1/login (botplatform-service, bot SDK direct)
404
+
405
+
**Endpoint:**`POST /v1/login` (also exposed at `POST /api/v1/login` for legacy Rocket.Chat wire compat — same handler)
406
+
**Reply:** synchronous HTTP response
407
+
408
+
Direct bot-SDK login at the home-site botplatform-service. Returns the **legacy Rocket.Chat 3-field shape**`{userId, authToken, me}` so existing bot SDKs need no code changes. The 8-field portal response (§2.5) is for web/mobile/desktop/admin clients only.
409
+
410
+
Bots discover their home-site URL externally (configured); they do not call portal first.
411
+
412
+
#### Request body
413
+
414
+
| Field | Type | Required | Notes |
415
+
|---|---|---|---|
416
+
|`username`| string | yes | Account name (matches `users.account`). Must hold the `bot` or `admin` role. |
417
+
|`password`| string | yes | Plaintext password. |
418
+
419
+
```json
420
+
{
421
+
"username": "name.shortcode.bot",
422
+
"password": "<secret>"
423
+
}
424
+
```
425
+
426
+
#### Success response
427
+
428
+
`HTTP 200`
429
+
430
+
| Field | Type | Notes |
431
+
|---|---|---|
432
+
|`status`| string | Always `"success"` on the 200 path (legacy envelope shape). |
433
+
|`data.userId`| string | 17-char user identifier; mirrored in the `X-User-Id` header that the client sends on subsequent calls. |
434
+
|`data.authToken`| string | 43-char base64url opaque session token (wire-identical to legacy Rocket.Chat — no `bp_` prefix). Sent on subsequent calls as the `X-Auth-Token` header. |
435
+
|`data.me`| object | The legacy `me` block; see [Me](#me) below. |
436
+
437
+
##### Me
438
+
439
+
| Field | Type | Notes |
440
+
|---|---|---|
441
+
|`_id`| string | Same as `data.userId`. |
442
+
|`username`| string | Same as the request's `username`. |
443
+
|`name`| string | Display name. |
444
+
|`active`| boolean | Always `true` on a successful login. |
445
+
|`roles`| string[]| Role tags (e.g. `["bot"]`, `["admin"]`). |
446
+
|`requirePasswordChange`| boolean | First-login flag, mirrored from the user doc. |
447
+
448
+
```json
449
+
{
450
+
"status": "success",
451
+
"data": {
452
+
"userId": "abcdef1234567890x",
453
+
"authToken": "<43-char base64url>",
454
+
"me": {
455
+
"_id": "abcdef1234567890x",
456
+
"username": "name.shortcode.bot",
457
+
"name": "FOD Bot",
458
+
"active": true,
459
+
"roles": ["bot"],
460
+
"requirePasswordChange": false
461
+
}
462
+
}
463
+
}
464
+
```
465
+
466
+
#### Error response
467
+
468
+
See [Error envelope](#6-error-envelope-reference). HTTP statuses:
469
+
470
+
| Status |`code`|`reason`| Notes |
471
+
|---|---|---|---|
472
+
| 400 |`bad_request`|`missing_fields`|`username` or `password` empty. |
473
+
| 401 |`unauthenticated`|`invalid_credentials`| Uniform: unknown account, wrong password, or account without `bot`/`admin` role. |
474
+
| 403 |`forbidden`|`wrong_site`|`users.siteId` != this service's configured `SITE_ID`. Bot SDK should redirect to its home site's URL. |
Sessions are permanent (no TTL). Per-user cap is `SESSIONS_MAX_PER_ACCOUNT` (default 100); on overflow, the oldest sessions are FIFO-evicted by `issuedAt` and their tokens immediately stop validating.
478
+
479
+
#### Triggered events — success path
480
+
481
+
`None — HTTP-only.`
482
+
483
+
#### Triggered events — error path
484
+
485
+
`None.`
486
+
487
+
---
488
+
489
+
### 2.6b HTTP — POST /v1/password/change (botplatform-service)
490
+
491
+
**Endpoint:**`POST /v1/password/change`
492
+
**Reply:** synchronous HTTP response
493
+
494
+
Authenticated password rotation. Used by the first-login flow (the `requirePasswordChange` flag returned by `/v1/login` directs the client here) and any subsequent rotation. The caller's current session stays valid; every OTHER live session for the user is revoked best-effort so a leaked old password cannot keep a parallel session alive.
495
+
496
+
#### Authentication
497
+
498
+
The session token is supplied via header (one of, Bearer wins if both are present):
499
+
500
+
-`Authorization: Bearer <authToken>` — preferred for new clients.
|`oldPassword`| string | yes | Plaintext old password. Verified server-side using `bcrypt(sha256_hex(plaintext))`. |
510
+
|`newPassword`| string | yes | Plaintext new password. Hashed with the same recipe before storage. No server-side strength checks; the client is responsible for any policy. |
-`users.services.password.bcrypt` is replaced with the new hash.
526
+
-`users.requirePasswordChange` is unset (so a follow-up login sees `requirePasswordChange:false`).
527
+
- Every session row for this user EXCEPT the caller's current session is deleted; their tokens immediately stop validating.
528
+
529
+
The caller's current session stays valid — no re-login is required, so the chat-frontend "first-login → change-pwd → land in chat" flow lands the user directly in the chat with no extra round trip.
530
+
531
+
#### Error response
532
+
533
+
| Status |`code`|`reason`| Notes |
534
+
|---|---|---|---|
535
+
| 400 |`bad_request`|`missing_fields`|`oldPassword` or `newPassword` empty. |
536
+
| 401 |`unauthenticated`|`invalid_token`| No token supplied OR the token hash is not in the sessions collection. |
537
+
| 401 |`unauthenticated`|`invalid_credentials`|`oldPassword` does not match the stored hash. |
### 2.7 HTTP — POST /v1/auth/validate (botplatform-service)
551
+
552
+
**Endpoint:**`POST /v1/auth/validate`
553
+
**Reply:** synchronous HTTP response
554
+
555
+
Validates a session `authToken` and returns the associated principal. Called by auth-service (§2.2) when minting a NATS JWT for a session-token holder, and by the gateway during request routing. Validation is local-DB only — cross-site routing is the caller's responsibility.
556
+
557
+
This endpoint is intended for **server-to-server use**; bot SDKs do not call it directly.
558
+
559
+
#### Request body
560
+
561
+
| Field | Type | Required | Notes |
562
+
|---|---|---|---|
563
+
|`authToken`| string | yes | The 43-char raw token returned by `/v1/login`. |
564
+
565
+
```json
566
+
{ "authToken": "<43-char base64url>" }
567
+
```
568
+
569
+
#### Success response
570
+
571
+
`HTTP 200`
572
+
573
+
| Field | Type | Notes |
574
+
|---|---|---|
575
+
|`valid`| boolean |`true` on a session match. |
576
+
|`principal.userId`| string | 17-char user identifier. |
577
+
|`principal.account`| string | The `{account}` used in NATS subjects. |
578
+
|`principal.siteId`| string | The user's home site. |
579
+
|`principal.roles`| string[]| Roles at the time the session was issued (denormalized). |
0 commit comments