@tideorg/mcp 1.4.2 → 1.9.1
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 +88 -0
- package/GAP_REGISTER.md +18 -8
- package/PRIVACY.md +41 -0
- package/README.md +207 -197
- package/adapters/AGENTS.md +3 -2
- package/adapters/CLAUDE.md +1 -1
- 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/security-gap-mapping.md +506 -0
- package/canon/security-runtime-probes.md +177 -0
- package/canon/tidecloak-bootstrap.md +21 -16
- package/canon/tidecloak-endpoints.md +60 -98
- package/canon/troubleshooting.md +115 -53
- package/canon/version-policy.md +8 -12
- package/mcp-server/dist/server.js +221 -19
- 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/deploy-tidecloak-docker.md +31 -28
- package/playbooks/diagnose-missing-roles-or-claims.md +2 -2
- package/playbooks/initialize-admin-and-link-account.md +22 -19
- 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/verify-jwt-server-side.md +20 -1
- package/prompts/build-private-customer-portal.md +1 -1
- package/prompts/security-gap-analysis.md +55 -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/scenario.md +2 -2
- package/reference-apps/git-pr-signing-service/bootstrap-sequence.md +8 -8
- 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/role-policy-matrix.md +12 -13
- package/reference-apps/iga-admin-governance/scenario.md +15 -13
- package/reference-apps/organisation-password-manager/bootstrap-sequence.md +5 -6
- package/reference-apps/policy-governed-signing/bootstrap-sequence.md +9 -9
- package/skills/tide-diagnostics/SKILL.md +1 -1
- package/skills/tide-integration/SKILL.md +9 -18
- 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-security-analyst/SKILL.md +182 -0
- package/skills/tide-setup/SKILL.md +1 -1
- package/canon/delegation.md +0 -195
- package/playbooks/setup-server-delegation.md +0 -142
package/canon/feature-mapping.md
CHANGED
|
@@ -102,55 +102,6 @@ if [ -n "$USER_KEY" ]; then echo "Linked"; fi
|
|
|
102
102
|
|
|
103
103
|
---
|
|
104
104
|
|
|
105
|
-
## Server-Side Delegation
|
|
106
|
-
|
|
107
|
-
### Delegation Token Exchange
|
|
108
|
-
|
|
109
|
-
**SDK surface**:
|
|
110
|
-
- `TideDelegation` class from `@tidecloak/server`
|
|
111
|
-
- `createTideFetch` from `@tidecloak/js`
|
|
112
|
-
- `requireDelegation()` and `handleDelegation()` Express middleware
|
|
113
|
-
- `req.delegation.fetch()` helper for admin API calls
|
|
114
|
-
|
|
115
|
-
**What it does**: Lets your server call TideCloak admin APIs on behalf of authenticated users without using master admin credentials.
|
|
116
|
-
|
|
117
|
-
**What is actually Tide-specific**:
|
|
118
|
-
- Two-hop chain of trust: browser DPoP key -> delegation request -> server ephemeral key
|
|
119
|
-
- DelegationToken:1 signing model validated by ORK network
|
|
120
|
-
- DPoP approval via ORK enclave (session key signs server's JKT)
|
|
121
|
-
- Threshold EdDSA signing of delegation token
|
|
122
|
-
|
|
123
|
-
**What is standard OAuth2**:
|
|
124
|
-
- Token exchange (RFC 8693) with `act` claim
|
|
125
|
-
- DPoP (RFC 9449) binding for proof-of-possession
|
|
126
|
-
|
|
127
|
-
**Agent implication**:
|
|
128
|
-
- Use `@tidecloak/server` for server-side admin calls. Do not use master admin credentials.
|
|
129
|
-
- `createTideFetch` handles the 419 interrupt pattern transparently on the browser side
|
|
130
|
-
- Delegation tokens are short-lived (max 600s), cached per user session
|
|
131
|
-
- Scoped delegation via `requested_roles` can restrict roles in the delegation token
|
|
132
|
-
|
|
133
|
-
**Verification**:
|
|
134
|
-
```javascript
|
|
135
|
-
// Server: admin route with delegation
|
|
136
|
-
app.get('/api/admin/users',
|
|
137
|
-
authenticate, requireAdmin,
|
|
138
|
-
getDelegation().requireDelegation(),
|
|
139
|
-
async (req, res) => {
|
|
140
|
-
const users = await req.delegation.fetch(adminUrl + '/users');
|
|
141
|
-
res.json(users);
|
|
142
|
-
}
|
|
143
|
-
);
|
|
144
|
-
|
|
145
|
-
// Browser: transparent delegation
|
|
146
|
-
const appFetch = createTideFetch(window.fetch);
|
|
147
|
-
const users = await appFetch('/api/admin/users');
|
|
148
|
-
```
|
|
149
|
-
|
|
150
|
-
**Full reference**: See `canon/delegation.md` and `playbooks/setup-server-delegation.md`.
|
|
151
|
-
|
|
152
|
-
---
|
|
153
|
-
|
|
154
105
|
## Authorization Features
|
|
155
106
|
|
|
156
107
|
### Role-Based Route Protection (Client-Side)
|
|
@@ -316,8 +267,8 @@ curl -H "Authorization: DPoP $TOKEN" -H "DPoP: $PROOF" https://api.example.com/p
|
|
|
316
267
|
### Encrypt / Decrypt (Self-Encryption)
|
|
317
268
|
|
|
318
269
|
**SDK surface**:
|
|
319
|
-
- `doEncrypt(
|
|
320
|
-
- `doDecrypt(
|
|
270
|
+
- `doEncrypt([{ data: plaintext, tags: [tag] }])` → array of ciphertexts
|
|
271
|
+
- `doDecrypt([{ encrypted: ciphertext, tags: [tag] }])` → array of plaintexts
|
|
321
272
|
|
|
322
273
|
**What it looks like**: Standard E2EE with application-side key management.
|
|
323
274
|
|
|
@@ -344,14 +295,14 @@ curl -H "Authorization: DPoP $TOKEN" -H "DPoP: $PROOF" https://api.example.com/p
|
|
|
344
295
|
**Workflow**:
|
|
345
296
|
1. Admin creates roles: `_tide_ssn.selfencrypt`, `_tide_ssn.selfdecrypt`
|
|
346
297
|
2. Admin assigns roles to users via IGA approval
|
|
347
|
-
3. User calls `doEncrypt(
|
|
348
|
-
4. User calls `doDecrypt('ssn'
|
|
298
|
+
3. User calls `doEncrypt([{ data: '123-45-6789', tags: ['ssn'] }])` → ciphertext stored in app DB
|
|
299
|
+
4. User calls `doDecrypt([{ encrypted: ciphertext, tags: ['ssn'] }])` → Fabric checks JWT for `_tide_ssn.selfdecrypt` role → threshold-decrypts session key → plaintext returned
|
|
349
300
|
|
|
350
301
|
**Verification**:
|
|
351
302
|
```javascript
|
|
352
|
-
const ciphertext = await iam.doEncrypt('
|
|
303
|
+
const [ciphertext] = await iam.doEncrypt([{ data: 'secret data', tags: ['test'] }]);
|
|
353
304
|
assert(ciphertext !== 'secret data');
|
|
354
|
-
const plaintext = await iam.doDecrypt('test'
|
|
305
|
+
const [plaintext] = await iam.doDecrypt([{ encrypted: ciphertext, tags: ['test'] }]);
|
|
355
306
|
assert(plaintext === 'secret data');
|
|
356
307
|
```
|
|
357
308
|
|
|
@@ -365,13 +316,15 @@ assert(plaintext === 'secret data');
|
|
|
365
316
|
|
|
366
317
|
### IGA Quorum Approval
|
|
367
318
|
|
|
319
|
+
> **⚠️ API SURFACE MIGRATION — confirmed by Tide 2026-07-07 (GAP-065).** The change-request endpoints have moved to iga-core's **`/admin/realms/{realm}/iga/change-requests/...`** surface, which **replaces** the legacy `/tide-admin/change-set/...` surface shown below. New shape: the mutating write is captured (HTTP 202 + a change-request id); approve per-id with `POST .../iga/change-requests/{id}/authorize` (200/403/409 — 409 = four-eyes re-sign) then `POST .../iga/change-requests/{id}/commit` (200/403/409/412/404 — 412 = quorum unmet); reject with `POST .../{id}/deny`; batch with `POST /iga/change-requests/bulk-authorize`; list with `GET /iga/change-requests?status=PENDING`; in Tide mode the enclave step is `GET`/`POST .../{id}/approval-model`. Enabling IGA is unchanged: `POST /tide-admin/toggle-iga` (realm attr `isIGAEnabled="true"`). **Also note the two-mode split** (`iga.attestor`): Tide (`=tide`, cryptographic VVK/VRK→ORK sealing) vs Tideless (`=simple`/unset, username attestation, server-enforced, NO crypto). See `canon/security-gap-mapping.md` (SG-07/SG-14/SG-16 + IGA-model note). **Full endpoint spec + payloads + the bootstrap approve/commit loop: `canon/iga-change-requests-api.md`.** The workflow and verification examples below now use the new surface; the legacy `/tide-admin/change-set/...` paths are retained only in the old→new mapping for reference.
|
|
320
|
+
|
|
368
321
|
**What it is**: Admin changes require multi-admin approval sealed by VVK threshold signatures. **VERIFIED** (IGA.mdx, SetupIGA.md, keylessh `init-tidecloak.sh`)
|
|
369
322
|
|
|
370
|
-
**Change-
|
|
323
|
+
**Change-request workflow** (legacy surface shown — see migration banner above; new surface is `/iga/change-requests/...`) **VERIFIED for legacy** (CHANGE_REQUEST_API.md, all exemplar init scripts):
|
|
371
324
|
1. **Create**: Admin action triggers draft creation automatically (DRAFT/PENDING)
|
|
372
|
-
2. **Approve**: `POST
|
|
373
|
-
3. **Enclave** (MultiAdmin mode): decode `changeSetDraftRequests`, present to enclave, submit via `POST .../tideAdminResources/add-review`
|
|
374
|
-
4. **Commit**: `POST
|
|
325
|
+
2. **Approve**: legacy `POST .../tide-admin/change-set/sign/batch` → new `POST .../iga/change-requests/{id}/authorize`
|
|
326
|
+
3. **Enclave** (MultiAdmin / Tide mode): decode `changeSetDraftRequests`, present to enclave, submit via legacy `POST .../tideAdminResources/add-review` → new `POST .../iga/change-requests/{id}/approval-model`
|
|
327
|
+
4. **Commit**: legacy `POST .../tide-admin/change-set/commit/batch` → new `POST .../iga/change-requests/{id}/commit`
|
|
375
328
|
|
|
376
329
|
**Quorum formula**: `max(1, floor(TotalAdmins * 0.7))` **VERIFIED** (SetupIGA.md). Note: this is the IGA admin quorum, distinct from the SSH policy `threshold` (per-role signing approval count).
|
|
377
330
|
|
|
@@ -394,21 +347,20 @@ assert(plaintext === 'secret data');
|
|
|
394
347
|
- Change requests have a 1-month (2628000s) cryptographic expiry enforced by the Tide enclave. No automatic DB cleanup — stale drafts persist until cancelled. Not configurable. **VERIFIED** (vendor confirmation, batch-02 Q-08, A-25 resolved)
|
|
395
348
|
- **Draft-triggering scope** **VERIFIED** (vendor confirmation, GAP-041 resolved): When IGA is enabled on a non-master realm, 18 specific admin API actions create change-set drafts. Not all mutating endpoints create drafts. Operations fall into two categories: DRAFT (requires quorum sign/commit) and ACTIVE (auto-approved, audit-only). Role creation gets ACTIVE status — no sign/commit needed. Role assignment to users gets DRAFT status — sign/commit required.
|
|
396
349
|
|
|
397
|
-
**Verification
|
|
350
|
+
**Verification** (current `/iga/change-requests/...` surface — see `canon/iga-change-requests-api.md`):
|
|
398
351
|
```bash
|
|
399
|
-
# List pending
|
|
400
|
-
curl "${TIDECLOAK_URL}/admin/realms/${REALM}/
|
|
401
|
-
-H "Authorization: Bearer ${ADMIN_TOKEN}" | jq '.[].
|
|
402
|
-
#
|
|
403
|
-
curl -X POST "${TIDECLOAK_URL}/admin/realms/${REALM}/
|
|
404
|
-
-H "Authorization: Bearer ${ADMIN_TOKEN}" \
|
|
405
|
-
-H "Content-Type: application/json" \
|
|
406
|
-
-d '{"changeSets":[{"changeSetId":"...","changeSetType":"USER_ROLE","actionType":"CREATE"}]}'
|
|
407
|
-
# Commit
|
|
408
|
-
curl -X POST "${TIDECLOAK_URL}/admin/realms/${REALM}/tide-admin/change-set/commit/batch" \
|
|
352
|
+
# List pending change requests (all types, single call)
|
|
353
|
+
curl "${TIDECLOAK_URL}/admin/realms/${REALM}/iga/change-requests?status=PENDING" \
|
|
354
|
+
-H "Authorization: Bearer ${ADMIN_TOKEN}" | jq '.[].id'
|
|
355
|
+
# Authorize (approve) one CR — body optional; FirstAdmin/Tideless signs server-side
|
|
356
|
+
curl -X POST "${TIDECLOAK_URL}/admin/realms/${REALM}/iga/change-requests/${CR_ID}/authorize" \
|
|
409
357
|
-H "Authorization: Bearer ${ADMIN_TOKEN}" \
|
|
410
|
-
-H "Content-Type: application/json"
|
|
411
|
-
|
|
358
|
+
-H "Content-Type: application/json" -d '{}'
|
|
359
|
+
# Commit (409/412 if not PENDING / under threshold or unmet dependency)
|
|
360
|
+
curl -X POST "${TIDECLOAK_URL}/admin/realms/${REALM}/iga/change-requests/${CR_ID}/commit" \
|
|
361
|
+
-H "Authorization: Bearer ${ADMIN_TOKEN}"
|
|
362
|
+
# Batch approve during bootstrap:
|
|
363
|
+
# POST .../iga/change-requests/bulk-authorize -d '{"actionTypeIn":["CREATE","DELETE"],"limit":100}'
|
|
412
364
|
```
|
|
413
365
|
|
|
414
366
|
**Common confusion**: IGA docs read as "approval workflow" without emphasizing threshold signatures. This is NOT procedural approval; it is cryptographic enforcement.
|
|
@@ -472,10 +424,10 @@ assert(result.signature !== null); // ORKs approved and signed
|
|
|
472
424
|
|
|
473
425
|
**Export endpoint** **VERIFIED** (vendor confirmation, GAP-044 resolved):
|
|
474
426
|
```
|
|
475
|
-
GET /admin/realms/{realm}/
|
|
427
|
+
GET /admin/realms/{realm}/vendorResources/get-installations-provider?clientId={client-uuid}&providerId=keycloak-oidc-keycloak-json
|
|
476
428
|
```
|
|
477
429
|
|
|
478
|
-
There is a single provider ID: `keycloak-oidc-keycloak-json`. The string `tidecloak-oidc-keycloak-json` does not exist in the codebase. TideCloak enriches the adapter JSON via this vendor endpoint by post-processing the standard provider output to inject Tide-specific fields.
|
|
430
|
+
This is the **realm-level** endpoint; the client is passed as the `clientId={uuid}` query param, NOT as a `/clients/{id}/` path segment (the per-client path returns a minimal adapter missing `jwk`). There is a single provider ID: `keycloak-oidc-keycloak-json`. The string `tidecloak-oidc-keycloak-json` does not exist in the codebase. TideCloak enriches the adapter JSON via this vendor endpoint by post-processing the standard provider output to inject Tide-specific fields.
|
|
479
431
|
|
|
480
432
|
**Loading locations** **VERIFIED** (keylessh configs):
|
|
481
433
|
- File: `data/tidecloak.json`
|
|
@@ -527,7 +479,7 @@ Content-Type: application/x-www-form-urlencoded
|
|
|
527
479
|
Params: email (string, required), isRagnarokEnabled (boolean, optional, defaults true)
|
|
528
480
|
Response: 200 with raw licensing JSON as text/plain
|
|
529
481
|
```
|
|
530
|
-
The endpoint creates the Tide IDP, `tide-vendor-key` component, provisions a free-tier license via the ORK network, and generates the initial VVK/VRK keys. Exactly two parameters — no hidden or undocumented ones. Sending JSON body causes `"Could not
|
|
482
|
+
The endpoint creates the Tide IDP, `tide-vendor-key` component, provisions a free-tier license via the ORK network, and generates the initial VVK/VRK keys. Exactly two parameters — no hidden or undocumented ones. Sending a JSON body (instead of `x-www-form-urlencoded`) causes the setup to fail with `"Tide realm setup failed"` / `"Could not set up the Tide realm"`.
|
|
531
483
|
|
|
532
484
|
**Tide realm defaults** **VERIFIED** (keylessh `realm.json`):
|
|
533
485
|
- Default roles: `_tide_enabled`, `offline_access`, `uma_authorization`
|
|
@@ -300,13 +300,11 @@ export function loadTideConfig(): TideConfig {
|
|
|
300
300
|
|
|
301
301
|
**Alternative pattern** **VERIFIED** (keylessh exemplar):
|
|
302
302
|
|
|
303
|
-
The `@tidecloak/verify` package provides a higher-level verification function. It
|
|
303
|
+
The `@tidecloak/verify` package provides a higher-level verification function. It ships a proper dual `exports` map (`import` → ESM, `require` → CJS), so a plain named import works in both ESM and CJS callers.
|
|
304
304
|
|
|
305
305
|
```typescript
|
|
306
|
-
// @tidecloak/verify
|
|
307
|
-
import
|
|
308
|
-
const _mod = (TideJWT as any)?.default ?? TideJWT;
|
|
309
|
-
const { verifyTideCloakToken } = _mod;
|
|
306
|
+
// @tidecloak/verify ships dual exports — use a plain named import
|
|
307
|
+
import { verifyTideCloakToken } from '@tidecloak/verify';
|
|
310
308
|
|
|
311
309
|
// config = parsed tidecloak.json adapter config
|
|
312
310
|
// token = raw JWT string (without "Bearer " prefix)
|
|
@@ -314,14 +312,12 @@ const { verifyTideCloakToken } = _mod;
|
|
|
314
312
|
const payload = await verifyTideCloakToken(config, token, allowedRoles);
|
|
315
313
|
```
|
|
316
314
|
|
|
317
|
-
**
|
|
315
|
+
**Note**: Older docs wrapped this in a defensive `const _mod = (TideJWT as any)?.default ?? TideJWT` interop dance against a single-export build. That is no longer required — the package now ships a dual `exports` map, so the plain named import above works. Do not reintroduce the dance; if a named import genuinely fails, you are on a very old version and should upgrade.
|
|
318
316
|
|
|
319
317
|
**Express middleware using `@tidecloak/verify`:**
|
|
320
318
|
|
|
321
319
|
```typescript
|
|
322
|
-
import
|
|
323
|
-
const _mod = (TideJWT as any)?.default ?? TideJWT;
|
|
324
|
-
const { verifyTideCloakToken } = _mod;
|
|
320
|
+
import { verifyTideCloakToken } from '@tidecloak/verify';
|
|
325
321
|
|
|
326
322
|
async function authenticate(req, res, next) {
|
|
327
323
|
const token = req.headers.authorization?.substring(7);
|
|
@@ -434,7 +430,7 @@ export function EncryptedForm() {
|
|
|
434
430
|
|
|
435
431
|
async function handleSubmit(plaintext: string) {
|
|
436
432
|
// Encrypt before sending to server
|
|
437
|
-
const ciphertext = await doEncrypt('ssn'
|
|
433
|
+
const [ciphertext] = await doEncrypt([{ data: plaintext, tags: ['ssn'] }]);
|
|
438
434
|
await fetch('/api/users', {
|
|
439
435
|
method: 'POST',
|
|
440
436
|
body: JSON.stringify({ ssn: ciphertext }),
|
|
@@ -443,7 +439,7 @@ export function EncryptedForm() {
|
|
|
443
439
|
|
|
444
440
|
async function handleLoad(ciphertext: string) {
|
|
445
441
|
// Decrypt after receiving from server
|
|
446
|
-
const plaintext = await doDecrypt('ssn'
|
|
442
|
+
const [plaintext] = await doDecrypt([{ encrypted: ciphertext, tags: ['ssn'] }]);
|
|
447
443
|
console.log(plaintext);
|
|
448
444
|
}
|
|
449
445
|
|
|
@@ -628,7 +624,9 @@ import { TideCloakContextProvider } from '@tidecloak/react';
|
|
|
628
624
|
import App from './App';
|
|
629
625
|
|
|
630
626
|
// Config file must be in public/tidecloak.json — do NOT import as a JS module
|
|
631
|
-
|
|
627
|
+
// DPoP is on by default (useDPoP defaults to { mode: 'strict' }). Pass useDPoP to pin the alg
|
|
628
|
+
// or relax the mode. There is no `enableDpop` flag. ES256 is the default algorithm.
|
|
629
|
+
const initOptions = { useDPoP: { mode: 'strict', alg: 'ES256' } };
|
|
632
630
|
|
|
633
631
|
ReactDOM.createRoot(document.getElementById('root')!).render(
|
|
634
632
|
<React.StrictMode>
|
|
@@ -651,7 +649,7 @@ import { useTideCloak, Authenticated, Unauthenticated } from '@tidecloak/react';
|
|
|
651
649
|
|
|
652
650
|
function App() {
|
|
653
651
|
return (
|
|
654
|
-
<TideCloakContextProvider configUrl="/tidecloak.json" initOptions={{
|
|
652
|
+
<TideCloakContextProvider configUrl="/tidecloak.json" initOptions={{ useDPoP: { mode: 'strict', alg: 'ES256' } }}>
|
|
655
653
|
<AppContent />
|
|
656
654
|
</TideCloakContextProvider>
|
|
657
655
|
);
|
|
@@ -973,7 +971,7 @@ const users = await response.json();
|
|
|
973
971
|
```typescript
|
|
974
972
|
// Encrypt
|
|
975
973
|
const plaintext = 'sensitive data';
|
|
976
|
-
const ciphertext = await iam.doEncrypt('ssn'
|
|
974
|
+
const [ciphertext] = await iam.doEncrypt([{ data: plaintext, tags: ['ssn'] }]);
|
|
977
975
|
|
|
978
976
|
// Send to server
|
|
979
977
|
await iam.secureFetch('/api/users', {
|
|
@@ -984,7 +982,7 @@ await iam.secureFetch('/api/users', {
|
|
|
984
982
|
// Decrypt
|
|
985
983
|
const response = await iam.secureFetch('/api/users/123');
|
|
986
984
|
const data = await response.json();
|
|
987
|
-
const plaintext = await iam.doDecrypt('ssn'
|
|
985
|
+
const [plaintext] = await iam.doDecrypt([{ encrypted: data.ssn, tags: ['ssn'] }]);
|
|
988
986
|
```
|
|
989
987
|
|
|
990
988
|
---
|
|
@@ -1100,17 +1098,66 @@ Every Tide-enabled app must have a working page at its configured `redirectUri`.
|
|
|
1100
1098
|
|
|
1101
1099
|
---
|
|
1102
1100
|
|
|
1101
|
+
## .NET / ASP.NET Core (via asgard)
|
|
1102
|
+
|
|
1103
|
+
ASP.NET Core APIs integrate with TideCloak through the **Tide.Asgard.AspNetCore** SDK rather than stock `Microsoft.AspNetCore.Authentication.OpenIdConnect`. The asgard SDK supplies the EdDSA `SignatureProvider` (Microsoft.IdentityModel.Tokens does not ship EdDSA) and an OAuth 2.0 Token Exchange client tuned for Tidecloak.
|
|
1104
|
+
|
|
1105
|
+
Out of scope for the current pack priority (Next.js > React > Vanilla per [AGENTS.md](../AGENTS.md)). This section exists so an agent asked about C# / ASP.NET Core routes the work to the right playbook instead of inventing OIDC wiring.
|
|
1106
|
+
|
|
1107
|
+
### Packages
|
|
1108
|
+
|
|
1109
|
+
| Package | Source | Purpose | Status |
|
|
1110
|
+
|---------|--------|---------|--------|
|
|
1111
|
+
| `Tide.Asgard.AspNetCore.Authentication` (v0.1.0) | `github.com/tide-foundation/asgard` — consume via `<ProjectReference>` until NuGet publication is verified | JWT bearer wiring, Ed25519 issuer-key extraction (`Utils.GetEd25519IssuerKey`), Token Exchange client (`AddTokenExchange`, `ITokenExchangeService`) | VERIFIED |
|
|
1112
|
+
| `Tide.Asgard.Core` (v0.1.0) | Same repo | EdDSA `SignatureProvider` / `SecurityKey` on top of `Microsoft.IdentityModel.Tokens 8.15.0` and `BouncyCastle.Cryptography 2.6.2` | VERIFIED |
|
|
1113
|
+
| `Tide.Asgard.AspNetCore.DPoP` | Same repo, no `<PackageId>` | DPoP proof-validation primitives. Validator is implemented (~540 lines), but `.WithDPoP(...)` registration is commented out at HEAD | OBSERVED_PATTERN |
|
|
1114
|
+
|
|
1115
|
+
### Wiring (canonical form)
|
|
1116
|
+
|
|
1117
|
+
Use the README form, not the Example's `Program.cs` — the Example has the `IssuerSigningKey` line commented out:
|
|
1118
|
+
|
|
1119
|
+
```csharp
|
|
1120
|
+
builder.Services.AddKeycloakWebApiAuthentication(builder.Configuration, options =>
|
|
1121
|
+
{
|
|
1122
|
+
options.RequireHttpsMetadata = false;
|
|
1123
|
+
options.TokenValidationParameters.IssuerSigningKey =
|
|
1124
|
+
Utils.GetEd25519IssuerKey(builder.Configuration);
|
|
1125
|
+
});
|
|
1126
|
+
|
|
1127
|
+
builder.Services.AddTokenExchange(builder.Configuration); // optional, RFC 8693
|
|
1128
|
+
```
|
|
1129
|
+
|
|
1130
|
+
`appsettings.json` must put the backend client's adapter config under a section literally named `Keycloak` — the section name is hardcoded in `Keycloak.AuthServices` and in `AddTokenExchange(IConfiguration)`.
|
|
1131
|
+
|
|
1132
|
+
### What the SDK does NOT do
|
|
1133
|
+
|
|
1134
|
+
- **Policy / contract signing or testing.** The TypeScript `asgard-tide` npm package (different artifact, same repo) handles wire-format models. There is no C# equivalent in the .NET SDK.
|
|
1135
|
+
- **Forseti contract enforcement.** Forseti contracts run as C# inside the ORK sandbox, not in your .NET API. See [custom-contracts.md](custom-contracts.md).
|
|
1136
|
+
- **Browser DPoP** is plumbed but not active. `useDPoP` on the SPA crashes Token Exchange (the DPoP path throws `NotImplementedException`).
|
|
1137
|
+
|
|
1138
|
+
### Full procedure
|
|
1139
|
+
|
|
1140
|
+
See [playbooks/protect-aspnet-core-asgard.md](../playbooks/protect-aspnet-core-asgard.md).
|
|
1141
|
+
|
|
1142
|
+
### Status note
|
|
1143
|
+
|
|
1144
|
+
PUC-092, PUC-093, PUC-094, PUC-095 in `notes/pack-update-candidates.md` planned ASP.NET Core docs around stock OpenIdConnect middleware. The asgard SDK supersedes those plans for the JWT-validation and token-exchange paths. The playbook references that supersession explicitly.
|
|
1145
|
+
|
|
1146
|
+
---
|
|
1147
|
+
|
|
1103
1148
|
## Key Dependencies
|
|
1104
1149
|
|
|
1105
1150
|
| Package | Stable version | Purpose |
|
|
1106
1151
|
|---------|---------------|---------|
|
|
1107
|
-
| `@tidecloak/js` | 0.13.
|
|
1108
|
-
| `@tidecloak/react` | 0.13.
|
|
1109
|
-
| `@tidecloak/nextjs` | 0.13.
|
|
1110
|
-
| `@tidecloak/verify` | 0.13.
|
|
1111
|
-
| `heimdall-tide` | 0.13.
|
|
1112
|
-
| `@tideorg/js` | 0.13.
|
|
1113
|
-
| `asgard-tide` | 0.13.
|
|
1152
|
+
| `@tidecloak/js` | 0.13.33 | Core SDK (vanilla JS) |
|
|
1153
|
+
| `@tidecloak/react` | 0.13.33 | React hooks and guards |
|
|
1154
|
+
| `@tidecloak/nextjs` | 0.13.33 | Next.js provider and hooks |
|
|
1155
|
+
| `@tidecloak/verify` | 0.13.33 | Server-side JWT verification (CJS) |
|
|
1156
|
+
| `heimdall-tide` | 0.13.33 | Policy signing, BasicCustomRequest |
|
|
1157
|
+
| `@tideorg/js` | 0.13.33 | Models, Contracts (Forseti) |
|
|
1158
|
+
| `asgard-tide` | 0.13.33 | Vendor validation |
|
|
1159
|
+
| `Tide.Asgard.AspNetCore.Authentication` (.NET) | 0.1.0 | ASP.NET Core JWT bearer + Token Exchange. NuGet publication unverified — consume via `<ProjectReference>` from `github.com/tide-foundation/asgard`. See [.NET / ASP.NET Core](#net--aspnet-core-via-asgard). |
|
|
1160
|
+
| `Tide.Asgard.Core` (.NET) | 0.1.0 | EdDSA `SignatureProvider` for `Microsoft.IdentityModel.Tokens`. Same repo, same caveat. |
|
|
1114
1161
|
|
|
1115
1162
|
All Tide packages are pre-1.0. Pin to exact versions in templates. See [canon/version-policy.md](version-policy.md) for the full version policy.
|
|
1116
1163
|
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# Hosting Options: Self-Hosted vs Partner-Hosted TideCloak
|
|
2
|
+
|
|
3
|
+
Where the TideCloak instance runs is an infrastructure decision, separate from how the app integrates with it. This file covers the two options, the trust model of each, and the Skycloak partner-hosted path.
|
|
4
|
+
|
|
5
|
+
**This is a pre-build branch (I-17).** Resolve where TideCloak will run before running bootstrap — the setup steps differ. It does **not** change the application wiring (SDK, provider, adapter JSON, JWT verification are identical either way).
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## The two options
|
|
10
|
+
|
|
11
|
+
| | Self-hosted | Partner-hosted (Skycloak) |
|
|
12
|
+
|---|---|---|
|
|
13
|
+
| Who runs TideCloak | You (Docker/Kubernetes/VM) | Skycloak, in their cloud |
|
|
14
|
+
| Setup playbook | `deploy-tidecloak-docker` / `start-tidecloak-dev` | `provision-tidecloak-skycloak` |
|
|
15
|
+
| Admin access | Master admin user/password you create | Admin Console SSO (Skycloak account); no admin password issued |
|
|
16
|
+
| Automation identity | Bootstrap master-admin token | Per-cluster OAuth2 confidential client `skycloak-automation-<cluster-id>` |
|
|
17
|
+
| You manage | OS, container, DB, upgrades, backups, TLS | Nothing — fully managed |
|
|
18
|
+
| Best for | Full control, air-gapped, existing infra | Teams who don't want to run auth infra |
|
|
19
|
+
|
|
20
|
+
Both terminate at the **same** application integration: adapter JSON with `jwk`/`vendorId`/`homeOrkUrl`, server-side JWT verification, DPoP. Choosing hosted does not weaken any invariant — it changes who operates the broker, not how the app enforces security.
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Trust model — why partner-hosting TideCloak is compatible with Tide
|
|
25
|
+
|
|
26
|
+
This is the load-bearing point, and it must be stated honestly: **hosting the TideCloak broker with a partner is an availability and metadata trust, not an integrity trust** — *because of* Tide's threshold model, not despite it.
|
|
27
|
+
|
|
28
|
+
**What the host CANNOT do** (guaranteed by Tide's invariants, regardless of who runs TideCloak):
|
|
29
|
+
- **Forge tokens.** JWT signing is threshold VVK across the ORK network; a compromised TideCloak server cannot mint valid tokens (I-09, I-02). This is the whole point of Tide — the broker is not the trust root.
|
|
30
|
+
- **Decrypt your data.** E2EE session keys are threshold-decrypted by the Fabric; plaintext never exists on the TideCloak server or any single ORK (I-11).
|
|
31
|
+
- **Extract keys.** Keys never exist in whole form anywhere the host can reach (I-01).
|
|
32
|
+
|
|
33
|
+
**What the host CAN do** (the honest caveats — surface these, do not bury them):
|
|
34
|
+
- **Availability.** They operate the broker; they can take it down, throttle it, or lose it. This is a normal managed-service dependency.
|
|
35
|
+
- **Metadata.** They see login timing, realm/client configuration, user identifiers, and IP-level traffic. They do not see passwords (threshold PRISM) or E2EE plaintext, but they see *who authenticates when*.
|
|
36
|
+
- **Admin-path position.** With Admin Console SSO, Skycloak's account system sits in the admin-authentication path. Anyone who compromises the Skycloak account that fronts the console reaches the admin console (subject to whatever governs admin actions inside the realm — see next point).
|
|
37
|
+
- **Tideless IGA is software-enforced by the host.** If the realm runs IGA in **Tideless mode** (`iga.attestor=simple`/unset), the admin-change quorum is enforced by TideCloak's own server logic — which the host operates. A malicious or compromised host could bypass it. **In Tide mode** (`iga.attestor=tide`, licensed), governance approvals are sealed cryptographically (VRK→Midgard→ORK) and the host cannot forge them. **For a partner-hosted deployment where governance integrity matters, use Tide mode.** (See I-10; the Tide/Tideless split is tracked in `GAP_REGISTER.md` GAP-065.)
|
|
38
|
+
|
|
39
|
+
**One-line summary for a user**: "You can let Skycloak host TideCloak without handing them the ability to impersonate your users or read your data — those require the ORK threshold, which the host doesn't control. What you're trusting them for is uptime and the metadata they can see."
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## Skycloak API reference
|
|
44
|
+
|
|
45
|
+
**INFERRED/VERIFIED tags** below reflect the public docs at `https://skycloak.io/docs/api/` as of 2026-07 (`API-Version: 2026-06-01.beta`). The provisioning *procedure* is in `playbooks/provision-tidecloak-skycloak.md`.
|
|
46
|
+
|
|
47
|
+
**Base URL**: `https://api.skycloak.io` **VERIFIED** (docs).
|
|
48
|
+
|
|
49
|
+
**Authentication** **VERIFIED** (docs):
|
|
50
|
+
- Header `API-Key: <key>` — created in the Skycloak dashboard (Workspace → API keys), shown once, treat as a secret.
|
|
51
|
+
- Header `API-Version: 2026-06-01.beta` — **required** on every request.
|
|
52
|
+
- Scopes: write includes read (e.g. `clusters:write` implies `clusters:read`). Credentials retrieval needs `clusters:credentials:read`. Missing scope → `403` with `API key does not have the required scope: <scope>`.
|
|
53
|
+
- Separately, an **OAuth2 client-credentials** path exists for cluster-level automation (Terraform/CI) — distinct from the Public API key. Each cluster provisions a confidential automation client `skycloak-automation-<cluster-id>` in the `master` realm.
|
|
54
|
+
|
|
55
|
+
**Endpoints** **VERIFIED** (docs; request/response field names below are **INFERRED** — confirm against the live response, they are not fully specified in the public docs):
|
|
56
|
+
|
|
57
|
+
| Method | Path | Purpose | Scope |
|
|
58
|
+
|---|---|---|---|
|
|
59
|
+
| GET | `/clusters` | List clusters | `clusters:read` |
|
|
60
|
+
| POST | `/clusters` | Create a cluster (async) | `clusters:write` |
|
|
61
|
+
| GET | `/clusters/{id}` | Get cluster status | `clusters:read` |
|
|
62
|
+
| GET | `/clusters/{id}/credentials` | Get cluster/automation credentials | `clusters:credentials:read` |
|
|
63
|
+
|
|
64
|
+
**Create-cluster inputs** **VERIFIED** (feature docs; exact JSON field names **INFERRED**):
|
|
65
|
+
- **Identity platform**: `Keycloak` or **`TideCloak`** — choose TideCloak for a Tide deployment. This is the field that makes the cluster a Tide broker.
|
|
66
|
+
- **Name**: descriptive identifier.
|
|
67
|
+
- **Version**: a latest-stable Keycloak/TideCloak version.
|
|
68
|
+
- **Size**: `Small` (DEV, 1 site), `Medium` (STAGING, 2 sites), `Large` (PROD, 3 sites).
|
|
69
|
+
- **Location/region**: US East Coast, Canada, Europe, Australia. Trial workspaces provision in the US; non-US regions need Developer plan or higher.
|
|
70
|
+
|
|
71
|
+
**Lifecycle** **VERIFIED** (docs): creation is asynchronous. Cluster goes `provisioning`/"Creating" → `available` or `failed` (~2–4 min; email on completion). **Poll `GET /clusters/{id}` until status is `available` before bootstrapping.**
|
|
72
|
+
|
|
73
|
+
**Result** **VERIFIED** (docs): cluster is reachable at `https://<cluster-id>.app.skycloak.io`. **No Keycloak admin username/password is issued** — admin console is reached via Admin Console SSO (Skycloak account); programmatic admin access uses the `skycloak-automation-<cluster-id>` OAuth2 client.
|
|
74
|
+
|
|
75
|
+
**Errors** **VERIFIED** (docs): RFC 9457 Problem Details JSON (`type`, `title`, `detail`, `status`, `instance`; validation adds an `errors[]` array with `field`/`detail`/`code`/`value`). Notable codes: `402 Payment Required` (action not on current plan), `403` (scope), `409` (name/state conflict), `422` (validation), `429` (rate limit, with `Retry-After`).
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## The critical open question (verify before promising a turnkey Tide setup)
|
|
80
|
+
|
|
81
|
+
Skycloak lists TideCloak as a cluster identity platform, but the public docs do **not** confirm that a hosted TideCloak cluster exposes the full **Tide vendor surface** the pack's bootstrap depends on:
|
|
82
|
+
- `POST /admin/realms/{realm}/vendorResources/setUpTideRealm` (creates Tide IDP, `tide-vendor-key`, provisions the free-tier license, generates VVK/VRK)
|
|
83
|
+
- IGA toggle and the change-request/change-set governance API
|
|
84
|
+
- Adapter export enriched with `jwk`/`vendorId`/`homeOrkUrl`
|
|
85
|
+
- Whether Tide **licensing** (the Stripe free-tier flow) is handled by Skycloak or still required from the operator
|
|
86
|
+
|
|
87
|
+
**Status: STILL_UNRESOLVED** — tracked in `GAP_REGISTER.md` (GAP-066). Until confirmed against a live hosted TideCloak cluster, treat the hosted path as: *provisioning is VERIFIED via Skycloak's API; the Tide-realm bootstrap on top of it is ASSUMED to use the same vendor endpoints and must be verified on the actual instance.* Do not tell a user the hosted path is fully turnkey for Tide until this is confirmed. If the vendor endpoints are absent, the honest answer is that Skycloak hosts the broker but Tide-realm setup needs the partner's Tide-specific provisioning (raise with the Tide/Skycloak teams).
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## Verification
|
|
92
|
+
|
|
93
|
+
A hosting-choice step is done when:
|
|
94
|
+
1. The self-host vs hosted branch was resolved **before** bootstrap (I-17).
|
|
95
|
+
2. If hosted: the cluster reports `available` and is reachable at its `*.app.skycloak.io` URL.
|
|
96
|
+
3. The trust-model caveats (availability, metadata, admin-path, Tideless-IGA) were stated to the operator, not just the benefits.
|
|
97
|
+
4. The adapter JSON exported from the hosted instance still contains `jwk`, `vendorId`, `homeOrkUrl` (I-05, I-13) — same requirement as self-hosted. If it doesn't, GAP-066 applies.
|
|
98
|
+
|
|
99
|
+
## Anti-patterns
|
|
100
|
+
|
|
101
|
+
- **AP-HOST-1** — Presenting partner-hosting as a security *downgrade* ("now a third party holds your auth"). It isn't, because of the threshold model — but state the real caveats (availability, metadata, Tideless-IGA), don't overcorrect into either fear or false comfort.
|
|
102
|
+
- **AP-HOST-2** — Claiming the hosted Tide path is fully turnkey before GAP-066 is resolved. Provisioning the cluster is verified; the Tide-realm bootstrap on top is not.
|
|
103
|
+
- **AP-HOST-3** — Putting the Skycloak API key or the `skycloak-automation-*` client secret in application code or the repo. These are operator/bootstrap secrets (like master admin creds, AP-41) — never in app runtime.
|
|
104
|
+
- **AP-HOST-4** — Hardcoding `API-Version` omission. Every Skycloak API call needs the `API-Version` header or it fails.
|
|
105
|
+
|
|
106
|
+
## Status Legend
|
|
107
|
+
|
|
108
|
+
- **VERIFIED** — from Skycloak public docs or Tide canon that carries its own sourcing
|
|
109
|
+
- **INFERRED** — strongly implied but not explicitly specified (e.g. exact JSON field names)
|
|
110
|
+
- **ASSUMED** — operator guidance where sources are silent (e.g. Tide vendor endpoints on hosted clusters)
|
|
111
|
+
- **STILL_UNRESOLVED** — open gap (GAP-066)
|