@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,2133 @@
|
|
|
1
|
+
# Tide Anti-Patterns
|
|
2
|
+
|
|
3
|
+
Common mistakes that defeat Tide's security properties or cause operational failures. Each anti-pattern includes why it's wrong and the correct approach.
|
|
4
|
+
|
|
5
|
+
**Critical**: These mistakes reduce Tide to generic auth, introduce security vulnerabilities, or break core functionality.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## AP-01: Treating Tide as Generic OIDC
|
|
10
|
+
|
|
11
|
+
**What it looks like**:
|
|
12
|
+
- Implementing custom password verification logic
|
|
13
|
+
- Using standard Keycloak JWKS endpoint
|
|
14
|
+
- Skipping adapter JSON Tide extensions
|
|
15
|
+
- Treating login flow as "just OAuth2/OIDC"
|
|
16
|
+
|
|
17
|
+
**Why it's wrong**:
|
|
18
|
+
- Tide authentication is threshold-verified (PRISM), not server-checked
|
|
19
|
+
- JWT signing is threshold protocol (VVK), not single-server signing
|
|
20
|
+
- Adapter JSON contains embedded JWKS and Fabric endpoints
|
|
21
|
+
- Treating Tide as generic OIDC loses all threshold enforcement
|
|
22
|
+
|
|
23
|
+
**Consequence**: Collapse to single-point-of-trust. Threshold security properties lost.
|
|
24
|
+
|
|
25
|
+
**Correct approach**:
|
|
26
|
+
```typescript
|
|
27
|
+
// ❌ WRONG: Fetch JWKS from remote endpoint
|
|
28
|
+
const JWKS = createRemoteJWKSet(new URL(`${issuer}/protocol/openid-connect/certs`));
|
|
29
|
+
|
|
30
|
+
// ❌ ALSO WRONG: Remote JWKS as fallback
|
|
31
|
+
try { await jwtVerify(token, localJWKS); }
|
|
32
|
+
catch { await jwtVerify(token, createRemoteJWKSet(certsUrl)); }
|
|
33
|
+
|
|
34
|
+
// ✅ CORRECT: Use embedded JWKS from adapter JSON, fail if missing
|
|
35
|
+
import { loadTideConfig } from './tidecloakConfig';
|
|
36
|
+
const config = loadTideConfig();
|
|
37
|
+
if (!config.jwk) throw new Error('Missing jwk in tidecloak.json. Re-export adapter.');
|
|
38
|
+
const JWKS = createLocalJWKSet(config.jwk);
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
**Related**: [Collapse Risk Register](../notes/source-audit.md#collapse-risk-register), Invariants I-04, I-05
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## AP-02: Relying on Client-Side Role Checks for Authorization
|
|
46
|
+
|
|
47
|
+
**What it looks like**:
|
|
48
|
+
```typescript
|
|
49
|
+
// ❌ WRONG: API trusts client-side role check
|
|
50
|
+
if (useTideCloak().hasRealmRole('admin')) {
|
|
51
|
+
// Show admin button
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
app.get('/api/admin/users', (req, res) => {
|
|
55
|
+
// Assumes client already checked role
|
|
56
|
+
return res.json({ users: [...] });
|
|
57
|
+
});
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
**Why it's wrong**:
|
|
61
|
+
- `hasRealmRole()` / `hasClientRole()` are UI gating, not authorization
|
|
62
|
+
- Attacker can bypass by calling API directly
|
|
63
|
+
- Client-side checks are easily modified
|
|
64
|
+
- No server-side enforcement = no security
|
|
65
|
+
|
|
66
|
+
**Note**: The SDK hook exports `hasRealmRole(role)` and `hasClientRole(role, client?)`. There is no generic `hasRole()` on the hook. Use `hasRealmRole` for realm roles, `hasClientRole` for client roles.
|
|
67
|
+
|
|
68
|
+
**Consequence**: Unauthorized access to protected APIs. Attacker can impersonate admin.
|
|
69
|
+
|
|
70
|
+
**Correct approach**:
|
|
71
|
+
```typescript
|
|
72
|
+
// ✅ CORRECT: UI gating for UX (realm role)
|
|
73
|
+
if (useTideCloak().hasRealmRole('admin')) {
|
|
74
|
+
return <AdminButton />; // Show/hide UI element
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ✅ CORRECT: Server-side authorization
|
|
78
|
+
app.get('/api/admin/users', async (req, res) => {
|
|
79
|
+
const jwt = await verifyTideJWT(extractToken(req));
|
|
80
|
+
if (!jwt.realm_access.roles.includes('admin')) {
|
|
81
|
+
return res.status(403).json({ error: 'Forbidden' });
|
|
82
|
+
}
|
|
83
|
+
return res.json({ users: [...] });
|
|
84
|
+
});
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
**Related**: Invariants I-03, I-08, [Feature Mapping](feature-mapping.md#protected-routes-vs-protected-apis)
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## AP-03: Generating Keys Locally
|
|
92
|
+
|
|
93
|
+
**What it looks like**:
|
|
94
|
+
```typescript
|
|
95
|
+
// ❌ WRONG: Local key generation for Tide operations
|
|
96
|
+
const keyPair = await crypto.subtle.generateKey(
|
|
97
|
+
{ name: 'ECDSA', namedCurve: 'P-256' },
|
|
98
|
+
true,
|
|
99
|
+
['sign', 'verify']
|
|
100
|
+
);
|
|
101
|
+
// Use keyPair for Tide operations
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
**Why it's wrong**:
|
|
105
|
+
- Tide keys are born fragmented across ORKs (DKG)
|
|
106
|
+
- Local key generation defeats never-whole-key invariant
|
|
107
|
+
- Single point of compromise; attacker can extract complete key
|
|
108
|
+
- Threshold enforcement requires distributed key shards
|
|
109
|
+
|
|
110
|
+
**Consequence**: All Tide security properties lost. Attacker with local key can forge tokens, decrypt data, bypass policy.
|
|
111
|
+
|
|
112
|
+
**Correct approach**:
|
|
113
|
+
```typescript
|
|
114
|
+
// ✅ CORRECT: Delegate to SDK/Fabric
|
|
115
|
+
const ciphertext = await iam.doEncrypt('tag', plaintext);
|
|
116
|
+
const signature = await tc.executeSignRequest(signRequest);
|
|
117
|
+
|
|
118
|
+
// Keys never exist locally; operations use Fabric threshold protocols
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
**Related**: Invariants I-01, I-02, I-09, [Concepts](concepts.md#never-whole-key-model-ineffable-cryptography)
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
## AP-04: Implementing Offline E2EE Fallback
|
|
126
|
+
|
|
127
|
+
**What it looks like**:
|
|
128
|
+
```typescript
|
|
129
|
+
// ❌ WRONG: Cache session keys for offline decrypt
|
|
130
|
+
let cachedSessionKey;
|
|
131
|
+
|
|
132
|
+
async function offlineDecrypt(ciphertext) {
|
|
133
|
+
if (!navigator.onLine && cachedSessionKey) {
|
|
134
|
+
return decryptLocally(ciphertext, cachedSessionKey);
|
|
135
|
+
}
|
|
136
|
+
return iam.doDecrypt('tag', ciphertext);
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
**Why it's wrong**:
|
|
141
|
+
- E2EE session keys are threshold-decrypted by Fabric
|
|
142
|
+
- Caching session keys defeats threshold enforcement
|
|
143
|
+
- Offline decryption requires complete key locally
|
|
144
|
+
- No Fabric participation = no threshold security
|
|
145
|
+
|
|
146
|
+
**Consequence**: Session keys exposed client-side. Attacker can decrypt all data with cached key. Threshold security lost.
|
|
147
|
+
|
|
148
|
+
**Correct approach**:
|
|
149
|
+
```typescript
|
|
150
|
+
// ✅ CORRECT: Require online Fabric access
|
|
151
|
+
async function decrypt(ciphertext) {
|
|
152
|
+
if (!navigator.onLine) {
|
|
153
|
+
throw new Error('E2EE requires online Fabric access');
|
|
154
|
+
}
|
|
155
|
+
return iam.doDecrypt('tag', ciphertext);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Handle offline state at application level, not E2EE level
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
**Related**: Invariants I-02, I-11, [Concepts](concepts.md#threshold-e2ee-hermetic-e2ee)
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
## AP-05: Single-Admin Bypass Paths
|
|
166
|
+
|
|
167
|
+
**What it looks like**:
|
|
168
|
+
```typescript
|
|
169
|
+
// ❌ WRONG: Admin override that skips IGA approval
|
|
170
|
+
app.post('/api/admin/emergency-create-user', async (req, res) => {
|
|
171
|
+
const jwt = await verifyTideJWT(extractToken(req));
|
|
172
|
+
if (jwt.realm_access.roles.includes('superadmin')) {
|
|
173
|
+
// Skip IGA approval for emergency
|
|
174
|
+
await createUserDirectly(req.body);
|
|
175
|
+
return res.json({ success: true });
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
**Why it's wrong**:
|
|
181
|
+
- IGA quorum enforcement prevents single-admin abuse
|
|
182
|
+
- Bypass paths defeat threshold governance
|
|
183
|
+
- "Emergency" paths are first attack vector
|
|
184
|
+
- Compromised superadmin can bypass all controls
|
|
185
|
+
|
|
186
|
+
**Consequence**: Single compromised admin can create backdoor accounts, grant unauthorized roles, bypass governance. IGA collapses to single-admin trust.
|
|
187
|
+
|
|
188
|
+
**Correct approach**:
|
|
189
|
+
```typescript
|
|
190
|
+
// ✅ CORRECT: All admin changes go through TideCloak admin API (IGA-governed)
|
|
191
|
+
// Creating a user via the standard admin endpoint automatically generates a
|
|
192
|
+
// change-set when IGA is enabled. There is no explicit "createChangeRequest" API.
|
|
193
|
+
app.post('/api/admin/create-user', async (req, res) => {
|
|
194
|
+
const jwt = await verifyTideJWT(extractToken(req));
|
|
195
|
+
if (!jwt.realm_access.roles.includes('admin')) {
|
|
196
|
+
return res.status(403).json({ error: 'Forbidden' });
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Call TideCloak admin API — IGA creates a draft change-set automatically
|
|
200
|
+
await fetch(`${TIDECLOAK_URL}/admin/realms/${REALM}/users`, {
|
|
201
|
+
method: 'POST',
|
|
202
|
+
headers: {
|
|
203
|
+
'Authorization': `Bearer ${adminToken}`,
|
|
204
|
+
'Content-Type': 'application/json'
|
|
205
|
+
},
|
|
206
|
+
body: JSON.stringify(req.body)
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
// Admin must then approve via /tide-admin/change-set/sign/batch
|
|
210
|
+
// and commit via /tide-admin/change-set/commit/batch
|
|
211
|
+
return res.json({ status: 'pending_approval' });
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
// No bypass; even emergency changes require quorum
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
**Related**: Invariants I-09, I-10, [Concepts](concepts.md#iga-identity-governance--administration)
|
|
218
|
+
|
|
219
|
+
---
|
|
220
|
+
|
|
221
|
+
## AP-06: Skipping DPoP Verification
|
|
222
|
+
|
|
223
|
+
**What it looks like**:
|
|
224
|
+
```typescript
|
|
225
|
+
// ❌ WRONG: Accept access token without DPoP proof
|
|
226
|
+
app.get('/api/protected', async (req, res) => {
|
|
227
|
+
const jwt = await verifyTideJWT(extractToken(req));
|
|
228
|
+
// Missing DPoP verification
|
|
229
|
+
return res.json({ data: [...] });
|
|
230
|
+
});
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
**Why it's wrong** (if DPoP is enabled):
|
|
234
|
+
- DPoP binds token to client key pair
|
|
235
|
+
- Without verification, stolen tokens can be replayed
|
|
236
|
+
- Attacker doesn't need private key to use token
|
|
237
|
+
- Token binding security property lost
|
|
238
|
+
|
|
239
|
+
**Consequence**: Token replay attacks succeed. Stolen access token is usable from any client.
|
|
240
|
+
|
|
241
|
+
**Correct approach**:
|
|
242
|
+
```typescript
|
|
243
|
+
// ✅ CORRECT: Verify DPoP if header present
|
|
244
|
+
app.get('/api/protected', async (req, res) => {
|
|
245
|
+
const token = extractToken(req);
|
|
246
|
+
|
|
247
|
+
// Verify DPoP if present
|
|
248
|
+
if (req.headers.dpop) {
|
|
249
|
+
await verifyDPoP(req, token);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const jwt = await verifyTideJWT(token);
|
|
253
|
+
return res.json({ data: [...] });
|
|
254
|
+
});
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
**Note**: DPoP is required for Tide's full security guarantees **VERIFIED** (vendor confirmation, GAP-032 resolved). ES256 is the default algorithm; EdDSA also supported.
|
|
258
|
+
|
|
259
|
+
**Related**: Invariants I-12, [Concepts](concepts.md#dpop-demonstration-of-proof-of-possession)
|
|
260
|
+
|
|
261
|
+
---
|
|
262
|
+
|
|
263
|
+
## AP-07: Missing CSP Whitelist
|
|
264
|
+
|
|
265
|
+
**What it looks like**:
|
|
266
|
+
```typescript
|
|
267
|
+
// ❌ WRONG: No CSP or overly restrictive frame-src
|
|
268
|
+
// Default CSP:
|
|
269
|
+
// Content-Security-Policy: frame-src 'self'
|
|
270
|
+
|
|
271
|
+
// Or no CSP headers at all
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
**Why it's wrong**:
|
|
275
|
+
- SWE loads via iframe from Tide domains
|
|
276
|
+
- Browser blocks cross-origin iframes without CSP whitelist
|
|
277
|
+
- SWE cannot load; all Tide operations fail silently
|
|
278
|
+
- Login hangs, E2EE timeouts, no visible errors
|
|
279
|
+
|
|
280
|
+
**Failure symptom**: Check browser console: `Refused to frame 'https://...' because it violates the following Content Security Policy directive: "frame-src 'self'"`.
|
|
281
|
+
|
|
282
|
+
**Consequence**: Complete Tide failure. Login broken. E2EE broken. No threshold operations work.
|
|
283
|
+
|
|
284
|
+
**Correct approach**:
|
|
285
|
+
```typescript
|
|
286
|
+
// ✅ CORRECT: frame-src '*' required for ORK re-homing
|
|
287
|
+
app.use(helmet({
|
|
288
|
+
contentSecurityPolicy: {
|
|
289
|
+
directives: {
|
|
290
|
+
frameSrc: ["'self'", '*']
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}));
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
**Related**: Invariants I-06, [Concepts](concepts.md#swe-secure-web-enclave)
|
|
297
|
+
|
|
298
|
+
---
|
|
299
|
+
|
|
300
|
+
## AP-08: Hardcoding 14/20 Threshold
|
|
301
|
+
|
|
302
|
+
**What it looks like**:
|
|
303
|
+
```typescript
|
|
304
|
+
// ❌ WRONG: Hardcoded threshold
|
|
305
|
+
const REQUIRED_ORKS = 14;
|
|
306
|
+
const TOTAL_ORKS = 20;
|
|
307
|
+
|
|
308
|
+
if (approvals.length < REQUIRED_ORKS) {
|
|
309
|
+
throw new Error('Insufficient ORK approvals');
|
|
310
|
+
}
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
**Why it's wrong**:
|
|
314
|
+
- Threshold is deployment-configurable via env vars
|
|
315
|
+
- Mainnet: 14/20, Test: 3/5, Custom: varies
|
|
316
|
+
- Hardcoding breaks test environments
|
|
317
|
+
- Code cannot adapt to different Fabric configurations
|
|
318
|
+
|
|
319
|
+
**Consequence**: Code fails in test/dev environments with different threshold. Deployment inflexibility.
|
|
320
|
+
|
|
321
|
+
**Correct approach**:
|
|
322
|
+
```typescript
|
|
323
|
+
// ✅ CORRECT: Read threshold from config/env
|
|
324
|
+
const THRESHOLD = parseInt(process.env.TIDE_VENDOR_THRESHOLD_SIGNING || '3');
|
|
325
|
+
const TOTAL = parseInt(process.env.TIDE_VENDOR_THRESHOLD_TOTAL || '5');
|
|
326
|
+
|
|
327
|
+
if (approvals.length < THRESHOLD) {
|
|
328
|
+
throw new Error(`Insufficient ORK approvals (got ${approvals.length}, need ${THRESHOLD})`);
|
|
329
|
+
}
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
**Related**: Invariants I-02, [Concepts](concepts.md#fabric-cybersecurity-fabric)
|
|
333
|
+
|
|
334
|
+
---
|
|
335
|
+
|
|
336
|
+
## AP-09: Treating BYOiD as Federated Login
|
|
337
|
+
|
|
338
|
+
**What it looks like**:
|
|
339
|
+
```markdown
|
|
340
|
+
<!-- ❌ WRONG: Docs that conflate BYOiD with OAuth2 federated login -->
|
|
341
|
+
"TideCloak supports Bring Your Own Identity, allowing users to log in with their existing accounts from Google, Microsoft, or other identity providers."
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
**Why it's wrong**:
|
|
345
|
+
- BYOiD is threshold password authentication (PRISM), not federated login
|
|
346
|
+
- Users bring their password, not an external identity
|
|
347
|
+
- No integration with Google/Microsoft/OAuth2 providers (unless via Keycloak federation, which is separate)
|
|
348
|
+
- Confusion leads to wrong implementation expectations
|
|
349
|
+
|
|
350
|
+
**Consequence**: Developer expects to configure OAuth2 providers. Actual BYOiD setup (PRISM threshold auth) is missed. Documentation misleads.
|
|
351
|
+
|
|
352
|
+
**Correct description**:
|
|
353
|
+
```markdown
|
|
354
|
+
<!-- ✅ CORRECT: Emphasize threshold password auth -->
|
|
355
|
+
"TideCloak uses threshold password authentication (BYOiD). User passwords are verified across T+ ORKs using PRISM (Threshold Oblivious Pseudorandom Function). No password hash is stored anywhere; no single party learns the password."
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
**Related**: [Concepts](concepts.md#threshold-password-authentication-byoid), [Terminology Map](../notes/terminology-map.md#terms-often-confused-with-generic-auth)
|
|
359
|
+
|
|
360
|
+
---
|
|
361
|
+
|
|
362
|
+
## AP-10: Treating IGA as Procedural Approval
|
|
363
|
+
|
|
364
|
+
**What it looks like**:
|
|
365
|
+
```markdown
|
|
366
|
+
<!-- ❌ WRONG: Docs that downplay threshold signatures -->
|
|
367
|
+
"IGA provides an approval workflow where admin changes require multi-admin sign-off before taking effect."
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
**Why it's wrong**:
|
|
371
|
+
- IGA approval is sealed by VVK threshold signatures, not procedural tracking
|
|
372
|
+
- Emphasis on "workflow" obscures cryptographic enforcement
|
|
373
|
+
- Sounds like generic approval system, not threshold-secured governance
|
|
374
|
+
- Developer may implement procedural bypass ("emergency override")
|
|
375
|
+
|
|
376
|
+
**Consequence**: IGA implemented as procedural workflow with bypass paths. Cryptographic enforcement lost.
|
|
377
|
+
|
|
378
|
+
**Correct description**:
|
|
379
|
+
```markdown
|
|
380
|
+
<!-- ✅ CORRECT: Emphasize threshold signatures -->
|
|
381
|
+
"IGA enforces quorum approval via VVK threshold signatures. Admin changes require max(1, floor(N*0.7)) approvals, sealed by threshold cryptography. No single admin can bypass; compromised admin cannot act unilaterally. Approval is cryptographic, not procedural."
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
**Related**: Invariants I-10, [Concepts](concepts.md#iga-identity-governance--administration)
|
|
385
|
+
|
|
386
|
+
---
|
|
387
|
+
|
|
388
|
+
## AP-11: Client-Side Policy Enforcement
|
|
389
|
+
|
|
390
|
+
**What it looks like**:
|
|
391
|
+
```typescript
|
|
392
|
+
// ❌ WRONG: Forseti policy logic implemented client-side
|
|
393
|
+
function checkSSHPolicy(user, destination) {
|
|
394
|
+
if (user.role === 'admin' || destination.startsWith('dev-')) {
|
|
395
|
+
return true; // Allow
|
|
396
|
+
}
|
|
397
|
+
return false;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
if (checkSSHPolicy(currentUser, targetServer)) {
|
|
401
|
+
await connectSSH(targetServer);
|
|
402
|
+
}
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
**Why it's wrong**:
|
|
406
|
+
- Forseti contracts execute in distributed ORK sandboxes, not client
|
|
407
|
+
- Client-side checks are easily bypassed
|
|
408
|
+
- No majority enforcement; single client can override
|
|
409
|
+
- Policy becomes suggestion, not enforcement
|
|
410
|
+
|
|
411
|
+
**Consequence**: Policy bypass. Attacker modifies client code to skip checks. No distributed enforcement.
|
|
412
|
+
|
|
413
|
+
**Correct approach**:
|
|
414
|
+
```typescript
|
|
415
|
+
// ✅ CORRECT: Submit contract to Fabric for distributed execution
|
|
416
|
+
const signRequest = await tc.createTideRequest({
|
|
417
|
+
contract: sshPolicyContract, // C# code executed on every ORK
|
|
418
|
+
modelName: 'DynamicApprovedCustom<SSH>:DynamicApprovedCustom<1>',
|
|
419
|
+
authFlow: 'Policy:1',
|
|
420
|
+
authorizer: tc.doken,
|
|
421
|
+
challengeData: { user: currentUser, destination: targetServer }
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
const result = await tc.executeSignRequest(signRequest);
|
|
425
|
+
// ORKs have independently evaluated policy; majority approved or rejected
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
**Related**: Invariants I-15, [Concepts](concepts.md#forseti-policy-engine)
|
|
429
|
+
|
|
430
|
+
---
|
|
431
|
+
|
|
432
|
+
## AP-12: Caching DPoP Headers
|
|
433
|
+
|
|
434
|
+
**What it looks like**:
|
|
435
|
+
```typescript
|
|
436
|
+
// ❌ WRONG: Reuse DPoP proof across requests
|
|
437
|
+
let cachedDPoPHeader;
|
|
438
|
+
|
|
439
|
+
async function apiCall(endpoint) {
|
|
440
|
+
if (!cachedDPoPHeader) {
|
|
441
|
+
cachedDPoPHeader = await generateDPoPProof();
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
return fetch(endpoint, {
|
|
445
|
+
headers: {
|
|
446
|
+
'Authorization': `DPoP ${token}`,
|
|
447
|
+
'DPoP': cachedDPoPHeader // Reused across requests
|
|
448
|
+
}
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
**Why it's wrong**:
|
|
454
|
+
- DPoP proof includes `htm` (HTTP method) and `htu` (HTTP URI)
|
|
455
|
+
- Each request requires fresh proof matching method and URL
|
|
456
|
+
- Cached proof fails verification on different endpoints
|
|
457
|
+
- `jti` replay protection rejects reused proofs
|
|
458
|
+
|
|
459
|
+
**Consequence**: DPoP verification fails. API calls rejected with 401.
|
|
460
|
+
|
|
461
|
+
**Correct approach**:
|
|
462
|
+
```typescript
|
|
463
|
+
// ✅ CORRECT: Generate fresh DPoP proof per request
|
|
464
|
+
async function apiCall(endpoint, method = 'GET') {
|
|
465
|
+
const dpopProof = await generateDPoPProof(method, endpoint); // Fresh for each request
|
|
466
|
+
|
|
467
|
+
return fetch(endpoint, {
|
|
468
|
+
method,
|
|
469
|
+
headers: {
|
|
470
|
+
'Authorization': `DPoP ${token}`,
|
|
471
|
+
'DPoP': dpopProof
|
|
472
|
+
}
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// Or use SDK's secureFetch — but it is NOT a drop-in fetch replacement.
|
|
477
|
+
// secureFetch has three requirements:
|
|
478
|
+
// 1. useDPoP must be in the config object (not as a JSX prop)
|
|
479
|
+
// 2. URLs must be absolute (new URL() throws on relative paths)
|
|
480
|
+
// 3. Authorization: Bearer <token> must be pre-set (SDK upgrades to DPoP scheme)
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
**Pitfall:** `useDPoP` as a JSX prop on `TideCloakProvider`. The provider only accepts `config` and `children`. `useDPoP` must be merged into the config object: `config={{ ...tcConfig, useDPoP: { mode: 'strict', alg: 'ES256' } }}`. VERIFIED (session-002).
|
|
484
|
+
|
|
485
|
+
**Pitfall:** `secureFetch('/api/vault')` — relative URL. The SDK calls `new URL(url)` internally, which throws `TypeError: Invalid URL` on relative paths. Always use absolute URLs: `secureFetch(\`\${window.location.origin}/api/vault\`)`. VERIFIED (session-002).
|
|
486
|
+
|
|
487
|
+
**Pitfall:** `secureFetch(url)` without pre-setting `Authorization: Bearer <token>`. The SDK only upgrades to DPoP scheme when it detects an existing Bearer token. Without it, the request is sent as plain fetch with no auth headers. VERIFIED (session-002).
|
|
488
|
+
|
|
489
|
+
**Pitfall:** Using regular `fetch` with `Bearer` header when `useDPoP` IS configured. DPoP-bound tokens require DPoP proof headers. Sending a DPoP-bound token as `Bearer` causes TideCloak to reject the request.
|
|
490
|
+
|
|
491
|
+
**Recommended `appFetch` wrapper** for DPoP-enabled apps:
|
|
492
|
+
```typescript
|
|
493
|
+
// lib/appFetch.ts
|
|
494
|
+
import { IAMService } from '@tidecloak/js';
|
|
495
|
+
|
|
496
|
+
export async function appFetch(path: string, init?: RequestInit): Promise<Response> {
|
|
497
|
+
const url = `${window.location.origin}${path}`;
|
|
498
|
+
const token = await IAMService.getToken();
|
|
499
|
+
return IAMService.secureFetch(url, {
|
|
500
|
+
...init,
|
|
501
|
+
headers: {
|
|
502
|
+
...init?.headers,
|
|
503
|
+
Authorization: `Bearer ${token}`,
|
|
504
|
+
},
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
```
|
|
508
|
+
|
|
509
|
+
**Why this pattern**: `IAMService.secureFetch` detects the `Authorization: Bearer` header and upgrades it to `Authorization: DPoP <token>` while attaching the DPoP proof. Without the pre-set Bearer header, the SDK sends a plain fetch with no auth. Without the absolute URL, `new URL()` throws. This wrapper handles both requirements.
|
|
510
|
+
|
|
511
|
+
**Related**: Invariants I-12, [Concepts](concepts.md#dpop-demonstration-of-proof-of-possession)
|
|
512
|
+
|
|
513
|
+
---
|
|
514
|
+
|
|
515
|
+
## AP-13: Using Adapter JSON Without Tide Extensions
|
|
516
|
+
|
|
517
|
+
**What it looks like**:
|
|
518
|
+
```typescript
|
|
519
|
+
// ❌ WRONG: Export generic Keycloak adapter
|
|
520
|
+
// Admin Console → Clients → myclient → Installation → Keycloak OIDC JSON
|
|
521
|
+
|
|
522
|
+
// Result: Missing jwk, vendorId, homeOrkUrl
|
|
523
|
+
{
|
|
524
|
+
"realm": "myrealm",
|
|
525
|
+
"auth-server-url": "https://tidecloak.example.com",
|
|
526
|
+
"resource": "myclient",
|
|
527
|
+
...
|
|
528
|
+
// No Tide extensions
|
|
529
|
+
}
|
|
530
|
+
```
|
|
531
|
+
|
|
532
|
+
**Why it's wrong**:
|
|
533
|
+
- JWT verification requires embedded JWKS (`jwk` field)
|
|
534
|
+
- Client SDK needs Fabric endpoint (`homeOrkUrl`)
|
|
535
|
+
- Licensing uses vendor identifier (`vendorId`)
|
|
536
|
+
- Generic adapter is insufficient for Tide operations
|
|
537
|
+
|
|
538
|
+
**Consequence**: JWT verification fails. Client cannot connect to Fabric. Operations fail with unclear errors.
|
|
539
|
+
|
|
540
|
+
**Correct approach**:
|
|
541
|
+
```bash
|
|
542
|
+
# ✅ CORRECT: Export Tide adapter with extensions
|
|
543
|
+
curl -X GET \
|
|
544
|
+
"${TIDECLOAK_URL}/admin/realms/${REALM}/clients/${CLIENT_ID}/vendorResources/get-installations-provider?providerId=keycloak-oidc-keycloak-json" \
|
|
545
|
+
-H "Authorization: Bearer ${ADMIN_TOKEN}" \
|
|
546
|
+
> data/tidecloak.json
|
|
547
|
+
|
|
548
|
+
# Verify Tide extensions present
|
|
549
|
+
jq 'has("jwk") and has("vendorId") and has("homeOrkUrl")' data/tidecloak.json
|
|
550
|
+
# Should output: true
|
|
551
|
+
```
|
|
552
|
+
|
|
553
|
+
**Related**: Invariants I-04, I-05, I-13, [Concepts](concepts.md#adapter-json)
|
|
554
|
+
|
|
555
|
+
---
|
|
556
|
+
|
|
557
|
+
## AP-14: Implementing Custom Token Refresh Logic
|
|
558
|
+
|
|
559
|
+
**What it looks like**:
|
|
560
|
+
```typescript
|
|
561
|
+
// ❌ WRONG: Reimplement token refresh
|
|
562
|
+
async function refreshToken() {
|
|
563
|
+
const refreshToken = localStorage.getItem('refresh_token');
|
|
564
|
+
const response = await fetch(`${tidecloakUrl}/realms/${realm}/protocol/openid-connect/token`, {
|
|
565
|
+
method: 'POST',
|
|
566
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
567
|
+
body: new URLSearchParams({
|
|
568
|
+
grant_type: 'refresh_token',
|
|
569
|
+
refresh_token: refreshToken,
|
|
570
|
+
client_id: clientId
|
|
571
|
+
})
|
|
572
|
+
});
|
|
573
|
+
const tokens = await response.json();
|
|
574
|
+
localStorage.setItem('access_token', tokens.access_token);
|
|
575
|
+
}
|
|
576
|
+
```
|
|
577
|
+
|
|
578
|
+
**Why it's wrong**:
|
|
579
|
+
- SDK already handles token refresh (including PRISM verification)
|
|
580
|
+
- Custom logic may skip silent SSO, causing visible redirects
|
|
581
|
+
- Manual token storage is error-prone
|
|
582
|
+
- May miss DPoP token binding renewal
|
|
583
|
+
|
|
584
|
+
**Consequence**: Token refresh fails. User forced to re-login. Corrupted auth state.
|
|
585
|
+
|
|
586
|
+
**Correct approach**:
|
|
587
|
+
```typescript
|
|
588
|
+
// ✅ CORRECT: Let SDK handle refresh
|
|
589
|
+
// SDK automatically refreshes via silent SSO iframe
|
|
590
|
+
// Listen to refresh events if needed
|
|
591
|
+
iam.on('authRefreshSuccess', () => {
|
|
592
|
+
console.log('Token refreshed automatically');
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
iam.on('authRefreshError', (err) => {
|
|
596
|
+
console.error('Refresh failed, re-login required', err);
|
|
597
|
+
iam.doLogin({ redirectUri: window.location.origin });
|
|
598
|
+
});
|
|
599
|
+
```
|
|
600
|
+
|
|
601
|
+
**Related**: [Concepts](concepts.md#silent-sso-token-refresh), [Framework Matrix](framework-matrix.md#silent-sso-setup)
|
|
602
|
+
|
|
603
|
+
---
|
|
604
|
+
|
|
605
|
+
## AP-15: Skipping `silent-check-sso.html`
|
|
606
|
+
|
|
607
|
+
**What it looks like**:
|
|
608
|
+
```bash
|
|
609
|
+
# ❌ WRONG: Missing silent SSO file
|
|
610
|
+
ls public/
|
|
611
|
+
# index.html, logo.png, ...
|
|
612
|
+
# No silent-check-sso.html
|
|
613
|
+
```
|
|
614
|
+
|
|
615
|
+
**Why it's wrong**:
|
|
616
|
+
- Silent token refresh requires hidden iframe
|
|
617
|
+
- TideCloak redirects to `/silent-check-sso.html` during refresh
|
|
618
|
+
- 404 on this file breaks silent refresh
|
|
619
|
+
- User forced to full re-login on every token expiration
|
|
620
|
+
|
|
621
|
+
**Consequence**: Silent refresh fails. Session appears to expire prematurely. Frequent forced re-logins. Poor UX.
|
|
622
|
+
|
|
623
|
+
**Correct approach**:
|
|
624
|
+
```html
|
|
625
|
+
<!-- ✅ CORRECT: Create public/silent-check-sso.html -->
|
|
626
|
+
<html>
|
|
627
|
+
<body>
|
|
628
|
+
<script>
|
|
629
|
+
parent.postMessage(location.href, location.origin);
|
|
630
|
+
</script>
|
|
631
|
+
</body>
|
|
632
|
+
</html>
|
|
633
|
+
```
|
|
634
|
+
|
|
635
|
+
**Verification**:
|
|
636
|
+
```bash
|
|
637
|
+
curl https://app.example.com/silent-check-sso.html
|
|
638
|
+
# Should return HTML with postMessage script
|
|
639
|
+
```
|
|
640
|
+
|
|
641
|
+
**Related**: Invariants I-07, [Framework Matrix](framework-matrix.md#silent-sso-setup)
|
|
642
|
+
|
|
643
|
+
---
|
|
644
|
+
|
|
645
|
+
## AP-16: Treating Hermetic E2EE as Client-Side Encryption
|
|
646
|
+
|
|
647
|
+
**What it looks like**:
|
|
648
|
+
```markdown
|
|
649
|
+
<!-- ❌ WRONG: Docs that imply client-side encryption with server key storage -->
|
|
650
|
+
"Hermetic E2EE encrypts data on the client side before sending to the server. Encryption keys are stored securely on the TideCloak server."
|
|
651
|
+
```
|
|
652
|
+
|
|
653
|
+
**Why it's wrong**:
|
|
654
|
+
- E2EE session keys are threshold-encrypted via CVK, not stored on server
|
|
655
|
+
- Decryption requires live Fabric threshold participation
|
|
656
|
+
- "Keys stored on server" defeats E2EE definition
|
|
657
|
+
- Plaintext never exists on server, Fabric, or ORKs
|
|
658
|
+
|
|
659
|
+
**Consequence**: Developer implements client-side encryption with server key storage. Threshold E2EE not used. Security property lost.
|
|
660
|
+
|
|
661
|
+
**Correct description**:
|
|
662
|
+
```markdown
|
|
663
|
+
<!-- ✅ CORRECT: Emphasize threshold decryption and no server access -->
|
|
664
|
+
"Threshold E2EE (Hermetic E2EE) encrypts data client-side using session keys. Session keys are threshold-encrypted via CVK shards across ORKs. Decryption requires live Fabric threshold participation. Plaintext exists only on user's device; no server, admin, or ORK can decrypt."
|
|
665
|
+
```
|
|
666
|
+
|
|
667
|
+
**Related**: Invariants I-11, [Concepts](concepts.md#threshold-e2ee-hermetic-e2ee)
|
|
668
|
+
|
|
669
|
+
---
|
|
670
|
+
|
|
671
|
+
## AP-17: Implementing Ragnarok Without Quorum
|
|
672
|
+
|
|
673
|
+
**What it looks like**:
|
|
674
|
+
```typescript
|
|
675
|
+
// ❌ WRONG: Single-admin Ragnarok trigger
|
|
676
|
+
app.post('/api/admin/ragnarok', async (req, res) => {
|
|
677
|
+
const jwt = await verifyTideJWT(extractToken(req));
|
|
678
|
+
if (jwt.realm_access.roles.includes('superadmin')) {
|
|
679
|
+
await executeFabricOffboarding(); // No quorum check
|
|
680
|
+
return res.json({ success: true });
|
|
681
|
+
}
|
|
682
|
+
});
|
|
683
|
+
```
|
|
684
|
+
|
|
685
|
+
**Why it's wrong**:
|
|
686
|
+
- Ragnarok is the sole deviation from never-whole-key invariant
|
|
687
|
+
- Reconstructs keys locally; irreversible
|
|
688
|
+
- Requires nested threshold quorum for this reason
|
|
689
|
+
- Single-admin path defeats governance
|
|
690
|
+
|
|
691
|
+
**Consequence**: Single compromised admin can trigger irreversible offboarding. Keys reconstructed without quorum approval.
|
|
692
|
+
|
|
693
|
+
**Correct approach**:
|
|
694
|
+
```typescript
|
|
695
|
+
// ✅ CORRECT: Ragnarok via IGA quorum approval
|
|
696
|
+
// Admin creates change-set for Ragnarok
|
|
697
|
+
const changeSet = await createChangeRequest({
|
|
698
|
+
type: 'FABRIC_OFFBOARDING',
|
|
699
|
+
data: { realm: realmId }
|
|
700
|
+
});
|
|
701
|
+
|
|
702
|
+
// Requires quorum approval (same as other admin changes)
|
|
703
|
+
// Sealed by threshold signatures before execution
|
|
704
|
+
// Irreversible; must have maximum scrutiny
|
|
705
|
+
```
|
|
706
|
+
|
|
707
|
+
**Related**: Invariants I-10, [Concepts](concepts.md#ragnarok-fabric-offboarding)
|
|
708
|
+
|
|
709
|
+
---
|
|
710
|
+
|
|
711
|
+
## AP-18: Mixing Tide Roles with Application Roles
|
|
712
|
+
|
|
713
|
+
**What it looks like**:
|
|
714
|
+
```typescript
|
|
715
|
+
// ❌ WRONG: Confuse Tide system roles with app roles
|
|
716
|
+
const tideRoles = ['_tide_enabled', '_tide_ssn.selfencrypt'];
|
|
717
|
+
const appRoles = ['admin', 'editor', 'viewer'];
|
|
718
|
+
|
|
719
|
+
// Check Tide role for app authorization
|
|
720
|
+
if (jwt.realm_access.roles.includes('_tide_ssn.selfencrypt')) {
|
|
721
|
+
// Grant admin access (WRONG)
|
|
722
|
+
return <AdminPanel />;
|
|
723
|
+
}
|
|
724
|
+
```
|
|
725
|
+
|
|
726
|
+
**Why it's wrong**:
|
|
727
|
+
- Tide roles (`_tide_*`) control cryptographic access (E2EE tags)
|
|
728
|
+
- Application roles (`admin`, `editor`) control feature access
|
|
729
|
+
- `_tide_ssn.selfencrypt` means "can encrypt SSN field", not "is admin"
|
|
730
|
+
- Mixing role types breaks security model
|
|
731
|
+
|
|
732
|
+
**Consequence**: User with E2EE role gains unintended app privileges. Authorization broken.
|
|
733
|
+
|
|
734
|
+
**Correct approach**:
|
|
735
|
+
```typescript
|
|
736
|
+
// ✅ CORRECT: Separate Tide roles from app roles
|
|
737
|
+
// Tide E2EE role check
|
|
738
|
+
const canEncryptSSN = jwt.realm_access.roles.includes('_tide_ssn.selfencrypt');
|
|
739
|
+
|
|
740
|
+
// App authorization role check
|
|
741
|
+
const isAdmin = jwt.realm_access.roles.includes('admin');
|
|
742
|
+
|
|
743
|
+
if (canEncryptSSN) {
|
|
744
|
+
// Allow SSN encryption only
|
|
745
|
+
await iam.doEncrypt('ssn', plaintext);
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
if (isAdmin) {
|
|
749
|
+
// Allow admin features
|
|
750
|
+
return <AdminPanel />;
|
|
751
|
+
}
|
|
752
|
+
```
|
|
753
|
+
|
|
754
|
+
**Related**: [Concepts](concepts.md#tag-based-e2ee-roles), [Feature Mapping](feature-mapping.md#e2ee-features)
|
|
755
|
+
|
|
756
|
+
---
|
|
757
|
+
|
|
758
|
+
## AP-19: Storing or Handling Passwords Server-Side
|
|
759
|
+
|
|
760
|
+
**What it looks like**:
|
|
761
|
+
```typescript
|
|
762
|
+
// ❌ WRONG: Backend receives and validates password
|
|
763
|
+
app.post('/api/login', async (req, res) => {
|
|
764
|
+
const { username, password } = req.body;
|
|
765
|
+
const user = await db.findUser(username);
|
|
766
|
+
if (await bcrypt.compare(password, user.passwordHash)) {
|
|
767
|
+
return res.json({ token: generateToken(user) });
|
|
768
|
+
}
|
|
769
|
+
});
|
|
770
|
+
```
|
|
771
|
+
|
|
772
|
+
**Why it's wrong**:
|
|
773
|
+
- TideCloak uses PRISM zero-knowledge verification
|
|
774
|
+
- Passwords never leave the browser; verification happens across ORKs
|
|
775
|
+
- Server receiving a password means Tide auth is bypassed entirely
|
|
776
|
+
- Re-creates single-point-of-trust the architecture eliminates
|
|
777
|
+
|
|
778
|
+
**Consequence**: Password exposed to server operator. Zero-knowledge property destroyed. Tide threshold authentication bypassed.
|
|
779
|
+
|
|
780
|
+
**Correct approach**:
|
|
781
|
+
```typescript
|
|
782
|
+
// ✅ CORRECT: Let TideCloak SDK handle authentication
|
|
783
|
+
// Frontend triggers login via SDK
|
|
784
|
+
const { login } = useTideCloak();
|
|
785
|
+
login({ redirectUri: window.location.origin });
|
|
786
|
+
|
|
787
|
+
// Backend only verifies the resulting JWT
|
|
788
|
+
app.get('/api/protected', async (req, res) => {
|
|
789
|
+
const jwt = await verifyTideJWT(extractToken(req));
|
|
790
|
+
// Password never touches this server
|
|
791
|
+
});
|
|
792
|
+
```
|
|
793
|
+
|
|
794
|
+
**Related**: Invariants I-01, I-02, [Concepts](concepts.md#threshold-password-authentication-byoid)
|
|
795
|
+
|
|
796
|
+
---
|
|
797
|
+
|
|
798
|
+
## AP-20: Decoding JWT Without Signature Verification
|
|
799
|
+
|
|
800
|
+
**What it looks like**:
|
|
801
|
+
```typescript
|
|
802
|
+
// ❌ WRONG: Decode without verify
|
|
803
|
+
import jwt_decode from 'jwt-decode';
|
|
804
|
+
const claims = jwt_decode(token);
|
|
805
|
+
if (claims.realm_access.roles.includes('admin')) {
|
|
806
|
+
// Trust unverified claims
|
|
807
|
+
}
|
|
808
|
+
```
|
|
809
|
+
|
|
810
|
+
**Why it's wrong**:
|
|
811
|
+
- Decoded-but-unverified JWT is meaningless; anyone can forge claims
|
|
812
|
+
- Tide JWTs are VVK threshold-signed; verification proves distributed consensus
|
|
813
|
+
- Skipping verification collapses threshold trust to zero trust
|
|
814
|
+
|
|
815
|
+
**Consequence**: Attacker crafts JWT with arbitrary claims. All authorization decisions compromised.
|
|
816
|
+
|
|
817
|
+
**Correct approach**:
|
|
818
|
+
```typescript
|
|
819
|
+
// ✅ CORRECT: Always verify signature against embedded JWKS
|
|
820
|
+
import TideJWT from "@tidecloak/verify";
|
|
821
|
+
const _mod = (TideJWT as any)?.default ?? TideJWT;
|
|
822
|
+
const { verifyTideCloakToken } = _mod;
|
|
823
|
+
|
|
824
|
+
const verified = await verifyTideCloakToken(token, tidecloakConfig);
|
|
825
|
+
// Only trust claims from verified token
|
|
826
|
+
```
|
|
827
|
+
|
|
828
|
+
**Related**: Invariants I-04, I-05, AP-01
|
|
829
|
+
|
|
830
|
+
---
|
|
831
|
+
|
|
832
|
+
## AP-21: Storing Dokens Server-Side
|
|
833
|
+
|
|
834
|
+
**What it looks like**:
|
|
835
|
+
```typescript
|
|
836
|
+
// ❌ WRONG: Persist Doken on server
|
|
837
|
+
app.post('/api/store-session', async (req, res) => {
|
|
838
|
+
const { doken } = req.body;
|
|
839
|
+
await redis.set(`doken:${userId}`, doken);
|
|
840
|
+
});
|
|
841
|
+
```
|
|
842
|
+
|
|
843
|
+
**Why it's wrong**:
|
|
844
|
+
- Dokens are session-bound to the SWE's ephemeral key
|
|
845
|
+
- A Doken cannot be used from a different context than the one that created it
|
|
846
|
+
- Storing is pointless (unusable) and a leak risk
|
|
847
|
+
- Compromised Doken store exposes session metadata
|
|
848
|
+
|
|
849
|
+
**Consequence**: Leaked Dokens with no operational benefit. False sense of session persistence.
|
|
850
|
+
|
|
851
|
+
**Correct approach**:
|
|
852
|
+
```typescript
|
|
853
|
+
// ✅ CORRECT: Let SDK manage Doken lifecycle in-browser
|
|
854
|
+
// Dokens live only in the SWE context
|
|
855
|
+
// Server uses JWTs for authorization, not Dokens
|
|
856
|
+
const { doken } = useTideCloak(); // Browser-only, never sent to server
|
|
857
|
+
```
|
|
858
|
+
|
|
859
|
+
**Related**: [Concepts](concepts.md#swe-secure-web-enclave)
|
|
860
|
+
|
|
861
|
+
---
|
|
862
|
+
|
|
863
|
+
## AP-22: Decrypting on the Server
|
|
864
|
+
|
|
865
|
+
**What it looks like**:
|
|
866
|
+
```typescript
|
|
867
|
+
// ❌ WRONG: Server-side decryption
|
|
868
|
+
app.get('/api/records/:id', async (req, res) => {
|
|
869
|
+
const encrypted = await db.getRecord(req.params.id);
|
|
870
|
+
const plaintext = await serverDecrypt(encrypted, serverKey);
|
|
871
|
+
return res.json({ data: plaintext });
|
|
872
|
+
});
|
|
873
|
+
```
|
|
874
|
+
|
|
875
|
+
**Why it's wrong**:
|
|
876
|
+
- Hermetic E2EE means plaintext exists only in the user's browser
|
|
877
|
+
- Server decryption is standard server-side encryption, not E2EE
|
|
878
|
+
- Server operator gains access to plaintext, defeating the security model
|
|
879
|
+
- Threshold decryption requires live Fabric participation from the client
|
|
880
|
+
|
|
881
|
+
**Consequence**: E2EE property destroyed. Server operator can read all data. No better than standard encryption.
|
|
882
|
+
|
|
883
|
+
**Correct approach**:
|
|
884
|
+
```typescript
|
|
885
|
+
// ✅ CORRECT: Decrypt only in the browser via SDK
|
|
886
|
+
const plaintext = await iam.doDecrypt('medical', ciphertext);
|
|
887
|
+
// Plaintext never leaves the browser
|
|
888
|
+
// Server stores and serves only ciphertext
|
|
889
|
+
```
|
|
890
|
+
|
|
891
|
+
**Related**: Invariants I-11, AP-04, AP-16, [Concepts](concepts.md#threshold-e2ee-hermetic-e2ee)
|
|
892
|
+
|
|
893
|
+
---
|
|
894
|
+
|
|
895
|
+
## AP-23: Skipping Tags or Reusing a Single Tag for All Encrypted Data
|
|
896
|
+
|
|
897
|
+
**What it looks like**:
|
|
898
|
+
```typescript
|
|
899
|
+
// ❌ WRONG: No tag
|
|
900
|
+
const encrypted = await iam.doEncrypt('', plaintext);
|
|
901
|
+
|
|
902
|
+
// ❌ WRONG: Same tag for everything
|
|
903
|
+
const encSSN = await iam.doEncrypt('data', ssn);
|
|
904
|
+
const encMedical = await iam.doEncrypt('data', medicalRecord);
|
|
905
|
+
const encNotes = await iam.doEncrypt('data', internalNotes);
|
|
906
|
+
```
|
|
907
|
+
|
|
908
|
+
**Why it's wrong**:
|
|
909
|
+
- Tags define who can decrypt; no tag means no access control
|
|
910
|
+
- One tag for all data means anyone with that role decrypts everything
|
|
911
|
+
- Different data sensitivity levels require different tags mapped to different roles
|
|
912
|
+
- Granular access control is the entire point of tag-based E2EE
|
|
913
|
+
|
|
914
|
+
**Consequence**: No field-level access control. Any user with the single decrypt role reads all encrypted data regardless of sensitivity.
|
|
915
|
+
|
|
916
|
+
**Correct approach**:
|
|
917
|
+
```typescript
|
|
918
|
+
// ✅ CORRECT: Meaningful tags per sensitivity level
|
|
919
|
+
const encSSN = await iam.doEncrypt('ssn', ssn); // _tide_ssn.selfdecrypt
|
|
920
|
+
const encMedical = await iam.doEncrypt('medical', record); // _tide_medical.selfdecrypt
|
|
921
|
+
const encNotes = await iam.doEncrypt('notes', notes); // _tide_notes.selfdecrypt
|
|
922
|
+
// Each tag has separate encrypt/decrypt roles with different user assignments
|
|
923
|
+
```
|
|
924
|
+
|
|
925
|
+
**Related**: AP-18, [Concepts](concepts.md#tag-based-e2ee-roles)
|
|
926
|
+
|
|
927
|
+
---
|
|
928
|
+
|
|
929
|
+
## AP-24: Using Self-Encryption for Shared Data Between Users
|
|
930
|
+
|
|
931
|
+
**What it looks like**:
|
|
932
|
+
```typescript
|
|
933
|
+
// ❌ WRONG: Encrypt with doEncrypt, expect other users to doDecrypt
|
|
934
|
+
// User A encrypts
|
|
935
|
+
const ciphertext = await iam.doEncrypt('shared', data);
|
|
936
|
+
await api.saveSharedDoc(ciphertext);
|
|
937
|
+
|
|
938
|
+
// User B tries to decrypt (FAILS even with _tide_shared.selfdecrypt role)
|
|
939
|
+
const plaintext = await iam.doDecrypt('shared', ciphertext);
|
|
940
|
+
```
|
|
941
|
+
|
|
942
|
+
**Why it's wrong**:
|
|
943
|
+
- Self-encryption (`doEncrypt`/`doDecrypt` without policy) binds ciphertext to the encrypting user's identity
|
|
944
|
+
- Another user with the same `selfdecrypt` role cannot decrypt data encrypted by someone else
|
|
945
|
+
- Sharing between users requires policy-governed VVK encryption with a Forseti contract
|
|
946
|
+
|
|
947
|
+
**Consequence**: Other users cannot decrypt shared data. Silent decryption failures. Data appears locked to the creator.
|
|
948
|
+
|
|
949
|
+
**Correct approach**:
|
|
950
|
+
```typescript
|
|
951
|
+
// ✅ CORRECT: Use policy-governed encryption for shared data
|
|
952
|
+
const ciphertext = await iam.doEncrypt(data, signedPolicyBytes);
|
|
953
|
+
// VVK (organizational key) encrypts; Forseti contract controls who can decrypt
|
|
954
|
+
// Any user whose role passes the contract's ValidateExecutor can decrypt
|
|
955
|
+
```
|
|
956
|
+
|
|
957
|
+
**Related**: AP-11, [Concepts](concepts.md#threshold-e2ee-hermetic-e2ee)
|
|
958
|
+
|
|
959
|
+
---
|
|
960
|
+
|
|
961
|
+
## AP-25: Using `_tide_*` Roles in Forseti ValidateExecutor
|
|
962
|
+
|
|
963
|
+
**What it looks like**:
|
|
964
|
+
```csharp
|
|
965
|
+
// ❌ WRONG: Checking voucher gate role in contract
|
|
966
|
+
public override bool ValidateExecutor(UserContext user) {
|
|
967
|
+
return user.HasRole("_tide_medical.selfdecrypt");
|
|
968
|
+
}
|
|
969
|
+
```
|
|
970
|
+
|
|
971
|
+
**Why it's wrong**:
|
|
972
|
+
- `_tide_{tag}.selfencrypt/selfdecrypt` roles are voucher gates only; they enable the ORK operation type
|
|
973
|
+
- Forseti contracts should check regular realm roles for access control decisions
|
|
974
|
+
- To restrict who can decrypt, remove the regular role from users, not the voucher gate roles
|
|
975
|
+
- Mixing voucher gates with policy logic conflates two separate layers
|
|
976
|
+
|
|
977
|
+
**Consequence**: Access control logic tied to wrong role type. Revoking a voucher gate disables the entire operation rather than just the policy decision.
|
|
978
|
+
|
|
979
|
+
**Correct approach**:
|
|
980
|
+
```csharp
|
|
981
|
+
// ✅ CORRECT: Check a regular realm role
|
|
982
|
+
public override bool ValidateExecutor(UserContext user) {
|
|
983
|
+
return user.HasRole("shared-data-access");
|
|
984
|
+
}
|
|
985
|
+
// Voucher gate roles (_tide_*) are assigned separately to enable the ORK operation
|
|
986
|
+
```
|
|
987
|
+
|
|
988
|
+
**Related**: AP-18, AP-11
|
|
989
|
+
|
|
990
|
+
---
|
|
991
|
+
|
|
992
|
+
## AP-26: Renaming Self-Encryption Roles to Enable Shared Decryption
|
|
993
|
+
|
|
994
|
+
**What it looks like**:
|
|
995
|
+
```typescript
|
|
996
|
+
// ❌ WRONG: Replace selfencrypt/selfdecrypt with encrypt/decrypt
|
|
997
|
+
// expecting this to make ciphertext decryptable by other users
|
|
998
|
+
// Before (correct for self-encryption):
|
|
999
|
+
// _tide_vault.selfencrypt / _tide_vault.selfdecrypt
|
|
1000
|
+
// After (incorrect "fix"):
|
|
1001
|
+
// _tide_vault.encrypt / _tide_vault.decrypt
|
|
1002
|
+
const ciphertext = await doEncrypt([{ data: secret, tags: ['vault'] }]);
|
|
1003
|
+
// Still uses doEncrypt from useTideCloak() — still self-encryption
|
|
1004
|
+
// Changing role names does not change the encryption model
|
|
1005
|
+
```
|
|
1006
|
+
|
|
1007
|
+
**Why it's wrong**:
|
|
1008
|
+
- The `doEncrypt`/`doDecrypt` calls on `useTideCloak()` always perform self-encryption regardless of the role suffix
|
|
1009
|
+
- Changing `selfencrypt` → `encrypt` in the role name does not switch the encryption model
|
|
1010
|
+
- Self-encryption binds ciphertext to the encrypting user's CVK. The role suffix is a naming convention; the SDK call path determines the encryption model.
|
|
1011
|
+
- To switch from self-encryption to shared encryption, you must change the SDK call to `IAMService.doEncrypt(data, signedPolicyBytes)`, deploy a Forseti contract, and sign a policy
|
|
1012
|
+
|
|
1013
|
+
**Consequence**: App appears to support shared access but does not. Other users still cannot decrypt. Data locked to creator. Silent failures.
|
|
1014
|
+
|
|
1015
|
+
**Correct approach**:
|
|
1016
|
+
- For self-encryption (user-private data): keep `_tide_{tag}.selfencrypt` / `_tide_{tag}.selfdecrypt` and use `doEncrypt`/`doDecrypt` from `useTideCloak()`
|
|
1017
|
+
- For shared encryption: use generic voucher gates (e.g., `_tide_x.selfencrypt`/`_tide_x.selfdecrypt`), add a regular realm role for the Forseti contract (e.g., `shared-data-access`), deploy a Forseti contract, sign the policy, and call `IAMService.doEncrypt(data, signedPolicyBytes)`
|
|
1018
|
+
- These are different SDK call paths and different bootstrap flows — not a role rename
|
|
1019
|
+
|
|
1020
|
+
**Related**: AP-24, [Concepts](concepts.md#self-encryption-vs-policy-governed-vvk-encryption)
|
|
1021
|
+
|
|
1022
|
+
---
|
|
1023
|
+
|
|
1024
|
+
## AP-27: Using `configFilePath` Instead of `configUrl`
|
|
1025
|
+
|
|
1026
|
+
**What it looks like**:
|
|
1027
|
+
```tsx
|
|
1028
|
+
// ❌ WRONG: Wrong prop name
|
|
1029
|
+
<TideCloakContextProvider configFilePath="/tidecloak.json">
|
|
1030
|
+
<App />
|
|
1031
|
+
</TideCloakContextProvider>
|
|
1032
|
+
```
|
|
1033
|
+
|
|
1034
|
+
**Why it's wrong**:
|
|
1035
|
+
- `TideCloakContextProvider` accepts `configUrl`, not `configFilePath`
|
|
1036
|
+
- Wrong prop name falls through to the default `/adapter.json`, which does not exist
|
|
1037
|
+
- Vite serves the HTML index page instead of JSON
|
|
1038
|
+
- Results in `SyntaxError: Unexpected token '<', "<!doctype "... is not valid JSON`
|
|
1039
|
+
|
|
1040
|
+
**Consequence**: SDK initialization fails. Cryptic JSON parse error. Application cannot start.
|
|
1041
|
+
|
|
1042
|
+
**Correct approach**:
|
|
1043
|
+
```tsx
|
|
1044
|
+
// ✅ CORRECT (React/Vite): Use configUrl pointing to public/tidecloak.json
|
|
1045
|
+
<TideCloakContextProvider configUrl="/tidecloak.json">
|
|
1046
|
+
<App />
|
|
1047
|
+
</TideCloakContextProvider>
|
|
1048
|
+
|
|
1049
|
+
// ✅ CORRECT (Next.js): Use configUrl pointing to API route that serves data/tidecloak.json
|
|
1050
|
+
<TideCloakContextProvider configUrl="/api/config">
|
|
1051
|
+
<App />
|
|
1052
|
+
</TideCloakContextProvider>
|
|
1053
|
+
```
|
|
1054
|
+
|
|
1055
|
+
**Note**: The SDK default `configUrl` is `/adapter.json`, not `/tidecloak.json`. Always set `configUrl` explicitly. For Next.js, config lives in `data/tidecloak.json` (server-side); serve it via an API route at `app/api/config/route.ts`.
|
|
1056
|
+
|
|
1057
|
+
**Related**: AP-13
|
|
1058
|
+
|
|
1059
|
+
---
|
|
1060
|
+
|
|
1061
|
+
## AP-28: Calling login() Before SDK Initialization
|
|
1062
|
+
|
|
1063
|
+
**What it looks like**:
|
|
1064
|
+
```tsx
|
|
1065
|
+
// ❌ WRONG: Guards render during init, exposing login too early
|
|
1066
|
+
function App() {
|
|
1067
|
+
return (
|
|
1068
|
+
<TideCloakContextProvider configUrl="/tidecloak.json">
|
|
1069
|
+
<Authenticated><Dashboard /></Authenticated>
|
|
1070
|
+
<Unauthenticated><LoginPage /></Unauthenticated>
|
|
1071
|
+
</TideCloakContextProvider>
|
|
1072
|
+
);
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
function LoginPage() {
|
|
1076
|
+
const { login } = useTideCloak();
|
|
1077
|
+
// login() called before SDK ready → "TideCloak client not initialized"
|
|
1078
|
+
return <button onClick={login}>Login</button>;
|
|
1079
|
+
}
|
|
1080
|
+
```
|
|
1081
|
+
|
|
1082
|
+
**Why it's wrong**:
|
|
1083
|
+
- `<Unauthenticated>` renders during initialization (user is not yet authenticated)
|
|
1084
|
+
- Child components get exposed before SDK is ready
|
|
1085
|
+
- Calling `login()` before `initIAM()` completes throws an error
|
|
1086
|
+
- The SDK exposes `isInitializing` (not `initialized`) to gate this
|
|
1087
|
+
|
|
1088
|
+
**Consequence**: Runtime error. Login button crashes the app on click.
|
|
1089
|
+
|
|
1090
|
+
**Correct approach**:
|
|
1091
|
+
```tsx
|
|
1092
|
+
// ✅ CORRECT: Block the tree until SDK is ready
|
|
1093
|
+
function AppContent() {
|
|
1094
|
+
const { isInitializing } = useTideCloak();
|
|
1095
|
+
if (isInitializing) return <p>Initializing TideCloak...</p>;
|
|
1096
|
+
return (
|
|
1097
|
+
<>
|
|
1098
|
+
<Authenticated><Dashboard /></Authenticated>
|
|
1099
|
+
<Unauthenticated><LoginPage /></Unauthenticated>
|
|
1100
|
+
</>
|
|
1101
|
+
);
|
|
1102
|
+
}
|
|
1103
|
+
```
|
|
1104
|
+
|
|
1105
|
+
**Related**: AP-14, AP-15
|
|
1106
|
+
|
|
1107
|
+
---
|
|
1108
|
+
|
|
1109
|
+
## AP-29: Using hasRealmRole for tide-realm-admin
|
|
1110
|
+
|
|
1111
|
+
**What it looks like**:
|
|
1112
|
+
```typescript
|
|
1113
|
+
// ❌ WRONG: Checking as realm role
|
|
1114
|
+
if (useTideCloak().hasRealmRole('tide-realm-admin')) {
|
|
1115
|
+
return <AdminPanel />;
|
|
1116
|
+
}
|
|
1117
|
+
// Always returns false; admin UI never shows
|
|
1118
|
+
```
|
|
1119
|
+
|
|
1120
|
+
**Why it's wrong**:
|
|
1121
|
+
- `tide-realm-admin` is a client role on the `realm-management` client, not a realm role
|
|
1122
|
+
- `hasRealmRole()` checks `realm_access.roles` in the JWT
|
|
1123
|
+
- The role lives under `resource_access["realm-management"].roles`
|
|
1124
|
+
- Using the wrong check means admins never see admin UI
|
|
1125
|
+
|
|
1126
|
+
**Consequence**: Admin functionality invisible. Developers assume role assignment is broken.
|
|
1127
|
+
|
|
1128
|
+
**Correct approach**:
|
|
1129
|
+
```typescript
|
|
1130
|
+
// ✅ CORRECT: Check as client role on realm-management
|
|
1131
|
+
if (useTideCloak().hasClientRole('tide-realm-admin', 'realm-management')) {
|
|
1132
|
+
return <AdminPanel />;
|
|
1133
|
+
}
|
|
1134
|
+
```
|
|
1135
|
+
|
|
1136
|
+
**Related**: AP-02, AP-18
|
|
1137
|
+
|
|
1138
|
+
---
|
|
1139
|
+
|
|
1140
|
+
## AP-30: Using Named ESM Imports with @tidecloak/verify
|
|
1141
|
+
|
|
1142
|
+
**What it looks like**:
|
|
1143
|
+
```typescript
|
|
1144
|
+
// ❌ WRONG: Named ESM import from CJS package
|
|
1145
|
+
import { verifyTideCloakToken } from "@tidecloak/verify";
|
|
1146
|
+
// Error: does not provide an export named 'verifyTideCloakToken'
|
|
1147
|
+
```
|
|
1148
|
+
|
|
1149
|
+
**Why it's wrong**:
|
|
1150
|
+
- `@tidecloak/verify` is a CJS package, not ESM
|
|
1151
|
+
- Named imports fail because CJS modules export a single `module.exports` object
|
|
1152
|
+
- CJS interop varies by runtime (tsx, node, bundlers wrap `.default` differently)
|
|
1153
|
+
|
|
1154
|
+
**Consequence**: Import error at startup. Server-side JWT verification completely broken.
|
|
1155
|
+
|
|
1156
|
+
**Correct approach**:
|
|
1157
|
+
```typescript
|
|
1158
|
+
// ✅ CORRECT: Default import with CJS interop
|
|
1159
|
+
import TideJWT from "@tidecloak/verify";
|
|
1160
|
+
const _mod = (TideJWT as any)?.default ?? TideJWT;
|
|
1161
|
+
const { verifyTideCloakToken } = _mod;
|
|
1162
|
+
```
|
|
1163
|
+
|
|
1164
|
+
**Related**: AP-01
|
|
1165
|
+
|
|
1166
|
+
---
|
|
1167
|
+
|
|
1168
|
+
## AP-31: Bypassing Change Request Workflow
|
|
1169
|
+
|
|
1170
|
+
**What it looks like**:
|
|
1171
|
+
```typescript
|
|
1172
|
+
// ❌ WRONG: Direct DB write for role assignment
|
|
1173
|
+
app.post('/api/admin/assign-role', async (req, res) => {
|
|
1174
|
+
await db.query('INSERT INTO user_role_mapping (user_id, role_id) VALUES ($1, $2)',
|
|
1175
|
+
[req.body.userId, req.body.roleId]);
|
|
1176
|
+
return res.json({ success: true });
|
|
1177
|
+
});
|
|
1178
|
+
```
|
|
1179
|
+
|
|
1180
|
+
**Why it's wrong**:
|
|
1181
|
+
- All role/permission changes must go through IGA draft/approve/commit lifecycle
|
|
1182
|
+
- Direct DB writes do not produce VVK-signed authorization proofs
|
|
1183
|
+
- ORKs reject JWTs that do not match a signed proof
|
|
1184
|
+
- Bypasses quorum governance entirely
|
|
1185
|
+
|
|
1186
|
+
**Consequence**: Users get roles in DB but JWTs lack valid proofs. ORKs reject operations. Governance defeated.
|
|
1187
|
+
|
|
1188
|
+
**Correct approach**:
|
|
1189
|
+
```typescript
|
|
1190
|
+
// ✅ CORRECT: Use IGA change request API
|
|
1191
|
+
const changeRequest = await adminClient.changeRequests.create({
|
|
1192
|
+
type: 'ROLE_ASSIGNMENT',
|
|
1193
|
+
userId: req.body.userId,
|
|
1194
|
+
roleId: req.body.roleId
|
|
1195
|
+
});
|
|
1196
|
+
// Change request goes through draft → approve (quorum) → commit
|
|
1197
|
+
```
|
|
1198
|
+
|
|
1199
|
+
**Related**: AP-05, AP-10, Invariants I-09, I-10
|
|
1200
|
+
|
|
1201
|
+
---
|
|
1202
|
+
|
|
1203
|
+
## AP-32: Destructuring ApprovalType/ExecutionType from Models.Policy
|
|
1204
|
+
|
|
1205
|
+
**What it looks like**:
|
|
1206
|
+
```typescript
|
|
1207
|
+
// ❌ WRONG: Destructure from Models.Policy (it's a class, not a namespace)
|
|
1208
|
+
const { ApprovalType, ExecutionType } = Models.Policy;
|
|
1209
|
+
// ApprovalType is undefined
|
|
1210
|
+
// TypeError: Cannot read properties of undefined (reading 'IMPLICIT')
|
|
1211
|
+
```
|
|
1212
|
+
|
|
1213
|
+
**Why it's wrong**:
|
|
1214
|
+
- `Models.Policy` is the Policy class itself
|
|
1215
|
+
- `ApprovalType` and `ExecutionType` are direct exports from `Models`, not nested under `Policy`
|
|
1216
|
+
- Destructuring from the class yields `undefined` for both
|
|
1217
|
+
|
|
1218
|
+
**Consequence**: Runtime TypeError. Policy construction fails.
|
|
1219
|
+
|
|
1220
|
+
**Correct approach**:
|
|
1221
|
+
```typescript
|
|
1222
|
+
// ✅ CORRECT: Destructure from Models directly
|
|
1223
|
+
const { Policy, ApprovalType, ExecutionType, BaseTideRequest } = Models;
|
|
1224
|
+
const policy = new Policy(
|
|
1225
|
+
ApprovalType.IMPLICIT,
|
|
1226
|
+
ExecutionType.SIGN,
|
|
1227
|
+
// ...
|
|
1228
|
+
);
|
|
1229
|
+
```
|
|
1230
|
+
|
|
1231
|
+
**Related**: AP-11
|
|
1232
|
+
|
|
1233
|
+
---
|
|
1234
|
+
|
|
1235
|
+
## AP-33: Building PolicySignRequest Manually
|
|
1236
|
+
|
|
1237
|
+
**What it looks like**:
|
|
1238
|
+
```typescript
|
|
1239
|
+
// ❌ WRONG: Manual construction with BaseTideRequest + TideMemory
|
|
1240
|
+
const request = new BaseTideRequest();
|
|
1241
|
+
request.setModelId("Policy:1");
|
|
1242
|
+
const memory = new TideMemory();
|
|
1243
|
+
memory.write(policy.toBytes());
|
|
1244
|
+
request.setPayload(memory.toBytes());
|
|
1245
|
+
```
|
|
1246
|
+
|
|
1247
|
+
**Why it's wrong**:
|
|
1248
|
+
- The ORK's `PolicySignRequestFactory` expects a specific internal structure
|
|
1249
|
+
- Manual construction produces bytes the ORK cannot parse
|
|
1250
|
+
- `PolicySignRequest.New()` from `heimdall-tide` handles the correct serialization
|
|
1251
|
+
|
|
1252
|
+
**Consequence**: ORK rejects the request with parse errors. Policy signing fails.
|
|
1253
|
+
|
|
1254
|
+
**Correct approach**:
|
|
1255
|
+
```typescript
|
|
1256
|
+
// ✅ CORRECT: Use the factory method
|
|
1257
|
+
import { PolicySignRequest } from "heimdall-tide";
|
|
1258
|
+
const signRequest = PolicySignRequest.New(policy);
|
|
1259
|
+
```
|
|
1260
|
+
|
|
1261
|
+
**Related**: AP-11
|
|
1262
|
+
|
|
1263
|
+
---
|
|
1264
|
+
|
|
1265
|
+
## AP-34: Mounting Project Root as H2 Data Volume
|
|
1266
|
+
|
|
1267
|
+
**What it looks like**:
|
|
1268
|
+
```yaml
|
|
1269
|
+
# ❌ WRONG: docker-compose.yml
|
|
1270
|
+
volumes:
|
|
1271
|
+
- .:/opt/keycloak/data/h2
|
|
1272
|
+
```
|
|
1273
|
+
|
|
1274
|
+
**Why it's wrong**:
|
|
1275
|
+
- Docker writes H2 database files as a different user (UID 1000)
|
|
1276
|
+
- Mounting project root causes `AccessDeniedException`
|
|
1277
|
+
- Database files mixed with source code
|
|
1278
|
+
- Container cannot start or corrupts on write
|
|
1279
|
+
|
|
1280
|
+
**Consequence**: TideCloak container fails to start. `AccessDeniedException` on H2 database files.
|
|
1281
|
+
|
|
1282
|
+
**Correct approach**:
|
|
1283
|
+
```yaml
|
|
1284
|
+
# ✅ CORRECT: Use a dedicated data subdirectory
|
|
1285
|
+
volumes:
|
|
1286
|
+
- ./data:/opt/keycloak/data/h2
|
|
1287
|
+
# Also ensure correct ownership:
|
|
1288
|
+
# mkdir -p ./data && sudo chown -R 1000:1000 ./data
|
|
1289
|
+
```
|
|
1290
|
+
|
|
1291
|
+
**Related**: AP-07
|
|
1292
|
+
|
|
1293
|
+
---
|
|
1294
|
+
|
|
1295
|
+
## AP-35: Skipping or Reordering Initialization Sequence
|
|
1296
|
+
|
|
1297
|
+
**What it looks like**:
|
|
1298
|
+
```bash
|
|
1299
|
+
# ❌ WRONG: Skip license activation, jump to IGA
|
|
1300
|
+
curl -X POST "$KC_URL/admin/realms" -d '{"realm":"myrealm","enabled":true}'
|
|
1301
|
+
curl -X POST "$KC_URL/admin/realms/myrealm/vendorResources/toggle-iga"
|
|
1302
|
+
# Missing: setUpTideRealm, approve/commit, user creation, invite link...
|
|
1303
|
+
```
|
|
1304
|
+
|
|
1305
|
+
**Why it's wrong**:
|
|
1306
|
+
- The initialization sequence is strict: (1) create realm, (2) setUpTideRealm with email, (3) enable IGA, (4) approve/commit client change requests, (5) create admin user + assign role, (6) generate invite link, (7) approve/commit user change requests, (8) update CustomAdminUIDomain, (9) download adapter config
|
|
1307
|
+
- Skipping steps or reordering causes silent failures
|
|
1308
|
+
- `setUpTideRealm` activates the license and VRK; without it, nothing else works
|
|
1309
|
+
|
|
1310
|
+
**Consequence**: Silent failures. Missing VRK. IGA toggle fails. Invite links not generated. Adapter config incomplete.
|
|
1311
|
+
|
|
1312
|
+
**Correct approach**:
|
|
1313
|
+
```bash
|
|
1314
|
+
# ✅ CORRECT: Follow exact sequence
|
|
1315
|
+
# 1. Create realm
|
|
1316
|
+
# 2. POST /vendorResources/setUpTideRealm (with email)
|
|
1317
|
+
# 3. POST /vendorResources/toggle-iga
|
|
1318
|
+
# 4. Approve + commit client change requests
|
|
1319
|
+
# 5. Create admin user + assign tide-realm-admin
|
|
1320
|
+
# 6. Generate invite link + wait for account linking
|
|
1321
|
+
# 7. Approve + commit user change requests
|
|
1322
|
+
# 8. Update CustomAdminUIDomain
|
|
1323
|
+
# 9. Download adapter config
|
|
1324
|
+
```
|
|
1325
|
+
|
|
1326
|
+
**Related**: AP-13, AP-27
|
|
1327
|
+
|
|
1328
|
+
---
|
|
1329
|
+
|
|
1330
|
+
## AP-36: Omitting Tide Environment Variables from Docker
|
|
1331
|
+
|
|
1332
|
+
**What it looks like**:
|
|
1333
|
+
```yaml
|
|
1334
|
+
# ❌ WRONG: Incomplete docker-compose.yml
|
|
1335
|
+
services:
|
|
1336
|
+
tidecloak:
|
|
1337
|
+
image: tideorg/tidecloak-dev:latest
|
|
1338
|
+
environment:
|
|
1339
|
+
KC_BOOTSTRAP_ADMIN_USERNAME: admin
|
|
1340
|
+
KC_BOOTSTRAP_ADMIN_PASSWORD: admin
|
|
1341
|
+
# Missing: SYSTEM_HOME_ORK, USER_HOME_ORK, THRESHOLD_T, THRESHOLD_N,
|
|
1342
|
+
# PAYER_PUBLIC, KC_HOSTNAME
|
|
1343
|
+
```
|
|
1344
|
+
|
|
1345
|
+
**Why it's wrong**:
|
|
1346
|
+
- Without `SYSTEM_HOME_ORK` and `USER_HOME_ORK`, the Tide IdP provider does not initialize
|
|
1347
|
+
- Without `THRESHOLD_T` and `THRESHOLD_N`, threshold parameters are undefined
|
|
1348
|
+
- Required actions like `link-tide-account-action` never register
|
|
1349
|
+
- Init script fails with "Could not find configuration for Required Action"
|
|
1350
|
+
|
|
1351
|
+
**Consequence**: TideCloak starts but Tide features are non-functional. Init scripts fail. No invite links. No threshold operations.
|
|
1352
|
+
|
|
1353
|
+
**Correct approach**:
|
|
1354
|
+
```yaml
|
|
1355
|
+
# ✅ CORRECT: All required environment variables
|
|
1356
|
+
services:
|
|
1357
|
+
tidecloak:
|
|
1358
|
+
image: tideorg/tidecloak-dev:latest
|
|
1359
|
+
environment:
|
|
1360
|
+
KC_BOOTSTRAP_ADMIN_USERNAME: admin
|
|
1361
|
+
KC_BOOTSTRAP_ADMIN_PASSWORD: admin
|
|
1362
|
+
KC_HOSTNAME: localhost
|
|
1363
|
+
SYSTEM_HOME_ORK: "https://orkeyedev01-g2.australiaeast.cloudapp.azure.com:443"
|
|
1364
|
+
USER_HOME_ORK: "https://orkeyedev01-g2.australiaeast.cloudapp.azure.com:443"
|
|
1365
|
+
THRESHOLD_T: "3"
|
|
1366
|
+
THRESHOLD_N: "5"
|
|
1367
|
+
PAYER_PUBLIC: "<your-payer-public-key>"
|
|
1368
|
+
```
|
|
1369
|
+
|
|
1370
|
+
**Related**: AP-35, AP-08
|
|
1371
|
+
|
|
1372
|
+
---
|
|
1373
|
+
|
|
1374
|
+
## AP-37: Auto-Approving IGA Role Assignments from Backend
|
|
1375
|
+
|
|
1376
|
+
**What it looks like**:
|
|
1377
|
+
```typescript
|
|
1378
|
+
// ❌ WRONG: Backend tries to complete approval programmatically
|
|
1379
|
+
app.post('/api/admin/quick-approve', async (req, res) => {
|
|
1380
|
+
const { changeRequestId } = req.body;
|
|
1381
|
+
await adminClient.changeRequests.approve(changeRequestId);
|
|
1382
|
+
await adminClient.changeRequests.commit(changeRequestId);
|
|
1383
|
+
return res.json({ success: true });
|
|
1384
|
+
});
|
|
1385
|
+
```
|
|
1386
|
+
|
|
1387
|
+
**Why it's wrong**:
|
|
1388
|
+
- Role assignments that produce VVK-signed UserContexts require the approval enclave in the admin's browser
|
|
1389
|
+
- The backend receives `requiresApprovalPopup: true` and cannot complete the signing
|
|
1390
|
+
- VVK signing is a threshold operation that must involve the SWE
|
|
1391
|
+
- Programmatic approval bypasses the cryptographic ceremony
|
|
1392
|
+
|
|
1393
|
+
**Consequence**: Approval call returns `requiresApprovalPopup: true` but signing never completes. Change request stuck. If somehow bypassed, JWTs lack valid proofs.
|
|
1394
|
+
|
|
1395
|
+
**Correct approach**:
|
|
1396
|
+
```typescript
|
|
1397
|
+
// ✅ CORRECT: Build frontend approval UI
|
|
1398
|
+
// Backend exposes pending change requests
|
|
1399
|
+
app.get('/api/admin/pending', async (req, res) => {
|
|
1400
|
+
const pending = await adminClient.changeRequests.list({ status: 'PENDING' });
|
|
1401
|
+
return res.json(pending);
|
|
1402
|
+
});
|
|
1403
|
+
|
|
1404
|
+
// Frontend presents approval UI; admin's browser enclave handles signing
|
|
1405
|
+
// Use batch endpoints for efficiency
|
|
1406
|
+
await fetch('/api/admin/sign/batch', {
|
|
1407
|
+
method: 'POST',
|
|
1408
|
+
headers: { 'Authorization': `Bearer ${adminToken}`, 'DPoP': dpopProof },
|
|
1409
|
+
body: JSON.stringify({ changeRequestIds })
|
|
1410
|
+
});
|
|
1411
|
+
// Approval popup appears in admin's browser → SWE performs threshold signing
|
|
1412
|
+
```
|
|
1413
|
+
|
|
1414
|
+
**Related**: AP-05, AP-10, AP-31, Invariants I-09, I-10
|
|
1415
|
+
|
|
1416
|
+
---
|
|
1417
|
+
|
|
1418
|
+
## AP-38: Duplicating tidecloak.json Config into NEXT_PUBLIC Env Vars
|
|
1419
|
+
|
|
1420
|
+
**What it looks like**:
|
|
1421
|
+
```bash
|
|
1422
|
+
# .env.local
|
|
1423
|
+
NEXT_PUBLIC_TIDECLOAK_URL=http://localhost:8080
|
|
1424
|
+
NEXT_PUBLIC_TIDECLOAK_REALM=my-app
|
|
1425
|
+
NEXT_PUBLIC_TIDECLOAK_CLIENT_ID=my-app-client
|
|
1426
|
+
NEXT_PUBLIC_TIDECLOAK_REDIRECT_URI=http://localhost:3000/auth/redirect
|
|
1427
|
+
```
|
|
1428
|
+
|
|
1429
|
+
```tsx
|
|
1430
|
+
// ❌ WRONG: Provider reads env vars instead of tidecloak.json
|
|
1431
|
+
<TideCloakContextProvider
|
|
1432
|
+
authServerUrl={process.env.NEXT_PUBLIC_TIDECLOAK_URL}
|
|
1433
|
+
realm={process.env.NEXT_PUBLIC_TIDECLOAK_REALM}
|
|
1434
|
+
clientId={process.env.NEXT_PUBLIC_TIDECLOAK_CLIENT_ID}
|
|
1435
|
+
redirectUri={process.env.NEXT_PUBLIC_TIDECLOAK_REDIRECT_URI}
|
|
1436
|
+
/>
|
|
1437
|
+
```
|
|
1438
|
+
|
|
1439
|
+
**Why it's wrong**:
|
|
1440
|
+
- `tidecloak.json` already contains `auth-server-url`, `realm`, `resource` (clientId), and all Tide-specific fields
|
|
1441
|
+
- Duplicating these into env vars creates two sources of truth
|
|
1442
|
+
- Env vars can drift from the actual adapter JSON exported from TideCloak
|
|
1443
|
+
- `NEXT_PUBLIC_*` env vars expose config in client-side bundles unnecessarily
|
|
1444
|
+
|
|
1445
|
+
**Consequence**: Config drift between env vars and adapter JSON. Debugging breaks because the two sources disagree. New developers add env vars instead of exporting the adapter.
|
|
1446
|
+
|
|
1447
|
+
**Correct approach**:
|
|
1448
|
+
```tsx
|
|
1449
|
+
// ✅ CORRECT: Import tidecloak.json directly
|
|
1450
|
+
import tcConfig from '../../data/tidecloak.json';
|
|
1451
|
+
|
|
1452
|
+
<TideCloakProvider config={tcConfig}>
|
|
1453
|
+
{children}
|
|
1454
|
+
</TideCloakProvider>
|
|
1455
|
+
```
|
|
1456
|
+
|
|
1457
|
+
For deployment without a file: use `CLIENT_ADAPTER` env var containing the full JSON. Do not split it into individual vars.
|
|
1458
|
+
|
|
1459
|
+
**Related**: I-05, I-13
|
|
1460
|
+
|
|
1461
|
+
---
|
|
1462
|
+
|
|
1463
|
+
## AP-39: Appending `start-dev` to TideCloak Docker Run
|
|
1464
|
+
|
|
1465
|
+
**What it looks like**:
|
|
1466
|
+
```bash
|
|
1467
|
+
# ❌ WRONG: Appending start-dev command
|
|
1468
|
+
docker run -d --name tidecloak \
|
|
1469
|
+
-p 8080:8080 \
|
|
1470
|
+
-e KC_BOOTSTRAP_ADMIN_USERNAME=admin \
|
|
1471
|
+
-e KC_BOOTSTRAP_ADMIN_PASSWORD=password \
|
|
1472
|
+
tideorg/tidecloak-dev:latest start-dev
|
|
1473
|
+
```
|
|
1474
|
+
|
|
1475
|
+
**Why it's wrong**:
|
|
1476
|
+
- TideCloak images are NOT vanilla Keycloak. The entrypoint is pre-configured.
|
|
1477
|
+
- Passing `start-dev` overrides the container's built-in entrypoint/command and breaks Tide-specific initialization.
|
|
1478
|
+
- This is a Keycloak convention (`kc.sh start-dev`) that does not apply to TideCloak Docker images.
|
|
1479
|
+
|
|
1480
|
+
**Consequence**: Container may start in wrong mode, fail to initialize Tide providers, or exit immediately.
|
|
1481
|
+
|
|
1482
|
+
**Correct approach**:
|
|
1483
|
+
```bash
|
|
1484
|
+
# ✅ CORRECT: No command argument — image handles startup internally
|
|
1485
|
+
sudo docker run -d --name tidecloak \
|
|
1486
|
+
-v ./data:/opt/keycloak/data/h2 \
|
|
1487
|
+
-p 8080:8080 \
|
|
1488
|
+
-e KC_BOOTSTRAP_ADMIN_USERNAME=admin \
|
|
1489
|
+
-e KC_BOOTSTRAP_ADMIN_PASSWORD=password \
|
|
1490
|
+
tideorg/tidecloak-dev:latest
|
|
1491
|
+
```
|
|
1492
|
+
|
|
1493
|
+
Do not append `start-dev`, `start`, or any other command to `docker run` for TideCloak images.
|
|
1494
|
+
|
|
1495
|
+
**Related**: AP-36
|
|
1496
|
+
|
|
1497
|
+
---
|
|
1498
|
+
|
|
1499
|
+
## AP-40: Using Wrong Docker Org for TideCloak Images
|
|
1500
|
+
|
|
1501
|
+
**What it looks like**:
|
|
1502
|
+
```bash
|
|
1503
|
+
# ❌ WRONG: tidecloak/ org does not exist
|
|
1504
|
+
docker run -d tideorg/tidecloak-dev:latest # ← this is correct
|
|
1505
|
+
docker run -d tidecloak/tidecloak-dev:latest # ← WRONG org name
|
|
1506
|
+
docker run -d tidecloak/tidecloak-stg-dev:latest # ← WRONG org name
|
|
1507
|
+
```
|
|
1508
|
+
|
|
1509
|
+
**Why it's wrong**:
|
|
1510
|
+
- TideCloak images are published under the `tideorg` Docker Hub organization, not `tidecloak`.
|
|
1511
|
+
- AI agents frequently hallucinate `tidecloak/tidecloak-dev` because the product is called TideCloak.
|
|
1512
|
+
- `docker pull tidecloak/tidecloak-dev` will fail with "repository does not exist".
|
|
1513
|
+
|
|
1514
|
+
**Consequence**: Image pull fails. Container never starts.
|
|
1515
|
+
|
|
1516
|
+
**Correct approach**:
|
|
1517
|
+
```bash
|
|
1518
|
+
# ✅ CORRECT: tideorg/ org
|
|
1519
|
+
docker run -d tideorg/tidecloak-dev:latest
|
|
1520
|
+
docker run -d tideorg/tidecloak-stg-dev:latest
|
|
1521
|
+
```
|
|
1522
|
+
|
|
1523
|
+
The org is `tideorg`, not `tidecloak`.
|
|
1524
|
+
|
|
1525
|
+
**Related**: AP-39
|
|
1526
|
+
|
|
1527
|
+
---
|
|
1528
|
+
|
|
1529
|
+
## Anti-Pattern Detection Checklist
|
|
1530
|
+
|
|
1531
|
+
### Code Review
|
|
1532
|
+
- [ ] No local key generation for Tide operations (AP-03)
|
|
1533
|
+
- [ ] No client-side policy enforcement (AP-11)
|
|
1534
|
+
- [ ] No single-admin bypass paths (AP-05)
|
|
1535
|
+
- [ ] No offline E2EE fallbacks (AP-04)
|
|
1536
|
+
- [ ] No hardcoded 14/20 threshold (AP-08)
|
|
1537
|
+
- [ ] No cached DPoP headers (AP-12)
|
|
1538
|
+
- [ ] Server-side JWT verification on all protected APIs (AP-02)
|
|
1539
|
+
- [ ] Embedded JWKS used, not remote endpoint (AP-01)
|
|
1540
|
+
- [ ] No `createRemoteJWKSet` import or usage (AP-01)
|
|
1541
|
+
- [ ] No server-side password handling (AP-19)
|
|
1542
|
+
- [ ] JWT signature always verified, never just decoded (AP-20)
|
|
1543
|
+
- [ ] Dokens never stored server-side (AP-21)
|
|
1544
|
+
- [ ] No server-side decryption of E2EE data (AP-22)
|
|
1545
|
+
- [ ] Meaningful unique tags on every encrypted field (AP-23)
|
|
1546
|
+
- [ ] Self-encryption not used for shared data (AP-24)
|
|
1547
|
+
- [ ] `_tide_*` roles not used in Forseti ValidateExecutor (AP-25)
|
|
1548
|
+
- [ ] No non-deterministic calls in Forseti contracts (AP-26)
|
|
1549
|
+
- [ ] `hasClientRole` used for tide-realm-admin, not `hasRealmRole` (AP-29)
|
|
1550
|
+
- [ ] Default import used for @tidecloak/verify (AP-30)
|
|
1551
|
+
- [ ] No direct DB writes bypassing change requests (AP-31)
|
|
1552
|
+
- [ ] `ApprovalType`/`ExecutionType` destructured from `Models`, not `Models.Policy` (AP-32)
|
|
1553
|
+
- [ ] `PolicySignRequest.New()` used, not manual construction (AP-33)
|
|
1554
|
+
- [ ] No backend auto-approval of IGA role assignments (AP-37)
|
|
1555
|
+
|
|
1556
|
+
### Deployment Check
|
|
1557
|
+
- [ ] CSP includes Tide domains (AP-07)
|
|
1558
|
+
- [ ] `silent-check-sso.html` exists (AP-15)
|
|
1559
|
+
- [ ] Adapter JSON has Tide extensions (AP-13)
|
|
1560
|
+
- [ ] DPoP verification implemented if enabled (AP-06)
|
|
1561
|
+
- [ ] `configUrl` prop used, not `configFilePath` (AP-27)
|
|
1562
|
+
- [ ] SDK initialization gate before login (AP-28)
|
|
1563
|
+
- [ ] No `NEXT_PUBLIC_TIDECLOAK_*` env vars duplicating tidecloak.json (AP-38)
|
|
1564
|
+
- [ ] Data volume is `./data`, not project root (AP-34)
|
|
1565
|
+
- [ ] Initialization sequence followed in correct order (AP-35)
|
|
1566
|
+
- [ ] All Tide environment variables present in Docker config (AP-36)
|
|
1567
|
+
|
|
1568
|
+
### Documentation Review
|
|
1569
|
+
- [ ] Tide not described as generic OIDC (AP-01)
|
|
1570
|
+
- [ ] BYOiD not conflated with federated login (AP-09)
|
|
1571
|
+
- [ ] IGA not downplayed as procedural workflow (AP-10)
|
|
1572
|
+
- [ ] E2EE not described as client-side encryption (AP-16)
|
|
1573
|
+
- [ ] UI gating vs authorization clearly distinguished (AP-02)
|
|
1574
|
+
|
|
1575
|
+
---
|
|
1576
|
+
|
|
1577
|
+
## Severity Classification
|
|
1578
|
+
|
|
1579
|
+
| Anti-Pattern | Security Impact | Operational Impact | Detection Difficulty |
|
|
1580
|
+
|--------------|----------------|-------------------|---------------------|
|
|
1581
|
+
| AP-01 | **CRITICAL** | High | Medium |
|
|
1582
|
+
| AP-02 | **CRITICAL** | Low | Easy |
|
|
1583
|
+
| AP-03 | **CRITICAL** | High | Easy |
|
|
1584
|
+
| AP-04 | **HIGH** | Medium | Medium |
|
|
1585
|
+
| AP-05 | **CRITICAL** | Low | Medium |
|
|
1586
|
+
| AP-06 | **HIGH** | Low | Easy |
|
|
1587
|
+
| AP-07 | **HIGH** | **CRITICAL** | Easy |
|
|
1588
|
+
| AP-08 | Low | **HIGH** | Easy |
|
|
1589
|
+
| AP-09 | **HIGH** (misleading) | Medium | Hard |
|
|
1590
|
+
| AP-10 | **HIGH** (misleading) | Medium | Hard |
|
|
1591
|
+
| AP-11 | **CRITICAL** | Low | Medium |
|
|
1592
|
+
| AP-12 | Low | **HIGH** | Easy |
|
|
1593
|
+
| AP-13 | **HIGH** | **CRITICAL** | Easy |
|
|
1594
|
+
| AP-14 | Low | **HIGH** | Medium |
|
|
1595
|
+
| AP-15 | Low | **HIGH** | Easy |
|
|
1596
|
+
| AP-16 | **HIGH** (misleading) | Low | Hard |
|
|
1597
|
+
| AP-17 | **CRITICAL** | **CRITICAL** | Medium |
|
|
1598
|
+
| AP-18 | **HIGH** | Low | Medium |
|
|
1599
|
+
| AP-19 | **CRITICAL** | Low | Easy |
|
|
1600
|
+
| AP-20 | **CRITICAL** | Low | Easy |
|
|
1601
|
+
| AP-21 | Medium | Low | Easy |
|
|
1602
|
+
| AP-22 | **CRITICAL** | Low | Easy |
|
|
1603
|
+
| AP-23 | **HIGH** | Low | Medium |
|
|
1604
|
+
| AP-24 | **HIGH** | Medium | Hard |
|
|
1605
|
+
| AP-25 | **HIGH** | Low | Medium |
|
|
1606
|
+
| AP-26 | **HIGH** | **HIGH** | Medium |
|
|
1607
|
+
| AP-27 | Low | **CRITICAL** | Easy |
|
|
1608
|
+
| AP-28 | Low | **HIGH** | Easy |
|
|
1609
|
+
| AP-29 | Low | **HIGH** | Easy |
|
|
1610
|
+
| AP-30 | Low | **CRITICAL** | Easy |
|
|
1611
|
+
| AP-31 | **CRITICAL** | Medium | Medium |
|
|
1612
|
+
| AP-32 | Low | **HIGH** | Easy |
|
|
1613
|
+
| AP-33 | Low | **HIGH** | Medium |
|
|
1614
|
+
| AP-34 | Low | **CRITICAL** | Easy |
|
|
1615
|
+
| AP-35 | **HIGH** | **CRITICAL** | Medium |
|
|
1616
|
+
| AP-36 | Medium | **CRITICAL** | Easy |
|
|
1617
|
+
| AP-37 | **CRITICAL** | **HIGH** | Medium |
|
|
1618
|
+
|
|
1619
|
+
**Impact levels**:
|
|
1620
|
+
- **CRITICAL**: Defeats core Tide security properties or causes complete failure
|
|
1621
|
+
- **HIGH**: Creates exploitable weakness or major operational failure
|
|
1622
|
+
- **Medium**: Degrades security or reliability
|
|
1623
|
+
- **Low**: Minor issue or inconvenience
|
|
1624
|
+
|
|
1625
|
+
---
|
|
1626
|
+
|
|
1627
|
+
## Recovery from Anti-Patterns
|
|
1628
|
+
|
|
1629
|
+
### If AP-01 detected (Generic OIDC treatment / remote JWKS)
|
|
1630
|
+
1. Remove all `createRemoteJWKSet` imports and usage
|
|
1631
|
+
2. Replace with `createLocalJWKSet(config.jwk)` using embedded JWKS from adapter JSON
|
|
1632
|
+
3. Add guard: `if (!config.jwk) throw new Error('Missing jwk in tidecloak.json')`
|
|
1633
|
+
4. Verify adapter has `jwk`, `vendorId`, `homeOrkUrl` fields
|
|
1634
|
+
5. If `jwk` is missing, re-export adapter from TideCloak with IGA enabled — do not add remote fallback
|
|
1635
|
+
6. Review all auth code for Tide-specific patterns
|
|
1636
|
+
|
|
1637
|
+
### If AP-02 detected (Client-side authorization)
|
|
1638
|
+
1. Add `verifyTideJWT()` to all protected API routes
|
|
1639
|
+
2. Move role checks server-side
|
|
1640
|
+
3. Keep client-side `hasRealmRole()` / `hasClientRole()` for UI gating only
|
|
1641
|
+
|
|
1642
|
+
### If AP-03 detected (Local key generation)
|
|
1643
|
+
1. Remove all local key generation code
|
|
1644
|
+
2. Replace with SDK operations (`doEncrypt`, `executeSignRequest`)
|
|
1645
|
+
3. Verify keys never assembled locally
|
|
1646
|
+
|
|
1647
|
+
### If AP-05 detected (Single-admin bypass)
|
|
1648
|
+
1. Remove bypass paths
|
|
1649
|
+
2. Route all admin changes through IGA change-set API
|
|
1650
|
+
3. Document that no emergency override exists
|
|
1651
|
+
|
|
1652
|
+
### If AP-07 detected (Missing CSP)
|
|
1653
|
+
1. Add CSP with Tide domain whitelist
|
|
1654
|
+
2. Test SWE iframe loads successfully
|
|
1655
|
+
3. Monitor browser console for CSP violations
|
|
1656
|
+
|
|
1657
|
+
### If AP-13 detected (Wrong adapter format)
|
|
1658
|
+
1. Re-export adapter via Tide-specific endpoint
|
|
1659
|
+
2. Verify `jwk`, `vendorId`, `homeOrkUrl` present
|
|
1660
|
+
3. Replace generic adapter in all environments
|
|
1661
|
+
|
|
1662
|
+
### If AP-19 detected (Server-side password handling)
|
|
1663
|
+
1. Remove all password fields from API request handlers
|
|
1664
|
+
2. Ensure login is delegated entirely to TideCloak SDK
|
|
1665
|
+
3. Backend must only verify JWTs, never receive credentials
|
|
1666
|
+
|
|
1667
|
+
### If AP-22 detected (Server-side decryption)
|
|
1668
|
+
1. Move all decryption to the browser via `iam.doDecrypt()`
|
|
1669
|
+
2. Server stores and serves ciphertext only
|
|
1670
|
+
3. Verify no plaintext exists in server logs or responses
|
|
1671
|
+
|
|
1672
|
+
### If AP-31 detected (Direct DB writes)
|
|
1673
|
+
1. Replace all direct DB role/permission writes with IGA change request API
|
|
1674
|
+
2. Verify all changes go through draft/approve/commit lifecycle
|
|
1675
|
+
3. Audit DB for rows not backed by VVK-signed proofs
|
|
1676
|
+
|
|
1677
|
+
### If AP-35 detected (Wrong init sequence)
|
|
1678
|
+
1. Reset the realm (delete and recreate)
|
|
1679
|
+
2. Follow the exact 9-step initialization sequence
|
|
1680
|
+
3. Verify each step completes before proceeding to the next
|
|
1681
|
+
|
|
1682
|
+
### If AP-36 detected (Missing Docker env vars)
|
|
1683
|
+
1. Add all required variables: `SYSTEM_HOME_ORK`, `USER_HOME_ORK`, `THRESHOLD_T`, `THRESHOLD_N`, `PAYER_PUBLIC`, `KC_HOSTNAME`
|
|
1684
|
+
2. Restart the container
|
|
1685
|
+
3. Verify `link-tide-account-action` is registered in Required Actions
|
|
1686
|
+
|
|
1687
|
+
---
|
|
1688
|
+
|
|
1689
|
+
## AP-41: Using Master Admin Credentials in Generated App Code
|
|
1690
|
+
|
|
1691
|
+
**What it looks like**:
|
|
1692
|
+
```typescript
|
|
1693
|
+
// ❌ WRONG: API route fetches users with hardcoded master credentials
|
|
1694
|
+
const tokenRes = await fetch(`${tcUrl}/realms/master/protocol/openid-connect/token`, {
|
|
1695
|
+
method: 'POST',
|
|
1696
|
+
body: new URLSearchParams({
|
|
1697
|
+
grant_type: 'password',
|
|
1698
|
+
username: 'admin',
|
|
1699
|
+
password: 'password',
|
|
1700
|
+
client_id: 'admin-cli',
|
|
1701
|
+
}),
|
|
1702
|
+
});
|
|
1703
|
+
```
|
|
1704
|
+
|
|
1705
|
+
**Why it's wrong**:
|
|
1706
|
+
- Master credentials (`admin`/`password`) are bootstrap-only. They should only appear in `scripts/init-tidecloak.sh`.
|
|
1707
|
+
- Hardcoding them in API routes leaks bootstrap secrets into the running app.
|
|
1708
|
+
- It bypasses the logged-in user's authorization scope — the app acts as a superadmin for every request.
|
|
1709
|
+
- It breaks when credentials change.
|
|
1710
|
+
|
|
1711
|
+
**Correct approach**: Forward the logged-in user's token to TideCloak admin APIs:
|
|
1712
|
+
```typescript
|
|
1713
|
+
// ✅ CORRECT: Use the logged-in user's token
|
|
1714
|
+
const userToken = req.headers.get('authorization')?.replace('Bearer ', '');
|
|
1715
|
+
const res = await fetch(`${tcUrl}/admin/realms/${realm}/users`, {
|
|
1716
|
+
headers: { Authorization: `Bearer ${userToken}` },
|
|
1717
|
+
});
|
|
1718
|
+
```
|
|
1719
|
+
|
|
1720
|
+
For client-side code, call same-origin API proxies instead of direct cross-origin TideCloak calls.
|
|
1721
|
+
|
|
1722
|
+
**Verification**: `grep -r "grant_type=password\|username=admin\|password=password" app/ src/ lib/ --include="*.ts" --include="*.tsx"` should return zero matches outside of `scripts/init*.sh`.
|
|
1723
|
+
|
|
1724
|
+
VERIFIED (learning-batch-003, L-03).
|
|
1725
|
+
|
|
1726
|
+
---
|
|
1727
|
+
|
|
1728
|
+
## AP-42: Missing @tidecloak/react ESM Webpack Alias
|
|
1729
|
+
|
|
1730
|
+
**What it looks like**: Login page shows a redirect loop. Provider fails silently. `useTideCloak()` returns undefined. No clear error message.
|
|
1731
|
+
|
|
1732
|
+
**Root cause**: `@tidecloak/react`'s CJS dist (`dist/cjs/index.js`) contains ESM `import` statements. When `@tidecloak/nextjs` does `require("@tidecloak/react")`, webpack follows the CJS path and hits ESM syntax.
|
|
1733
|
+
|
|
1734
|
+
**Correct approach**: Add webpack alias in `next.config.ts`:
|
|
1735
|
+
```typescript
|
|
1736
|
+
import path from "path";
|
|
1737
|
+
// inside webpack config:
|
|
1738
|
+
config.resolve.alias = {
|
|
1739
|
+
...config.resolve.alias,
|
|
1740
|
+
"@tidecloak/react": path.resolve(
|
|
1741
|
+
__dirname,
|
|
1742
|
+
"node_modules/@tidecloak/react/dist/esm/index.js"
|
|
1743
|
+
),
|
|
1744
|
+
};
|
|
1745
|
+
```
|
|
1746
|
+
|
|
1747
|
+
Use `path.resolve()`, NOT `require.resolve()` — the latter throws `ERR_PACKAGE_PATH_NOT_EXPORTED` because the package's `exports` field is misconfigured.
|
|
1748
|
+
|
|
1749
|
+
This is required in addition to the `strictExportPresence = false` fix. See `canon/framework-matrix.md` for the complete webpack config.
|
|
1750
|
+
|
|
1751
|
+
VERIFIED (learning-batch-003, L-04).
|
|
1752
|
+
|
|
1753
|
+
---
|
|
1754
|
+
|
|
1755
|
+
## AP-43: Re-Verifying DPoP Proofs on App API Routes When Using secureFetch
|
|
1756
|
+
|
|
1757
|
+
**What it looks like**:
|
|
1758
|
+
```typescript
|
|
1759
|
+
// ❌ WRONG: Attempt RFC 9449 DPoP proof verification on secureFetch requests
|
|
1760
|
+
import { jwtVerify, importJWK } from 'jose';
|
|
1761
|
+
|
|
1762
|
+
const dpopHeader = req.headers.get('dpop');
|
|
1763
|
+
const { payload } = await jwtVerify(dpopHeader, publicKey); // Throws: Invalid Compact JWS
|
|
1764
|
+
```
|
|
1765
|
+
|
|
1766
|
+
**Why it's wrong**:
|
|
1767
|
+
- `secureFetch` from `@tidecloak/js` sends DPoP proofs in a Tide-specific format, not standard RFC 9449 compact JWS
|
|
1768
|
+
- `jwtVerify()` rejects the non-standard format with `401: Invalid Compact JWS`
|
|
1769
|
+
- DPoP binding is already enforced at TideCloak during token issuance — the `cnf.jkt` claim proves binding
|
|
1770
|
+
- JWT signature verification via embedded JWKS (VVK) is the primary security layer
|
|
1771
|
+
|
|
1772
|
+
**Consequence**: All API requests via `secureFetch` return 401. Login works but no API calls succeed.
|
|
1773
|
+
|
|
1774
|
+
**Correct approach**:
|
|
1775
|
+
```typescript
|
|
1776
|
+
// ✅ CORRECT: Verify JWT signature only. cnf.jkt proves DPoP binding at issuance.
|
|
1777
|
+
const jwt = await verifyTideJWT(token);
|
|
1778
|
+
// The presence of cnf.jkt confirms TideCloak bound this token with DPoP.
|
|
1779
|
+
// No need to re-verify the proof on the app side.
|
|
1780
|
+
```
|
|
1781
|
+
|
|
1782
|
+
**When to use DPoP proof verification**: Only if your app sends manual DPoP proofs (not via `secureFetch`). If using `secureFetch`, skip DPoP proof verification entirely.
|
|
1783
|
+
|
|
1784
|
+
VERIFIED (LEARNINGS-batch-005, L-06).
|
|
1785
|
+
|
|
1786
|
+
**Related**: I-12, [verify-jwt-server-side.md](../playbooks/verify-jwt-server-side.md) Step 4
|
|
1787
|
+
|
|
1788
|
+
---
|
|
1789
|
+
|
|
1790
|
+
## AP-44: Using In-Memory Stores in Next.js API Routes
|
|
1791
|
+
|
|
1792
|
+
**What it looks like**:
|
|
1793
|
+
```typescript
|
|
1794
|
+
// ❌ WRONG: Module-level Map in API route
|
|
1795
|
+
const vaultStore = new Map<string, any>();
|
|
1796
|
+
|
|
1797
|
+
export async function POST(req: NextRequest) {
|
|
1798
|
+
vaultStore.set(key, value); // Stored in memory
|
|
1799
|
+
}
|
|
1800
|
+
|
|
1801
|
+
export async function GET(req: NextRequest) {
|
|
1802
|
+
return Response.json(vaultStore.get(key)); // Empty after hot reload
|
|
1803
|
+
}
|
|
1804
|
+
```
|
|
1805
|
+
|
|
1806
|
+
**Why it's wrong**:
|
|
1807
|
+
- Next.js App Router in dev mode reimports modules on every file change (hot reload)
|
|
1808
|
+
- Module-level variables reset on reimport — all in-memory state is lost
|
|
1809
|
+
- POST stores data, but the next GET returns empty because the Map was reset
|
|
1810
|
+
- This affects `Map`, `Array`, `Set`, or any module-level mutable state
|
|
1811
|
+
|
|
1812
|
+
**Consequence**: Data loss between requests during development. POST succeeds, GET returns 404/empty. Appears as intermittent bug.
|
|
1813
|
+
|
|
1814
|
+
**Correct approach**:
|
|
1815
|
+
```typescript
|
|
1816
|
+
// ✅ CORRECT: Use filesystem for state that must survive between requests
|
|
1817
|
+
import { readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
1818
|
+
import { join } from 'path';
|
|
1819
|
+
|
|
1820
|
+
const DATA_DIR = join(process.cwd(), 'data');
|
|
1821
|
+
mkdirSync(DATA_DIR, { recursive: true });
|
|
1822
|
+
|
|
1823
|
+
function readStore(): Record<string, any> {
|
|
1824
|
+
try {
|
|
1825
|
+
return JSON.parse(readFileSync(join(DATA_DIR, 'store.json'), 'utf-8'));
|
|
1826
|
+
} catch { return {}; }
|
|
1827
|
+
}
|
|
1828
|
+
|
|
1829
|
+
function writeStore(data: Record<string, any>): void {
|
|
1830
|
+
writeFileSync(join(DATA_DIR, 'store.json'), JSON.stringify(data, null, 2));
|
|
1831
|
+
}
|
|
1832
|
+
```
|
|
1833
|
+
|
|
1834
|
+
For production, use a database. For demos and dev, filesystem is sufficient and survives hot reload.
|
|
1835
|
+
|
|
1836
|
+
VERIFIED (LEARNINGS-batch-005, L-08).
|
|
1837
|
+
|
|
1838
|
+
**Related**: None (general Next.js dev behavior, not Tide-specific)
|
|
1839
|
+
|
|
1840
|
+
---
|
|
1841
|
+
|
|
1842
|
+
## AP-45: Rejecting DPoP-Upgraded Authorization Headers on Server
|
|
1843
|
+
|
|
1844
|
+
**What it looks like**:
|
|
1845
|
+
```typescript
|
|
1846
|
+
// ❌ WRONG: Only accepts Bearer scheme
|
|
1847
|
+
if (!authHeader.startsWith('Bearer ')) {
|
|
1848
|
+
return Response.json({ error: 'Unauthorized' }, { status: 401 });
|
|
1849
|
+
}
|
|
1850
|
+
const token = authHeader.substring(7);
|
|
1851
|
+
```
|
|
1852
|
+
|
|
1853
|
+
**Why it's wrong**:
|
|
1854
|
+
- When `useDPoP` is enabled, `IAMService.secureFetch` upgrades `Authorization: Bearer <token>` to `Authorization: DPoP <token>`
|
|
1855
|
+
- Server middleware that only checks for `Bearer` silently rejects all DPoP-upgraded requests with 401
|
|
1856
|
+
- The client sees the request "succeed" (no thrown error) but the API returns 401, so data operations fail
|
|
1857
|
+
|
|
1858
|
+
**Consequence**: All API calls via `secureFetch` return 401 when DPoP is enabled. Data appears to save on the frontend but is silently rejected by the server.
|
|
1859
|
+
|
|
1860
|
+
**Correct approach**:
|
|
1861
|
+
```typescript
|
|
1862
|
+
// ✅ CORRECT: Accept both Bearer and DPoP schemes
|
|
1863
|
+
let token: string;
|
|
1864
|
+
if (authHeader.startsWith('Bearer ')) {
|
|
1865
|
+
token = authHeader.substring(7);
|
|
1866
|
+
} else if (authHeader.startsWith('DPoP ')) {
|
|
1867
|
+
token = authHeader.substring(5);
|
|
1868
|
+
} else {
|
|
1869
|
+
return Response.json({ error: 'Unauthorized' }, { status: 401 });
|
|
1870
|
+
}
|
|
1871
|
+
```
|
|
1872
|
+
|
|
1873
|
+
Or use `extractToken()` from the template which handles both schemes.
|
|
1874
|
+
|
|
1875
|
+
VERIFIED (LEARNINGS-session-003, L-01).
|
|
1876
|
+
|
|
1877
|
+
**Related**: I-12, AP-43
|
|
1878
|
+
|
|
1879
|
+
---
|
|
1880
|
+
|
|
1881
|
+
## AP-46: Eager Config Loading in Next.js Server Modules
|
|
1882
|
+
|
|
1883
|
+
**What it looks like**:
|
|
1884
|
+
```typescript
|
|
1885
|
+
// ❌ WRONG: Module-level config loading
|
|
1886
|
+
const config = loadTideConfig();
|
|
1887
|
+
const JWKS = createLocalJWKSet(config.jwk);
|
|
1888
|
+
```
|
|
1889
|
+
|
|
1890
|
+
**Why it's wrong**:
|
|
1891
|
+
- Next.js 16 evaluates module-level code during `next build` for static page generation
|
|
1892
|
+
- If `tidecloak.json` doesn't exist yet or has no `jwk` field, the build crashes
|
|
1893
|
+
- This happens before the app runs — the config may not be ready at build time
|
|
1894
|
+
|
|
1895
|
+
**Consequence**: `next build --webpack` fails with "Adapter JSON missing jwk field" even though the config will exist at runtime.
|
|
1896
|
+
|
|
1897
|
+
**Correct approach**:
|
|
1898
|
+
```typescript
|
|
1899
|
+
// ✅ CORRECT: Lazy initialization on first use
|
|
1900
|
+
let _config: ReturnType<typeof loadTideConfig> | null = null;
|
|
1901
|
+
let _jwks: ReturnType<typeof createLocalJWKSet> | null = null;
|
|
1902
|
+
|
|
1903
|
+
function getConfig() {
|
|
1904
|
+
if (!_config) {
|
|
1905
|
+
_config = loadTideConfig();
|
|
1906
|
+
_jwks = createLocalJWKSet(_config.jwk);
|
|
1907
|
+
}
|
|
1908
|
+
return { config: _config, JWKS: _jwks! };
|
|
1909
|
+
}
|
|
1910
|
+
```
|
|
1911
|
+
|
|
1912
|
+
VERIFIED (LEARNINGS-session-003, L-03).
|
|
1913
|
+
|
|
1914
|
+
**Related**: None (general Next.js build behavior)
|
|
1915
|
+
|
|
1916
|
+
---
|
|
1917
|
+
|
|
1918
|
+
## AP-47: Calling Admin API Without Required Permissions
|
|
1919
|
+
|
|
1920
|
+
TideCloak admin endpoints (`/admin/realms/{realm}/users`, etc.) require the calling user to have appropriate admin permissions — client roles on `realm-management` such as `tide-realm-admin`, `view-users`, `manage-users`, etc. If a user lacks the required permissions, the API returns 403. This is authorization working as intended. Ensure the user has the correct roles assigned before calling admin endpoints.
|
|
1921
|
+
|
|
1922
|
+
VERIFIED (LEARNINGS-batch-006, L-07).
|
|
1923
|
+
|
|
1924
|
+
**Related**: AP-41 (master admin credentials in app code)
|
|
1925
|
+
|
|
1926
|
+
---
|
|
1927
|
+
|
|
1928
|
+
## AP-48: Re-encoding doEncrypt Output as Bytes
|
|
1929
|
+
|
|
1930
|
+
**What it looks like**:
|
|
1931
|
+
```typescript
|
|
1932
|
+
// ❌ WRONG: Treat doEncrypt result as bytes
|
|
1933
|
+
const encrypted = await IAMService.doEncrypt([{ data: plaintext, tags: [tag] }]);
|
|
1934
|
+
const bytes = new TextEncoder().encode(encrypted[0]);
|
|
1935
|
+
const stored = btoa(String.fromCharCode(...bytes));
|
|
1936
|
+
// Double-encoded → doDecrypt throws "RangeError: Index out of range"
|
|
1937
|
+
```
|
|
1938
|
+
|
|
1939
|
+
**Why it's wrong**:
|
|
1940
|
+
- `doEncrypt` returns **base64 strings**, not `Uint8Array`
|
|
1941
|
+
- Re-encoding the string as bytes produces double-encoded ciphertext
|
|
1942
|
+
- `doDecrypt` cannot parse the result
|
|
1943
|
+
|
|
1944
|
+
**Consequence**: Encryption appears to succeed. Decryption fails with `RangeError: Index out of range`.
|
|
1945
|
+
|
|
1946
|
+
**Correct approach**:
|
|
1947
|
+
```typescript
|
|
1948
|
+
// ✅ CORRECT: Store the string directly
|
|
1949
|
+
const encrypted = await IAMService.doEncrypt([{ data: plaintext, tags: [tag] }]);
|
|
1950
|
+
const ciphertext = String(encrypted[0]); // already base64
|
|
1951
|
+
// Store ciphertext as-is in database
|
|
1952
|
+
|
|
1953
|
+
// ✅ CORRECT: Decrypt by passing the stored string back
|
|
1954
|
+
const decrypted = await IAMService.doDecrypt([{ encrypted: ciphertext, tags: [tag] }]);
|
|
1955
|
+
```
|
|
1956
|
+
|
|
1957
|
+
**Type check**: `typeof encrypted[0] === "string"` must be true. If it's not a string, something is wrong.
|
|
1958
|
+
|
|
1959
|
+
**`doDecrypt` return type caveat**: `IAMService.doDecrypt` may return `Uint8Array[]` instead of `string[]` for policy-governed decryption. The `useTideCloak()` convenience `doDecrypt` is typed as `string[]` but the underlying `IAMService` call may differ. Use defensive decoding:
|
|
1960
|
+
```typescript
|
|
1961
|
+
const result = await IAMService.doDecrypt([{ encrypted: ciphertext, tags: [tag] }]);
|
|
1962
|
+
const plaintext = result[0] instanceof Uint8Array
|
|
1963
|
+
? new TextDecoder().decode(result[0])
|
|
1964
|
+
: String(result[0]);
|
|
1965
|
+
```
|
|
1966
|
+
VERIFIED (LEARNINGS-batch-006 L-08, LEARNINGS-batch-007 L-05).
|
|
1967
|
+
|
|
1968
|
+
**Related**: None
|
|
1969
|
+
|
|
1970
|
+
---
|
|
1971
|
+
|
|
1972
|
+
## AP-49: Replacing Existing Server Auth Instead of Layering Tide
|
|
1973
|
+
|
|
1974
|
+
**What it looks like**: Removing the existing app's authentication system (phone auth, email auth, server registration) entirely and replacing it with Tide login. All existing server APIs start returning 401 because the server expects its original credentials.
|
|
1975
|
+
|
|
1976
|
+
**Why it's wrong**: Many apps have server-side infrastructure (messaging, storage, notifications) tightly coupled to their original auth system. Removing it breaks the app's core functionality. Tide authentication gates access to the app and protects keys at rest — it doesn't replace server-side session management.
|
|
1977
|
+
|
|
1978
|
+
**Correct approach**: Layer Tide on top of the existing auth. Use a "gate" pattern:
|
|
1979
|
+
1. App start → Tide login required (gate component wraps the app)
|
|
1980
|
+
2. If Tide authenticated but no server registration → show original registration flow
|
|
1981
|
+
3. If both authenticated → full app access
|
|
1982
|
+
4. On subsequent launches → Tide auto-refreshes, server credentials persist → straight to app
|
|
1983
|
+
|
|
1984
|
+
Tide is an access gate, not a replacement for existing server auth. VERIFIED (TIDE_LEARNINGS-001 L-19).
|
|
1985
|
+
|
|
1986
|
+
**Related**: Playbook `add-auth-nextjs-existing`
|
|
1987
|
+
|
|
1988
|
+
---
|
|
1989
|
+
|
|
1990
|
+
## AP-50: Passing Uint8Array to doEncrypt/doDecrypt Instead of Array-of-Objects
|
|
1991
|
+
|
|
1992
|
+
**What it looks like**:
|
|
1993
|
+
```typescript
|
|
1994
|
+
// ❌ WRONG: Raw data, not wrapped in array-of-objects
|
|
1995
|
+
const result = await doEncrypt(myBytes);
|
|
1996
|
+
// Throws: "Pass array as parameter" or "All entries must be an object"
|
|
1997
|
+
```
|
|
1998
|
+
|
|
1999
|
+
**Why it's wrong**: `doEncrypt` and `doDecrypt` expect `Array<{data, tags}>` and `Array<{encrypted, tags}>` respectively. Raw data or plain arrays throw.
|
|
2000
|
+
|
|
2001
|
+
**Correct approach**:
|
|
2002
|
+
```typescript
|
|
2003
|
+
// ✅ CORRECT: Array of objects with data and tags
|
|
2004
|
+
const encrypted = await doEncrypt([{ data: myString, tags: ['mytag'] }]);
|
|
2005
|
+
const ciphertext = String(encrypted[0]); // base64 string
|
|
2006
|
+
|
|
2007
|
+
const decrypted = await doDecrypt([{ encrypted: ciphertext, tags: ['mytag'] }]);
|
|
2008
|
+
```
|
|
2009
|
+
|
|
2010
|
+
**String-in/string-out pattern**: Always pass data as strings (base64 for binary). String input → base64 encrypted string output. Avoids Uint8Array serialization issues during storage. For binary data:
|
|
2011
|
+
```typescript
|
|
2012
|
+
const base64 = btoa(String.fromCharCode(...myBytes));
|
|
2013
|
+
const encrypted = await doEncrypt([{ data: base64, tags: ['keys'] }]);
|
|
2014
|
+
// Store encrypted[0] as string
|
|
2015
|
+
```
|
|
2016
|
+
|
|
2017
|
+
The tag must match realm roles: tag `'keys'` requires `_tide_keys.selfencrypt` and `_tide_keys.selfdecrypt`.
|
|
2018
|
+
|
|
2019
|
+
VERIFIED (TIDE_LEARNINGS-001 L-15, L-23).
|
|
2020
|
+
|
|
2021
|
+
**Related**: AP-48
|
|
2022
|
+
|
|
2023
|
+
---
|
|
2024
|
+
|
|
2025
|
+
## AP-51: Calling IAMService.getInstance()
|
|
2026
|
+
|
|
2027
|
+
`IAMService` from `@tidecloak/js` is a pre-instantiated singleton. The import IS the instance. Do not call `IAMService.getInstance()` — it does not exist and throws `is not a function`. Use `IAMService` directly. VERIFIED (ripple-learnings L-06).
|
|
2028
|
+
|
|
2029
|
+
---
|
|
2030
|
+
|
|
2031
|
+
## AP-52: Passing Destructured Config to initIAM
|
|
2032
|
+
|
|
2033
|
+
Do not pass `{ url, realm, clientId }` to `initIAM()`. The SDK reads `config["auth-server-url"]`, `config.realm`, `config.resource`, `config.vendorId`, `config.homeOrkUrl`, and `config["client-origin-auth-" + origin]`. Pass the full adapter JSON object from `tidecloak.json`. VERIFIED (ripple-learnings L-07).
|
|
2034
|
+
|
|
2035
|
+
---
|
|
2036
|
+
|
|
2037
|
+
## AP-53: Passing JSON Object to createTideRequest
|
|
2038
|
+
|
|
2039
|
+
`createTideRequest(encoded)` takes a single `Uint8Array` — a pre-encoded `BaseTideRequest`. Do not pass `{ contract, modelName, authFlow, ... }`. Build the request with `new BaseTideRequest(name, version, authFlow, draft)`, call `request.encode()`, then pass the result. VERIFIED (ripple-learnings L-08).
|
|
2040
|
+
|
|
2041
|
+
---
|
|
2042
|
+
|
|
2043
|
+
## AP-54: Policy Params as Plain Object
|
|
2044
|
+
|
|
2045
|
+
`new Policy({ params: { key: value } })` throws "object is not iterable". The `PolicyParameters` constructor calls `new Map(data)` which needs `[key, value]` pairs. Use: `params: [['Role', 'myRole'], ['Resource', 'myResource']]`. VERIFIED (ripple-learnings L-11).
|
|
2046
|
+
|
|
2047
|
+
---
|
|
2048
|
+
|
|
2049
|
+
## AP-55: Storing Raw Signature Instead of policy.toBytes()
|
|
2050
|
+
|
|
2051
|
+
After `executeSignRequest`, the result is the VVK signature array. Do not store this directly as the "signed policy." Set `policy.signature` to the signature bytes, then call `policy.toBytes()` to get the serialized policy with signature attached. The ORK expects the full serialized Policy, not just the signature. VERIFIED (ripple-learnings L-16).
|
|
2052
|
+
|
|
2053
|
+
---
|
|
2054
|
+
|
|
2055
|
+
## AP-56: Wrong Forseti Contract Namespace
|
|
2056
|
+
|
|
2057
|
+
Do not use `using Tide.Ork.Classes.Forseti;` or `using Tide.Forseti;` — these namespaces do not exist. The correct namespace is `using Ork.Forseti.Sdk;`. The contract class must be named `Contract` and implement `IAccessPolicy`. Method signatures use context objects: `ValidateData(DataContext ctx)`, `ValidateApprovers(ApproversContext ctx)`, `ValidateExecutor(ExecutorContext ctx)`. Use `PolicyDecision.Allow()` not `PolicyDecision.Approve()`. VERIFIED (ripple-learnings L-13).
|
|
2058
|
+
|
|
2059
|
+
---
|
|
2060
|
+
|
|
2061
|
+
## AP-57: Storing request.encode() Instead of policy.toBytes() for Signing
|
|
2062
|
+
|
|
2063
|
+
Do not store the output of `PolicySignRequest.encode()` or `BaseTideRequest.encode()` as the policy bytes for later signing requests. `encode()` includes the full request envelope (authorization data, signatures, expiry) plus the policy. The ORK's `addPolicy()` expects only the serialized `Policy` object from `policy.toBytes()`.
|
|
2064
|
+
|
|
2065
|
+
**Error when wrong**: `System.ArgumentOutOfRangeException: Index out of range` at `Ork.Shared.Models.Contracts.Policy.From(ReadOnlyMemory data)` during ORK PreSign.
|
|
2066
|
+
|
|
2067
|
+
**Correct**:
|
|
2068
|
+
```typescript
|
|
2069
|
+
// Store raw policy bytes (after VVK signature is attached)
|
|
2070
|
+
policy.signature = vvkSignatureBytes;
|
|
2071
|
+
const signedPolicyBytes = policy.toBytes(); // Store THIS
|
|
2072
|
+
```
|
|
2073
|
+
|
|
2074
|
+
**Wrong**:
|
|
2075
|
+
```typescript
|
|
2076
|
+
const encoded = initializedRequest.encode(); // Includes auth envelope — DO NOT store as policy
|
|
2077
|
+
```
|
|
2078
|
+
|
|
2079
|
+
Related: AP-55 (storing raw signature instead of policy.toBytes()). AP-57 is the inverse error — storing too much instead of too little. VERIFIED (LEARNINGS-ratidefy-batch-001 L-13).
|
|
2080
|
+
|
|
2081
|
+
---
|
|
2082
|
+
|
|
2083
|
+
## AP-58: Using Static IAMService for secureFetch/getToken in React/Next.js Apps
|
|
2084
|
+
|
|
2085
|
+
Do not import `IAMService` from `@tidecloak/js` and call `IAMService.getToken()` or `IAMService.secureFetch()` when using `@tidecloak/nextjs`'s `TideCloakContextProvider`. The static `IAMService` class is not initialized by the React provider — its token and auth state are null.
|
|
2086
|
+
|
|
2087
|
+
**Error when wrong**: 403 Forbidden on all TideCloak admin API calls, or null/invalid tokens.
|
|
2088
|
+
|
|
2089
|
+
**Correct**:
|
|
2090
|
+
```typescript
|
|
2091
|
+
const { secureFetch, getToken } = useTideCloak();
|
|
2092
|
+
// Pass to utility functions that need authenticated fetch
|
|
2093
|
+
initAdmin(secureFetch, getToken);
|
|
2094
|
+
```
|
|
2095
|
+
|
|
2096
|
+
**Wrong**:
|
|
2097
|
+
```typescript
|
|
2098
|
+
import { IAMService } from '@tidecloak/js';
|
|
2099
|
+
const token = await IAMService.getToken(); // Returns null — not connected to React provider
|
|
2100
|
+
```
|
|
2101
|
+
|
|
2102
|
+
**Exception**: `(IAMService as any)._tc` works for ORK signing operations (doken, `createTideRequest`, `executeSignRequest`) because the React provider does initialize the internal `_tc` instance. But `IAMService.getToken()` and `IAMService.secureFetch()` do not route through the provider. VERIFIED (LEARNINGS-ratidefy-batch-001 L-20).
|
|
2103
|
+
|
|
2104
|
+
---
|
|
2105
|
+
|
|
2106
|
+
## AP-59: Assuming initializeTideRequest Mutates In Place
|
|
2107
|
+
|
|
2108
|
+
The `initializeTideRequest` function from the `useTideCloak()` React context returns a **new initialized object**. It does NOT mutate the original request. Calling `encode()` or `getUniqueId()` on the original object after initialization fails.
|
|
2109
|
+
|
|
2110
|
+
**Error when wrong**: `Must initialize request to generate unique id` or `encode()` returns uninitialized data.
|
|
2111
|
+
|
|
2112
|
+
**Correct**:
|
|
2113
|
+
```typescript
|
|
2114
|
+
const initialized = await initializeTideRequest(policyRequest);
|
|
2115
|
+
const bytes = initialized.encode(); // Use returned object
|
|
2116
|
+
const id = initialized.getUniqueId();
|
|
2117
|
+
```
|
|
2118
|
+
|
|
2119
|
+
**Wrong**:
|
|
2120
|
+
```typescript
|
|
2121
|
+
await initializeTideRequest(policyRequest);
|
|
2122
|
+
const bytes = policyRequest.encode(); // FAILS — original not initialized
|
|
2123
|
+
```
|
|
2124
|
+
|
|
2125
|
+
VERIFIED (LEARNINGS-ratidefy-batch-001 L-25).
|
|
2126
|
+
|
|
2127
|
+
---
|
|
2128
|
+
|
|
2129
|
+
## Status Legend
|
|
2130
|
+
|
|
2131
|
+
- **VERIFIED** - Directly sourced from documentation or keylessh exemplar
|
|
2132
|
+
- **INFERRED** - Strongly implied by source material
|
|
2133
|
+
- **REQUIRES_RUNTIME_VALIDATION** - Single-app evidence; needs confirmation
|