@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,544 @@
|
|
|
1
|
+
# Protect Routes in Next.js (Client-Side UI Gating)
|
|
2
|
+
|
|
3
|
+
Implement client-side route guards that redirect unauthenticated users to login.
|
|
4
|
+
|
|
5
|
+
**CRITICAL**: This is UI gating only, NOT real authorization. Protected routes can be bypassed. For API protection, see [protect-api-nextjs.md](protect-api-nextjs.md).
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## When to Use
|
|
10
|
+
|
|
11
|
+
- Want to hide pages from unauthenticated users (UX improvement)
|
|
12
|
+
- Want to redirect to login if user not authenticated
|
|
13
|
+
- Want to show different UI based on authentication status
|
|
14
|
+
|
|
15
|
+
**Do not use** for:
|
|
16
|
+
- Protecting sensitive APIs (use server-side JWT verification)
|
|
17
|
+
- Preventing unauthorized access to data (use server-side checks)
|
|
18
|
+
- Security enforcement (route guards are UI convenience only)
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Prerequisites
|
|
23
|
+
|
|
24
|
+
- Tide authentication installed ([add-auth-nextjs-fresh.md](add-auth-nextjs-fresh.md))
|
|
25
|
+
- `TideCloakProvider` wrapping app
|
|
26
|
+
- Login flow working
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## Security Warning
|
|
31
|
+
|
|
32
|
+
**Route protection is NOT authorization.**
|
|
33
|
+
|
|
34
|
+
Client-side route guards can be bypassed:
|
|
35
|
+
- User disables JavaScript
|
|
36
|
+
- User modifies client code
|
|
37
|
+
- User directly calls APIs bypassing UI
|
|
38
|
+
|
|
39
|
+
**Real authorization** happens server-side with JWT verification.
|
|
40
|
+
|
|
41
|
+
See [canon/feature-mapping.md Protected Routes vs Protected APIs](../canon/feature-mapping.md#protected-routes-vs-protected-apis) for distinction.
|
|
42
|
+
|
|
43
|
+
See [canon/invariants.md I-08](../canon/invariants.md#i-08-ui-gating-is-not-authorization) for security rule.
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## Files to Inspect First
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
# Check router type
|
|
51
|
+
ls -la app/ 2>/dev/null && echo "App Router" || echo "Pages Router"
|
|
52
|
+
|
|
53
|
+
# List pages/routes to protect
|
|
54
|
+
find app/ pages/ -name "page.tsx" -o -name "*.tsx" 2>/dev/null | grep -v "_app\|_document\|layout"
|
|
55
|
+
|
|
56
|
+
# Check Next.js version to determine correct request interception filename
|
|
57
|
+
NEXT_MAJOR=$(node -e "try{console.log(require('next/package.json').version.split('.')[0])}catch{console.log('unknown')}" 2>/dev/null)
|
|
58
|
+
echo "Next.js major: $NEXT_MAJOR"
|
|
59
|
+
# 16+ → proxy.ts | 15 or earlier → middleware.ts
|
|
60
|
+
cat proxy.ts 2>/dev/null || cat middleware.ts 2>/dev/null
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## Implementation Patterns
|
|
66
|
+
|
|
67
|
+
### App Router: Layout-Based Guards
|
|
68
|
+
|
|
69
|
+
**When**: Protect entire section (e.g., `/dashboard/*`, `/admin/*`)
|
|
70
|
+
|
|
71
|
+
Create protected layout:
|
|
72
|
+
|
|
73
|
+
```typescript
|
|
74
|
+
// app/dashboard/layout.tsx
|
|
75
|
+
'use client';
|
|
76
|
+
|
|
77
|
+
import { useTideCloak } from '@tidecloak/nextjs';
|
|
78
|
+
import { useEffect } from 'react';
|
|
79
|
+
|
|
80
|
+
export default function DashboardLayout({
|
|
81
|
+
children
|
|
82
|
+
}: {
|
|
83
|
+
children: React.ReactNode
|
|
84
|
+
}) {
|
|
85
|
+
const { authenticated, isInitializing, login } = useTideCloak();
|
|
86
|
+
|
|
87
|
+
useEffect(() => {
|
|
88
|
+
if (!isInitializing && !authenticated) {
|
|
89
|
+
login();
|
|
90
|
+
}
|
|
91
|
+
}, [isInitializing, authenticated, login]);
|
|
92
|
+
|
|
93
|
+
// Block render until SDK is ready and user is authenticated
|
|
94
|
+
if (isInitializing || !authenticated) {
|
|
95
|
+
return <div>Checking authentication...</div>;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return <>{children}</>;
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
**Why `isInitializing`**: The SDK is not ready immediately. Calling `login()` before initialization completes throws `"TideCloak client not initialized"`. Always gate on `isInitializing` before calling `login()`. See [canon/anti-patterns.md AP-28](../canon/anti-patterns.md#ap-28-calling-login-before-sdk-initialization).
|
|
103
|
+
|
|
104
|
+
All pages under `app/dashboard/` are now gated.
|
|
105
|
+
|
|
106
|
+
**Pros**: One file protects entire section
|
|
107
|
+
**Cons**: Shared layout means all pages share UI chrome
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
### App Router: Page-Level Guards
|
|
112
|
+
|
|
113
|
+
**When**: Protect individual pages, not entire section
|
|
114
|
+
|
|
115
|
+
```typescript
|
|
116
|
+
// app/profile/page.tsx
|
|
117
|
+
'use client';
|
|
118
|
+
|
|
119
|
+
import { useTideCloak } from '@tidecloak/nextjs';
|
|
120
|
+
import { useEffect } from 'react';
|
|
121
|
+
|
|
122
|
+
export default function ProfilePage() {
|
|
123
|
+
const { authenticated, isInitializing, getValueFromIdToken, login } = useTideCloak();
|
|
124
|
+
|
|
125
|
+
useEffect(() => {
|
|
126
|
+
if (!isInitializing && !authenticated) {
|
|
127
|
+
login();
|
|
128
|
+
}
|
|
129
|
+
}, [isInitializing, authenticated, login]);
|
|
130
|
+
|
|
131
|
+
if (isInitializing || !authenticated) {
|
|
132
|
+
return <div>Redirecting to login...</div>;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return (
|
|
136
|
+
<div>
|
|
137
|
+
<h1>Profile: {getValueFromIdToken("preferred_username")}</h1>
|
|
138
|
+
{/* page content */}
|
|
139
|
+
</div>
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
**Pros**: Fine-grained control per page
|
|
145
|
+
**Cons**: Repeat guard logic in every protected page
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
149
|
+
### App Router: Reusable Guard Component
|
|
150
|
+
|
|
151
|
+
**When**: Many pages need same protection logic
|
|
152
|
+
|
|
153
|
+
Create guard component:
|
|
154
|
+
|
|
155
|
+
```typescript
|
|
156
|
+
// components/AuthGuard.tsx
|
|
157
|
+
'use client';
|
|
158
|
+
|
|
159
|
+
import { useTideCloak } from '@tidecloak/nextjs';
|
|
160
|
+
import { useEffect } from 'react';
|
|
161
|
+
import type { ReactNode } from 'react';
|
|
162
|
+
|
|
163
|
+
export function AuthGuard({ children }: { children: ReactNode }) {
|
|
164
|
+
const { authenticated, isInitializing, login } = useTideCloak();
|
|
165
|
+
|
|
166
|
+
useEffect(() => {
|
|
167
|
+
if (!isInitializing && !authenticated) {
|
|
168
|
+
login();
|
|
169
|
+
}
|
|
170
|
+
}, [isInitializing, authenticated, login]);
|
|
171
|
+
|
|
172
|
+
if (isInitializing || !authenticated) {
|
|
173
|
+
return <div>Redirecting to login...</div>;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return <>{children}</>;
|
|
177
|
+
}
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Use in pages:
|
|
181
|
+
|
|
182
|
+
```typescript
|
|
183
|
+
// app/profile/page.tsx
|
|
184
|
+
import { AuthGuard } from '@/components/AuthGuard';
|
|
185
|
+
|
|
186
|
+
export default function ProfilePage() {
|
|
187
|
+
return (
|
|
188
|
+
<AuthGuard>
|
|
189
|
+
<h1>Profile</h1>
|
|
190
|
+
{/* page content */}
|
|
191
|
+
</AuthGuard>
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
**Pros**: Reusable, consistent behavior
|
|
197
|
+
**Cons**: Extra component wrapper
|
|
198
|
+
|
|
199
|
+
---
|
|
200
|
+
|
|
201
|
+
### Pages Router: getServerSideProps Redirect
|
|
202
|
+
|
|
203
|
+
**When**: Pages Router, want server-side redirect before render
|
|
204
|
+
|
|
205
|
+
```typescript
|
|
206
|
+
// pages/dashboard.tsx
|
|
207
|
+
import { GetServerSidePropsContext } from 'next';
|
|
208
|
+
|
|
209
|
+
export async function getServerSideProps(context: GetServerSidePropsContext) {
|
|
210
|
+
// Note: This checks cookies, not Tide JWT validity
|
|
211
|
+
// For real auth, verify JWT server-side
|
|
212
|
+
const hasCookie = context.req.cookies['some-auth-cookie'];
|
|
213
|
+
|
|
214
|
+
if (!hasCookie) {
|
|
215
|
+
return {
|
|
216
|
+
redirect: {
|
|
217
|
+
destination: '/login',
|
|
218
|
+
permanent: false
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return {
|
|
224
|
+
props: {}
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export default function DashboardPage() {
|
|
229
|
+
return <div>Dashboard</div>;
|
|
230
|
+
}
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
**Warning**: This pattern checks cookie presence, not JWT validity. Not real authorization. Server-side JWT verification required for API protection.
|
|
234
|
+
|
|
235
|
+
---
|
|
236
|
+
|
|
237
|
+
### Pages Router: Client-Side Guard
|
|
238
|
+
|
|
239
|
+
```typescript
|
|
240
|
+
// pages/profile.tsx
|
|
241
|
+
import { useTideCloak } from '@tidecloak/nextjs';
|
|
242
|
+
import { useEffect } from 'react';
|
|
243
|
+
|
|
244
|
+
export default function ProfilePage() {
|
|
245
|
+
const { authenticated, isInitializing, getValueFromIdToken, login } = useTideCloak();
|
|
246
|
+
|
|
247
|
+
useEffect(() => {
|
|
248
|
+
if (!isInitializing && !authenticated) {
|
|
249
|
+
login();
|
|
250
|
+
}
|
|
251
|
+
}, [isInitializing, authenticated, login]);
|
|
252
|
+
|
|
253
|
+
if (isInitializing || !authenticated) {
|
|
254
|
+
return <div>Redirecting...</div>;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return <div>Profile: {getValueFromIdToken("preferred_username")}</div>;
|
|
258
|
+
}
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
---
|
|
262
|
+
|
|
263
|
+
## Role-Based Route Guards (UI Only)
|
|
264
|
+
|
|
265
|
+
Show/hide UI elements based on roles.
|
|
266
|
+
|
|
267
|
+
**CRITICAL**: This is UI gating only. API must verify roles server-side.
|
|
268
|
+
|
|
269
|
+
```typescript
|
|
270
|
+
// app/admin/page.tsx
|
|
271
|
+
'use client';
|
|
272
|
+
|
|
273
|
+
import { useTideCloak } from '@tidecloak/nextjs';
|
|
274
|
+
import { useEffect } from 'react';
|
|
275
|
+
|
|
276
|
+
export default function AdminPage() {
|
|
277
|
+
const { authenticated, isInitializing, hasRealmRole, login } = useTideCloak();
|
|
278
|
+
|
|
279
|
+
useEffect(() => {
|
|
280
|
+
if (!isInitializing && !authenticated) {
|
|
281
|
+
login();
|
|
282
|
+
}
|
|
283
|
+
}, [isInitializing, authenticated, login]);
|
|
284
|
+
|
|
285
|
+
if (isInitializing || !authenticated) {
|
|
286
|
+
return <div>Checking authentication...</div>;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// UI gating: hide admin panel if user lacks role
|
|
290
|
+
if (!hasRealmRole('admin')) {
|
|
291
|
+
return (
|
|
292
|
+
<div>
|
|
293
|
+
<h1>Access Denied</h1>
|
|
294
|
+
<p>You do not have admin privileges.</p>
|
|
295
|
+
</div>
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return (
|
|
300
|
+
<div>
|
|
301
|
+
<h1>Admin Panel</h1>
|
|
302
|
+
{/* admin content */}
|
|
303
|
+
</div>
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
**Attacker can bypass**: Modify client code to skip `hasRealmRole()` check. Real protection happens in API routes.
|
|
309
|
+
|
|
310
|
+
---
|
|
311
|
+
|
|
312
|
+
## Verification Checklist
|
|
313
|
+
|
|
314
|
+
### Route Protection
|
|
315
|
+
|
|
316
|
+
- [ ] Unauthenticated user redirected to Tide login
|
|
317
|
+
- [ ] After login, user redirected back to original URL
|
|
318
|
+
- [ ] Protected pages not accessible without auth
|
|
319
|
+
- [ ] Public pages (login, home) still accessible
|
|
320
|
+
|
|
321
|
+
### Role-Based UI
|
|
322
|
+
|
|
323
|
+
- [ ] Admin-only UI elements hidden for non-admin users
|
|
324
|
+
- [ ] User sees appropriate UI for their role
|
|
325
|
+
- [ ] No errors when user lacks expected role
|
|
326
|
+
|
|
327
|
+
### UX Quality
|
|
328
|
+
|
|
329
|
+
- [ ] No flash of protected content before redirect (loading state shown)
|
|
330
|
+
- [ ] Return URL preserved after login (user sent to page they requested)
|
|
331
|
+
- [ ] Consistent behavior across all protected routes
|
|
332
|
+
|
|
333
|
+
### Security (Negative Test — requires [protect-api-nextjs.md](protect-api-nextjs.md) completed first)
|
|
334
|
+
|
|
335
|
+
- [ ] ✅ API calls from browser DevTools return 401 without valid JWT (even if route guard bypassed)
|
|
336
|
+
- [ ] ✅ Disabling JavaScript and accessing protected route still protected by API-level auth
|
|
337
|
+
- [ ] ✅ Modifying client code to skip route guard does NOT bypass API protection
|
|
338
|
+
|
|
339
|
+
**If any security checks fail**, API protection is missing or broken. Complete [protect-api-nextjs.md](protect-api-nextjs.md) first.
|
|
340
|
+
|
|
341
|
+
---
|
|
342
|
+
|
|
343
|
+
## Common Failures
|
|
344
|
+
|
|
345
|
+
### Flash of Protected Content
|
|
346
|
+
|
|
347
|
+
**Symptom**: User briefly sees protected content before redirect to login.
|
|
348
|
+
|
|
349
|
+
**Cause**: Auth state checked after component renders.
|
|
350
|
+
|
|
351
|
+
**Fix**: Show loading state while checking auth:
|
|
352
|
+
```typescript
|
|
353
|
+
if (!authenticated) {
|
|
354
|
+
return <div>Checking authentication...</div>;
|
|
355
|
+
}
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
---
|
|
359
|
+
|
|
360
|
+
### Infinite Redirect Loop
|
|
361
|
+
|
|
362
|
+
**Symptom**: Page redirects to login, login redirects to page, infinite loop.
|
|
363
|
+
|
|
364
|
+
**Cause**: Login page itself has auth guard.
|
|
365
|
+
|
|
366
|
+
**Fix**: Exclude login/public pages from guards:
|
|
367
|
+
```typescript
|
|
368
|
+
// Do NOT add guard to login page
|
|
369
|
+
// app/login/page.tsx
|
|
370
|
+
export default function LoginPage() {
|
|
371
|
+
// No AuthGuard here
|
|
372
|
+
return <div>Login</div>;
|
|
373
|
+
}
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
---
|
|
377
|
+
|
|
378
|
+
### Return URL Lost After Login
|
|
379
|
+
|
|
380
|
+
**Symptom**: User redirected to login, but after login sent to home instead of original page.
|
|
381
|
+
|
|
382
|
+
**Cause**: Return URL not preserved.
|
|
383
|
+
|
|
384
|
+
**Fix**: Tide SDK handles this automatically via the redirect URIs configured in TideCloak and `tidecloak.json`. Verify the redirect URI in TideCloak client settings matches the app's actual URL.
|
|
385
|
+
|
|
386
|
+
---
|
|
387
|
+
|
|
388
|
+
### Role Check Always Fails
|
|
389
|
+
|
|
390
|
+
**Symptom**: `hasRealmRole('admin')` always returns false even for admin users.
|
|
391
|
+
|
|
392
|
+
**Cause**: Role name mismatch or role not in token.
|
|
393
|
+
|
|
394
|
+
**Fix**:
|
|
395
|
+
```typescript
|
|
396
|
+
// Verify role name — decode the token to inspect claims
|
|
397
|
+
const { getValueFromToken } = useTideCloak();
|
|
398
|
+
const realmAccess = getValueFromToken("realm_access");
|
|
399
|
+
console.log('User roles:', realmAccess?.roles);
|
|
400
|
+
// Check if 'admin' is in the array
|
|
401
|
+
|
|
402
|
+
// Verify role assigned in TideCloak
|
|
403
|
+
// Admin Console → Users → {user} → Role Mappings
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
---
|
|
407
|
+
|
|
408
|
+
## Repair Path
|
|
409
|
+
|
|
410
|
+
If route protection broken:
|
|
411
|
+
|
|
412
|
+
1. **Verify auth works**:
|
|
413
|
+
```typescript
|
|
414
|
+
// Add to protected page
|
|
415
|
+
const { authenticated, user } = useTideCloak();
|
|
416
|
+
console.log('Auth:', authenticated, 'User:', user);
|
|
417
|
+
// Should log: Auth: true, User: {name: "...", ...}
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
2. **Check provider wraps app**:
|
|
421
|
+
```bash
|
|
422
|
+
# Verify TideCloakProvider in layout or _app
|
|
423
|
+
grep -r "TideCloakProvider" app/layout.tsx pages/_app.tsx
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
3. **Test in incognito**:
|
|
427
|
+
- Clear browser state may not be complete
|
|
428
|
+
- Incognito ensures fresh session
|
|
429
|
+
|
|
430
|
+
4. **Verify API protection separate**:
|
|
431
|
+
```bash
|
|
432
|
+
# Call protected API directly (bypass UI)
|
|
433
|
+
curl http://localhost:3000/api/admin/users
|
|
434
|
+
# Should return 401 even if route guard bypassed
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
If API returns data without JWT, route guards are meaningless. Fix API protection first.
|
|
438
|
+
|
|
439
|
+
---
|
|
440
|
+
|
|
441
|
+
## Do Not Do This
|
|
442
|
+
|
|
443
|
+
### ❌ Do Not Rely on Route Guards for Security
|
|
444
|
+
|
|
445
|
+
```typescript
|
|
446
|
+
// ❌ WRONG: Route guard as only protection
|
|
447
|
+
// app/admin/page.tsx - guarded
|
|
448
|
+
// app/api/admin/users/route.ts - NO JWT verification
|
|
449
|
+
|
|
450
|
+
export async function GET() {
|
|
451
|
+
// Assumes only admins can reach this because route is guarded
|
|
452
|
+
return Response.json({ users: [...] });
|
|
453
|
+
}
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
**Why**: Attacker calls API directly, bypassing route guard. No security.
|
|
457
|
+
|
|
458
|
+
**Correct**: Verify JWT in API. See [protect-api-nextjs.md](protect-api-nextjs.md).
|
|
459
|
+
|
|
460
|
+
---
|
|
461
|
+
|
|
462
|
+
### ❌ Do Not Use proxy.ts (or legacy middleware.ts) for Auth Checks
|
|
463
|
+
|
|
464
|
+
```typescript
|
|
465
|
+
// ❌ WRONG: Next.js proxy/middleware checks auth
|
|
466
|
+
// proxy.ts (or middleware.ts in legacy projects)
|
|
467
|
+
import { NextResponse } from 'next/server';
|
|
468
|
+
|
|
469
|
+
export function middleware(req) {
|
|
470
|
+
const token = req.cookies.get('token');
|
|
471
|
+
if (!token) {
|
|
472
|
+
return NextResponse.redirect('/login');
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
**Why**:
|
|
478
|
+
1. `proxy.ts` runs on the edge, cannot verify JWT signature (no access to adapter JSON)
|
|
479
|
+
2. Checks cookie presence, not JWT validity
|
|
480
|
+
3. Not real authorization
|
|
481
|
+
|
|
482
|
+
**Version rule**: Next.js 16+ uses `proxy.ts`. Next.js 15 and earlier use `middleware.ts`. Check the installed version before creating or looking for this file. See [canon/framework-matrix.md](../canon/framework-matrix.md#request-interception--route-protection-ui-only).
|
|
483
|
+
|
|
484
|
+
**Correct**: Client-side route guards for UX, server-side JWT verification for security.
|
|
485
|
+
|
|
486
|
+
---
|
|
487
|
+
|
|
488
|
+
### ❌ Do Not Mix Auth Check with Data Fetching
|
|
489
|
+
|
|
490
|
+
```typescript
|
|
491
|
+
// ❌ WRONG: Auth check in same component as data fetch
|
|
492
|
+
export default function DashboardPage() {
|
|
493
|
+
const { authenticated } = useTideCloak();
|
|
494
|
+
const [data, setData] = useState(null);
|
|
495
|
+
|
|
496
|
+
useEffect(() => {
|
|
497
|
+
fetch('/api/dashboard').then(r => r.json()).then(setData);
|
|
498
|
+
}, []);
|
|
499
|
+
|
|
500
|
+
if (!authenticated) {
|
|
501
|
+
return <div>Login required</div>;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
return <div>{data?.message}</div>;
|
|
505
|
+
}
|
|
506
|
+
```
|
|
507
|
+
|
|
508
|
+
**Why**: API call fires before auth check completes. API receives unauthenticated request.
|
|
509
|
+
|
|
510
|
+
**Correct**: Check auth first, then fetch:
|
|
511
|
+
```typescript
|
|
512
|
+
useEffect(() => {
|
|
513
|
+
if (authenticated) {
|
|
514
|
+
fetch('/api/dashboard').then(r => r.json()).then(setData);
|
|
515
|
+
}
|
|
516
|
+
}, [authenticated]);
|
|
517
|
+
```
|
|
518
|
+
|
|
519
|
+
Or use SDK's `secureFetch()` (if available) which adds auth headers automatically.
|
|
520
|
+
|
|
521
|
+
---
|
|
522
|
+
|
|
523
|
+
## Known Uncertainties
|
|
524
|
+
|
|
525
|
+
None specific to route protection. Inherits uncertainties from auth setup (CSP, DPoP, etc.). See [add-auth-nextjs-fresh.md Known Uncertainties](add-auth-nextjs-fresh.md#known-uncertainties).
|
|
526
|
+
|
|
527
|
+
---
|
|
528
|
+
|
|
529
|
+
## Next Steps
|
|
530
|
+
|
|
531
|
+
After route protection working:
|
|
532
|
+
|
|
533
|
+
1. [Protect APIs](protect-api-nextjs.md) - Server-side JWT verification (REQUIRED for security). **Must complete before RBAC.**
|
|
534
|
+
2. [Add RBAC](add-rbac-nextjs.md) - Role-based access control (depends on API protection)
|
|
535
|
+
3. [Diagnose broken login](diagnose-broken-login.md) - If route guards fail
|
|
536
|
+
|
|
537
|
+
---
|
|
538
|
+
|
|
539
|
+
## References
|
|
540
|
+
|
|
541
|
+
- [canon/feature-mapping.md](../canon/feature-mapping.md#protected-routes-vs-protected-apis) - Route vs API protection
|
|
542
|
+
- [canon/invariants.md I-08](../canon/invariants.md#i-08-ui-gating-is-not-authorization) - UI gating is not authorization
|
|
543
|
+
- [canon/anti-patterns.md AP-02](../canon/anti-patterns.md#ap-02-relying-on-client-side-role-checks-for-authorization) - Client-side auth mistakes
|
|
544
|
+
- [protect-api-nextjs.md](protect-api-nextjs.md) - Real API protection
|