@tideorg/mcp 1.4.1 → 1.9.0
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/CHANGELOG.md +81 -0
- package/GAP_REGISTER.md +19 -9
- package/PRIVACY.md +41 -0
- package/README.md +204 -120
- package/adapters/AGENTS.md +3 -2
- package/adapters/CLAUDE.md +1 -1
- package/adapters/replit.md +0 -0
- package/canon/anti-patterns.md +55 -62
- package/canon/concepts.md +46 -34
- package/canon/custom-contracts.md +12 -8
- package/canon/feature-mapping.md +27 -75
- package/canon/framework-matrix.md +69 -22
- package/canon/hosting-options.md +111 -0
- package/canon/iga-change-requests-api.md +211 -0
- package/canon/invariants.md +33 -35
- package/canon/redirect-handler.md +0 -0
- package/canon/security-gap-mapping.md +506 -0
- package/canon/security-runtime-probes.md +177 -0
- package/canon/tidecloak-bootstrap.md +84 -15
- package/canon/tidecloak-endpoints.md +60 -98
- package/canon/troubleshooting.md +201 -53
- package/canon/version-policy.md +8 -12
- package/mcp-server/dist/http.d.ts +2 -0
- package/mcp-server/dist/http.js +46 -0
- package/mcp-server/dist/index.d.ts +0 -0
- package/mcp-server/dist/index.js +2 -601
- package/mcp-server/dist/server.d.ts +2 -0
- package/mcp-server/dist/server.js +617 -0
- package/package.json +48 -45
- package/playbooks/add-auth-nextjs-existing.md +33 -17
- package/playbooks/add-auth-nextjs-fresh.md +1 -1
- package/playbooks/add-rbac-nextjs.md +7 -2
- package/playbooks/bootstrap-realm-from-template.md +33 -18
- package/playbooks/configure-e2ee-roles-and-policies.md +0 -0
- package/playbooks/deploy-tidecloak-docker.md +31 -28
- package/playbooks/diagnose-broken-login.md +0 -0
- package/playbooks/diagnose-missing-roles-or-claims.md +2 -2
- package/playbooks/initialize-admin-and-link-account.md +22 -19
- package/playbooks/migrate-from-existing-auth.md +0 -0
- package/playbooks/protect-api-nextjs.md +40 -1
- package/playbooks/protect-aspnet-core-asgard.md +313 -0
- package/playbooks/protect-routes-nextjs.md +24 -10
- package/playbooks/provision-tidecloak-skycloak.md +213 -0
- package/playbooks/setup-forseti-e2ee.md +32 -50
- package/playbooks/setup-iga-admin-panel.md +113 -171
- package/playbooks/start-tidecloak-dev.md +0 -0
- package/playbooks/verify-jwt-server-side.md +20 -1
- package/prompts/add-admin-approval-flow.md +0 -0
- package/prompts/build-private-customer-portal.md +1 -1
- package/prompts/migrate-generic-auth-to-tide.md +0 -0
- package/prompts/secure-existing-app.md +0 -0
- package/prompts/security-gap-analysis.md +55 -0
- package/reference-apps/INDEX.md +0 -0
- package/reference-apps/encrypted-communication/anti-patterns.md +3 -3
- package/reference-apps/encrypted-communication/bootstrap-sequence.md +2 -2
- package/reference-apps/encrypted-communication/manifest.yaml +0 -0
- package/reference-apps/encrypted-communication/role-policy-matrix.md +0 -0
- package/reference-apps/encrypted-communication/scenario.md +2 -2
- package/reference-apps/git-pr-signing-service/anti-patterns.md +0 -0
- package/reference-apps/git-pr-signing-service/bootstrap-sequence.md +8 -8
- package/reference-apps/git-pr-signing-service/manifest.yaml +0 -0
- package/reference-apps/git-pr-signing-service/role-policy-matrix.md +0 -0
- package/reference-apps/git-pr-signing-service/scenario.md +0 -0
- package/reference-apps/iga-admin-governance/anti-patterns.md +18 -16
- package/reference-apps/iga-admin-governance/bootstrap-sequence.md +10 -5
- package/reference-apps/iga-admin-governance/manifest.yaml +0 -0
- package/reference-apps/iga-admin-governance/role-policy-matrix.md +12 -13
- package/reference-apps/iga-admin-governance/scenario.md +15 -13
- package/reference-apps/organisation-password-manager/anti-patterns.md +0 -0
- package/reference-apps/organisation-password-manager/bootstrap-sequence.md +5 -6
- package/reference-apps/organisation-password-manager/manifest.yaml +0 -0
- package/reference-apps/organisation-password-manager/role-policy-matrix.md +0 -0
- package/reference-apps/organisation-password-manager/scenario.md +0 -0
- package/reference-apps/policy-governed-signing/anti-patterns.md +0 -0
- package/reference-apps/policy-governed-signing/bootstrap-sequence.md +9 -9
- package/reference-apps/policy-governed-signing/manifest.yaml +0 -0
- package/reference-apps/policy-governed-signing/role-policy-matrix.md +0 -0
- package/reference-apps/policy-governed-signing/scenario.md +0 -0
- package/skills/tide-diagnostics/SKILL.md +1 -1
- package/skills/tide-integration/SKILL.md +9 -18
- package/skills/tide-learning-capture/SKILL.md +0 -0
- package/skills/tide-mcp-qa/SKILL.md +139 -0
- package/skills/tide-rbac-and-e2ee/SKILL.md +5 -5
- package/skills/tide-reviewer/SKILL.md +1 -1
- package/skills/tide-route-and-api-protection/SKILL.md +0 -0
- package/skills/tide-scenario-resolver/SKILL.md +0 -0
- package/skills/tide-security-analyst/SKILL.md +182 -0
- package/skills/tide-setup/SKILL.md +1 -1
- package/skills/tide-solutions-architect/SKILL.md +0 -0
- package/canon/delegation.md +0 -195
- package/playbooks/setup-server-delegation.md +0 -142
|
@@ -289,24 +289,27 @@ const executeTideRequest = async (request, waitForAll = false) => {
|
|
|
289
289
|
WRONG: GET /admin/realms/{realm}/tide-admin/realm-policy
|
|
290
290
|
-> { status: "none" } (even when the policy exists)
|
|
291
291
|
|
|
292
|
-
|
|
293
|
-
|
|
292
|
+
WRONG: GET /realms/{realm}/tide-policy-resources/admin-policy
|
|
293
|
+
-> does NOT exist on current main (the public policy endpoint was removed)
|
|
294
|
+
|
|
295
|
+
CORRECT: GET /admin/realms/{realm}/iga/role-policies (admin bearer required)
|
|
296
|
+
-> JSON; the signed admin policy bytes are in the `policy` field
|
|
294
297
|
```
|
|
295
298
|
|
|
296
|
-
The correct endpoint
|
|
299
|
+
The correct endpoint is an admin API route and **requires an admin bearer token**. Proxy through your backend so the browser never holds the admin credential.
|
|
297
300
|
|
|
298
|
-
**CORS: Browser cannot fetch this endpoint directly.** TideCloak and the app are on different origins. Direct browser fetch returns an opaque/blocked response. Create a same-origin API route that proxies the request server-side. VERIFIED (session-001).
|
|
301
|
+
**CORS: Browser cannot fetch this endpoint directly.** TideCloak and the app are on different origins, and this is a privileged admin API. Direct browser fetch returns an opaque/blocked response (and would leak the admin bearer). Create a same-origin API route that proxies the request server-side with the admin token. VERIFIED (session-001).
|
|
299
302
|
|
|
300
303
|
**If the proxy route is protected with `withAuth` (correct per I-03), the client must use authenticated fetch** (e.g., `appFetch` or `secureFetch` with Bearer header). A bare `fetch()` without Authorization returns 401. The resulting empty/error response produces undefined policy bytes, causing `PolicyAuthorizationFlowException: "Model does not have a policy passed with it"` at signing time. VERIFIED (LEARNINGS-batch-009 L-08).
|
|
301
304
|
|
|
302
305
|
**There is no REST API for deploying Forseti contracts.** Do not try `PUT` or `POST` to `/tide-admin/forseti-contracts` — these endpoints do not exist on the dev image (404/405). Contract deployment happens via `PolicySignRequest.addForsetiContractToUpload(contractSource)` in the browser signing flow (Step 4). VERIFIED (session-001).
|
|
303
306
|
|
|
304
|
-
**The
|
|
307
|
+
**The signed bytes arrive as a base64 string in the `policy` field, not raw binary.** Read the `policy` field and decode before using:
|
|
305
308
|
|
|
306
309
|
```js
|
|
307
|
-
// Backend proxy
|
|
308
|
-
const
|
|
309
|
-
const raw = Buffer.from(
|
|
310
|
+
// Backend proxy (holds the admin bearer)
|
|
311
|
+
const json = await tcRes.json();
|
|
312
|
+
const raw = Buffer.from(json.policy, "base64");
|
|
310
313
|
const bytes = Array.from(new Uint8Array(raw));
|
|
311
314
|
res.json({ policyBytes: bytes });
|
|
312
315
|
|
|
@@ -315,12 +318,12 @@ const data = await res.json();
|
|
|
315
318
|
const adminPolicyBytes = new Uint8Array(data.policyBytes);
|
|
316
319
|
```
|
|
317
320
|
|
|
318
|
-
**Common mistake:** Treating the base64 string as raw bytes (passing each character's char code as a byte value). If admin policy bytes start with `[65, 81, 65, 65, ...]` (ASCII for "AQAA..."), you are passing base64 text as byte values instead of decoding it. The ORK will fail with `Index out of range` because the policy structure is garbage.
|
|
321
|
+
**Common mistake:** Treating the base64 `policy` string as raw bytes (passing each character's char code as a byte value). If admin policy bytes start with `[65, 81, 65, 65, ...]` (ASCII for "AQAA..."), you are passing base64 text as byte values instead of decoding it. The ORK will fail with `Index out of range` because the policy structure is garbage.
|
|
319
322
|
|
|
320
|
-
**Common mistake:** Malformed URL from trailing slash in `auth-server-url`. The adapter JSON `auth-server-url` may include a trailing slash (e.g., `http://localhost:8080/`). Constructing the admin policy URL without stripping it produces `http://localhost:8080//realms/...` (double slash), which returns HTML or 404 instead of policy bytes. Always normalize:
|
|
323
|
+
**Common mistake:** Malformed URL from trailing slash in `auth-server-url`. The adapter JSON `auth-server-url` may include a trailing slash (e.g., `http://localhost:8080/`). Constructing the admin policy URL without stripping it produces `http://localhost:8080//admin/realms/...` (double slash), which returns HTML or 404 instead of policy bytes. Always normalize:
|
|
321
324
|
```js
|
|
322
325
|
const baseUrl = config["auth-server-url"].replace(/\/+$/, "");
|
|
323
|
-
const adminPolicyUrl = `${baseUrl}/realms/${config.realm}/
|
|
326
|
+
const adminPolicyUrl = `${baseUrl}/admin/realms/${config.realm}/iga/role-policies`;
|
|
324
327
|
```
|
|
325
328
|
|
|
326
329
|
---
|
|
@@ -528,50 +531,29 @@ These are prep steps. None produce VVK-signed bytes.
|
|
|
528
531
|
|
|
529
532
|
Role assignments that produce VVK-signed UserContexts MUST be manually approved through the approval enclave in the admin's browser. You cannot automate this from a backend script.
|
|
530
533
|
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
```
|
|
534
|
-
POST /tide-admin/change-set/sign
|
|
535
|
-
Response: { "requiresApprovalPopup": true, "changeSetDraftRequests": "base64..." }
|
|
536
|
-
```
|
|
537
|
-
|
|
538
|
-
The `requiresApprovalPopup: true` response means the backend CANNOT complete the signing. The VVK-signed UserContext binds a specific user's `tideUserKey` to specific roles. This signing must happen through the ORK threshold network via the Tide enclave running in the admin's browser.
|
|
539
|
-
|
|
540
|
-
**Always use `/batch` endpoints for approval:**
|
|
534
|
+
This is Tide MultiAdmin mode: the VVK-signed UserContext binds a specific user's `tideUserKey` to specific roles, and the signing must happen through the ORK threshold network via the Tide enclave in the admin's browser. The backend CANNOT complete it — a bare `authorize` on such a CR requires the two-phase `approval-model` enclave exchange. (Endpoints: current `/iga/change-requests/...` surface — see `canon/iga-change-requests-api.md`.)
|
|
541
535
|
|
|
542
536
|
```js
|
|
543
|
-
// 1. Backend
|
|
537
|
+
// 1. Backend performs the governed change (role assignment) → 202, a CR is created
|
|
544
538
|
POST /admin/realms/{realm}/users/{userId}/role-mappings/realm
|
|
545
539
|
|
|
546
|
-
// 2. Frontend fetches pending
|
|
547
|
-
GET /admin/realms/{realm}/
|
|
540
|
+
// 2. Frontend fetches pending CRs
|
|
541
|
+
GET /admin/realms/{realm}/iga/change-requests?status=PENDING // objects keyed by `id`
|
|
548
542
|
|
|
549
|
-
// 3.
|
|
550
|
-
|
|
551
|
-
|
|
543
|
+
// 3. For each CR, fetch the enclave challenge (Tide MultiAdmin)
|
|
544
|
+
GET /admin/realms/{realm}/iga/change-requests/{id}/approval-model
|
|
545
|
+
// → { changeRequestId, requestModel: "base64..." }
|
|
552
546
|
|
|
553
|
-
// 4. Collect
|
|
554
|
-
const enclaveRequests =
|
|
555
|
-
.filter(r => r.requiresApprovalPopup)
|
|
556
|
-
.map(r => ({ id: r.changesetId, request: base64ToBytes(r.changeSetDraftRequests) }));
|
|
547
|
+
// 4. Collect the challenges, pass to ONE enclave approval popup
|
|
548
|
+
const enclaveRequests = pending.map(cr => ({ id: cr.id, request: base64ToBytes(cr.requestModel) }));
|
|
557
549
|
const approvals = await tc.requestTideOperatorApproval(enclaveRequests);
|
|
558
550
|
|
|
559
|
-
// 5. Submit each signed
|
|
560
|
-
POST /admin/realms/{realm}/
|
|
561
|
-
|
|
562
|
-
// 6. Commit all in batch
|
|
563
|
-
POST /admin/realms/{realm}/tide-admin/change-set/commit/batch
|
|
564
|
-
{ changeSets: [{ changeSetId, changeSetType, actionType }] }
|
|
565
|
-
```
|
|
551
|
+
// 5. Submit each signed doken back to the same endpoint
|
|
552
|
+
POST /admin/realms/{realm}/iga/change-requests/{id}/approval-model
|
|
553
|
+
{ "requestModel": "<base64 doken>" } // → { recorded, authCount, threshold }
|
|
566
554
|
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
```js
|
|
570
|
-
const formData = new FormData();
|
|
571
|
-
formData.append("changeSetId", id);
|
|
572
|
-
formData.append("actionType", actionType);
|
|
573
|
-
formData.append("changeSetType", changeSetType);
|
|
574
|
-
formData.append("requests", btoa(String.fromCharCode(...signedBytes)));
|
|
555
|
+
// 6. Commit each CR once ready (readyToCommit === true)
|
|
556
|
+
POST /admin/realms/{realm}/iga/change-requests/{id}/commit // 412 if still under threshold
|
|
575
557
|
```
|
|
576
558
|
|
|
577
559
|
After commit: the user must refresh their token (or re-login) for the new role to appear in their doken. Use `IAMService.forceUpdateToken()` or `useTideCloak().forceRefreshToken()`.
|
|
@@ -656,8 +638,8 @@ The forseti-crypto-quickstart is the reference implementation that works against
|
|
|
656
638
|
- [ ] `modelId` is `["PolicyEnabledEncryption:1", "PolicyEnabledDecryption:1"]` (array, not string)
|
|
657
639
|
- [ ] `PolicySignRequest` imported from `heimdall-tide`
|
|
658
640
|
- [ ] `BaseTideRequest.decode()` called on `createTideRequest` result before approval
|
|
659
|
-
- [ ] Admin policy fetched from `GET /realms/{realm}/tide-policy-resources/admin-policy`
|
|
660
|
-
- [ ] Admin policy bytes are base64-decoded (not raw char codes)
|
|
641
|
+
- [ ] Admin policy fetched (admin bearer) from `GET /admin/realms/{realm}/iga/role-policies`; signed bytes read from the `policy` field (the public `tide-policy-resources/admin-policy` endpoint does not exist on main)
|
|
642
|
+
- [ ] Admin policy bytes are base64-decoded from the `policy` field (not raw char codes)
|
|
661
643
|
- [ ] Approval popup opens and admin approves
|
|
662
644
|
- [ ] `executeSignRequest` called with `waitForAll = true`
|
|
663
645
|
- [ ] `policy.signature = signatures[0]` assigned before `policy.toBytes()`
|
|
@@ -701,8 +683,8 @@ The forseti-crypto-quickstart is the reference implementation that works against
|
|
|
701
683
|
|
|
702
684
|
### `{ status: "none" }` when fetching admin policy
|
|
703
685
|
|
|
704
|
-
**Cause:** Using the wrong endpoint `GET /admin/realms/{realm}/tide-admin/realm-policy`.
|
|
705
|
-
**Fix:** Use `GET /realms/{realm}/
|
|
686
|
+
**Cause:** Using the wrong endpoint `GET /admin/realms/{realm}/tide-admin/realm-policy`. (The old public `GET /realms/{realm}/tide-policy-resources/admin-policy` endpoint no longer exists on main either.)
|
|
687
|
+
**Fix:** Use `GET /admin/realms/{realm}/iga/role-policies` with an admin bearer token and read the signed bytes from the `policy` field.
|
|
706
688
|
|
|
707
689
|
### Admin policy bytes start with `[65, 81, 65, 65, ...]`
|
|
708
690
|
|
|
@@ -12,6 +12,13 @@ Build an admin UI for managing IGA change requests with quorum governance.
|
|
|
12
12
|
|
|
13
13
|
**Do not use** if you only need end-user authentication. See [add-auth-nextjs-fresh.md](add-auth-nextjs-fresh.md) instead.
|
|
14
14
|
|
|
15
|
+
**A custom panel is optional.** The stack already ships two working admin consoles that manage IGA change requests out of the box:
|
|
16
|
+
|
|
17
|
+
- **tide-console SPA** — a built Vite/React admin UI bundled inside the `tidecloak-key-provider` jar and served by TideCloak.
|
|
18
|
+
- **Legacy keycloak.v2 console** — the stock Keycloak admin UI, which on the shipped build carries a native `tide-change-requests` section.
|
|
19
|
+
|
|
20
|
+
Build a custom panel only when you need to embed CR governance inside your own product UI. Otherwise use one of the shipped consoles.
|
|
21
|
+
|
|
15
22
|
---
|
|
16
23
|
|
|
17
24
|
## Prerequisites
|
|
@@ -34,190 +41,136 @@ All endpoints below are relative to:
|
|
|
34
41
|
|
|
35
42
|
Example: `http://localhost:8080/admin/realms/myrealm`
|
|
36
43
|
|
|
44
|
+
The consolidated IGA REST surface is rooted at `/admin/realms/{realm}/iga` (server class `IgaAdminResource`). Change requests live under `/iga/change-requests`.
|
|
45
|
+
|
|
46
|
+
> **Legacy bootstrap-compat surface.** The canonical bootstrap script (`localtest/init/init_tidecloak.sh`) and the `tidecloak-js` `AdminAPI` still call an older `/tide-admin/change-set/{type}/requests`, `/tide-admin/change-set/sign`, and `/tide-admin/change-set/commit` surface. That path still exists for backward compatibility and is fine for scripted bootstrap approval, but the `/iga/*` surface documented below is the current API for a live admin panel. Do not mix the two.
|
|
47
|
+
|
|
37
48
|
---
|
|
38
49
|
|
|
39
50
|
## Change Request Lifecycle
|
|
40
51
|
|
|
52
|
+
Current `/iga/change-requests/...` surface (replaces legacy `/tide-admin/change-set/...`, confirmed by Tide 2026-07-07). **Full spec: `canon/iga-change-requests-api.md`.**
|
|
53
|
+
|
|
41
54
|
```
|
|
42
|
-
1. Admin
|
|
43
|
-
->
|
|
44
|
-
2. Admin reviews:
|
|
45
|
-
3. Admin approves: POST /
|
|
46
|
-
->
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
55
|
+
1. Admin performs a governed change (role assignment, group change, etc.)
|
|
56
|
+
-> The write returns 202 Accepted; a change request is created (PENDING)
|
|
57
|
+
2. Admin reviews: GET /iga/change-requests?status=PENDING
|
|
58
|
+
3. Admin approves: POST /iga/change-requests/{id}/authorize (body {} optional)
|
|
59
|
+
-> Tide MultiAdmin mode: sign via the enclave two-phase exchange
|
|
60
|
+
GET/POST /iga/change-requests/{id}/approval-model
|
|
61
|
+
-> FirstAdmin / Tideless mode: authorize signs server-side, no popup
|
|
62
|
+
4. Once threshold met: POST /iga/change-requests/{id}/commit (412 if under threshold)
|
|
63
|
+
5. Or reject: POST /iga/change-requests/{id}/deny
|
|
50
64
|
```
|
|
51
65
|
|
|
66
|
+
`authorize` (simple lane: firstAdmin/Tideless) records an approval and **never commits** — it refuses tide-multiAdmin CRs. `approve` is the multiAdmin enclave lane: it approves AND auto-commits once quorum is met (this is the deployed console "Authorize" button). `commit` is the explicit apply-only step for a CR that already has enough approvals (412 if sub-quorum). Use `bulk-authorize` to act on many at once.
|
|
67
|
+
|
|
52
68
|
---
|
|
53
69
|
|
|
54
70
|
## Endpoints
|
|
55
71
|
|
|
56
|
-
###
|
|
72
|
+
### List / Get
|
|
57
73
|
|
|
58
74
|
```
|
|
59
|
-
GET /
|
|
75
|
+
GET /iga/change-requests?status=PENDING # PENDING|APPROVED|DENIED|CANCELLED
|
|
76
|
+
GET /iga/change-requests/{id} # single CR (keyed by `id`)
|
|
60
77
|
```
|
|
61
78
|
|
|
62
|
-
|
|
79
|
+
Each CR carries `id`, `entityType`, `actionType`, `status`, `readyToCommit`, `threshold`, `authorizers[]`, `dependsOn`/`blocked`. Derive dashboard counts from the list (there is no separate counts endpoint on the new surface; no per-type request endpoints and no `all/requests` path either).
|
|
63
80
|
|
|
64
|
-
###
|
|
81
|
+
### Authorize (Sign)
|
|
65
82
|
|
|
66
83
|
```
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
GET /tide-admin/change-set/roles/requests # Role changes only
|
|
70
|
-
GET /tide-admin/change-set/clients/requests # Client changes only
|
|
71
|
-
GET /tide-admin/change-set/groups/requests # Group changes only
|
|
84
|
+
POST /iga/change-requests/{id}/authorize # body {} optional → 200/403/409 (409 = four-eyes)
|
|
85
|
+
POST /iga/change-requests/bulk-authorize # {"actionTypeIn":["CREATE","DELETE"],"limit":100} → 429 if a bulk run is active
|
|
72
86
|
```
|
|
73
87
|
|
|
74
|
-
###
|
|
88
|
+
### Commit / Deny
|
|
75
89
|
|
|
76
90
|
```
|
|
77
|
-
POST /
|
|
91
|
+
POST /iga/change-requests/{id}/commit # → 200 (APPROVED) / 412 (under threshold or unmet dependency)
|
|
92
|
+
POST /iga/change-requests/{id}/deny # → 204
|
|
78
93
|
```
|
|
79
94
|
|
|
80
|
-
###
|
|
95
|
+
### Enclave Review (Tide MultiAdmin)
|
|
81
96
|
|
|
82
97
|
```
|
|
83
|
-
|
|
84
|
-
POST /
|
|
98
|
+
GET /iga/change-requests/{id}/approval-model # → { changeRequestId, requestModel(base64) }
|
|
99
|
+
POST /iga/change-requests/{id}/approval-model # { "requestModel": "<base64 doken>" } → { recorded, authCount, threshold }
|
|
85
100
|
```
|
|
86
101
|
|
|
87
|
-
|
|
102
|
+
The two-phase `approval-model` exchange replaces the legacy `tideAdminResources/add-review`. In FirstAdmin / Tideless mode, `authorize` signs server-side and no enclave step is needed.
|
|
103
|
+
|
|
104
|
+
### Deprecated: add-review / add-rejection
|
|
88
105
|
|
|
89
106
|
```
|
|
90
|
-
POST /tideAdminResources/add-review #
|
|
91
|
-
POST /tideAdminResources/add-rejection #
|
|
107
|
+
POST /tideAdminResources/add-review # 410 Gone (deprecated)
|
|
108
|
+
POST /tideAdminResources/add-rejection # 410 Gone (deprecated)
|
|
92
109
|
```
|
|
93
110
|
|
|
94
|
-
|
|
111
|
+
Both endpoints were removed as part of the IGA decoupling and now return **`410 Gone`**. Approval goes through `/iga/change-requests/{id}/authorize` (plus the `approval-model` enclave exchange in Tide MultiAdmin mode) and rejection through `/iga/change-requests/{id}/deny`.
|
|
95
112
|
|
|
96
|
-
|
|
97
|
-
// Rejection example — must use FormData, not JSON
|
|
98
|
-
const formData = new FormData();
|
|
99
|
-
formData.append("changeSetId", draftRecordId);
|
|
100
|
-
formData.append("actionType", actionType);
|
|
101
|
-
formData.append("changeSetType", changeSetType);
|
|
102
|
-
|
|
103
|
-
await fetch(`${TC_URL}/admin/realms/${realm}/tideAdminResources/add-rejection`, {
|
|
104
|
-
method: 'POST',
|
|
105
|
-
headers: { 'Authorization': `Bearer ${adminToken}` },
|
|
106
|
-
body: formData // NOT JSON.stringify
|
|
107
|
-
});
|
|
108
|
-
```
|
|
109
|
-
|
|
110
|
-
### Activity and Comments
|
|
113
|
+
### Comments
|
|
111
114
|
|
|
112
115
|
```
|
|
113
|
-
GET
|
|
114
|
-
|
|
116
|
+
GET/POST /iga/change-requests/{id}/comments # { "comment": "..." } (≤2000 chars)
|
|
117
|
+
PUT/DELETE /iga/change-requests/{id}/comments/{commentId}
|
|
115
118
|
```
|
|
116
119
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
## Approve (Sign) Request -- Code Example
|
|
120
|
-
|
|
121
|
-
```typescript
|
|
122
|
-
const response = await fetch(`${TC_URL}/admin/realms/${realm}/tide-admin/change-set/sign/batch`, {
|
|
123
|
-
method: 'POST',
|
|
124
|
-
headers: {
|
|
125
|
-
'Authorization': `Bearer ${adminToken}`,
|
|
126
|
-
'Content-Type': 'application/json'
|
|
127
|
-
},
|
|
128
|
-
body: JSON.stringify({
|
|
129
|
-
changeSets: [{
|
|
130
|
-
changeSetId: "draft-record-id",
|
|
131
|
-
changeSetType: "USER_ROLE",
|
|
132
|
-
actionType: "CREATE",
|
|
133
|
-
policyRoleId: "optional-role-uuid", // omit to use tide-realm-admin policy
|
|
134
|
-
dynamicData: ["optional-base64"] // custom Forseti contract input
|
|
135
|
-
}]
|
|
136
|
-
})
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
// If Tide multi-admin mode:
|
|
140
|
-
// response[0].requiresApprovalPopup === true
|
|
141
|
-
// -> decode changeSetDraftRequests (base64)
|
|
142
|
-
// -> present to Tide enclave for admin signing
|
|
143
|
-
// -> submit via POST /tideAdminResources/add-review
|
|
144
|
-
```
|
|
120
|
+
There is no per-request `/activity` endpoint on the current surface; comments are the audit thread.
|
|
145
121
|
|
|
146
122
|
---
|
|
147
123
|
|
|
148
|
-
## TypeScript Client
|
|
124
|
+
## TypeScript Client Usage
|
|
125
|
+
|
|
126
|
+
There is no `@keycloak/keycloak-admin-client` `KcAdminClient.tideUsersExt` extension on `tidecloak-js` main. The change-request surface is a hand-rolled fetch client, `AdminAPI` (`packages/tidecloak-js/src/AdminAPI.js`), exposed through `@tidecloak/react`. It targets the legacy bootstrap-compat `/tide-admin/change-set/*` endpoints:
|
|
149
127
|
|
|
150
128
|
```typescript
|
|
151
|
-
import
|
|
129
|
+
import { AdminAPI } from "@tidecloak/react";
|
|
152
130
|
|
|
153
|
-
const
|
|
154
|
-
await client.auth({
|
|
155
|
-
username: "admin",
|
|
156
|
-
password: "admin",
|
|
157
|
-
grantType: "password",
|
|
158
|
-
clientId: "admin-cli"
|
|
159
|
-
});
|
|
131
|
+
const api = new AdminAPI({ baseUrl, realm, token });
|
|
160
132
|
|
|
161
|
-
//
|
|
162
|
-
const
|
|
133
|
+
// List (combines user + role change requests)
|
|
134
|
+
const pending = await api.getPendingChangeSets();
|
|
135
|
+
const userCrs = await api.getUserChangeRequests();
|
|
136
|
+
const roleCrs = await api.getRoleChangeRequests();
|
|
163
137
|
|
|
164
|
-
//
|
|
165
|
-
|
|
138
|
+
// Approve a single change set (sent as FormData, single object — not { changeSets: [...] })
|
|
139
|
+
await api.approveChangeSet({ changeSetId, actionType, changeSetType });
|
|
166
140
|
|
|
167
|
-
//
|
|
168
|
-
await
|
|
169
|
-
changeSets: [{ changeSetId: "id", changeSetType: "USER_ROLE", actionType: "CREATE" }]
|
|
170
|
-
});
|
|
141
|
+
// Multi-admin: approve with an enclave signature
|
|
142
|
+
await api.approveChangeSetWithSignature(/* signed payload */);
|
|
171
143
|
|
|
172
144
|
// Commit
|
|
173
|
-
await
|
|
174
|
-
changeSets: [{ changeSetId: "id", changeSetType: "USER_ROLE", actionType: "CREATE" }]
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
// Activity and comments
|
|
178
|
-
const activity = await client.tideUsersExt.getChangeSetActivity({ id: "draft-id" });
|
|
179
|
-
await client.tideUsersExt.addChangeSetComment({ id: "draft-id", comment: "LGTM" });
|
|
145
|
+
await api.commitChangeSet({ changeSetId, actionType, changeSetType });
|
|
180
146
|
```
|
|
181
147
|
|
|
148
|
+
There are no `getChangeSetCounts`, `getAllChangeSetRequests`, `getChangeSetActivity`, or `addChangeSetComment` methods. For a panel built on the current `/iga/*` surface, call the `/iga/change-requests/*` endpoints (see the **Endpoints** section above and `canon/iga-change-requests-api.md`) directly with `fetch` rather than via this SDK.
|
|
149
|
+
|
|
182
150
|
---
|
|
183
151
|
|
|
184
152
|
## Policy Management Endpoints
|
|
185
153
|
|
|
186
154
|
```
|
|
187
|
-
|
|
188
|
-
GET /
|
|
189
|
-
POST /
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
GET /tide-admin/ssh-policies # List role policies
|
|
193
|
-
POST /tide-iga-provider/role-policy/{roleId}/init-cert # Store signed policy on role (JSON: initCert required, initCertSig optional; no doken field)
|
|
194
|
-
GET /tide-admin/role-policies # Roles with policy status
|
|
155
|
+
GET /iga/forseti-contracts # List contracts
|
|
156
|
+
GET /iga/forseti-contracts/{id} # Get one contract
|
|
157
|
+
POST /iga/forseti-contracts # Create a Forseti contract (POST, not PUT)
|
|
158
|
+
DELETE /iga/forseti-contracts/{id} # Delete a contract
|
|
159
|
+
GET /iga/role-policies # Roles with policy status
|
|
195
160
|
```
|
|
196
161
|
|
|
162
|
+
Create is `POST /iga/forseti-contracts` (there is no PUT). Policy-template, SSH-policy, and `role-policy/{roleId}/init-cert` endpoints are not part of the current surface.
|
|
163
|
+
|
|
197
164
|
---
|
|
198
165
|
|
|
199
|
-
##
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
| `USER_ROLE` | User-to-role assignment | DRAFT |
|
|
208
|
-
| `USER_GROUP_MEMBERSHIP` | User-to-group membership | DRAFT |
|
|
209
|
-
| `ROLE` | Role definition change | ACTIVE (auto, creation) / DRAFT (deletion if users assigned) |
|
|
210
|
-
| `COMPOSITE_ROLE` | Composite role change | DRAFT |
|
|
211
|
-
| `CLIENT` | Client configuration change | DRAFT (when users exist) |
|
|
212
|
-
| `CLIENT_DEFAULT_USER_CONTEXT` | Client scope mappings | DRAFT |
|
|
213
|
-
| `CLIENT_FULLSCOPE` | Client full-scope toggle | DRAFT |
|
|
214
|
-
| `GROUP` | Group definition change | — |
|
|
215
|
-
| `GROUP_ROLE` | Group-to-role assignment | DRAFT |
|
|
216
|
-
| `GROUP_MOVE` | Group hierarchy change | DRAFT |
|
|
217
|
-
| `REALM_LICENSING` | Realm license change | DRAFT |
|
|
218
|
-
| `POLICY` | Policy change (derived from USER_ROLE) | DRAFT |
|
|
219
|
-
|
|
220
|
-
**ACTIVE vs DRAFT**: Role creation and default-role assignment get ACTIVE status (recorded for audit but non-blocking — no sign/commit needed). All other actions get DRAFT status requiring quorum sign/commit. Master realm and IGA-disabled realms are always exempt.
|
|
166
|
+
## Change Request Taxonomy
|
|
167
|
+
|
|
168
|
+
The current surface does not use a `ChangeSetType` enum. Each change request carries two free-form String columns on `IgaChangeRequestEntity`:
|
|
169
|
+
|
|
170
|
+
- **`entityType`** — one of: `USER`, `REALM`, `ROLE`, `GROUP`, `CLIENT`, `CLIENT_SCOPE`, `ORGANIZATION`, `COMPOSITE_ROLE`, `CLIENT_SCOPE_CLIENT`, `CLIENT_SCOPE_ROLE`, `PROTOCOL_MAPPER`, `REALM_DEFAULT_SCOPE`, `SCOPE_MAPPING`.
|
|
171
|
+
- **`actionType`** — a free-form verb such as `CREATE`, `SET`, `UPDATE`, `ASSIGN`, or an `ADOPT_*` variant.
|
|
172
|
+
|
|
173
|
+
Read both values back from the `GET /iga/change-requests` response; do not hardcode a fixed enum. Some change requests (e.g. role creation and default-role adoption) commit automatically as part of the login-closure converge and never need an explicit authorize/commit; the rest require quorum authorize/commit. The master realm and IGA-disabled realms are exempt from governance entirely.
|
|
221
174
|
|
|
222
175
|
## Status Values
|
|
223
176
|
|
|
@@ -235,13 +188,13 @@ GET /tide-admin/role-policies # Roles with policy status
|
|
|
235
188
|
|
|
236
189
|
A working IGA admin panel must include:
|
|
237
190
|
|
|
238
|
-
1. **Pending list** -- Display
|
|
191
|
+
1. **Pending list** -- Display change requests via `GET /iga/change-requests?status=PENDING` (keyed by `id`).
|
|
239
192
|
|
|
240
|
-
2. **Approve button** -- Triggers `POST /
|
|
193
|
+
2. **Approve button** -- Triggers `POST /iga/change-requests/{id}/authorize` per selected CR (or `POST /iga/change-requests/bulk-authorize` for a batch).
|
|
241
194
|
|
|
242
|
-
3. **Enclave popup** --
|
|
195
|
+
3. **Enclave popup** -- In Tide MultiAdmin mode, complete the two-phase enclave exchange: `GET /iga/change-requests/{id}/approval-model` → sign in the enclave → `POST /iga/change-requests/{id}/approval-model` with the base64 doken. In FirstAdmin/Tideless mode, `authorize` signs server-side and no popup is needed.
|
|
243
196
|
|
|
244
|
-
4. **Batch support** --
|
|
197
|
+
4. **Batch support** -- Use `POST /iga/change-requests/bulk-authorize` (`{"actionTypeIn":[...],"limit":N}`) to approve many CRs at once; commit each ready CR (`readyToCommit === true`).
|
|
245
198
|
|
|
246
199
|
5. **Role check** -- Only users with admin roles should see the panel. Verify the admin token includes the required realm-admin or delegated role before rendering the UI. This is UI gating only; the server enforces authorization on every API call.
|
|
247
200
|
|
|
@@ -251,22 +204,21 @@ A working IGA admin panel must include:
|
|
|
251
204
|
|
|
252
205
|
### Lifecycle
|
|
253
206
|
|
|
254
|
-
- [ ]
|
|
255
|
-
- [ ]
|
|
256
|
-
- [ ] In
|
|
257
|
-
- [ ] Committing
|
|
258
|
-
- [ ]
|
|
207
|
+
- [ ] A governed write returns 202 and a change request appears via `GET /iga/change-requests?status=PENDING`
|
|
208
|
+
- [ ] `POST .../{id}/authorize` records an approval (409 if the same admin re-signs)
|
|
209
|
+
- [ ] In Tide MultiAdmin mode, the `approval-model` enclave exchange completes and increments `authCount`
|
|
210
|
+
- [ ] Committing a ready CR (`readyToCommit`) applies the change (APPROVED); 412 while under threshold
|
|
211
|
+
- [ ] `POST .../{id}/deny` removes it from the pending list (DENIED)
|
|
259
212
|
|
|
260
213
|
### Endpoints
|
|
261
214
|
|
|
262
|
-
- [ ] `GET
|
|
263
|
-
- [ ] `
|
|
264
|
-
- [ ] `POST
|
|
265
|
-
- [ ] `POST
|
|
266
|
-
- [ ] `POST /
|
|
267
|
-
- [ ] `POST /
|
|
268
|
-
- [ ] `
|
|
269
|
-
- [ ] `POST .../{id}/comments` persists and appears in activity
|
|
215
|
+
- [ ] `GET /iga/change-requests?status=PENDING` returns all pending CRs across types
|
|
216
|
+
- [ ] `POST /iga/change-requests/{id}/authorize` records the approval
|
|
217
|
+
- [ ] `POST /iga/change-requests/bulk-authorize` authorizes a batch (429 if a bulk run is active)
|
|
218
|
+
- [ ] `POST /iga/change-requests/{id}/commit` applies the change; 412 under threshold
|
|
219
|
+
- [ ] `POST /iga/change-requests/{id}/deny` sets status to DENIED
|
|
220
|
+
- [ ] `GET/POST /iga/change-requests/{id}/comments` persist and read back
|
|
221
|
+
- [ ] `POST /tideAdminResources/add-review` and `/add-rejection` return `410 Gone` (deprecated — do not use)
|
|
270
222
|
|
|
271
223
|
### Authorization
|
|
272
224
|
|
|
@@ -278,33 +230,33 @@ A working IGA admin panel must include:
|
|
|
278
230
|
|
|
279
231
|
## Common Failures
|
|
280
232
|
|
|
281
|
-
###
|
|
233
|
+
### Enclave Step Skipped in Tide MultiAdmin Mode
|
|
282
234
|
|
|
283
|
-
**Symptom**: Approve
|
|
235
|
+
**Symptom**: Approve appears to succeed but the change request stays in PENDING and `authCount` does not increase.
|
|
284
236
|
|
|
285
|
-
**Cause**:
|
|
237
|
+
**Cause**: In Tide MultiAdmin mode, a bare `authorize` is not enough — the admin must sign the enclave challenge via the two-phase `approval-model` exchange.
|
|
286
238
|
|
|
287
|
-
**Fix**:
|
|
239
|
+
**Fix**: `GET /iga/change-requests/{id}/approval-model`, decode `requestModel`, sign it in the Tide enclave, then `POST /iga/change-requests/{id}/approval-model` with `{ "requestModel": "<base64 doken>" }`. (FirstAdmin/Tideless mode signs server-side on `authorize` — no enclave step.)
|
|
288
240
|
|
|
289
241
|
---
|
|
290
242
|
|
|
291
|
-
### Committing Before Enough Approvals
|
|
243
|
+
### Committing Before Enough Approvals (412)
|
|
292
244
|
|
|
293
|
-
**Symptom**: `POST
|
|
245
|
+
**Symptom**: `POST /iga/change-requests/{id}/commit` returns **412 Precondition Failed**.
|
|
294
246
|
|
|
295
|
-
**Cause**: The
|
|
247
|
+
**Cause**: The CR is under threshold, or a dependency has not committed yet.
|
|
296
248
|
|
|
297
|
-
**Fix**:
|
|
249
|
+
**Fix**: Only commit CRs where `readyToCommit === true`. If still short, collect more approvals; if `blocked`, commit the CR named in `dependsOn` first (see the commit-in-passes loop in `canon/iga-change-requests-api.md`).
|
|
298
250
|
|
|
299
251
|
---
|
|
300
252
|
|
|
301
|
-
###
|
|
253
|
+
### Re-signing the Same CR (409)
|
|
302
254
|
|
|
303
|
-
**Symptom**:
|
|
255
|
+
**Symptom**: `POST /iga/change-requests/{id}/authorize` returns **409 Conflict**.
|
|
304
256
|
|
|
305
|
-
**Cause**:
|
|
257
|
+
**Cause**: Four-eyes enforcement — the same admin cannot sign a CR twice, or the CR is no longer PENDING.
|
|
306
258
|
|
|
307
|
-
**Fix**:
|
|
259
|
+
**Fix**: Have a *different* admin authorize, or refresh the list; do not retry the same admin's signature.
|
|
308
260
|
|
|
309
261
|
---
|
|
310
262
|
|
|
@@ -314,31 +266,21 @@ A working IGA admin panel must include:
|
|
|
314
266
|
|
|
315
267
|
**Cause**: Admin token not included or expired.
|
|
316
268
|
|
|
317
|
-
**Fix**: Obtain a fresh admin token before each batch of calls
|
|
318
|
-
|
|
319
|
-
---
|
|
320
|
-
|
|
321
|
-
### Rejection Silently Fails
|
|
322
|
-
|
|
323
|
-
**Symptom**: Clicking "Reject" appears to succeed, but the change request stays in PENDING. No error returned.
|
|
324
|
-
|
|
325
|
-
**Cause**: The rejection endpoint (`POST /tideAdminResources/add-rejection`) expects `multipart/form-data`. If you send `Content-Type: application/json`, the server accepts the request but ignores the payload fields.
|
|
326
|
-
|
|
327
|
-
**Fix**: Use `FormData`, not `JSON.stringify`. Do not set `Content-Type` manually — the browser sets the correct multipart boundary automatically. See the code example in the Endpoints section above.
|
|
269
|
+
**Fix**: Obtain a fresh admin token before each batch of calls and handle token refresh.
|
|
328
270
|
|
|
329
271
|
---
|
|
330
272
|
|
|
331
|
-
###
|
|
273
|
+
### Calling a Deprecated add-review / add-rejection Endpoint
|
|
332
274
|
|
|
333
|
-
**Symptom**:
|
|
275
|
+
**Symptom**: `POST /tideAdminResources/add-review` or `/add-rejection` returns `410 Gone`.
|
|
334
276
|
|
|
335
|
-
**Cause**:
|
|
277
|
+
**Cause**: Both endpoints were removed in the IGA decoupling. They are no longer functional.
|
|
336
278
|
|
|
337
|
-
**Fix**:
|
|
279
|
+
**Fix**: Approve via `POST /iga/change-requests/{id}/authorize` (passing the enclave-signed approval in the body) and reject via `POST /iga/change-requests/{id}/deny`.
|
|
338
280
|
|
|
339
281
|
---
|
|
340
282
|
|
|
341
283
|
## References
|
|
342
284
|
|
|
343
|
-
-
|
|
344
|
-
-
|
|
285
|
+
- Current surface: `IgaAdminResource` (`tidecloak-iga-extensions` iga-core) — `/iga/change-requests/*`, `/iga/forseti-contracts`, `/iga/role-policies`.
|
|
286
|
+
- Legacy bootstrap-compat surface: `/tide-admin/change-set/*`, still used by `localtest/init/init_tidecloak.sh` and the `tidecloak-js` `AdminAPI` client.
|
|
File without changes
|
|
@@ -21,7 +21,26 @@ Complete guide to implementing server-side JWT verification for Tide.
|
|
|
21
21
|
|
|
22
22
|
---
|
|
23
23
|
|
|
24
|
-
##
|
|
24
|
+
## Recommended First: the Shipped `verifyTideCloakToken` Helper
|
|
25
|
+
|
|
26
|
+
Before hand-rolling anything, know that Tide ships a one-call verifier. `@tidecloak/verify` exports `verifyTideCloakToken`, re-exported via `@tidecloak/nextjs/server`:
|
|
27
|
+
|
|
28
|
+
```typescript
|
|
29
|
+
import { verifyTideCloakToken } from '@tidecloak/nextjs/server'; // or '@tidecloak/verify'
|
|
30
|
+
import tcConfig from '../data/tidecloak.json';
|
|
31
|
+
|
|
32
|
+
// Arg order: (config, token, allowedRoles=[]) — config FIRST.
|
|
33
|
+
const payload = await verifyTideCloakToken(tcConfig, token, ['admin']);
|
|
34
|
+
// payload = decoded claims on success, `false` on any failure.
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
It does local-JWKS signature verification (embedded `jwk`), issuer + `azp` validation, time-claim checks with `clockTolerance`, and optional role enforcement across realm and client roles. For the majority of API-protection needs this is all you require — see [protect-api-nextjs.md](protect-api-nextjs.md).
|
|
38
|
+
|
|
39
|
+
The complete manual implementation below is the **"under the hood" alternative**. Reach for it when you need something `verifyTideCloakToken` does not do: app-side DPoP proof re-verification, distinct 401 vs 403 responses, `iat` future-dating rejection, or an Express integration.
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## Complete Implementation (Manual)
|
|
25
44
|
|
|
26
45
|
If you already created `lib/auth/tidecloakConfig.ts`, `lib/auth/tideJWT.ts`, and `lib/auth/protect.ts` from [protect-api-nextjs.md](protect-api-nextjs.md), the versions below **replace** them. This playbook adds DPoP verification, client-role checking in `hasRole`, `extractToken` (supports both Bearer and DPoP schemes), and `iat` future-validation.
|
|
27
46
|
|
|
File without changes
|
|
@@ -48,7 +48,7 @@ Copy-paste this prompt to an AI coding agent to scaffold a customer portal with
|
|
|
48
48
|
> **If TideCloak is running but checks 2-4 fail**, follow the `tide-setup` skill to complete setup. Use playbook `add-auth-nextjs-fresh`.
|
|
49
49
|
>
|
|
50
50
|
> **Package versions** (before writing package.json):
|
|
51
|
-
> 1. Run `npm view @tidecloak/nextjs version` to get the current stable version. Pin it exactly (e.g., `"0.13.
|
|
51
|
+
> 1. Run `npm view @tidecloak/nextjs version` to get the current stable version. Pin it exactly (e.g., `"0.13.33"`). Do not use `"latest"` or `"^"` for `@tidecloak/*` packages.
|
|
52
52
|
> 2. If the resolved version is 0.99.x, skip it. Use the highest non-0.99.x version instead. If unsure, fall back to the versions in `canon/version-policy.md`.
|
|
53
53
|
> 3. Run `npm view next version` and `npm view react version` to get current stable framework versions. Use caret ranges (e.g., `"^15.0.0"`). Align React and React DOM to the same major.
|
|
54
54
|
> 4. Do not use `--force` or `--legacy-peer-deps` as the default install strategy. If peer dependency conflicts arise, resolve them by aligning versions.
|
|
File without changes
|
|
File without changes
|