@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.
Files changed (76) hide show
  1. package/GAP_REGISTER.md +117 -0
  2. package/README.md +38 -0
  3. package/adapters/AGENTS.md +241 -0
  4. package/adapters/CLAUDE.md +146 -0
  5. package/adapters/replit.md +224 -0
  6. package/canon/anti-patterns.md +2133 -0
  7. package/canon/concepts.md +816 -0
  8. package/canon/custom-contracts.md +533 -0
  9. package/canon/delegation.md +195 -0
  10. package/canon/feature-mapping.md +637 -0
  11. package/canon/framework-matrix.md +1125 -0
  12. package/canon/invariants.md +851 -0
  13. package/canon/redirect-handler.md +254 -0
  14. package/canon/tidecloak-bootstrap.md +287 -0
  15. package/canon/tidecloak-endpoints.md +294 -0
  16. package/canon/troubleshooting.md +1494 -0
  17. package/canon/version-policy.md +89 -0
  18. package/mcp-server/dist/index.d.ts +2 -0
  19. package/mcp-server/dist/index.js +605 -0
  20. package/package.json +45 -0
  21. package/playbooks/add-auth-nextjs-existing.md +799 -0
  22. package/playbooks/add-auth-nextjs-fresh.md +654 -0
  23. package/playbooks/add-rbac-nextjs.md +190 -0
  24. package/playbooks/bootstrap-realm-from-template.md +170 -0
  25. package/playbooks/configure-e2ee-roles-and-policies.md +196 -0
  26. package/playbooks/deploy-tidecloak-docker.md +792 -0
  27. package/playbooks/diagnose-broken-login.md +234 -0
  28. package/playbooks/diagnose-missing-roles-or-claims.md +253 -0
  29. package/playbooks/initialize-admin-and-link-account.md +235 -0
  30. package/playbooks/migrate-from-existing-auth.md +198 -0
  31. package/playbooks/protect-api-nextjs.md +451 -0
  32. package/playbooks/protect-routes-nextjs.md +544 -0
  33. package/playbooks/setup-forseti-e2ee.md +756 -0
  34. package/playbooks/setup-iga-admin-panel.md +344 -0
  35. package/playbooks/setup-server-delegation.md +142 -0
  36. package/playbooks/start-tidecloak-dev.md +130 -0
  37. package/playbooks/verify-jwt-server-side.md +439 -0
  38. package/prompts/add-admin-approval-flow.md +50 -0
  39. package/prompts/build-private-customer-portal.md +85 -0
  40. package/prompts/migrate-generic-auth-to-tide.md +67 -0
  41. package/prompts/secure-existing-app.md +80 -0
  42. package/reference-apps/INDEX.md +87 -0
  43. package/reference-apps/encrypted-communication/anti-patterns.md +123 -0
  44. package/reference-apps/encrypted-communication/bootstrap-sequence.md +152 -0
  45. package/reference-apps/encrypted-communication/manifest.yaml +100 -0
  46. package/reference-apps/encrypted-communication/role-policy-matrix.md +80 -0
  47. package/reference-apps/encrypted-communication/scenario.md +134 -0
  48. package/reference-apps/git-pr-signing-service/anti-patterns.md +61 -0
  49. package/reference-apps/git-pr-signing-service/bootstrap-sequence.md +157 -0
  50. package/reference-apps/git-pr-signing-service/manifest.yaml +80 -0
  51. package/reference-apps/git-pr-signing-service/role-policy-matrix.md +99 -0
  52. package/reference-apps/git-pr-signing-service/scenario.md +169 -0
  53. package/reference-apps/iga-admin-governance/anti-patterns.md +133 -0
  54. package/reference-apps/iga-admin-governance/bootstrap-sequence.md +153 -0
  55. package/reference-apps/iga-admin-governance/manifest.yaml +87 -0
  56. package/reference-apps/iga-admin-governance/role-policy-matrix.md +67 -0
  57. package/reference-apps/iga-admin-governance/scenario.md +126 -0
  58. package/reference-apps/organisation-password-manager/anti-patterns.md +71 -0
  59. package/reference-apps/organisation-password-manager/bootstrap-sequence.md +127 -0
  60. package/reference-apps/organisation-password-manager/manifest.yaml +86 -0
  61. package/reference-apps/organisation-password-manager/role-policy-matrix.md +59 -0
  62. package/reference-apps/organisation-password-manager/scenario.md +107 -0
  63. package/reference-apps/policy-governed-signing/anti-patterns.md +67 -0
  64. package/reference-apps/policy-governed-signing/bootstrap-sequence.md +128 -0
  65. package/reference-apps/policy-governed-signing/manifest.yaml +78 -0
  66. package/reference-apps/policy-governed-signing/role-policy-matrix.md +62 -0
  67. package/reference-apps/policy-governed-signing/scenario.md +113 -0
  68. package/skills/tide-diagnostics/SKILL.md +99 -0
  69. package/skills/tide-integration/SKILL.md +136 -0
  70. package/skills/tide-learning-capture/SKILL.md +174 -0
  71. package/skills/tide-rbac-and-e2ee/SKILL.md +214 -0
  72. package/skills/tide-reviewer/SKILL.md +133 -0
  73. package/skills/tide-route-and-api-protection/SKILL.md +201 -0
  74. package/skills/tide-scenario-resolver/SKILL.md +81 -0
  75. package/skills/tide-setup/SKILL.md +218 -0
  76. package/skills/tide-solutions-architect/SKILL.md +130 -0
@@ -0,0 +1,439 @@
1
+ # Verify JWT Server-Side (Complete Implementation)
2
+
3
+ Complete guide to implementing server-side JWT verification for Tide.
4
+
5
+ ---
6
+
7
+ ## When to Use
8
+
9
+ - Implementing API authentication
10
+ - Need complete JWT verification code
11
+ - Want production-ready implementation
12
+ - Referenced by other playbooks
13
+
14
+ ---
15
+
16
+ ## Prerequisites
17
+
18
+ - Adapter JSON exported from TideCloak
19
+ - Adapter JSON has `jwk` field (required for Tide's JWT verification model)
20
+ - Node.js project (Next.js or Express)
21
+
22
+ ---
23
+
24
+ ## Complete Implementation
25
+
26
+ If you already created `lib/auth/tidecloakConfig.ts`, `lib/auth/tideJWT.ts`, and `lib/auth/protect.ts` from [protect-api-nextjs.md](protect-api-nextjs.md), the versions below **replace** them. This playbook adds DPoP verification, client-role checking in `hasRole`, `extractToken` (supports both Bearer and DPoP schemes), and `iat` future-validation.
27
+
28
+ ### Step 1: Install Dependencies
29
+
30
+ ```bash
31
+ npm install jose
32
+ ```
33
+
34
+ `jose` library: industry-standard JWT verification.
35
+
36
+ ---
37
+
38
+ ### Step 2: Adapter JSON Loader
39
+
40
+ ```typescript
41
+ // lib/auth/tidecloakConfig.ts
42
+ import { readFileSync } from 'fs';
43
+ import { join } from 'path';
44
+
45
+ export interface TidecloakConfig {
46
+ realm: string;
47
+ 'auth-server-url': string;
48
+ 'ssl-required': string;
49
+ resource: string;
50
+ 'public-client': boolean;
51
+ 'confidential-port': number;
52
+ jwk: { keys: any[] }; // Required: Embedded JWKS for local verification
53
+ vendorId?: string;
54
+ homeOrkUrl?: string;
55
+ }
56
+
57
+ export function loadTideConfig(): TidecloakConfig {
58
+ // Priority: env var > file
59
+ if (process.env.CLIENT_ADAPTER) {
60
+ return JSON.parse(process.env.CLIENT_ADAPTER);
61
+ }
62
+
63
+ const configPath = join(process.cwd(), 'data', 'tidecloak.json');
64
+ const config = JSON.parse(readFileSync(configPath, 'utf-8'));
65
+
66
+ if (!config.jwk) {
67
+ throw new Error(
68
+ 'Adapter JSON missing jwk field. ' +
69
+ 'This field is required for Tide JWT verification. ' +
70
+ 'Export via Tide-specific endpoint (providerId=keycloak-oidc-keycloak-json), not generic Keycloak adapter.'
71
+ );
72
+ }
73
+
74
+ return config;
75
+ }
76
+ ```
77
+
78
+ ---
79
+
80
+ ### Step 3: JWT Verification
81
+
82
+ ```typescript
83
+ // lib/auth/tideJWT.ts
84
+ import { jwtVerify, createLocalJWKSet } from 'jose';
85
+ import type { JWTPayload, JWTVerifyOptions } from 'jose';
86
+ import { loadTideConfig } from './tidecloakConfig';
87
+
88
+ // Lazy initialization — do NOT load config at module level.
89
+ // Next.js 16 evaluates module-level code during `next build` for static
90
+ // page generation. If tidecloak.json doesn't exist yet (placeholder or
91
+ // pre-bootstrap), eager loading throws at build time.
92
+ let _jwks: ReturnType<typeof createLocalJWKSet> | null = null;
93
+ let _config: ReturnType<typeof loadTideConfig> | null = null;
94
+
95
+ function getConfig() {
96
+ if (!_config) {
97
+ _config = loadTideConfig();
98
+ if (!_config.jwk) {
99
+ throw new Error(
100
+ 'Adapter JSON missing jwk field. ' +
101
+ 'This field is required for Tide JWT verification. ' +
102
+ 'Export via Tide-specific endpoint (providerId=keycloak-oidc-keycloak-json), not generic Keycloak adapter.'
103
+ );
104
+ }
105
+ _jwks = createLocalJWKSet(_config.jwk);
106
+ }
107
+ return { config: _config, JWKS: _jwks! };
108
+ }
109
+
110
+ export async function verifyTideJWT(token: string): Promise<JWTPayload> {
111
+ const { config, JWKS } = getConfig();
112
+ const options: JWTVerifyOptions = {
113
+ issuer: `${config['auth-server-url'].replace(/\/+$/, '')}/realms/${config.realm}`,
114
+ };
115
+
116
+ const { payload } = await jwtVerify(token, JWKS, options);
117
+
118
+ // TideCloak access tokens use azp (authorized party) for the client ID.
119
+ // The aud claim typically contains "account", not the client ID.
120
+ if (payload.azp !== config.resource) {
121
+ throw new Error('Token azp does not match client');
122
+ }
123
+
124
+ const now = Math.floor(Date.now() / 1000);
125
+
126
+ if (payload.exp && payload.exp < now) {
127
+ throw new Error('Token expired');
128
+ }
129
+
130
+ if (payload.iat && payload.iat > now + 60) {
131
+ throw new Error('Token issued in future');
132
+ }
133
+
134
+ return payload;
135
+ }
136
+
137
+ export function hasRole(payload: JWTPayload, role: string): boolean {
138
+ // Check realm roles
139
+ const realmRoles = (payload.realm_access as any)?.roles || [];
140
+ if (realmRoles.includes(role)) {
141
+ return true;
142
+ }
143
+
144
+ // Check client roles
145
+ const resourceAccess = payload.resource_access as any;
146
+ if (resourceAccess) {
147
+ for (const client of Object.values(resourceAccess)) {
148
+ const clientRoles = (client as any)?.roles || [];
149
+ if (clientRoles.includes(role)) {
150
+ return true;
151
+ }
152
+ }
153
+ }
154
+
155
+ return false;
156
+ }
157
+
158
+ export function extractToken(authHeader: string | null): string {
159
+ if (!authHeader) {
160
+ throw new Error('Missing Authorization header');
161
+ }
162
+
163
+ if (authHeader.startsWith('Bearer ')) {
164
+ return authHeader.substring(7);
165
+ }
166
+
167
+ if (authHeader.startsWith('DPoP ')) {
168
+ return authHeader.substring(5);
169
+ }
170
+
171
+ throw new Error('Invalid Authorization header format');
172
+ }
173
+ ```
174
+
175
+ ---
176
+
177
+ ### Step 4: DPoP Verification (Optional Defense-in-Depth)
178
+
179
+ **Caveat: App-side DPoP proof re-verification is incompatible with `secureFetch`.** When the client uses `IAMService.secureFetch` from `@tidecloak/js`, the DPoP proofs it sends are in a Tide-specific format, not standard RFC 9449 compact JWS. Calling `jwtVerify()` on the `dpop` header fails with `401: Invalid Compact JWS`. DPoP binding is already enforced at TideCloak during token issuance — the `cnf.jkt` claim in the JWT proves the token was DPoP-bound. **JWT signature verification via embedded JWKS (VVK) is the primary security layer.** VERIFIED (LEARNINGS-batch-005 L-06).
180
+
181
+ **If your app uses `secureFetch`**: Skip this step. The `withAuth` middleware from Step 5 should check for `cnf.jkt` presence (proving DPoP binding at issuance) but should NOT attempt to re-verify the DPoP proof itself. Remove or disable the `verifyDPoP()` call in `withAuth`.
182
+
183
+ **If your app uses manual DPoP (not `secureFetch`)**: The verification below applies. When the `TideCloakProvider` has `useDPoP={{ mode: 'strict', alg: 'ES256' }}`, TideCloak binds access tokens with a `cnf.jkt` claim.
184
+
185
+ ```typescript
186
+ // lib/auth/dpop.ts
187
+ import { jwtVerify, decodeJwt, importJWK } from 'jose';
188
+ import { createHash } from 'crypto';
189
+
190
+ const jtiCache = new Map<string, number>();
191
+ const JTI_TTL_MS = 2 * 60 * 1000; // 2 minutes
192
+
193
+ export async function verifyDPoP(
194
+ req: { method: string; url: string; headers: Headers },
195
+ accessToken: string
196
+ ): Promise<void> {
197
+ const dpopHeader = req.headers.get('dpop');
198
+ if (!dpopHeader) {
199
+ throw new Error('DPoP header missing');
200
+ }
201
+
202
+ // Parse the DPoP proof header (not the access token)
203
+ const parts = dpopHeader.split('.');
204
+ if (parts.length !== 3) throw new Error('Invalid DPoP JWT structure');
205
+
206
+ const headerJson = JSON.parse(
207
+ Buffer.from(parts[0], 'base64url').toString()
208
+ );
209
+
210
+ // Validate typ and alg from the DPoP proof header
211
+ if (headerJson.typ !== 'dpop+jwt') {
212
+ throw new Error('Invalid DPoP type');
213
+ }
214
+
215
+ const supportedAlgs = ['ES256', 'EdDSA'];
216
+ if (!supportedAlgs.includes(headerJson.alg)) {
217
+ throw new Error(`Unsupported DPoP algorithm: ${headerJson.alg}`);
218
+ }
219
+
220
+ // Must have jwk in proof header
221
+ const jwk = headerJson.jwk;
222
+ if (!jwk) throw new Error('Missing jwk in DPoP header');
223
+
224
+ // Verify signature using the embedded public key
225
+ const publicKey = await importJWK(jwk, headerJson.alg);
226
+ const { payload } = await jwtVerify(dpopHeader, publicKey);
227
+
228
+ // Verify htm (HTTP method)
229
+ if (payload.htm !== req.method) {
230
+ throw new Error('DPoP htm mismatch');
231
+ }
232
+
233
+ // Verify htu (HTTP URI, no query string)
234
+ const url = new URL(req.url);
235
+ const expectedHtu = `${url.protocol}//${url.host}${url.pathname}`;
236
+ if (payload.htu !== expectedHtu) {
237
+ throw new Error('DPoP htu mismatch');
238
+ }
239
+
240
+ // Verify iat freshness (120s window)
241
+ const now = Math.floor(Date.now() / 1000);
242
+ const iat = payload.iat as number;
243
+ if (iat < now - 120 || iat > now + 120) {
244
+ throw new Error('DPoP iat out of range');
245
+ }
246
+
247
+ // Verify jti not replayed
248
+ const jti = payload.jti as string;
249
+ if (!jti || jtiCache.has(jti)) {
250
+ throw new Error('DPoP jti missing or replayed');
251
+ }
252
+ jtiCache.set(jti, Date.now() + JTI_TTL_MS);
253
+
254
+ // Purge expired jtis periodically
255
+ if (jtiCache.size > 1000) {
256
+ const cutoff = Date.now();
257
+ jtiCache.forEach((expiry, key) => {
258
+ if (expiry < cutoff) jtiCache.delete(key);
259
+ });
260
+ }
261
+
262
+ // Verify cnf.jkt binding: access token's cnf.jkt must match
263
+ // the JWK Thumbprint (RFC 7638) of the DPoP proof's public key
264
+ const tokenPayload = decodeJwt(accessToken);
265
+ const expectedJkt = computeJwkThumbprint(jwk);
266
+
267
+ if ((tokenPayload.cnf as any)?.jkt !== expectedJkt) {
268
+ throw new Error('DPoP cnf.jkt mismatch — token not bound to this key');
269
+ }
270
+ }
271
+
272
+ /**
273
+ * Compute JWK Thumbprint per RFC 7638.
274
+ * Uses canonical JSON with alphabetically sorted required members only.
275
+ */
276
+ function computeJwkThumbprint(jwk: any): string {
277
+ let canonical: string;
278
+ if (jwk.kty === 'EC') {
279
+ canonical = `{"crv":"${jwk.crv}","kty":"${jwk.kty}","x":"${jwk.x}","y":"${jwk.y}"}`;
280
+ } else if (jwk.kty === 'OKP') {
281
+ canonical = `{"crv":"${jwk.crv}","kty":"${jwk.kty}","x":"${jwk.x}"}`;
282
+ } else if (jwk.kty === 'RSA') {
283
+ canonical = `{"e":"${jwk.e}","kty":"${jwk.kty}","n":"${jwk.n}"}`;
284
+ } else {
285
+ throw new Error(`Unsupported key type for JWK thumbprint: ${jwk.kty}`);
286
+ }
287
+ return createHash('sha256').update(canonical).digest('base64url');
288
+ }
289
+ ```
290
+
291
+ ---
292
+
293
+ ### Step 5: Middleware (Next.js App Router)
294
+
295
+ ```typescript
296
+ // lib/auth/protect.ts
297
+ import { NextRequest } from 'next/server';
298
+ import { verifyTideJWT, hasRole, extractToken } from './tideJWT';
299
+ import { verifyDPoP } from './dpop';
300
+ import type { JWTPayload } from 'jose';
301
+
302
+ type AuthHandler = (req: NextRequest, jwt: JWTPayload) => Promise<Response>;
303
+
304
+ export function withAuth(handler: AuthHandler) {
305
+ return async (req: NextRequest) => {
306
+ const authHeader = req.headers.get('authorization');
307
+
308
+ try {
309
+ const token = extractToken(authHeader);
310
+ const jwt = await verifyTideJWT(token);
311
+
312
+ // DPoP binding check: if cnf.jkt is present, TideCloak bound
313
+ // this token with DPoP at issuance. This proves the token was
314
+ // DPoP-bound — no further proof verification is needed when
315
+ // using secureFetch (its proofs are Tide-specific, not RFC 9449).
316
+ //
317
+ // If you use manual DPoP (not secureFetch), uncomment the
318
+ // verifyDPoP call below to also verify the proof.
319
+ //
320
+ // const cnfJkt = (jwt as any).cnf?.jkt;
321
+ // if (cnfJkt) {
322
+ // if (!req.headers.get('dpop')) {
323
+ // return Response.json(
324
+ // { error: 'DPoP proof required for DPoP-bound token' },
325
+ // { status: 401 }
326
+ // );
327
+ // }
328
+ // await verifyDPoP(req, token);
329
+ // }
330
+
331
+ return handler(req, jwt);
332
+ } catch (err) {
333
+ console.error('Auth failed:', err);
334
+ return Response.json(
335
+ { error: err instanceof Error ? err.message : 'Unauthorized' },
336
+ { status: 401 }
337
+ );
338
+ }
339
+ };
340
+ }
341
+
342
+ export function withRole(role: string, handler: AuthHandler) {
343
+ return withAuth(async (req, jwt) => {
344
+ if (!hasRole(jwt, role)) {
345
+ return Response.json({ error: 'Forbidden' }, { status: 403 });
346
+ }
347
+ return handler(req, jwt);
348
+ });
349
+ }
350
+ ```
351
+
352
+ ---
353
+
354
+ ### Step 6: Express Middleware
355
+
356
+ ```typescript
357
+ // server/auth.ts (Express)
358
+ import express from 'express';
359
+ import { verifyTideJWT, hasRole, extractToken } from './lib/auth/tideJWT';
360
+ import { verifyDPoP } from './lib/auth/dpop';
361
+
362
+ export async function authenticate(
363
+ req: express.Request,
364
+ res: express.Response,
365
+ next: express.NextFunction
366
+ ) {
367
+ const authHeader = req.headers.authorization;
368
+
369
+ try {
370
+ const token = extractToken(authHeader ?? null);
371
+ const jwt = await verifyTideJWT(token);
372
+
373
+ // DPoP enforcement: if token has cnf.jkt, it is DPoP-bound.
374
+ // Server must verify the DPoP proof. Without proof, reject.
375
+ const cnfJkt = (jwt as any).cnf?.jkt;
376
+ if (cnfJkt) {
377
+ if (!req.headers.dpop) {
378
+ return res.status(401).json({ error: 'DPoP proof required for DPoP-bound token' });
379
+ }
380
+ await verifyDPoP(req as any, token);
381
+ }
382
+
383
+ req.user = jwt;
384
+ next();
385
+ } catch (err) {
386
+ res.status(401).json({ error: 'Unauthorized' });
387
+ }
388
+ }
389
+
390
+ export function requireRole(role: string) {
391
+ return (req: express.Request, res: express.Response, next: express.NextFunction) => {
392
+ if (!req.user || !hasRole(req.user, role)) {
393
+ return res.status(403).json({ error: 'Forbidden' });
394
+ }
395
+ next();
396
+ };
397
+ }
398
+
399
+ // Usage
400
+ app.get('/api/admin/users', authenticate, requireRole('admin'), (req, res) => {
401
+ res.json({ users: [...] });
402
+ });
403
+ ```
404
+
405
+ ---
406
+
407
+ ## Verification Checklist
408
+
409
+ - [ ] Valid JWT verified successfully
410
+ - [ ] Expired JWT rejected
411
+ - [ ] Malformed JWT rejected
412
+ - [ ] Wrong issuer rejected
413
+ - [ ] Wrong audience rejected
414
+ - [ ] Role check works
415
+ - [ ] DPoP-bound token (has `cnf.jkt`) without DPoP proof header is rejected (401)
416
+ - [ ] DPoP-bound token with valid DPoP proof is accepted
417
+ - [ ] DPoP proof with wrong `htm` (method) is rejected
418
+ - [ ] DPoP proof with wrong `htu` (URL) is rejected
419
+ - [ ] Replayed DPoP `jti` is rejected
420
+
421
+ ---
422
+
423
+ ## Common Failures
424
+
425
+ See [canon/troubleshooting.md T-02](../canon/troubleshooting.md#t-02-jwt-verification-fails-401-unauthorized).
426
+
427
+ ---
428
+
429
+ ## Known Uncertainties
430
+
431
+ None. All critical requirements documented above.
432
+
433
+ ---
434
+
435
+ ## References
436
+
437
+ - [protect-api-nextjs.md](protect-api-nextjs.md) - API protection guide
438
+ - [canon/invariants.md I-03, I-04](../canon/invariants.md) - JWT verification rules
439
+ - keylessh: `server/lib/auth/tideJWT.ts`, `server/auth.ts` (DC-01, DC-02)
@@ -0,0 +1,50 @@
1
+ # Prompt: Add an Admin-Only or Approval-Driven Workflow
2
+
3
+ Copy-paste this prompt to an AI coding agent to add role-gated admin functionality to an existing Tide-enabled app.
4
+
5
+ ---
6
+
7
+ ## The Prompt
8
+
9
+ > Add an admin-only workflow to my existing Tide-enabled Next.js app. Only users with the `admin` role should be able to access admin pages and admin API routes.
10
+ >
11
+ > **Assumptions**:
12
+ > - The app already has Tide auth working (login/logout functional).
13
+ > - TideCloak is running. The `admin` role exists in the realm and is assigned to at least one user.
14
+ > - I may or may not have server-side JWT verification in place yet.
15
+ >
16
+ > **Inspect first**:
17
+ > 1. Confirm Tide setup: `@tidecloak/nextjs` in `package.json`, `TideCloakProvider` in layout, `data/tidecloak.json` with `jwk` field.
18
+ > 2. Check if `lib/auth/tideJWT.ts` and `lib/auth/protect.ts` exist.
19
+ > 3. Check if existing admin API routes already verify JWT: `grep -r "verifyTideJWT\|withAuth\|withRole" app/api/admin/ lib/auth/`
20
+ > 4. Check if existing admin pages use only `hasRealmRole()` / `hasClientRole()` for protection (UI gating only — not real authorization).
21
+ >
22
+ > **If Tide setup checks fail**, stop. Use the `tide-setup` skill first.
23
+ >
24
+ > **If `lib/auth/tideJWT.ts` does not exist**, use the `tide-route-and-api-protection` skill first (playbooks `protect-api-nextjs` then `verify-jwt-server-side`).
25
+ >
26
+ > **Once server-side auth is confirmed**, add admin RBAC:
27
+ > 1. **Admin API routes**: Wrap with `withRole('admin', handler)` from `lib/auth/protect.ts`. Follow playbook `add-rbac-nextjs`.
28
+ > 2. **Admin pages**: Add client-side `hasRole('admin')` checks for UI gating (show/hide admin content). This is UX only — the API enforces the real check.
29
+ > 3. **Verify**: `curl` admin API with a non-admin token returns 403. Admin token returns 200. Bypassing the UI (direct curl) still enforces the role check.
30
+ >
31
+ > **Do not**:
32
+ > - Treat `hasRole('admin')` on the client as sufficient protection. It is UI gating only.
33
+ > - Create admin bypass paths or emergency override routes.
34
+ > - Use Next.js `proxy.ts` (or legacy `middleware.ts`) for role enforcement. It runs at the edge and cannot verify JWT signatures.
35
+ > - Implement a custom approval queue unless you have confirmed the TideCloak IGA workflow is appropriate. IGA uses cryptographic quorum enforcement — it is not a simple approval table.
36
+ >
37
+ > **About IGA (multi-admin approval)**:
38
+ > If IGA is enabled on the realm, admin changes (user creation, role assignment) already require cryptographic quorum approval inside TideCloak. Do not build a custom approval system that duplicates or bypasses this. Deep governance configuration (quorum tuning, custom Forseti contracts) is only partially documented — see playbook `setup-iga-admin-panel` for what is currently available.
39
+
40
+ ---
41
+
42
+ ## Acceptance Criteria
43
+
44
+ - [ ] Admin API returns 403 for non-admin user
45
+ - [ ] Admin API returns 200 for admin user
46
+ - [ ] Admin pages show "Access Denied" for non-admin (UI gating)
47
+ - [ ] Direct curl to admin API without token returns 401
48
+ - [ ] Direct curl with non-admin token returns 403
49
+ - [ ] No admin bypass paths exist in the codebase
50
+ - [ ] Post-auth redirect handler exists at configured `redirectUri` path
@@ -0,0 +1,85 @@
1
+ # Prompt: Build a Private Customer Portal with Tide
2
+
3
+ Copy-paste this prompt to an AI coding agent to scaffold a customer portal with Tide authentication and protected APIs.
4
+
5
+ ---
6
+
7
+ ## The Prompt
8
+
9
+ > Build a private customer portal using Next.js and Tide authentication.
10
+ >
11
+ > **Route first, then act.** Use the development team model. Do not bundle all work into one pass.
12
+ >
13
+ > **Team sequence for this task**:
14
+ > 1. **Scenario Resolver** — confirm this is a fresh app with standard auth (not shared encryption, not retrofit)
15
+ > 2. **Setup / Platform Engineer** — bootstrap TideCloak if not running
16
+ > 3. **Application Engineer** — wire SDK, provider, config, redirect handler, CSP, DPoP auth page
17
+ > 4. **Security Engineer** — route guards + API protection + JWT/DPoP verification
18
+ > 5. **IAM / Policy Engineer** — if roles or encryption needed
19
+ > 6. **Reviewer / QA Engineer** — compliance check before handoff
20
+ >
21
+ > **Scenario check**: Check `reference-apps/INDEX.md` for a matching scenario pattern. If one matches, use its manifest for roles, policies, and playbook sequence.
22
+ >
23
+ > **Assumptions**:
24
+ > - This is a fresh Next.js project (App Router).
25
+ > - I want login, protected pages, and protected API routes.
26
+ >
27
+ > **Default dev flow** (for a new project):
28
+ > ```
29
+ > npm install
30
+ > npm run init # starts TideCloak, creates realm, exports tidecloak.json
31
+ > npm run dev
32
+ > ```
33
+ > The init script handles TideCloak bootstrap automatically. Docker, curl, and jq are required. You must open the invite link printed by the script to link your admin Tide account.
34
+ >
35
+ > **Inspect first** (if not starting fresh):
36
+ > 1. Is TideCloak running? `curl -sf http://localhost:8080 > /dev/null && echo "Running" || echo "Not running"`
37
+ > 2. Check if `@tidecloak/nextjs` is in `package.json`.
38
+ > 3. Check if `TideCloakProvider` (Next.js) or `TideCloakContextProvider` (React) is in `app/layout.tsx` or `app/providers.tsx`.
39
+ > 4. Check if `data/tidecloak.json` exists with `jwk`, `vendorId`, and `homeOrkUrl` fields.
40
+ >
41
+ > **If TideCloak is not running and no init script exists**, bootstrap manually:
42
+ > 1. Playbook `start-tidecloak-dev` — start the Docker container
43
+ > 2. Playbook `bootstrap-realm-from-template` — create realm, enable licensing + IGA
44
+ > 3. Playbook `initialize-admin-and-link-account` — create admin, link Tide account, export adapter JSON
45
+ >
46
+ > All three playbooks are sequential and mandatory. Bootstrap is NOT complete until a user with `tideUserKey` exists and `data/tidecloak.json` is exported.
47
+ >
48
+ > **If TideCloak is running but checks 2-4 fail**, follow the `tide-setup` skill to complete setup. Use playbook `add-auth-nextjs-fresh`.
49
+ >
50
+ > **Package versions** (before writing package.json):
51
+ > 1. Run `npm view @tidecloak/nextjs version` to get the current stable version. Pin it exactly (e.g., `"0.13.26"`). Do not use `"latest"` or `"^"` for `@tidecloak/*` packages.
52
+ > 2. If the resolved version is 0.99.x, skip it. Use the highest non-0.99.x version instead. If unsure, fall back to the versions in `canon/version-policy.md`.
53
+ > 3. Run `npm view next version` and `npm view react version` to get current stable framework versions. Use caret ranges (e.g., `"^15.0.0"`). Align React and React DOM to the same major.
54
+ > 4. Do not use `--force` or `--legacy-peer-deps` as the default install strategy. If peer dependency conflicts arise, resolve them by aligning versions.
55
+ > 5. After writing package.json, include a short version-selection summary in your output: which versions you chose and why.
56
+ >
57
+ > **Build sequence** (follow playbooks in order):
58
+ > 1. **Setup**: Playbook `add-auth-nextjs-fresh`. Includes SDK install, provider wiring with DPoP, adapter JSON, silent SSO file, post-auth redirect handler, and CSP.
59
+ > 2. **Login**: Use `useTideCloak()` hook. Verify login redirects to TideCloak and back.
60
+ > 3. **Protected pages**: Playbook `protect-routes-nextjs`. These are UI gating only — not real authorization.
61
+ > 4. **Protected APIs**: Playbooks `protect-api-nextjs` then `verify-jwt-server-side`. This is where real authorization happens. Every API route returning customer data must verify JWT server-side.
62
+ > 5. **Verify**: Unauthenticated `curl` to API returns 401. Valid token returns 200. Direct API call bypassing UI still enforces auth.
63
+ >
64
+ > **Do not**: Create fake auth helpers, plain-text role cookies, or ad hoc login logic. Do not treat `hasRole()` or route guards as real API protection. See `adapters/AGENTS.md` Forbidden Shortcuts for the full list.
65
+ >
66
+ > **If I later need roles** (admin vs customer), follow playbook `add-rbac-nextjs`. Only after API protection is verified.
67
+ >
68
+ > **If I later need to encrypt customer data**, default to self-encryption (user-bound — only the encrypting user can decrypt). Run playbook `configure-e2ee-roles-and-policies` to set up `_tide_{tag}.selfencrypt`/`.selfdecrypt` roles, then follow playbook `add-rbac-nextjs` (E2EE section). Do NOT rename roles to `encrypt`/`decrypt` — the suffix does not change the model.
69
+ >
70
+ > **If I explicitly need multiple users to decrypt the same data**, that is a different encryption model (policy-governed VVK). Route to `setup-forseti-e2ee`. It requires a Forseti contract, policy signing, and `IAMService.doEncrypt(data, policyBytes)`. Do not treat this as a role rename from self-encryption.
71
+
72
+ ---
73
+
74
+ ## Acceptance Criteria
75
+
76
+ - [ ] TideCloak running with realm configured and adapter JSON exported
77
+ - [ ] Login redirects to TideCloak and returns authenticated
78
+ - [ ] Protected pages redirect unauthenticated users to login
79
+ - [ ] API routes return 401 without valid JWT
80
+ - [ ] API routes return 200 with valid JWT
81
+ - [ ] Direct API call (bypassing UI) still returns 401/403
82
+ - [ ] Browser console shows no CSP violations or 404s
83
+ - [ ] DPoP is enabled on the provider
84
+ - [ ] `silent-check-sso.html` exists in `public/`
85
+ - [ ] Post-auth redirect handler exists at configured `redirectUri` path (no 404 after login)
@@ -0,0 +1,67 @@
1
+ # Prompt: Migrate from Generic Auth to Tide (Constrained — Inspection and Planning Only)
2
+
3
+ Copy-paste this prompt to an AI coding agent to assess an existing auth setup and plan a migration to Tide. This prompt is intentionally constrained because a universal migration recipe does not yet exist.
4
+
5
+ ---
6
+
7
+ ## Important Limitation
8
+
9
+ There is no fully documented Keycloak-to-TideCloak migration path (GAP-023, STILL_UNRESOLVED). All Tide exemplars are greenfield. This prompt guides inspection, planning, and partial implementation using the available setup and protection playbooks. It does not provide an automated end-to-end migration.
10
+
11
+ ---
12
+
13
+ ## The Prompt
14
+
15
+ > I have an existing Next.js app with authentication. I want to migrate to Tide. Help me assess the current setup and plan the migration.
16
+ >
17
+ > **This is a planning and incremental migration prompt, not a one-shot rewrite.** A universal Tide migration recipe is not yet available. Work with what the Tide playbooks support today.
18
+ >
19
+ > **Inspect first** (before any code changes):
20
+ > 1. What auth is currently in use? `grep -r "NextAuth\|next-auth\|passport\|jsonwebtoken\|jose\|auth0\|clerk\|supabase" package.json lib/ app/ pages/ --include="*.ts" --include="*.tsx" 2>/dev/null | head -20`
21
+ > 2. Where is the auth provider wired? `grep -r "SessionProvider\|AuthProvider\|ClerkProvider" app/layout.tsx pages/_app.tsx 2>/dev/null`
22
+ > 3. How are API routes protected today? `grep -r "getServerSession\|getToken\|requireAuth\|authenticate\|verifyToken" app/api/ pages/api/ lib/ --include="*.ts" 2>/dev/null | head -20`
23
+ > 4. How are routes guarded? `grep -r "useSession\|isAuthenticated\|PrivateRoute\|AuthGuard" app/ pages/ components/ --include="*.tsx" 2>/dev/null | head -10`
24
+ > 5. Is there existing role/permission logic? `grep -r "role\|permission\|isAdmin\|hasRole" app/ lib/ --include="*.ts" --include="*.tsx" 2>/dev/null | head -10`
25
+ > 6. Does any proxy/middleware handle auth? `cat proxy.ts 2>/dev/null || cat middleware.ts 2>/dev/null`
26
+ >
27
+ > **Produce an assessment** (do not start coding yet):
28
+ > - List the current auth provider and its integration points (provider wrapper, API middleware, route guards, session storage).
29
+ > - Identify which API routes are currently protected and how.
30
+ > - Identify any role or permission logic.
31
+ > - Identify what can be replaced incrementally vs what requires simultaneous changes.
32
+ >
33
+ > **Package versions** (when installing Tide packages):
34
+ > - Run `npm view @tidecloak/nextjs version` before adding to package.json. Pin the exact version. Do not use `"latest"`.
35
+ > - If the resolved version is 0.99.x, skip it. Fall back to `canon/version-policy.md`.
36
+ > - Do not use `--force` or `--legacy-peer-deps`. Align versions to resolve peer conflicts.
37
+ >
38
+ > **Incremental migration order** (using existing Tide playbooks):
39
+ > 1. Add Tide provider alongside existing auth — playbook `add-auth-nextjs-existing`. Both systems coexist.
40
+ > 2. Verify Tide login works independently. Do not proceed until confirmed.
41
+ > 3. Migrate one API route to Tide JWT verification as a pilot — playbooks `protect-api-nextjs` then `verify-jwt-server-side`.
42
+ > 4. Migrate remaining API routes.
43
+ > 5. Replace route guards with Tide `useTideCloak()` guards — playbook `protect-routes-nextjs`. These are UI-only in both old and new systems.
44
+ > 6. Add RBAC on Tide claims if needed — playbook `add-rbac-nextjs`.
45
+ > 7. Remove old auth provider and its dependencies. Verify everything works with Tide only.
46
+ >
47
+ > **What cannot be migrated automatically**:
48
+ > - **Realm migration** — no Keycloak-to-TideCloak procedure documented. You may need to recreate the realm. (GAP-023)
49
+ > - **Users** — must be created in TideCloak and linked via Tide account linking. No bulk migration tool.
50
+ > - **Sessions** — cannot be transferred. Users re-login after the switch.
51
+ > - **Passwords** — Tide uses threshold PRISM. Existing password hashes are not transferable.
52
+ >
53
+ > **Do not**: Claim a universal migration recipe exists. Do not silently remove old auth before confirming Tide login works. Do not create shim layers that fake Tide behavior using old tokens. See `adapters/AGENTS.md` Forbidden Shortcuts for additional prohibitions.
54
+ >
55
+ > **If the app is not yet connected to TideCloak at all**, start with the `tide-setup` skill.
56
+
57
+ ---
58
+
59
+ ## Acceptance Criteria
60
+
61
+ - [ ] Assessment document produced listing current auth integration points
62
+ - [ ] Tide provider added and login verified before any old auth is removed
63
+ - [ ] Each migrated API route verified: 401 without Tide JWT, 200 with valid Tide JWT
64
+ - [ ] Old auth removed only after all routes and APIs confirmed working with Tide
65
+ - [ ] No shim layers, fake auth helpers, or plain-text role cookies in the final codebase
66
+ - [ ] DPoP enabled from the start
67
+ - [ ] Post-auth redirect handler exists at configured `redirectUri` path