@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,451 @@
|
|
|
1
|
+
# Protect API Routes in Next.js (Server-Side JWT Verification)
|
|
2
|
+
|
|
3
|
+
Implement real authorization using server-side JWT verification.
|
|
4
|
+
|
|
5
|
+
**CRITICAL**: This is where actual security enforcement happens. Route guards are UI only. APIs must verify JWTs.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## When to Use
|
|
10
|
+
|
|
11
|
+
- Protecting API routes that return sensitive data
|
|
12
|
+
- Enforcing authorization before data mutations
|
|
13
|
+
- Verifying user identity server-side
|
|
14
|
+
- Enforcing role-based access control
|
|
15
|
+
|
|
16
|
+
**Always use** for any API that requires authentication.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Prerequisites
|
|
21
|
+
|
|
22
|
+
- Tide authentication installed ([add-auth-nextjs-fresh.md](add-auth-nextjs-fresh.md))
|
|
23
|
+
- Adapter JSON with `jwk` field (present when IGA is enabled on the realm)
|
|
24
|
+
- API routes to protect (App Router: `app/api/*/route.ts`, Pages Router: `pages/api/*.ts`)
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Security Model
|
|
29
|
+
|
|
30
|
+
### Tide JWT Verification
|
|
31
|
+
|
|
32
|
+
1. Extract JWT from `Authorization` header
|
|
33
|
+
2. Verify signature using **embedded JWKS from adapter JSON** (not remote endpoint)
|
|
34
|
+
3. Validate `iss` (issuer), `azp` (authorized party), `exp`, `iat`
|
|
35
|
+
4. Extract roles from `realm_access.roles` and `resource_access`
|
|
36
|
+
5. Enforce role requirements
|
|
37
|
+
|
|
38
|
+
**Required**: Tide adapter JSON includes the `jwk` field with embedded JWKS. This enables local JWT verification without network calls. The embedded JWKS is present due to Tide's non-rotating vendor-verifiable key (VVK) model, which locks the system to trust one pre-defined public key, preventing token forgery even if the IAM is compromised.
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## Files to Create
|
|
43
|
+
|
|
44
|
+
1. `lib/auth/tidecloakConfig.ts` - Load adapter JSON
|
|
45
|
+
2. `lib/auth/tideJWT.ts` - JWT verification logic
|
|
46
|
+
3. `lib/auth/protect.ts` - Reusable auth middleware (optional)
|
|
47
|
+
4. Update API routes with auth checks
|
|
48
|
+
|
|
49
|
+
See [verify-jwt-server-side.md](verify-jwt-server-side.md) for complete implementation.
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## Diagnose Existing API Routes
|
|
54
|
+
|
|
55
|
+
If you already have API routes and need to check whether they are correctly protected, run through this checklist:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
# 1. Does it read from Authorization header (not cookies)?
|
|
59
|
+
grep -r "headers.*authorization\|Authorization" app/api/ --include="*.ts"
|
|
60
|
+
# FAIL if: reads from cookies, session, or has no auth check at all
|
|
61
|
+
|
|
62
|
+
# 2. Does it verify JWT with embedded JWKS (not remote endpoint)?
|
|
63
|
+
grep -r "createLocalJWKSet\|verifyTideJWT\|jwtVerify" lib/auth/ --include="*.ts"
|
|
64
|
+
# FAIL if: uses createRemoteJWKSet (forbidden), dictionary lookup, or no verification
|
|
65
|
+
|
|
66
|
+
# 3. Does it validate issuer and audience?
|
|
67
|
+
grep -r "issuer\|audience" lib/auth/ --include="*.ts"
|
|
68
|
+
# FAIL if: no issuer/audience check in verification options
|
|
69
|
+
|
|
70
|
+
# 4. Does it enforce roles after verification?
|
|
71
|
+
grep -r "hasRole\|withRole\|realm_access" app/api/ lib/auth/ --include="*.ts"
|
|
72
|
+
# FAIL if: returns data without any role check after JWT is verified
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Any FAIL means the API route is not properly protected. Use the implementation below to fix it.
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## Quick Implementation (App Router)
|
|
80
|
+
|
|
81
|
+
This section creates minimal versions of `lib/auth/tidecloakConfig.ts`, `lib/auth/tideJWT.ts`, and `lib/auth/protect.ts`. If you also follow [verify-jwt-server-side.md](verify-jwt-server-side.md), its richer versions **replace** these files (adds DPoP verification, client-role checking, `extractToken`, and `iat` validation).
|
|
82
|
+
|
|
83
|
+
### Step 0: Install Dependencies
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
npm install jose
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
### Step 1: Load Adapter JSON
|
|
92
|
+
|
|
93
|
+
```typescript
|
|
94
|
+
// lib/auth/tidecloakConfig.ts
|
|
95
|
+
import { readFileSync } from 'fs';
|
|
96
|
+
import { join } from 'path';
|
|
97
|
+
|
|
98
|
+
export interface TidecloakConfig {
|
|
99
|
+
realm: string;
|
|
100
|
+
'auth-server-url': string;
|
|
101
|
+
resource: string;
|
|
102
|
+
jwk: { keys: any[] }; // Tide extension: embedded JWKS for local JWT verification
|
|
103
|
+
vendorId?: string;
|
|
104
|
+
homeOrkUrl?: string;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function loadTideConfig(): TidecloakConfig {
|
|
108
|
+
// Priority: env var > file
|
|
109
|
+
if (process.env.CLIENT_ADAPTER) {
|
|
110
|
+
return JSON.parse(process.env.CLIENT_ADAPTER);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const configPath = join(process.cwd(), 'data', 'tidecloak.json');
|
|
114
|
+
return JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
### Step 2: Create JWT Verification
|
|
121
|
+
|
|
122
|
+
```typescript
|
|
123
|
+
// lib/auth/tideJWT.ts
|
|
124
|
+
import { jwtVerify, createLocalJWKSet } from 'jose';
|
|
125
|
+
import type { JWTPayload } from 'jose';
|
|
126
|
+
import { loadTideConfig } from './tidecloakConfig';
|
|
127
|
+
|
|
128
|
+
// Lazy initialization — do NOT load config at module level.
|
|
129
|
+
// Next.js 16 evaluates module-level code during `next build` for static
|
|
130
|
+
// page generation. If tidecloak.json doesn't exist yet (placeholder or
|
|
131
|
+
// pre-bootstrap), eager loading throws at build time.
|
|
132
|
+
let _jwks: ReturnType<typeof createLocalJWKSet> | null = null;
|
|
133
|
+
let _config: ReturnType<typeof loadTideConfig> | null = null;
|
|
134
|
+
|
|
135
|
+
function getConfig() {
|
|
136
|
+
if (!_config) {
|
|
137
|
+
_config = loadTideConfig();
|
|
138
|
+
if (!_config.jwk) {
|
|
139
|
+
throw new Error('Adapter JSON missing jwk field');
|
|
140
|
+
}
|
|
141
|
+
_jwks = createLocalJWKSet(_config.jwk);
|
|
142
|
+
}
|
|
143
|
+
return { config: _config, JWKS: _jwks! };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export async function verifyTideJWT(token: string): Promise<JWTPayload> {
|
|
147
|
+
const { config, JWKS } = getConfig();
|
|
148
|
+
const { payload } = await jwtVerify(token, JWKS, {
|
|
149
|
+
issuer: `${config['auth-server-url'].replace(/\/+$/, '')}/realms/${config.realm}`,
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
// TideCloak access tokens use azp (authorized party) for the client ID.
|
|
153
|
+
// The aud claim typically contains "account", not the client ID.
|
|
154
|
+
if (payload.azp !== config.resource) {
|
|
155
|
+
throw new Error('Token azp does not match client');
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const now = Math.floor(Date.now() / 1000);
|
|
159
|
+
if (payload.exp && payload.exp < now) {
|
|
160
|
+
throw new Error('Token expired');
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return payload;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function hasRole(payload: JWTPayload, role: string): boolean {
|
|
167
|
+
const realmRoles = (payload.realm_access as any)?.roles || [];
|
|
168
|
+
return realmRoles.includes(role);
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
**Install dependency**:
|
|
173
|
+
```bash
|
|
174
|
+
npm install jose
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
### Step 3: Protect API Route
|
|
180
|
+
|
|
181
|
+
```typescript
|
|
182
|
+
// app/api/users/route.ts
|
|
183
|
+
import { NextRequest } from 'next/server';
|
|
184
|
+
import { verifyTideJWT, hasRole } from '@/lib/auth/tideJWT';
|
|
185
|
+
|
|
186
|
+
export async function GET(req: NextRequest) {
|
|
187
|
+
const authHeader = req.headers.get('authorization');
|
|
188
|
+
if (!authHeader) {
|
|
189
|
+
return Response.json({ error: 'Unauthorized' }, { status: 401 });
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Accept both Bearer and DPoP schemes.
|
|
193
|
+
// When useDPoP is enabled, secureFetch upgrades Bearer to DPoP.
|
|
194
|
+
let token: string;
|
|
195
|
+
if (authHeader.startsWith('Bearer ')) {
|
|
196
|
+
token = authHeader.substring(7);
|
|
197
|
+
} else if (authHeader.startsWith('DPoP ')) {
|
|
198
|
+
token = authHeader.substring(5);
|
|
199
|
+
} else {
|
|
200
|
+
return Response.json({ error: 'Unauthorized' }, { status: 401 });
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
try {
|
|
204
|
+
const jwt = await verifyTideJWT(token);
|
|
205
|
+
|
|
206
|
+
// Optional: Enforce role
|
|
207
|
+
if (!hasRole(jwt, 'admin')) {
|
|
208
|
+
return Response.json({ error: 'Forbidden' }, { status: 403 });
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Proceed with protected operation
|
|
212
|
+
return Response.json({ users: [...] });
|
|
213
|
+
} catch (err) {
|
|
214
|
+
console.error('JWT verification failed:', err);
|
|
215
|
+
return Response.json({ error: 'Invalid token' }, { status: 401 });
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
---
|
|
221
|
+
|
|
222
|
+
## Reusable Middleware Pattern
|
|
223
|
+
|
|
224
|
+
Create middleware for consistent auth:
|
|
225
|
+
|
|
226
|
+
```typescript
|
|
227
|
+
// lib/auth/protect.ts
|
|
228
|
+
import { NextRequest } from 'next/server';
|
|
229
|
+
import { verifyTideJWT, hasRole } from './tideJWT';
|
|
230
|
+
import type { JWTPayload } from 'jose';
|
|
231
|
+
|
|
232
|
+
type AuthenticatedHandler = (
|
|
233
|
+
req: NextRequest,
|
|
234
|
+
jwt: JWTPayload
|
|
235
|
+
) => Promise<Response>;
|
|
236
|
+
|
|
237
|
+
export function withAuth(handler: AuthenticatedHandler) {
|
|
238
|
+
return async (req: NextRequest) => {
|
|
239
|
+
const authHeader = req.headers.get('authorization');
|
|
240
|
+
if (!authHeader) {
|
|
241
|
+
return Response.json({ error: 'Unauthorized' }, { status: 401 });
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Accept both Bearer and DPoP schemes.
|
|
245
|
+
// When useDPoP is enabled, secureFetch upgrades Bearer to DPoP.
|
|
246
|
+
let token: string;
|
|
247
|
+
if (authHeader.startsWith('Bearer ')) {
|
|
248
|
+
token = authHeader.substring(7);
|
|
249
|
+
} else if (authHeader.startsWith('DPoP ')) {
|
|
250
|
+
token = authHeader.substring(5);
|
|
251
|
+
} else {
|
|
252
|
+
return Response.json({ error: 'Unauthorized' }, { status: 401 });
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
try {
|
|
256
|
+
const jwt = await verifyTideJWT(token);
|
|
257
|
+
return handler(req, jwt);
|
|
258
|
+
} catch (err) {
|
|
259
|
+
return Response.json({ error: 'Invalid token' }, { status: 401 });
|
|
260
|
+
}
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export function withRole(role: string, handler: AuthenticatedHandler) {
|
|
265
|
+
return withAuth(async (req, jwt) => {
|
|
266
|
+
if (!hasRole(jwt, role)) {
|
|
267
|
+
return Response.json({ error: 'Forbidden' }, { status: 403 });
|
|
268
|
+
}
|
|
269
|
+
return handler(req, jwt);
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
**Use in routes**:
|
|
275
|
+
|
|
276
|
+
```typescript
|
|
277
|
+
// app/api/admin/users/route.ts
|
|
278
|
+
import { withRole } from '@/lib/auth/protect';
|
|
279
|
+
|
|
280
|
+
export const GET = withRole('admin', async (req, jwt) => {
|
|
281
|
+
return Response.json({ users: [...] });
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
export const POST = withRole('admin', async (req, jwt) => {
|
|
285
|
+
const body = await req.json();
|
|
286
|
+
// Create user
|
|
287
|
+
return Response.json({ success: true });
|
|
288
|
+
});
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
---
|
|
292
|
+
|
|
293
|
+
## Pages Router (pages/api)
|
|
294
|
+
|
|
295
|
+
```typescript
|
|
296
|
+
// pages/api/users.ts
|
|
297
|
+
import type { NextApiRequest, NextApiResponse } from 'next';
|
|
298
|
+
import { verifyTideJWT, hasRole } from '@/lib/auth/tideJWT';
|
|
299
|
+
|
|
300
|
+
export default async function handler(
|
|
301
|
+
req: NextApiRequest,
|
|
302
|
+
res: NextApiResponse
|
|
303
|
+
) {
|
|
304
|
+
const authHeader = req.headers.authorization;
|
|
305
|
+
if (!authHeader) {
|
|
306
|
+
return res.status(401).json({ error: 'Unauthorized' });
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Accept both Bearer and DPoP schemes
|
|
310
|
+
let token: string;
|
|
311
|
+
if (authHeader.startsWith('Bearer ')) {
|
|
312
|
+
token = authHeader.substring(7);
|
|
313
|
+
} else if (authHeader.startsWith('DPoP ')) {
|
|
314
|
+
token = authHeader.substring(5);
|
|
315
|
+
} else {
|
|
316
|
+
return res.status(401).json({ error: 'Unauthorized' });
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
try {
|
|
320
|
+
const jwt = await verifyTideJWT(token);
|
|
321
|
+
|
|
322
|
+
if (!hasRole(jwt, 'admin')) {
|
|
323
|
+
return res.status(403).json({ error: 'Forbidden' });
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
res.json({ users: [...] });
|
|
327
|
+
} catch (err) {
|
|
328
|
+
res.status(401).json({ error: 'Invalid token' });
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
---
|
|
334
|
+
|
|
335
|
+
## DPoP Verification (Required When useDPoP Is Configured)
|
|
336
|
+
|
|
337
|
+
**Required for Tide's full security guarantees**: DPoP (Demonstration of Proof-of-Possession) prevents session hijacking and provides phishing-resistant authentication.
|
|
338
|
+
|
|
339
|
+
When `useDPoP` is configured on the client-side `TideCloakProvider`, the access token contains a `cnf.jkt` claim binding it to the DPoP key. The server **must** verify the DPoP proof for these tokens. A DPoP-bound token sent without a valid proof is a stolen or replayed token — reject it with 401.
|
|
340
|
+
|
|
341
|
+
See [verify-jwt-server-side.md DPoP section](verify-jwt-server-side.md#step-4-dpop-verification-required-for-full-security) for complete implementation.
|
|
342
|
+
|
|
343
|
+
---
|
|
344
|
+
|
|
345
|
+
## Verification Checklist
|
|
346
|
+
|
|
347
|
+
### API Protection
|
|
348
|
+
|
|
349
|
+
- [ ] Unauthenticated request returns 401
|
|
350
|
+
- [ ] Invalid JWT returns 401
|
|
351
|
+
- [ ] Expired JWT returns 401
|
|
352
|
+
- [ ] Valid JWT returns 200
|
|
353
|
+
- [ ] Wrong role returns 403
|
|
354
|
+
- [ ] Correct role returns 200
|
|
355
|
+
|
|
356
|
+
### Server-Side Tests
|
|
357
|
+
|
|
358
|
+
```bash
|
|
359
|
+
# Test without token
|
|
360
|
+
curl http://localhost:3000/api/users
|
|
361
|
+
# Should return 401
|
|
362
|
+
|
|
363
|
+
# Test with valid token (get from browser DevTools)
|
|
364
|
+
TOKEN="eyJ..."
|
|
365
|
+
curl -H "Authorization: Bearer $TOKEN" http://localhost:3000/api/users
|
|
366
|
+
# Should return 200 + data
|
|
367
|
+
|
|
368
|
+
# Test with malformed token
|
|
369
|
+
curl -H "Authorization: Bearer invalid" http://localhost:3000/api/users
|
|
370
|
+
# Should return 401
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
---
|
|
374
|
+
|
|
375
|
+
## Common Failures
|
|
376
|
+
|
|
377
|
+
### "Adapter JSON missing jwk field"
|
|
378
|
+
|
|
379
|
+
**Cause**: Using generic Keycloak adapter instead of Tide adapter.
|
|
380
|
+
|
|
381
|
+
**Fix**: Re-export adapter via Tide-specific endpoint using `providerId=keycloak-oidc-keycloak-json`. The `jwk` field is present when IGA is enabled on the realm. If absent, verify IGA is enabled and re-export. See [add-auth-nextjs-fresh.md Step 2](add-auth-nextjs-fresh.md#step-2-export-adapter-json-from-tidecloak).
|
|
382
|
+
|
|
383
|
+
---
|
|
384
|
+
|
|
385
|
+
### JWT Verification Always Fails
|
|
386
|
+
|
|
387
|
+
**Cause**: Wrong JWKS source or issuer mismatch.
|
|
388
|
+
|
|
389
|
+
**Fix**: See [canon/troubleshooting.md T-02](../canon/troubleshooting.md#t-02-jwt-verification-fails-401-unauthorized).
|
|
390
|
+
|
|
391
|
+
---
|
|
392
|
+
|
|
393
|
+
### Role Check Always Fails
|
|
394
|
+
|
|
395
|
+
**Fix**: See [diagnose-missing-roles-or-claims.md](diagnose-missing-roles-or-claims.md).
|
|
396
|
+
|
|
397
|
+
---
|
|
398
|
+
|
|
399
|
+
## Do Not Do This
|
|
400
|
+
|
|
401
|
+
### ❌ Do Not Use Remote JWKS Endpoint
|
|
402
|
+
|
|
403
|
+
```typescript
|
|
404
|
+
// ❌ WRONG: Fetch JWKS from remote endpoint (forbidden)
|
|
405
|
+
const JWKS = createRemoteJWKSet(
|
|
406
|
+
new URL(`${issuer}/protocol/openid-connect/certs`)
|
|
407
|
+
);
|
|
408
|
+
|
|
409
|
+
// ❌ ALSO WRONG: Remote as fallback
|
|
410
|
+
try { await jwtVerify(token, localJWKS); }
|
|
411
|
+
catch { await jwtVerify(token, createRemoteJWKSet(certsUrl)); }
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
**Why**: Tide JWTs must be verified against embedded JWKS from adapter JSON only. `createRemoteJWKSet` is forbidden. If `jwk` is missing, fix the adapter export — do not add remote fallback. See [canon/invariants.md I-04](../canon/invariants.md#i-04) and [canon/anti-patterns.md AP-01](../canon/anti-patterns.md#ap-01-treating-tide-as-generic-oidc).
|
|
415
|
+
|
|
416
|
+
---
|
|
417
|
+
|
|
418
|
+
### ❌ Do Not Skip JWT Verification
|
|
419
|
+
|
|
420
|
+
```typescript
|
|
421
|
+
// ❌ WRONG: Trust client-side auth state
|
|
422
|
+
export async function GET(req) {
|
|
423
|
+
// Assumes route guard blocked unauthenticated users
|
|
424
|
+
return Response.json({ sensitiveData: [...] });
|
|
425
|
+
}
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
**Why**: Route guards can be bypassed. See [canon/invariants.md I-03](../canon/invariants.md#i-03-server-side-jwt-verification-required).
|
|
429
|
+
|
|
430
|
+
---
|
|
431
|
+
|
|
432
|
+
## Known Uncertainties
|
|
433
|
+
|
|
434
|
+
None. All critical requirements documented above.
|
|
435
|
+
|
|
436
|
+
---
|
|
437
|
+
|
|
438
|
+
## Next Steps
|
|
439
|
+
|
|
440
|
+
1. [Add RBAC](add-rbac-nextjs.md) - Implement role hierarchies
|
|
441
|
+
2. [Verify JWT server-side](verify-jwt-server-side.md) - Complete JWT verification guide
|
|
442
|
+
3. [Diagnose missing roles](diagnose-missing-roles-or-claims.md) - Fix role issues
|
|
443
|
+
|
|
444
|
+
---
|
|
445
|
+
|
|
446
|
+
## References
|
|
447
|
+
|
|
448
|
+
- [verify-jwt-server-side.md](verify-jwt-server-side.md) - Complete implementation
|
|
449
|
+
- [canon/invariants.md I-03, I-04](../canon/invariants.md) - JWT verification rules
|
|
450
|
+
- [canon/anti-patterns.md AP-01, AP-02](../canon/anti-patterns.md) - Auth mistakes
|
|
451
|
+
- keylessh: `server/lib/auth/tideJWT.ts` (DC-01)
|