@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,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
|