Skip to content

Commit 8fa6495

Browse files
committed
docs(client-api): document bot-platform login endpoints + extended /auth
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
1 parent 46a459e commit 8fa6495

1 file changed

Lines changed: 314 additions & 5 deletions

File tree

docs/client-api.md

Lines changed: 314 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -158,15 +158,18 @@ The exact event subjects a client may receive as a result of an RPC are listed u
158158
**Endpoint:** `POST /auth`
159159
**Reply:** synchronous HTTP response
160160

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.
162162

163163
#### Request body
164164

165165
| Field | Type | Required | Notes |
166166
|---|---|---|---|
167-
| `ssoToken` | string | yes | OIDC-issued SSO token. |
167+
| `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. |
168169
| `natsPublicKey` | string | yes | The client's NATS user public NKey (must pass `nkeys.IsValidPublicUserKey`). |
169170

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+
170173
```json
171174
{
172175
"ssoToken": "<sso-token>",
@@ -210,11 +213,15 @@ See [Error envelope](#6-error-envelope-reference). HTTP statuses:
210213

211214
| Status | `code` | `reason` | Example body |
212215
|---|---|---|---|
213-
| 400 | `bad_request` || `{ "code": "bad_request", "error": "ssoToken and natsPublicKey are required" }` |
214-
| 400 | `bad_request` || `{ "code": "bad_request", "error": "invalid natsPublicKey format" }` |
216+
| 400 | `bad_request` | `missing_fields` | `{ "code": "bad_request", "reason": "missing_fields", "error": "natsPublicKey is required" }` |
217+
| 400 | `bad_request` | `invalid_nkey` | `{ "code": "bad_request", "reason": "invalid_nkey", "error": "invalid natsPublicKey format" }` |
218+
| 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" }` |
215220
| 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. |
216221
| 401 | `unauthenticated` | `sso_token_expired` | `{ "code": "unauthenticated", "reason": "sso_token_expired", "error": "SSO token has expired, please re-login" }` |
217222
| 401 | `unauthenticated` | `invalid_sso_token` | `{ "code": "unauthenticated", "reason": "invalid_sso_token", "error": "invalid SSO token" }` |
223+
| 401 | `unauthenticated` | `invalid_token` | `{ "code": "unauthenticated", "reason": "invalid_token", "error": "session token invalid" }` — botplatform session token failed validation. |
224+
| 503 | `unavailable` | `upstream_unavailable` | `{ "code": "unavailable", "reason": "upstream_unavailable", "error": "botplatform unavailable" }` — auth-service cannot reach botplatform to validate a session token. |
218225
| 500 | `internal` || `{ "code": "internal", "error": "internal error" }` — the real cause is logged server-side and never sent to the client. |
219226

220227
The returned `natsJwt` has a server-configured lifetime (default 2h). Clients should re-call `POST /auth` to refresh before it expires.
@@ -256,7 +263,12 @@ GET /api/userInfo?account=alice
256263

257264
#### Success response
258265

259-
`HTTP 200`
266+
`HTTP 200`. The response shape is **role-aware**:
267+
268+
- For regular SSO users (account NOT carrying `bot` or `admin` in `users.roles`): the existing rich employee shape below.
269+
- For **bot/admin** accounts: a minimal 5-field shape that omits `employeeId` and only carries the URL bundle the SDK / admin UI needs to bootstrap.
270+
271+
##### Rich shape (SSO users — default)
260272

261273
| Field | Type | Notes |
262274
|---|---|---|
@@ -278,6 +290,22 @@ GET /api/userInfo?account=alice
278290
}
279291
```
280292

293+
##### Minimal shape (bot/admin accounts)
294+
295+
When `users.roles` contains `bot` or `admin`, the response omits `employeeId`. All five remaining fields are exactly as documented above.
296+
297+
```json
298+
{
299+
"account": "p_admin",
300+
"authServiceUrl": "https://auth.site-a.example.com",
301+
"baseUrl": "https://site-a.example.com",
302+
"natsUrl": "wss://nats.site-a.example.com",
303+
"siteId": "site-a"
304+
}
305+
```
306+
307+
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+
281309
#### Error response
282310

283311
See [Error envelope](#6-error-envelope-reference). HTTP statuses:
@@ -299,6 +327,287 @@ See [Error envelope](#6-error-envelope-reference). HTTP statuses:
299327

300328
---
301329

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. |
363+
| `authServiceUrl` | string | Home-site auth-service URL — call `POST {authServiceUrl}/auth` next. |
364+
| `baseUrl` | string | Home-site HTTP origin (site-scoped). |
365+
| `natsUrl` | string | Home-site NATS WebSocket URL. |
366+
| `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. |
367+
368+
```json
369+
{
370+
"userId": "abcdef1234567890x",
371+
"authToken": "<43-char base64url>",
372+
"account": "p_admin",
373+
"siteId": "site-a",
374+
"authServiceUrl": "https://auth.site-a.example.com",
375+
"baseUrl": "https://site-a.example.com",
376+
"natsUrl": "wss://nats.site-a.example.com",
377+
"requirePasswordChange": false
378+
}
379+
```
380+
381+
#### Error response
382+
383+
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. |
475+
| 500 | `internal` || Mongo failure; cause logged server-side. |
476+
477+
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.
501+
- `X-Auth-Token: <authToken>` — legacy Rocket.Chat shape.
502+
503+
Missing / unknown / mistyped token → `401 invalid_token`.
504+
505+
#### Request body
506+
507+
| Field | Type | Required | Notes |
508+
|---|---|---|---|
509+
| `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. |
511+
512+
```json
513+
{ "oldPassword": "<old>", "newPassword": "<new>" }
514+
```
515+
516+
#### Success response
517+
518+
`HTTP 200`
519+
520+
```json
521+
{ "status": "success" }
522+
```
523+
524+
Side effects on success:
525+
- `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. |
538+
| 500 | `internal` || Mongo failure; cause logged server-side. |
539+
540+
#### Triggered events — success path
541+
542+
`None — HTTP-only.`
543+
544+
#### Triggered events — error path
545+
546+
`None.`
547+
548+
---
549+
550+
### 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). |
580+
581+
```json
582+
{
583+
"valid": true,
584+
"principal": {
585+
"userId": "abcdef1234567890x",
586+
"account": "name.shortcode.bot",
587+
"siteId": "site-a",
588+
"roles": ["bot"]
589+
}
590+
}
591+
```
592+
593+
#### Error response
594+
595+
| Status | `code` | `reason` | Notes |
596+
|---|---|---|---|
597+
| 400 | `bad_request` | `missing_fields` | `authToken` empty. |
598+
| 401 | `unauthenticated` | `invalid_token` | Token hash not found. Body carries `{"valid": false, ...}`. |
599+
| 500 | `internal` || Mongo failure. |
600+
601+
#### Triggered events — success path
602+
603+
`None — HTTP-only.`
604+
605+
#### Triggered events — error path
606+
607+
`None.`
608+
609+
---
610+
302611
### 2.4 HTTP — Protected file/image upload/download
303612

304613
HTTP endpoints on `upload-service` for protected file uploads and downloads,

0 commit comments

Comments
 (0)