@tideorg/mcp 1.4.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/GAP_REGISTER.md +117 -0
- package/README.md +38 -0
- package/adapters/AGENTS.md +241 -0
- package/adapters/CLAUDE.md +146 -0
- package/adapters/replit.md +224 -0
- package/canon/anti-patterns.md +2133 -0
- package/canon/concepts.md +816 -0
- package/canon/custom-contracts.md +533 -0
- package/canon/delegation.md +195 -0
- package/canon/feature-mapping.md +637 -0
- package/canon/framework-matrix.md +1125 -0
- package/canon/invariants.md +851 -0
- package/canon/redirect-handler.md +254 -0
- package/canon/tidecloak-bootstrap.md +287 -0
- package/canon/tidecloak-endpoints.md +294 -0
- package/canon/troubleshooting.md +1494 -0
- package/canon/version-policy.md +89 -0
- package/mcp-server/dist/index.d.ts +2 -0
- package/mcp-server/dist/index.js +605 -0
- package/package.json +45 -0
- package/playbooks/add-auth-nextjs-existing.md +799 -0
- package/playbooks/add-auth-nextjs-fresh.md +654 -0
- package/playbooks/add-rbac-nextjs.md +190 -0
- package/playbooks/bootstrap-realm-from-template.md +170 -0
- package/playbooks/configure-e2ee-roles-and-policies.md +196 -0
- package/playbooks/deploy-tidecloak-docker.md +792 -0
- package/playbooks/diagnose-broken-login.md +234 -0
- package/playbooks/diagnose-missing-roles-or-claims.md +253 -0
- package/playbooks/initialize-admin-and-link-account.md +235 -0
- package/playbooks/migrate-from-existing-auth.md +198 -0
- package/playbooks/protect-api-nextjs.md +451 -0
- package/playbooks/protect-routes-nextjs.md +544 -0
- package/playbooks/setup-forseti-e2ee.md +756 -0
- package/playbooks/setup-iga-admin-panel.md +344 -0
- package/playbooks/setup-server-delegation.md +142 -0
- package/playbooks/start-tidecloak-dev.md +130 -0
- package/playbooks/verify-jwt-server-side.md +439 -0
- package/prompts/add-admin-approval-flow.md +50 -0
- package/prompts/build-private-customer-portal.md +85 -0
- package/prompts/migrate-generic-auth-to-tide.md +67 -0
- package/prompts/secure-existing-app.md +80 -0
- package/reference-apps/INDEX.md +87 -0
- package/reference-apps/encrypted-communication/anti-patterns.md +123 -0
- package/reference-apps/encrypted-communication/bootstrap-sequence.md +152 -0
- package/reference-apps/encrypted-communication/manifest.yaml +100 -0
- package/reference-apps/encrypted-communication/role-policy-matrix.md +80 -0
- package/reference-apps/encrypted-communication/scenario.md +134 -0
- package/reference-apps/git-pr-signing-service/anti-patterns.md +61 -0
- package/reference-apps/git-pr-signing-service/bootstrap-sequence.md +157 -0
- package/reference-apps/git-pr-signing-service/manifest.yaml +80 -0
- package/reference-apps/git-pr-signing-service/role-policy-matrix.md +99 -0
- package/reference-apps/git-pr-signing-service/scenario.md +169 -0
- package/reference-apps/iga-admin-governance/anti-patterns.md +133 -0
- package/reference-apps/iga-admin-governance/bootstrap-sequence.md +153 -0
- package/reference-apps/iga-admin-governance/manifest.yaml +87 -0
- package/reference-apps/iga-admin-governance/role-policy-matrix.md +67 -0
- package/reference-apps/iga-admin-governance/scenario.md +126 -0
- package/reference-apps/organisation-password-manager/anti-patterns.md +71 -0
- package/reference-apps/organisation-password-manager/bootstrap-sequence.md +127 -0
- package/reference-apps/organisation-password-manager/manifest.yaml +86 -0
- package/reference-apps/organisation-password-manager/role-policy-matrix.md +59 -0
- package/reference-apps/organisation-password-manager/scenario.md +107 -0
- package/reference-apps/policy-governed-signing/anti-patterns.md +67 -0
- package/reference-apps/policy-governed-signing/bootstrap-sequence.md +128 -0
- package/reference-apps/policy-governed-signing/manifest.yaml +78 -0
- package/reference-apps/policy-governed-signing/role-policy-matrix.md +62 -0
- package/reference-apps/policy-governed-signing/scenario.md +113 -0
- package/skills/tide-diagnostics/SKILL.md +99 -0
- package/skills/tide-integration/SKILL.md +136 -0
- package/skills/tide-learning-capture/SKILL.md +174 -0
- package/skills/tide-rbac-and-e2ee/SKILL.md +214 -0
- package/skills/tide-reviewer/SKILL.md +133 -0
- package/skills/tide-route-and-api-protection/SKILL.md +201 -0
- package/skills/tide-scenario-resolver/SKILL.md +81 -0
- package/skills/tide-setup/SKILL.md +218 -0
- package/skills/tide-solutions-architect/SKILL.md +130 -0
|
@@ -0,0 +1,637 @@
|
|
|
1
|
+
# Tide Feature Mapping
|
|
2
|
+
|
|
3
|
+
Maps Tide SDK features to their actual security properties and implementation requirements.
|
|
4
|
+
|
|
5
|
+
**Critical Rule**: UI gating is NOT authorization. Protected routes are NOT protected APIs. Clearly distinguish client-side convenience from server-side enforcement.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Authentication Features
|
|
10
|
+
|
|
11
|
+
### Login / SSO / Logout
|
|
12
|
+
|
|
13
|
+
**SDK surface**:
|
|
14
|
+
- `doLogin()`, `doLogout()` (Vanilla JS)
|
|
15
|
+
- `useTideCloak().login()`, `useTideCloak().logout()` (React/Next.js)
|
|
16
|
+
|
|
17
|
+
**What it looks like**: Standard OIDC login flow with redirect to TideCloak.
|
|
18
|
+
|
|
19
|
+
**What is actually Tide-specific**:
|
|
20
|
+
- Password verification is threshold (PRISM across T+ ORKs) **VERIFIED**
|
|
21
|
+
- No password hash stored anywhere **VERIFIED**
|
|
22
|
+
- Each ORK verifies password challenge independently **VERIFIED**
|
|
23
|
+
- Attacker compromising TideCloak server cannot learn passwords **VERIFIED**
|
|
24
|
+
|
|
25
|
+
**What is generic OIDC**:
|
|
26
|
+
- Redirect flow (authorization code grant)
|
|
27
|
+
- Token endpoint calls
|
|
28
|
+
- Cookie/session management
|
|
29
|
+
- Silent SSO via iframe
|
|
30
|
+
|
|
31
|
+
**Agent implication**:
|
|
32
|
+
- Standard SDK methods work; do not implement custom password flows
|
|
33
|
+
- Login redirect, token exchange, refresh are OIDC standard
|
|
34
|
+
- Threshold verification is transparent to SDK consumer
|
|
35
|
+
- **Do not present login UX as the differentiator**; backend threshold verification is the actual Tide feature
|
|
36
|
+
|
|
37
|
+
**Verification**:
|
|
38
|
+
```javascript
|
|
39
|
+
// Login succeeds
|
|
40
|
+
await iam.doLogin({ redirectUri: window.location.origin });
|
|
41
|
+
// Check token is present
|
|
42
|
+
const token = await iam.getToken();
|
|
43
|
+
assert(token !== null);
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
**Common confusion**: Login UI looks like any OIDC provider. The difference is server-side: TideCloak delegates password verification to Fabric, not database check.
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
### Silent SSO Token Refresh
|
|
51
|
+
|
|
52
|
+
**SDK surface**:
|
|
53
|
+
- `silent-check-sso.html` in `public/`
|
|
54
|
+
- Automatic refresh via SDK
|
|
55
|
+
|
|
56
|
+
**What it looks like**: Standard OIDC silent refresh via hidden iframe.
|
|
57
|
+
|
|
58
|
+
**What is Tide-specific**: None. This is standard Keycloak silent SSO.
|
|
59
|
+
|
|
60
|
+
**Agent implication**:
|
|
61
|
+
- Required file: `public/silent-check-sso.html` **VERIFIED**
|
|
62
|
+
- Corrupted state detection pattern from keylessh **VERIFIED** (keylessh `AuthContext.tsx`):
|
|
63
|
+
```javascript
|
|
64
|
+
// Check for stale auth state on startup
|
|
65
|
+
const storageKeys = Object.keys(localStorage);
|
|
66
|
+
const hasIamData = storageKeys.some(k => k.includes('iam') || k.includes('auth'));
|
|
67
|
+
if (hasIamData && !initSuccess) {
|
|
68
|
+
// Offer reset via ?reset=true
|
|
69
|
+
window.location.href = '/?reset=true';
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
**Failure modes**:
|
|
74
|
+
- CSP blocks iframe: silent refresh fails, requires full re-login
|
|
75
|
+
- Stale localStorage: IAM init hangs, 10s timeout triggers error
|
|
76
|
+
- Network timeout: falls back to full redirect
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
### Account Linking (Admin Onboarding)
|
|
81
|
+
|
|
82
|
+
**What it is**: New admin users must link their Tide account to activate threshold features. **VERIFIED** (keylessh `init-tidecloak.sh`)
|
|
83
|
+
|
|
84
|
+
**Workflow**:
|
|
85
|
+
1. Create user in TideCloak admin console
|
|
86
|
+
2. Generate linking URL via `POST /admin/realms/{realm}/users/{user-id}/tideAdminResources/get-required-action-link` with `lifespan=43200` (12 hours) **VERIFIED**
|
|
87
|
+
3. User opens link, completes linking in browser
|
|
88
|
+
4. Poll user attributes for `tideUserKey` and `vuid`
|
|
89
|
+
5. When both present, linking complete
|
|
90
|
+
|
|
91
|
+
**Agent implication**:
|
|
92
|
+
- Required action: `link-tide-account-action` **VERIFIED** (keylessh `realm.json`)
|
|
93
|
+
- Linking URL is time-limited (default 12 hours)
|
|
94
|
+
- Whether mandatory for non-admin users is unclear **PARTIALLY_RESOLVED** (GAP-030)
|
|
95
|
+
|
|
96
|
+
**Verification**:
|
|
97
|
+
```bash
|
|
98
|
+
# Poll for completion
|
|
99
|
+
USER_KEY=$(curl -s ... /admin/realms/myrealm/users/$USER_ID | jq -r '.attributes.tideUserKey[0]')
|
|
100
|
+
if [ -n "$USER_KEY" ]; then echo "Linked"; fi
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
---
|
|
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
|
+
## Authorization Features
|
|
155
|
+
|
|
156
|
+
### Role-Based Route Protection (Client-Side)
|
|
157
|
+
|
|
158
|
+
**SDK surface**:
|
|
159
|
+
- `useTideCloak().hasRealmRole(roleName)` — check realm roles
|
|
160
|
+
- `useTideCloak().hasClientRole(roleName, clientId?)` — check client roles
|
|
161
|
+
- `iam.hasRealmRole(roleName)` / `iam.hasClientRole(roleName, clientId?)`
|
|
162
|
+
|
|
163
|
+
The SDK hook does not export a generic `hasRole()`. Use `hasRealmRole` for realm roles and `hasClientRole` for client roles. `tide-realm-admin` is a client role on `realm-management` — use `hasClientRole('tide-realm-admin', 'realm-management')`, not `hasRealmRole`.
|
|
164
|
+
|
|
165
|
+
**What it looks like**: Protected routes that check user roles before rendering components.
|
|
166
|
+
|
|
167
|
+
**What is actually happening**: UI gating only. This is client-side convenience, NOT server-side authorization.
|
|
168
|
+
|
|
169
|
+
**Security property**: **NONE**. An attacker can bypass by modifying client code or replaying tokens.
|
|
170
|
+
|
|
171
|
+
**Agent implication**:
|
|
172
|
+
- **DO NOT** rely on `hasRealmRole()` / `hasClientRole()` for API authorization
|
|
173
|
+
- **DO** use them for UI/UX decisions (show/hide buttons, render different components)
|
|
174
|
+
- **DO** implement server-side role verification for protected APIs (see JWT Verification below)
|
|
175
|
+
|
|
176
|
+
**Example**:
|
|
177
|
+
```javascript
|
|
178
|
+
// CLIENT-SIDE (UI gating only)
|
|
179
|
+
if (useTideCloak().hasRealmRole('admin')) {
|
|
180
|
+
return <AdminPanel />; // Attacker can bypass
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// SERVER-SIDE (real authorization)
|
|
184
|
+
app.get('/api/admin/users', (req, res) => {
|
|
185
|
+
const jwt = verifyJWT(req.headers.authorization); // Verify signature
|
|
186
|
+
if (!jwt.realm_access.roles.includes('admin')) {
|
|
187
|
+
return res.status(403).json({ error: 'Forbidden' });
|
|
188
|
+
}
|
|
189
|
+
// Proceed with admin operation
|
|
190
|
+
});
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
**Common confusion**: SDK docs show role checks for route protection without emphasizing this is UI-only. Do not treat UI gating as authorization.
|
|
194
|
+
|
|
195
|
+
---
|
|
196
|
+
|
|
197
|
+
### JWT Verification (Server-Side Authorization)
|
|
198
|
+
|
|
199
|
+
**What it is**: Real authorization. Verifies threshold-signed JWT on server-side before granting API access.
|
|
200
|
+
|
|
201
|
+
**Tide-specific properties**:
|
|
202
|
+
- JWT signed by VVK threshold protocol (T+ ORKs) **VERIFIED**
|
|
203
|
+
- Signature verified against embedded JWKS from adapter JSON (local-only, no remote fallback) **VERIFIED** (keylessh `tideJWT.ts`). If `jwk` is missing, this is a setup failure — re-export adapter with IGA enabled.
|
|
204
|
+
- Each VVK ORK independently verified claims before partial-signing **VERIFIED**
|
|
205
|
+
|
|
206
|
+
**Server-side verification pattern** **VERIFIED** (keylessh `server/lib/auth/tideJWT.ts`):
|
|
207
|
+
```typescript
|
|
208
|
+
import { createLocalJWKSet, jwtVerify } from 'jose';
|
|
209
|
+
import { loadTideConfig } from './tidecloakConfig';
|
|
210
|
+
|
|
211
|
+
const config = loadTideConfig();
|
|
212
|
+
// Use embedded JWKS only (I-04). Do not use createRemoteJWKSet.
|
|
213
|
+
const JWKS = createLocalJWKSet(config.jwk);
|
|
214
|
+
|
|
215
|
+
async function verifyTideJWT(token: string) {
|
|
216
|
+
const { payload } = await jwtVerify(token, JWKS, {
|
|
217
|
+
issuer: config['auth-server-url'].replace(/\/+$/, '') + '/realms/' + config.realm,
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
// TideCloak uses azp (not aud) for client ID
|
|
221
|
+
if (payload.azp !== config.resource) throw new Error('Token azp mismatch');
|
|
222
|
+
|
|
223
|
+
const now = Math.floor(Date.now() / 1000);
|
|
224
|
+
if (payload.exp < now) throw new Error('Token expired');
|
|
225
|
+
if (payload.iat > now + 60) throw new Error('Token issued in future');
|
|
226
|
+
|
|
227
|
+
return payload;
|
|
228
|
+
}
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
**Agent implication**:
|
|
232
|
+
- **DO NOT** fetch JWKS from `{realm}/protocol/openid-connect/certs`; use adapter JSON
|
|
233
|
+
- **DO** verify `iss` (issuer), `azp` (authorized party), `exp`, `iat`
|
|
234
|
+
- **DO** check roles in `realm_access.roles` or `resource_access` after signature verification
|
|
235
|
+
- Adapter JSON field: `jwk: { keys: JWK[] }` (only present when IGA enabled)
|
|
236
|
+
|
|
237
|
+
**Verification**:
|
|
238
|
+
```bash
|
|
239
|
+
# Extract public key from adapter JSON
|
|
240
|
+
jq '.jwk.keys[0]' data/tidecloak.json
|
|
241
|
+
# Verify JWT signature against this key
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
**Common confusion**: SDK docs mention `@tidecloak/verify` package but do not emphasize embedded JWKS usage. Authentication API docs omit this entirely.
|
|
245
|
+
|
|
246
|
+
---
|
|
247
|
+
|
|
248
|
+
### DPoP Token Binding (API Security)
|
|
249
|
+
|
|
250
|
+
**What it is**: RFC 9449 standard. Binds access tokens to client key pair. Server must verify proof on every request. **VERIFIED** (keylessh `server/auth.ts`)
|
|
251
|
+
|
|
252
|
+
**Tide-specific configuration** **VERIFIED** (vendor confirmation, GAP-032 resolved):
|
|
253
|
+
```typescript
|
|
254
|
+
// ES256 is the default and recommended for new projects
|
|
255
|
+
useDPoP: { mode: 'strict', alg: 'ES256' }
|
|
256
|
+
// EdDSA also supported (with automatic browser fallback)
|
|
257
|
+
useDPoP: { mode: 'strict', alg: 'EdDSA' }
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
**Client-side** (automatic via SDK):
|
|
261
|
+
- SDK generates ephemeral key pair
|
|
262
|
+
- Each request includes `DPoP` header with fresh proof
|
|
263
|
+
- Proof signs HTTP method + URL + timestamp + nonce
|
|
264
|
+
- Algorithm negotiated via server's `dpop_signing_alg_values_supported` OIDC metadata
|
|
265
|
+
|
|
266
|
+
**Server-side verification pattern** **VERIFIED** (keylessh `server/auth.ts`):
|
|
267
|
+
1. Extract `DPoP` header
|
|
268
|
+
2. Verify JWT: `typ: "dpop+jwt"`, `alg` matches expected (ES256 or EdDSA)
|
|
269
|
+
3. Verify signature against `jwk` in proof
|
|
270
|
+
4. Check `htm` matches HTTP method
|
|
271
|
+
5. Check `htu` matches request URL (ignore query string)
|
|
272
|
+
6. Check `iat` freshness (120s window)
|
|
273
|
+
7. Check `jti` not replayed (2-min TTL in-memory cache)
|
|
274
|
+
8. Extract access token, verify `cnf.jkt` matches proof thumbprint
|
|
275
|
+
|
|
276
|
+
**Agent implication**:
|
|
277
|
+
- DPoP is per-request; do not cache headers
|
|
278
|
+
- Server must maintain `jti` replay cache with TTL
|
|
279
|
+
- DPoP is required for Tide's full security guarantees **VERIFIED** (vendor confirmation, GAP-032 resolved)
|
|
280
|
+
|
|
281
|
+
**Verification**:
|
|
282
|
+
```bash
|
|
283
|
+
# Request with DPoP
|
|
284
|
+
curl -H "Authorization: DPoP $TOKEN" -H "DPoP: $PROOF" https://api.example.com/protected
|
|
285
|
+
# Server verifies proof before granting access
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
**Common confusion**: DPoP is NOT bearer token. Replaying `Authorization` header without fresh `DPoP` proof fails.
|
|
289
|
+
|
|
290
|
+
---
|
|
291
|
+
|
|
292
|
+
## E2EE Features
|
|
293
|
+
|
|
294
|
+
### Choosing the Encryption Model
|
|
295
|
+
|
|
296
|
+
**Decision tree** — answer before writing any encryption code:
|
|
297
|
+
|
|
298
|
+
| Question | If yes | If no |
|
|
299
|
+
|----------|--------|-------|
|
|
300
|
+
| Does only the encrypting user need to decrypt? | **Self-encryption**. Use `doEncrypt`/`doDecrypt` from `useTideCloak()`. Roles: `_tide_{tag}.selfencrypt`/`.selfdecrypt`. | Continue below. |
|
|
301
|
+
| Do multiple users need to decrypt the same ciphertext? | **Policy-governed VVK encryption**. Use `IAMService.doEncrypt(data, signedPolicyBytes)`. Requires Forseti contract + signed policy + voucher gates + contract role. See playbook `setup-forseti-e2ee`. | Self-encryption is sufficient. |
|
|
302
|
+
|
|
303
|
+
**These are different SDK call paths, different bootstrap flows, and different security models.** Switching from self-encryption to policy-governed encryption is not a role rename. It requires:
|
|
304
|
+
1. A Forseti contract deployed to the ORK network
|
|
305
|
+
2. A Policy object constructed and signed via the 5-step flow
|
|
306
|
+
3. Admin policy attached before commit
|
|
307
|
+
4. Signed policy bytes stored server-side
|
|
308
|
+
5. `IAMService.doEncrypt(data, policyBytes)` instead of `doEncrypt(data, tags)`
|
|
309
|
+
|
|
310
|
+
**If self-encryption fails** (e.g., "User has not been given any access"), fix the self-encryption setup:
|
|
311
|
+
- Check `_tide_{tag}.selfencrypt`/`.selfdecrypt` roles exist and are assigned
|
|
312
|
+
- Check user has re-logged in after role assignment
|
|
313
|
+
- Check IGA change requests are approved and committed
|
|
314
|
+
- Do NOT rename roles to `_tide_{tag}.encrypt`/`.decrypt` — this does not fix anything (AP-26)
|
|
315
|
+
|
|
316
|
+
### Encrypt / Decrypt (Self-Encryption)
|
|
317
|
+
|
|
318
|
+
**SDK surface**:
|
|
319
|
+
- `doEncrypt(tag, plaintext)` → ciphertext
|
|
320
|
+
- `doDecrypt(tag, ciphertext)` → plaintext
|
|
321
|
+
|
|
322
|
+
**What it looks like**: Standard E2EE with application-side key management.
|
|
323
|
+
|
|
324
|
+
**What is actually Tide-specific**:
|
|
325
|
+
- Session key encrypted via CVK threshold across ORKs **VERIFIED**
|
|
326
|
+
- Decryption requires live Fabric threshold participation **VERIFIED**
|
|
327
|
+
- Plaintext never exists on server, admin console, or any ORK **VERIFIED**
|
|
328
|
+
- Enforcement is cryptographic: Fabric won't decrypt without role proof in JWT **VERIFIED**
|
|
329
|
+
|
|
330
|
+
**Tag-based role enforcement** **VERIFIED** (e2ee.md):
|
|
331
|
+
- Role pattern: `_tide_<tag>.selfencrypt`, `_tide_<tag>.selfdecrypt`
|
|
332
|
+
- Admin creates roles in TideCloak console
|
|
333
|
+
- Tag names are fully application-defined, no reserved prefixes or values. Case-sensitive, passed to ORK as-is (UTF-8, no normalization), max ~237 chars for self-encryption. **VERIFIED** (vendor confirmation, batch-02 Q-10, A-26 resolved). For policy-based encryption, tags are opaque data passed to the Forseti contract — no `_tide_` roles needed.
|
|
334
|
+
- User must have matching role for encrypt/decrypt to succeed
|
|
335
|
+
- Role check is embedded in JWT; Fabric verifies before threshold-decrypting session key
|
|
336
|
+
|
|
337
|
+
**Agent implication**:
|
|
338
|
+
- Both `Uint8Array` (binary) and string inputs supported **VERIFIED** (vendor confirmation, GAP-013 resolved)
|
|
339
|
+
- No application-level max payload size. Payloads <32 bytes use ElGamal; >=32 bytes use hybrid AES-256-GCM + ElGamal. Practical ceiling ~28.6 MB (Kestrel default). **VERIFIED** (vendor confirmation, GAP-013 resolved)
|
|
340
|
+
- Ciphertext is a TideMemory-serialized envelope. Base64 string when input was string, raw `Uint8Array` when input was `Uint8Array`. Overhead ~157B / ~217B before base64. Versioned (v1), stable. **VERIFIED** (vendor confirmation, batch-02 Q-05, A-28 resolved)
|
|
341
|
+
- Requires online Fabric access; no offline decryption **VERIFIED**
|
|
342
|
+
- Both `selfencrypt` AND `selfdecrypt` roles needed to round-trip **INFERRED** (A-27)
|
|
343
|
+
|
|
344
|
+
**Workflow**:
|
|
345
|
+
1. Admin creates roles: `_tide_ssn.selfencrypt`, `_tide_ssn.selfdecrypt`
|
|
346
|
+
2. Admin assigns roles to users via IGA approval
|
|
347
|
+
3. User calls `doEncrypt('ssn', '123-45-6789')` → ciphertext stored in app DB
|
|
348
|
+
4. User calls `doDecrypt('ssn', ciphertext)` → Fabric checks JWT for `_tide_ssn.selfdecrypt` role → threshold-decrypts session key → plaintext returned
|
|
349
|
+
|
|
350
|
+
**Verification**:
|
|
351
|
+
```javascript
|
|
352
|
+
const ciphertext = await iam.doEncrypt('test', 'secret data');
|
|
353
|
+
assert(ciphertext !== 'secret data');
|
|
354
|
+
const plaintext = await iam.doDecrypt('test', ciphertext);
|
|
355
|
+
assert(plaintext === 'secret data');
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
**Common confusion**: E2EE is NOT client-side encryption with server key storage. Decryption requires live Fabric threshold. Do not implement offline decrypt fallback.
|
|
359
|
+
|
|
360
|
+
**Common confusion**: Renaming `_tide_{tag}.selfencrypt` to `_tide_{tag}.encrypt` does NOT switch from self-encryption to shared encryption. The role suffix is a naming convention. The SDK call path (`doEncrypt` vs `IAMService.doEncrypt(data, policyBytes)`) determines the encryption model. See AP-26 in `canon/anti-patterns.md`.
|
|
361
|
+
|
|
362
|
+
---
|
|
363
|
+
|
|
364
|
+
## Governance Features
|
|
365
|
+
|
|
366
|
+
### IGA Quorum Approval
|
|
367
|
+
|
|
368
|
+
**What it is**: Admin changes require multi-admin approval sealed by VVK threshold signatures. **VERIFIED** (IGA.mdx, SetupIGA.md, keylessh `init-tidecloak.sh`)
|
|
369
|
+
|
|
370
|
+
**Change-set workflow** **VERIFIED** (CHANGE_REQUEST_API.md, all exemplar init scripts):
|
|
371
|
+
1. **Create**: Admin action triggers draft creation automatically (DRAFT/PENDING)
|
|
372
|
+
2. **Approve**: `POST /admin/realms/{realm}/tide-admin/change-set/sign/batch`
|
|
373
|
+
3. **Enclave** (MultiAdmin mode): decode `changeSetDraftRequests`, present to enclave, submit via `POST .../tideAdminResources/add-review`
|
|
374
|
+
4. **Commit**: `POST /admin/realms/{realm}/tide-admin/change-set/commit/batch`
|
|
375
|
+
|
|
376
|
+
**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
|
+
|
|
378
|
+
**What is Tide-specific**:
|
|
379
|
+
- Approval is cryptographic, not procedural **VERIFIED**
|
|
380
|
+
- VVK ORKs seal approved changes with threshold signatures (Authorization Proofing) **VERIFIED**
|
|
381
|
+
- VVK ORKs verify future JWT claims against these proofs **VERIFIED**
|
|
382
|
+
- No single admin can bypass **VERIFIED**
|
|
383
|
+
- Two signing modes: FirstAdmin (VRK, no popup) vs MultiAdmin (enclave challenge) **VERIFIED** (CHANGE_REQUEST_API.md)
|
|
384
|
+
|
|
385
|
+
**What is generic workflow**:
|
|
386
|
+
- Change request tracking
|
|
387
|
+
- Approval UI
|
|
388
|
+
- Audit trail
|
|
389
|
+
|
|
390
|
+
**Agent implication**:
|
|
391
|
+
- Enable IGA via `POST /admin/realms/{realm}/tide-admin/toggle-iga` **VERIFIED** (all exemplar init scripts)
|
|
392
|
+
- Canonical ordering: license → IGA → E2EE (only valid sequence) **VERIFIED** (vendor confirmation, batch-02 Q-04, A-21/A-22 resolved)
|
|
393
|
+
- After commit, token refresh required for roles to appear in doken (up to 120s delay) **VERIFIED** (test-cases F3)
|
|
394
|
+
- 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
|
+
- **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
|
+
|
|
397
|
+
**Verification**:
|
|
398
|
+
```bash
|
|
399
|
+
# List pending user change-sets
|
|
400
|
+
curl "${TIDECLOAK_URL}/admin/realms/${REALM}/tide-admin/change-set/users/requests" \
|
|
401
|
+
-H "Authorization: Bearer ${ADMIN_TOKEN}" | jq '.[].draftRecordId'
|
|
402
|
+
# Sign (approve)
|
|
403
|
+
curl -X POST "${TIDECLOAK_URL}/admin/realms/${REALM}/tide-admin/change-set/sign/batch" \
|
|
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" \
|
|
409
|
+
-H "Authorization: Bearer ${ADMIN_TOKEN}" \
|
|
410
|
+
-H "Content-Type: application/json" \
|
|
411
|
+
-d '{"changeSets":[{"changeSetId":"...","changeSetType":"USER_ROLE","actionType":"CREATE"}]}'
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
**Common confusion**: IGA docs read as "approval workflow" without emphasizing threshold signatures. This is NOT procedural approval; it is cryptographic enforcement.
|
|
415
|
+
|
|
416
|
+
---
|
|
417
|
+
|
|
418
|
+
### Forseti Policy Enforcement
|
|
419
|
+
|
|
420
|
+
**What it is**: Programmable C# contracts executed in every ORK sandbox. Majority of ORKs must approve for operation to proceed. **VERIFIED** (Forseti.mdx, tier2-protocol-forseti.mdx, keylessh `sshPolicy.ts`)
|
|
421
|
+
|
|
422
|
+
**SDK surface**:
|
|
423
|
+
```typescript
|
|
424
|
+
const signRequest = await tc.createTideRequest({
|
|
425
|
+
contract: "using System; ...", // C# code
|
|
426
|
+
modelName: "BasicCustom<APP>:BasicCustom<1>",
|
|
427
|
+
authFlow: "Policy:1",
|
|
428
|
+
authorizer: tc.doken,
|
|
429
|
+
challengeData: { ... }
|
|
430
|
+
});
|
|
431
|
+
const result = await tc.executeSignRequest(signRequest);
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
**What is Tide-specific**:
|
|
435
|
+
- Real C# contract execution in every ORK **VERIFIED**
|
|
436
|
+
- Sandbox enforces namespace restrictions **VERIFIED** (Forseti.mdx)
|
|
437
|
+
- No single ORK can bypass; majority enforcement **VERIFIED**
|
|
438
|
+
- Compromised ORK cannot override policy **VERIFIED**
|
|
439
|
+
|
|
440
|
+
**What is generic**:
|
|
441
|
+
- Contract compilation and packaging (application-side)
|
|
442
|
+
|
|
443
|
+
**Agent implication**:
|
|
444
|
+
- Sandbox: five-layer security model with namespace block-list, gas metering (default 50,000 gas) **VERIFIED** (vendor confirmation, GAP-008 resolved)
|
|
445
|
+
- Contract debugging: `PolicyDecision.Deny("message")` propagates to client. Internal ORK errors still opaque (GAP-018).
|
|
446
|
+
- Doken accessed via `tc.doken` **VERIFIED** (keylessh `tideSsh.ts`)
|
|
447
|
+
- Doken `exp` copied from SSO access token; auto-refreshed with token refresh **VERIFIED** (vendor confirmation, GAP-017 resolved)
|
|
448
|
+
|
|
449
|
+
**Verification**:
|
|
450
|
+
```javascript
|
|
451
|
+
const result = await tc.executeSignRequest(signRequest);
|
|
452
|
+
assert(result.signature !== null); // ORKs approved and signed
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
**Common confusion**: Forseti is NOT authorization middleware config. Real compiled C# executes in distributed ORK sandboxes.
|
|
456
|
+
|
|
457
|
+
---
|
|
458
|
+
|
|
459
|
+
## Infrastructure Features
|
|
460
|
+
|
|
461
|
+
### Adapter JSON Configuration
|
|
462
|
+
|
|
463
|
+
**What it is**: Config file with Tide extensions for embedded JWKS, vendor ID, home ORK endpoint. **VERIFIED** (keylessh `tidecloakConfig.ts`)
|
|
464
|
+
|
|
465
|
+
**Tide-specific fields**:
|
|
466
|
+
- `jwk: { keys: JWK[] }` - Local JWT verification **VERIFIED**
|
|
467
|
+
- `vendorId: string` - Tide vendor identifier **VERIFIED**
|
|
468
|
+
- `homeOrkUrl: string` - Home ORK endpoint **VERIFIED**
|
|
469
|
+
|
|
470
|
+
**Generic Keycloak fields**:
|
|
471
|
+
- `realm`, `auth-server-url`, `ssl-required`, `resource`, `public-client`, `confidential-port`
|
|
472
|
+
|
|
473
|
+
**Export endpoint** **VERIFIED** (vendor confirmation, GAP-044 resolved):
|
|
474
|
+
```
|
|
475
|
+
GET /admin/realms/{realm}/clients/{client-id}/vendorResources/get-installations-provider?providerId=keycloak-oidc-keycloak-json
|
|
476
|
+
```
|
|
477
|
+
|
|
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.
|
|
479
|
+
|
|
480
|
+
**Loading locations** **VERIFIED** (keylessh configs):
|
|
481
|
+
- File: `data/tidecloak.json`
|
|
482
|
+
- Env var: `CLIENT_ADAPTER` (JSON string)
|
|
483
|
+
- Env var: `TIDECLOAK_CONFIG_B64` (base64)
|
|
484
|
+
|
|
485
|
+
**Agent implication**:
|
|
486
|
+
- `jwk` field is only present when IGA is enabled on the realm. Validate at startup.
|
|
487
|
+
- Do not use generic Keycloak adapter parsers; Tide extensions required
|
|
488
|
+
|
|
489
|
+
**Verification**:
|
|
490
|
+
```bash
|
|
491
|
+
jq '.jwk, .vendorId, .homeOrkUrl' data/tidecloak.json
|
|
492
|
+
# All three fields must be present
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
---
|
|
496
|
+
|
|
497
|
+
### CSP for SWE Iframe
|
|
498
|
+
|
|
499
|
+
**What it is**: Content Security Policy whitelist for SWE iframe to function. **VERIFIED** (keylessh `server/index.ts`)
|
|
500
|
+
|
|
501
|
+
**Required CSP directive** **VERIFIED** (vendor confirmation, GAP-028 resolved):
|
|
502
|
+
```
|
|
503
|
+
frame-src 'self' *
|
|
504
|
+
```
|
|
505
|
+
|
|
506
|
+
`frame-src '*'` is required because users can re-home their SWE session to any ORK. No fixed domain list.
|
|
507
|
+
|
|
508
|
+
**Failure symptom**: SWE iframe silently fails; login hangs, E2EE operations timeout, no visible errors (check browser console for CSP violations).
|
|
509
|
+
|
|
510
|
+
**Verification**:
|
|
511
|
+
```bash
|
|
512
|
+
# Check response headers
|
|
513
|
+
curl -I https://app.example.com | grep -i content-security-policy
|
|
514
|
+
# Verify frame-src includes Tide domains
|
|
515
|
+
```
|
|
516
|
+
|
|
517
|
+
---
|
|
518
|
+
|
|
519
|
+
### Realm Initialization
|
|
520
|
+
|
|
521
|
+
**What it is**: Automated realm setup with Tide defaults. **VERIFIED** (keylessh `init-tidecloak.sh`, `realm.json`)
|
|
522
|
+
|
|
523
|
+
**Setup endpoint** **VERIFIED** (vendor confirmation, GAP-029 strengthened):
|
|
524
|
+
```
|
|
525
|
+
POST /admin/realms/{realm}/vendorResources/setUpTideRealm
|
|
526
|
+
Content-Type: application/x-www-form-urlencoded
|
|
527
|
+
Params: email (string, required), isRagnarokEnabled (boolean, optional, defaults true)
|
|
528
|
+
Response: 200 with raw licensing JSON as text/plain
|
|
529
|
+
```
|
|
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 create tide realm (email is null)"`.
|
|
531
|
+
|
|
532
|
+
**Tide realm defaults** **VERIFIED** (keylessh `realm.json`):
|
|
533
|
+
- Default roles: `_tide_enabled`, `offline_access`, `uma_authorization`
|
|
534
|
+
- Browser flow: `tidebrowser` (replaces standard Keycloak flow)
|
|
535
|
+
- Token lifetimes: `accessTokenLifespan: 600`, `ssoSessionIdleTimeout: 1800`, `ssoSessionMaxLifespan: 36000`
|
|
536
|
+
- Protocol mappers: `tideUserKey`→`tideuserkey`, `vuid`→`vuid`, Tide IGA Role Mapper
|
|
537
|
+
- Required action: `link-tide-account-action`
|
|
538
|
+
|
|
539
|
+
**Agent implication**:
|
|
540
|
+
- Protocol mappers are auto-created on client creation **VERIFIED** (vendor confirmation, GAP-036 resolved)
|
|
541
|
+
- `tidebrowser` flow: confirmed across 4 exemplar realm.json files **VERIFIED**
|
|
542
|
+
- Token lifetimes: `accessTokenLifespan: 600` confirmed across all exemplar realm.json files **VERIFIED**
|
|
543
|
+
- Declare `_tide_enabled` in realm.json template — `setUpTideRealm` does not create it **VERIFIED** (vendor confirmation, GAP-031 resolved)
|
|
544
|
+
|
|
545
|
+
**Verification**:
|
|
546
|
+
```bash
|
|
547
|
+
# Check realm configuration
|
|
548
|
+
curl .../admin/realms/myrealm | jq '.browserFlow, .accessTokenLifespan'
|
|
549
|
+
```
|
|
550
|
+
|
|
551
|
+
---
|
|
552
|
+
|
|
553
|
+
## Feature Comparison: Generic vs Tide-Specific
|
|
554
|
+
|
|
555
|
+
| Feature | Generic OIDC | Tide-Specific | Enforcement Location |
|
|
556
|
+
|---------|-------------|---------------|---------------------|
|
|
557
|
+
| Login redirect flow | ✓ | - | Client + TideCloak |
|
|
558
|
+
| Password verification | ✓ (DB hash) | ✓ (threshold PRISM) | Fabric ORKs |
|
|
559
|
+
| JWT signing | ✓ (server key) | ✓ (threshold VVK) | Fabric ORKs |
|
|
560
|
+
| JWT verification | ✓ (remote JWKS) | ✓ (embedded JWKS, local-only) | Application server |
|
|
561
|
+
| Token refresh | ✓ | - | Client + TideCloak |
|
|
562
|
+
| Role-based UI gating | ✓ | - | Client (NOT real authorization) |
|
|
563
|
+
| Role-based API authorization | ✓ | ✓ (threshold-verified roles) | Application server + Fabric |
|
|
564
|
+
| E2EE | - | ✓ (threshold decrypt) | Fabric ORKs |
|
|
565
|
+
| Admin approval workflow | ✓ (procedural) | ✓ (threshold signatures) | Fabric ORKs |
|
|
566
|
+
| Policy enforcement | - | ✓ (Forseti contracts) | Fabric ORKs |
|
|
567
|
+
| DPoP token binding | ✓ (RFC 9449) | ✓ (ES256 default, EdDSA supported) | Application server |
|
|
568
|
+
|
|
569
|
+
---
|
|
570
|
+
|
|
571
|
+
## Protected Routes vs Protected APIs
|
|
572
|
+
|
|
573
|
+
### Protected Routes (Client-Side)
|
|
574
|
+
|
|
575
|
+
**What they are**: UI components that check auth state before rendering.
|
|
576
|
+
|
|
577
|
+
**Security property**: UI convenience only. Attacker can bypass by modifying client code.
|
|
578
|
+
|
|
579
|
+
**Example**:
|
|
580
|
+
```javascript
|
|
581
|
+
// Next.js middleware (runs client-side or edge)
|
|
582
|
+
if (!session || !session.roles.includes('admin')) {
|
|
583
|
+
return NextResponse.redirect('/login');
|
|
584
|
+
}
|
|
585
|
+
```
|
|
586
|
+
|
|
587
|
+
**Agent implication**: Use for UX decisions, NOT security enforcement.
|
|
588
|
+
|
|
589
|
+
---
|
|
590
|
+
|
|
591
|
+
### Protected APIs (Server-Side)
|
|
592
|
+
|
|
593
|
+
**What they are**: API endpoints that verify JWT signature and claims before granting access.
|
|
594
|
+
|
|
595
|
+
**Security property**: Real authorization. Threshold-signed JWT cannot be forged.
|
|
596
|
+
|
|
597
|
+
**Example**:
|
|
598
|
+
```javascript
|
|
599
|
+
app.get('/api/admin/users', async (req, res) => {
|
|
600
|
+
try {
|
|
601
|
+
const jwt = await verifyTideJWT(extractToken(req));
|
|
602
|
+
if (!jwt.realm_access.roles.includes('admin')) {
|
|
603
|
+
return res.status(403).json({ error: 'Forbidden' });
|
|
604
|
+
}
|
|
605
|
+
// Proceed with admin operation
|
|
606
|
+
} catch (err) {
|
|
607
|
+
return res.status(401).json({ error: 'Unauthorized' });
|
|
608
|
+
}
|
|
609
|
+
});
|
|
610
|
+
```
|
|
611
|
+
|
|
612
|
+
**Agent implication**: Always verify JWT on server-side for protected operations.
|
|
613
|
+
|
|
614
|
+
---
|
|
615
|
+
|
|
616
|
+
## UI Gating vs Real Authorization
|
|
617
|
+
|
|
618
|
+
| Aspect | UI Gating | Real Authorization |
|
|
619
|
+
|--------|-----------|-------------------|
|
|
620
|
+
| **Location** | Client-side | Server-side |
|
|
621
|
+
| **Purpose** | UX decisions (show/hide buttons) | Security enforcement |
|
|
622
|
+
| **Bypass risk** | High (modify client code) | Low (threshold signatures) |
|
|
623
|
+
| **Tide involvement** | None (standard React patterns) | Yes (threshold JWT verification) |
|
|
624
|
+
| **Example** | `if (hasRealmRole('admin')) <AdminButton />` | `verifyJWT(token) && hasRole(jwt, 'admin')` |
|
|
625
|
+
|
|
626
|
+
**Critical rule**: Never rely on UI gating for security. Always verify on server-side.
|
|
627
|
+
|
|
628
|
+
---
|
|
629
|
+
|
|
630
|
+
## Status Legend
|
|
631
|
+
|
|
632
|
+
- **VERIFIED** - Directly sourced from documentation or keylessh exemplar
|
|
633
|
+
- **INFERRED** - Strongly implied by source material
|
|
634
|
+
- **ASSUMED** - Operator guidance where sources are silent
|
|
635
|
+
- **REQUIRES_RUNTIME_VALIDATION** - Single-app evidence; needs confirmation
|
|
636
|
+
- **STILL_UNRESOLVED** - Open gap
|
|
637
|
+
- **PARTIALLY_RESOLVED** - Partial evidence; gaps remain
|