@tidecloak/nextjs 0.14.32-staging → 0.14.33
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/README.md
CHANGED
|
@@ -6,7 +6,7 @@ Add TideCloak authentication to your Next.js app.
|
|
|
6
6
|
npm install @tidecloak/nextjs
|
|
7
7
|
```
|
|
8
8
|
|
|
9
|
-
> New to TideCloak? Use our [Next.js template](
|
|
9
|
+
> New to TideCloak? Use our [Next.js template](https://github.com/tide-foundation/tidecloak-js/blob/main/packages/tidecloak-create-nextjs/README.md) to get started quickly.
|
|
10
10
|
|
|
11
11
|
---
|
|
12
12
|
|
|
@@ -27,13 +27,13 @@ npm install @tidecloak/nextjs
|
|
|
27
27
|
| Best for | Simple apps | High-security apps |
|
|
28
28
|
| Setup complexity | Easy | Medium |
|
|
29
29
|
| Client-side token access | Yes | No |
|
|
30
|
-
|
|
|
30
|
+
| Route protection (proxy/middleware) | Yes | Yes |
|
|
31
31
|
|
|
32
32
|
---
|
|
33
33
|
|
|
34
34
|
## Requirements
|
|
35
35
|
|
|
36
|
-
- Next.js 13.
|
|
36
|
+
- Next.js 13.5+ (App Router or Pages Router)
|
|
37
37
|
- React 18+
|
|
38
38
|
- A TideCloak server ([setup guide](https://github.com/tide-foundation/tidecloak-gettingstarted))
|
|
39
39
|
- A registered client in your TideCloak realm
|
|
@@ -45,12 +45,12 @@ npm install @tidecloak/nextjs
|
|
|
45
45
|
- `<TideCloakProvider>` - Application-level context
|
|
46
46
|
- `useTideCloak()` - Hook for auth state and actions
|
|
47
47
|
- `<Authenticated>` / `<Unauthenticated>` - UI guards
|
|
48
|
-
- `
|
|
49
|
-
- `
|
|
48
|
+
- `createTideCloakProxy()` - Route protection in `proxy.ts` (Next.js 16+)
|
|
49
|
+
- `createTideCloakMiddleware()` - Route protection in `middleware.ts` (Next.js 13.5 to 15)
|
|
50
50
|
- `verifyTideCloakToken()` - Server-side JWT verification
|
|
51
51
|
- `doEncrypt()` / `doDecrypt()` - Tag-based encryption
|
|
52
52
|
|
|
53
|
-
> **DPoP is opt-in** (sender-constrained tokens). By default you get a plain, unbound access token. Pass `useDPoP: { mode: "auto" }` or `{ mode: "strict" }` in your provider config to turn it on
|
|
53
|
+
> **DPoP is opt-in** (sender-constrained tokens). By default you get a plain, unbound access token. Pass `useDPoP: { mode: "auto" }` or `{ mode: "strict" }` in your provider config to turn it on. See the [`@tidecloak/js` DPoP docs](https://github.com/tide-foundation/tidecloak-js/blob/main/packages/tidecloak-js/docs/FRONT_CHANNEL.md#dpop-opt-in).
|
|
54
54
|
|
|
55
55
|
---
|
|
56
56
|
|
|
@@ -15,11 +15,12 @@ interface JWK {
|
|
|
15
15
|
export interface TidecloakConfig {
|
|
16
16
|
realm: string;
|
|
17
17
|
"auth-server-url": string;
|
|
18
|
-
"ssl-required"
|
|
19
|
-
resource
|
|
20
|
-
"public-client"
|
|
21
|
-
"confidential-port"
|
|
22
|
-
|
|
18
|
+
"ssl-required"?: string;
|
|
19
|
+
resource?: string;
|
|
20
|
+
"public-client"?: boolean;
|
|
21
|
+
"confidential-port"?: number;
|
|
22
|
+
/** Local JWKS. When absent, keys are fetched from the realm's certs endpoint. */
|
|
23
|
+
jwk?: {
|
|
23
24
|
keys: JWK[];
|
|
24
25
|
};
|
|
25
26
|
[key: string]: unknown;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tidecloakMiddleware.d.ts","sourceRoot":"","sources":["../../../src/server/tidecloakMiddleware.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAEvD,OAAO,EAEL,kBAAkB,EACnB,MAAM,iBAAiB,CAAA;AAExB,UAAU,GAAG;IACT,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;IAEb,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,CAAC,CAAC,EAAE,MAAM,CAAC;IACX,CAAC,CAAC,EAAE,MAAM,CAAC;IAEX,CAAC,CAAC,EAAE,MAAM,CAAC;IACX,CAAC,CAAC,EAAE,MAAM,CAAC;IAEX,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CAC1B;AAED,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,iBAAiB,EAAE,MAAM,CAAC;IAC1B,cAAc,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"tidecloakMiddleware.d.ts","sourceRoot":"","sources":["../../../src/server/tidecloakMiddleware.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAEvD,OAAO,EAEL,kBAAkB,EACnB,MAAM,iBAAiB,CAAA;AAExB,UAAU,GAAG;IACT,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;IAEb,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,CAAC,CAAC,EAAE,MAAM,CAAC;IACX,CAAC,CAAC,EAAE,MAAM,CAAC;IAEX,CAAC,CAAC,EAAE,MAAM,CAAC;IACX,CAAC,CAAC,EAAE,MAAM,CAAC;IAEX,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CAC1B;AAED,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,iBAAiB,EAAE,MAAM,CAAC;IAC1B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,iFAAiF;IACjF,GAAG,CAAC,EAAE;QACJ,IAAI,EAAE,GAAG,EAAE,CAAC;KACb,CAAC;IAEF,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED;;;;;;GAMG;AACH,MAAM,WAAW,qBAAqB;IACpC,oFAAoF;IACpF,MAAM,EAAE,eAAe,CAAA;IACvB,2DAA2D;IAC3D,eAAe,CAAC,EAAE,kBAAkB,CAAA;IACpC,+EAA+E;IAC/E,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,uEAAuE;IACvE,SAAS,CAAC,EAAE,CAAC,GAAG,EAAE;QAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,EAAE,GAAG,EAAE,WAAW,KAAK,YAAY,GAAG,IAAI,CAAA;IACpF,kFAAkF;IAClF,SAAS,CAAC,EAAE,CAAC,GAAG,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;KAAE,EAAE,GAAG,EAAE,WAAW,KAAK,YAAY,GAAG,IAAI,CAAA;IAC5F,0EAA0E;IAC1E,SAAS,CAAC,EAAE,CAAC,GAAG,EAAE;QAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,EAAE,GAAG,EAAE,WAAW,KAAK,YAAY,GAAG,IAAI,CAAA;IACpF,wDAAwD;IACxD,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,WAAW,KAAK,YAAY,CAAA;CACvD;AAOD;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,qBAAqB,SAM7B,WAAW,oCAwDlD"}
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
# Front-Channel Mode (Next.js)
|
|
2
|
+
|
|
3
|
+
The default mode for Next.js apps. Your browser handles login and tokens directly.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Setup
|
|
8
|
+
|
|
9
|
+
### 1. Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install @tidecloak/nextjs
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
### 2. Add Provider
|
|
16
|
+
|
|
17
|
+
Download `tidecloak.json` (your client adapter config) from your TideCloak admin console and put it in your project root. `TideCloakProvider` takes its contents as `config`.
|
|
18
|
+
|
|
19
|
+
**App Router:** `app/layout.tsx`
|
|
20
|
+
|
|
21
|
+
```tsx
|
|
22
|
+
import { TideCloakProvider } from '@tidecloak/nextjs';
|
|
23
|
+
import adapter from '../tidecloak.json';
|
|
24
|
+
|
|
25
|
+
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
|
26
|
+
return (
|
|
27
|
+
<html lang="en">
|
|
28
|
+
<body>
|
|
29
|
+
<TideCloakProvider config={adapter}>
|
|
30
|
+
{children}
|
|
31
|
+
</TideCloakProvider>
|
|
32
|
+
</body>
|
|
33
|
+
</html>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
**Pages Router:** `pages/_app.tsx`
|
|
39
|
+
|
|
40
|
+
```tsx
|
|
41
|
+
import { TideCloakProvider } from '@tidecloak/nextjs';
|
|
42
|
+
import adapter from '../tidecloak.json';
|
|
43
|
+
|
|
44
|
+
function MyApp({ Component, pageProps }) {
|
|
45
|
+
return (
|
|
46
|
+
<TideCloakProvider config={adapter}>
|
|
47
|
+
<Component {...pageProps} />
|
|
48
|
+
</TideCloakProvider>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export default MyApp;
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### 3. Create Redirect Page
|
|
56
|
+
|
|
57
|
+
**App Router:** `app/auth/redirect/page.tsx`
|
|
58
|
+
|
|
59
|
+
```tsx
|
|
60
|
+
'use client';
|
|
61
|
+
|
|
62
|
+
import { useEffect } from 'react';
|
|
63
|
+
import { useRouter } from 'next/navigation';
|
|
64
|
+
import { useTideCloak } from '@tidecloak/nextjs';
|
|
65
|
+
|
|
66
|
+
export default function RedirectPage() {
|
|
67
|
+
const { authenticated, isInitializing } = useTideCloak();
|
|
68
|
+
const router = useRouter();
|
|
69
|
+
|
|
70
|
+
useEffect(() => {
|
|
71
|
+
if (!isInitializing) {
|
|
72
|
+
router.push(authenticated ? '/dashboard' : '/');
|
|
73
|
+
}
|
|
74
|
+
}, [authenticated, isInitializing, router]);
|
|
75
|
+
|
|
76
|
+
return <p>Loading...</p>;
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## Using the Hook
|
|
83
|
+
|
|
84
|
+
```tsx
|
|
85
|
+
'use client';
|
|
86
|
+
|
|
87
|
+
import { useTideCloak } from '@tidecloak/nextjs';
|
|
88
|
+
|
|
89
|
+
export default function Header() {
|
|
90
|
+
const { authenticated, login, logout } = useTideCloak();
|
|
91
|
+
|
|
92
|
+
return (
|
|
93
|
+
<header>
|
|
94
|
+
{authenticated ? (
|
|
95
|
+
<button onClick={logout}>Log Out</button>
|
|
96
|
+
) : (
|
|
97
|
+
<button onClick={login}>Log In</button>
|
|
98
|
+
)}
|
|
99
|
+
</header>
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## Available Values
|
|
107
|
+
|
|
108
|
+
```tsx
|
|
109
|
+
const {
|
|
110
|
+
authenticated, // true if logged in
|
|
111
|
+
isInitializing, // true while SDK starts up
|
|
112
|
+
token, // access token
|
|
113
|
+
tokenExp, // token expiry timestamp
|
|
114
|
+
login, // log in function
|
|
115
|
+
logout, // log out function
|
|
116
|
+
refreshToken, // refresh token function
|
|
117
|
+
getValueFromToken, // get value from access token
|
|
118
|
+
getValueFromIdToken, // get value from ID token
|
|
119
|
+
hasRealmRole, // check realm role
|
|
120
|
+
hasClientRole, // check client role
|
|
121
|
+
doEncrypt, // encrypt data
|
|
122
|
+
doDecrypt, // decrypt data
|
|
123
|
+
} = useTideCloak();
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
## Guard Components
|
|
129
|
+
|
|
130
|
+
```tsx
|
|
131
|
+
'use client';
|
|
132
|
+
|
|
133
|
+
import { Authenticated, Unauthenticated } from '@tidecloak/nextjs';
|
|
134
|
+
|
|
135
|
+
export default function Dashboard() {
|
|
136
|
+
return (
|
|
137
|
+
<>
|
|
138
|
+
<Authenticated>
|
|
139
|
+
<h1>Welcome to your dashboard!</h1>
|
|
140
|
+
</Authenticated>
|
|
141
|
+
|
|
142
|
+
<Unauthenticated>
|
|
143
|
+
<p>Please log in.</p>
|
|
144
|
+
</Unauthenticated>
|
|
145
|
+
</>
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
## Route Protection
|
|
153
|
+
|
|
154
|
+
Protect routes with server-side auth checks.
|
|
155
|
+
|
|
156
|
+
### Next.js 16+ (proxy.ts)
|
|
157
|
+
|
|
158
|
+
Create `proxy.ts` at your project root:
|
|
159
|
+
|
|
160
|
+
```ts
|
|
161
|
+
import { NextResponse } from 'next/server';
|
|
162
|
+
import tidecloakConfig from './tidecloak.json';
|
|
163
|
+
import { createTideCloakProxy } from '@tidecloak/nextjs/server';
|
|
164
|
+
|
|
165
|
+
export const proxy = createTideCloakProxy({
|
|
166
|
+
config: tidecloakConfig,
|
|
167
|
+
protectedRoutes: {
|
|
168
|
+
'/admin/*': ['admin'],
|
|
169
|
+
'/api/private/*': ['user'],
|
|
170
|
+
},
|
|
171
|
+
onFailure: ({ token }, req) => NextResponse.redirect(new URL('/login', req.url)),
|
|
172
|
+
onError: (err, req) => NextResponse.rewrite(new URL('/error', req.url)),
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
// Optional: limit which paths run the proxy
|
|
176
|
+
export const config = {
|
|
177
|
+
matcher: ['/admin/:path*', '/api/private/:path*'],
|
|
178
|
+
};
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
> `export const config = { matcher }` works in `proxy.ts`. Don't set `runtime` there: proxy always runs on the Node.js runtime.
|
|
182
|
+
|
|
183
|
+
### Next.js 13.5 to 15 (middleware.ts)
|
|
184
|
+
|
|
185
|
+
Create `middleware.ts` at your project root:
|
|
186
|
+
|
|
187
|
+
```ts
|
|
188
|
+
import { NextResponse } from 'next/server';
|
|
189
|
+
import tidecloakConfig from './tidecloak.json';
|
|
190
|
+
import { createTideCloakMiddleware } from '@tidecloak/nextjs/server';
|
|
191
|
+
|
|
192
|
+
export default createTideCloakMiddleware({
|
|
193
|
+
config: tidecloakConfig,
|
|
194
|
+
protectedRoutes: {
|
|
195
|
+
'/admin/*': ['admin'],
|
|
196
|
+
'/api/private/*': ['user'],
|
|
197
|
+
},
|
|
198
|
+
onFailure: ({ token }, req) => NextResponse.redirect(new URL('/login', req.url)),
|
|
199
|
+
onError: (err, req) => NextResponse.rewrite(new URL('/error', req.url)),
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
export const config = {
|
|
203
|
+
matcher: [
|
|
204
|
+
'/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico)).*)',
|
|
205
|
+
'/api/(.*)',
|
|
206
|
+
],
|
|
207
|
+
};
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
### Options
|
|
211
|
+
|
|
212
|
+
Both `createTideCloakProxy` and `createTideCloakMiddleware` accept the same options:
|
|
213
|
+
|
|
214
|
+
| Option | Type | Description |
|
|
215
|
+
| ----------------- | ------------------------------------------------- | -------------------------------------------------------------------------------------------- |
|
|
216
|
+
| `config` | `tidecloak.json` contents | Your TideCloak client adapter config. |
|
|
217
|
+
| `protectedRoutes` | `Record<string, string[]>` | Map of route pattern → allowed roles (see pattern rules below). |
|
|
218
|
+
| `cookieName` | `string` (default `"kcToken"`) | Name of the cookie holding the access token. |
|
|
219
|
+
| `onRequest` | `(ctx, req) => NextResponse \| void` | Runs before auth checks; return a response to short-circuit. |
|
|
220
|
+
| `onSuccess` | `({ payload }, req) => NextResponse \| void` | Runs after a token + role check passes. |
|
|
221
|
+
| `onFailure` | `({ token }, req) => NextResponse \| void` | Runs when verification/role check fails. If omitted, responds `403`. |
|
|
222
|
+
| `onError` | `(err, req) => NextResponse` | Unexpected-error fallback. **Note the error is the first argument.** If omitted, the error is rethrown. |
|
|
223
|
+
|
|
224
|
+
**`protectedRoutes` pattern rules:**
|
|
225
|
+
|
|
226
|
+
- **Prefix**: `"/dashboard"` matches `/dashboard` and any sub-path.
|
|
227
|
+
- **Glob**: `*` becomes a wildcard. A trailing `/*` also matches the bare base path, so `"/admin/*"` protects **both** `/admin` and `/admin/anything` (it will not match `/administrator`).
|
|
228
|
+
- **`"OPTIONS"`**: matches requests by HTTP method instead of path.
|
|
229
|
+
|
|
230
|
+
---
|
|
231
|
+
|
|
232
|
+
## Server-Side Token Verification
|
|
233
|
+
|
|
234
|
+
**App Router:** `app/api/secure/route.ts`
|
|
235
|
+
|
|
236
|
+
```ts
|
|
237
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
238
|
+
import { verifyTideCloakToken } from '@tidecloak/nextjs/server';
|
|
239
|
+
import config from '../../../tidecloak.json';
|
|
240
|
+
|
|
241
|
+
export async function GET(req: NextRequest) {
|
|
242
|
+
const token = req.cookies.get('kcToken')?.value || '';
|
|
243
|
+
const payload = await verifyTideCloakToken(config, token, ['user']);
|
|
244
|
+
|
|
245
|
+
if (!payload) {
|
|
246
|
+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return NextResponse.json({ data: 'Secure data' });
|
|
250
|
+
}
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
---
|
|
254
|
+
|
|
255
|
+
## Encrypting & Decrypting Data
|
|
256
|
+
|
|
257
|
+
```tsx
|
|
258
|
+
const { doEncrypt, doDecrypt } = useTideCloak();
|
|
259
|
+
|
|
260
|
+
// Encrypt
|
|
261
|
+
const [encrypted] = await doEncrypt([
|
|
262
|
+
{ data: "sensitive info", tags: ["personal"] }
|
|
263
|
+
]);
|
|
264
|
+
|
|
265
|
+
// Decrypt
|
|
266
|
+
const [decrypted] = await doDecrypt([
|
|
267
|
+
{ encrypted, tags: ["personal"] }
|
|
268
|
+
]);
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
### Data Types
|
|
272
|
+
|
|
273
|
+
The `data` property **must** be either a string or a `Uint8Array` (raw bytes).
|
|
274
|
+
|
|
275
|
+
```tsx
|
|
276
|
+
// This will FAIL - objects not allowed
|
|
277
|
+
await doEncrypt([{ data: { name: "John" }, tags: ["user"] }]);
|
|
278
|
+
|
|
279
|
+
// This works - use strings
|
|
280
|
+
await doEncrypt([{ data: JSON.stringify({ name: "John" }), tags: ["user"] }]);
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
### Permissions
|
|
284
|
+
|
|
285
|
+
- Encryption requires `_tide_<tag>.selfencrypt` role
|
|
286
|
+
- Decryption requires `_tide_<tag>.selfdecrypt` role
|
|
287
|
+
- Users need roles matching **every** tag on a payload
|
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
# Hybrid/BFF Mode (Next.js)
|
|
2
|
+
|
|
3
|
+
For Next.js apps that need extra security. Tokens stay on your server, not in the browser.
|
|
4
|
+
|
|
5
|
+
Next.js is ideal for hybrid mode because you can use API routes to handle the token exchange.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## How It Works
|
|
10
|
+
|
|
11
|
+
1. User clicks "Login"
|
|
12
|
+
2. Browser redirects to TideCloak login page
|
|
13
|
+
3. User logs in
|
|
14
|
+
4. TideCloak redirects back with an authorization code
|
|
15
|
+
5. Your **Next.js API route** exchanges the code for tokens
|
|
16
|
+
6. API route creates a session (e.g., HTTP-only cookie)
|
|
17
|
+
7. Tokens stay on your server, not in the browser
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## When to Use This
|
|
22
|
+
|
|
23
|
+
- Apps with sensitive data
|
|
24
|
+
- When you don't want tokens in the browser
|
|
25
|
+
- Apps that need server-side session control
|
|
26
|
+
- When you want to use Next.js API routes for auth
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## Setup
|
|
31
|
+
|
|
32
|
+
### 1. Config
|
|
33
|
+
|
|
34
|
+
Create your hybrid config:
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
// lib/tidecloakConfig.ts
|
|
38
|
+
export const hybridConfig = {
|
|
39
|
+
authMode: "hybrid",
|
|
40
|
+
oidc: {
|
|
41
|
+
authorizationEndpoint: "https://auth.example.com/realms/myrealm/protocol/openid-connect/auth",
|
|
42
|
+
clientId: "my-app",
|
|
43
|
+
redirectUri: "https://myapp.com/auth/callback",
|
|
44
|
+
scope: "openid profile email"
|
|
45
|
+
},
|
|
46
|
+
tokenExchange: {
|
|
47
|
+
endpoint: "/api/auth/callback" // Your Next.js API route
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### 2. Login Page
|
|
53
|
+
|
|
54
|
+
**App Router:** `app/login/page.tsx`
|
|
55
|
+
|
|
56
|
+
```tsx
|
|
57
|
+
'use client';
|
|
58
|
+
|
|
59
|
+
import { useEffect, useState } from 'react';
|
|
60
|
+
import { useSearchParams } from 'next/navigation';
|
|
61
|
+
import { IAMService } from '@tidecloak/js';
|
|
62
|
+
import { hybridConfig } from '@/lib/tidecloakConfig';
|
|
63
|
+
|
|
64
|
+
export default function LoginPage() {
|
|
65
|
+
const [ready, setReady] = useState(false);
|
|
66
|
+
const searchParams = useSearchParams();
|
|
67
|
+
const returnUrl = searchParams.get('return') || '/dashboard';
|
|
68
|
+
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
IAMService.loadConfig(hybridConfig).then(() => setReady(true));
|
|
71
|
+
}, []);
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<div>
|
|
75
|
+
<h1>Login</h1>
|
|
76
|
+
<button
|
|
77
|
+
disabled={!ready}
|
|
78
|
+
onClick={() => IAMService.doLogin(returnUrl)}
|
|
79
|
+
>
|
|
80
|
+
Login with TideCloak
|
|
81
|
+
</button>
|
|
82
|
+
</div>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### 3. Callback Page
|
|
88
|
+
|
|
89
|
+
**App Router:** `app/auth/callback/page.tsx`
|
|
90
|
+
|
|
91
|
+
```tsx
|
|
92
|
+
'use client';
|
|
93
|
+
|
|
94
|
+
import { useEffect, useState } from 'react';
|
|
95
|
+
import { useRouter } from 'next/navigation';
|
|
96
|
+
import { IAMService } from '@tidecloak/js';
|
|
97
|
+
import { hybridConfig } from '@/lib/tidecloakConfig';
|
|
98
|
+
|
|
99
|
+
export default function CallbackPage() {
|
|
100
|
+
const [error, setError] = useState<string | null>(null);
|
|
101
|
+
const router = useRouter();
|
|
102
|
+
|
|
103
|
+
useEffect(() => {
|
|
104
|
+
IAMService.initIAM(hybridConfig)
|
|
105
|
+
.then(authenticated => {
|
|
106
|
+
if (authenticated) {
|
|
107
|
+
const returnUrl = IAMService.getReturnUrl() || '/dashboard';
|
|
108
|
+
router.push(returnUrl);
|
|
109
|
+
} else {
|
|
110
|
+
setError('Login failed');
|
|
111
|
+
}
|
|
112
|
+
})
|
|
113
|
+
.catch(err => setError(err.message));
|
|
114
|
+
}, [router]);
|
|
115
|
+
|
|
116
|
+
if (error) {
|
|
117
|
+
return <div>Error: {error}</div>;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return <div>Logging in...</div>;
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### 4. API Route (Token Exchange)
|
|
125
|
+
|
|
126
|
+
**App Router:** `app/api/auth/callback/route.ts`
|
|
127
|
+
|
|
128
|
+
```ts
|
|
129
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
130
|
+
import {
|
|
131
|
+
exchangeCodeForTokens,
|
|
132
|
+
parseAuthCodeData,
|
|
133
|
+
setSessionCookie
|
|
134
|
+
} from '@tidecloak/nextjs/server';
|
|
135
|
+
import { createSession } from '@/lib/sessionStore';
|
|
136
|
+
|
|
137
|
+
export async function POST(req: NextRequest) {
|
|
138
|
+
const body = await req.json();
|
|
139
|
+
const authData = parseAuthCodeData(body);
|
|
140
|
+
|
|
141
|
+
if (!authData) {
|
|
142
|
+
return NextResponse.json({ error: 'Invalid auth data' }, { status: 400 });
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const result = await exchangeCodeForTokens({
|
|
146
|
+
authServerUrl: process.env.TIDECLOAK_URL!,
|
|
147
|
+
realm: process.env.TIDECLOAK_REALM!,
|
|
148
|
+
clientId: process.env.TIDECLOAK_CLIENT_ID!,
|
|
149
|
+
}, authData);
|
|
150
|
+
|
|
151
|
+
if (!result.success) {
|
|
152
|
+
return NextResponse.json({ error: result.error }, { status: 401 });
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Store tokens server-side and create session
|
|
156
|
+
const sessionId = createSession(result.tokens);
|
|
157
|
+
|
|
158
|
+
const response = NextResponse.json({ success: true });
|
|
159
|
+
setSessionCookie(response, sessionId, { maxAge: result.tokens.expires_in });
|
|
160
|
+
|
|
161
|
+
return response;
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
---
|
|
166
|
+
|
|
167
|
+
## Full Example with Session Management
|
|
168
|
+
|
|
169
|
+
### Session Store
|
|
170
|
+
|
|
171
|
+
```ts
|
|
172
|
+
// lib/sessionStore.ts
|
|
173
|
+
import type { TokenResponse } from '@tidecloak/nextjs/server';
|
|
174
|
+
|
|
175
|
+
interface Session {
|
|
176
|
+
tokens: TokenResponse;
|
|
177
|
+
userId: string;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const sessions = new Map<string, Session>();
|
|
181
|
+
|
|
182
|
+
export function createSession(tokens: TokenResponse): string {
|
|
183
|
+
const sessionId = crypto.randomUUID();
|
|
184
|
+
// Decode user ID from access token (it's a JWT)
|
|
185
|
+
const payload = JSON.parse(atob(tokens.access_token.split('.')[1]));
|
|
186
|
+
sessions.set(sessionId, { tokens, userId: payload.sub });
|
|
187
|
+
return sessionId;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function getSession(sessionId: string): Session | undefined {
|
|
191
|
+
return sessions.get(sessionId);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export function updateSession(sessionId: string, tokens: TokenResponse): void {
|
|
195
|
+
const session = sessions.get(sessionId);
|
|
196
|
+
if (session) {
|
|
197
|
+
session.tokens = tokens;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export function deleteSession(sessionId: string): void {
|
|
202
|
+
sessions.delete(sessionId);
|
|
203
|
+
}
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### API Route with Session
|
|
207
|
+
|
|
208
|
+
```ts
|
|
209
|
+
// app/api/auth/callback/route.ts
|
|
210
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
211
|
+
import {
|
|
212
|
+
exchangeCodeForTokens,
|
|
213
|
+
parseAuthCodeData,
|
|
214
|
+
setSessionCookie
|
|
215
|
+
} from '@tidecloak/nextjs/server';
|
|
216
|
+
import { createSession } from '@/lib/sessionStore';
|
|
217
|
+
|
|
218
|
+
export async function POST(req: NextRequest) {
|
|
219
|
+
const body = await req.json();
|
|
220
|
+
const authData = parseAuthCodeData(body);
|
|
221
|
+
|
|
222
|
+
if (!authData) {
|
|
223
|
+
return NextResponse.json({ error: 'Invalid auth data' }, { status: 400 });
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const result = await exchangeCodeForTokens({
|
|
227
|
+
authServerUrl: process.env.TIDECLOAK_URL!,
|
|
228
|
+
realm: process.env.TIDECLOAK_REALM!,
|
|
229
|
+
clientId: process.env.TIDECLOAK_CLIENT_ID!,
|
|
230
|
+
}, authData);
|
|
231
|
+
|
|
232
|
+
if (!result.success) {
|
|
233
|
+
return NextResponse.json({ error: result.error }, { status: 401 });
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const sessionId = createSession(result.tokens);
|
|
237
|
+
|
|
238
|
+
const response = NextResponse.json({ success: true });
|
|
239
|
+
setSessionCookie(response, sessionId, { maxAge: 60 * 60 * 24 });
|
|
240
|
+
|
|
241
|
+
return response;
|
|
242
|
+
}
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
### Protected API Route
|
|
246
|
+
|
|
247
|
+
```ts
|
|
248
|
+
// app/api/user/route.ts
|
|
249
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
250
|
+
import { getSessionFromRequest } from '@tidecloak/nextjs/server';
|
|
251
|
+
import { getSession } from '@/lib/sessionStore';
|
|
252
|
+
|
|
253
|
+
export async function GET(req: NextRequest) {
|
|
254
|
+
const sessionId = getSessionFromRequest(req);
|
|
255
|
+
|
|
256
|
+
if (!sessionId) {
|
|
257
|
+
return NextResponse.json({ error: 'Not authenticated' }, { status: 401 });
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const session = getSession(sessionId);
|
|
261
|
+
if (!session) {
|
|
262
|
+
return NextResponse.json({ error: 'Session expired' }, { status: 401 });
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Use server-side tokens for API calls
|
|
266
|
+
const userInfo = await fetch(
|
|
267
|
+
`${process.env.TIDECLOAK_URL}/realms/${process.env.TIDECLOAK_REALM}/protocol/openid-connect/userinfo`,
|
|
268
|
+
{
|
|
269
|
+
headers: { Authorization: `Bearer ${session.tokens.access_token}` },
|
|
270
|
+
}
|
|
271
|
+
);
|
|
272
|
+
|
|
273
|
+
return NextResponse.json(await userInfo.json());
|
|
274
|
+
}
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
### Token Refresh
|
|
278
|
+
|
|
279
|
+
```ts
|
|
280
|
+
// app/api/auth/refresh/route.ts
|
|
281
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
282
|
+
import {
|
|
283
|
+
refreshAccessToken,
|
|
284
|
+
getSessionFromRequest,
|
|
285
|
+
setSessionCookie
|
|
286
|
+
} from '@tidecloak/nextjs/server';
|
|
287
|
+
import { getSession, updateSession } from '@/lib/sessionStore';
|
|
288
|
+
|
|
289
|
+
export async function POST(req: NextRequest) {
|
|
290
|
+
const sessionId = getSessionFromRequest(req);
|
|
291
|
+
|
|
292
|
+
if (!sessionId) {
|
|
293
|
+
return NextResponse.json({ error: 'Not authenticated' }, { status: 401 });
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const session = getSession(sessionId);
|
|
297
|
+
if (!session?.tokens.refresh_token) {
|
|
298
|
+
return NextResponse.json({ error: 'No refresh token' }, { status: 401 });
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const result = await refreshAccessToken({
|
|
302
|
+
authServerUrl: process.env.TIDECLOAK_URL!,
|
|
303
|
+
realm: process.env.TIDECLOAK_REALM!,
|
|
304
|
+
clientId: process.env.TIDECLOAK_CLIENT_ID!,
|
|
305
|
+
}, session.tokens.refresh_token);
|
|
306
|
+
|
|
307
|
+
if (!result.success) {
|
|
308
|
+
return NextResponse.json({ error: result.error }, { status: 401 });
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
updateSession(sessionId, result.tokens);
|
|
312
|
+
|
|
313
|
+
const response = NextResponse.json({ success: true });
|
|
314
|
+
setSessionCookie(response, sessionId, { maxAge: result.tokens.expires_in });
|
|
315
|
+
|
|
316
|
+
return response;
|
|
317
|
+
}
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
### Logout
|
|
321
|
+
|
|
322
|
+
```ts
|
|
323
|
+
// app/api/auth/logout/route.ts
|
|
324
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
325
|
+
import { getSessionFromRequest, clearSessionCookie } from '@tidecloak/nextjs/server';
|
|
326
|
+
import { deleteSession } from '@/lib/sessionStore';
|
|
327
|
+
|
|
328
|
+
export async function POST(req: NextRequest) {
|
|
329
|
+
const sessionId = getSessionFromRequest(req);
|
|
330
|
+
|
|
331
|
+
if (sessionId) {
|
|
332
|
+
deleteSession(sessionId);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const response = NextResponse.json({ success: true });
|
|
336
|
+
clearSessionCookie(response);
|
|
337
|
+
|
|
338
|
+
return response;
|
|
339
|
+
}
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
---
|
|
343
|
+
|
|
344
|
+
## Limitations
|
|
345
|
+
|
|
346
|
+
In hybrid mode, tokens are on your server, so these client-side methods won't work:
|
|
347
|
+
|
|
348
|
+
- `getToken()`, `getIDToken()`
|
|
349
|
+
- `getName()`, `hasRealmRole()`, `hasClientRole()`
|
|
350
|
+
- `getValueFromToken()`, `getValueFromIDToken()`
|
|
351
|
+
- `doEncrypt()`, `doDecrypt()`
|
|
352
|
+
|
|
353
|
+
Use these instead:
|
|
354
|
+
- `isLoggedIn()` - Check if user completed login flow
|
|
355
|
+
- `getReturnUrl()` - Get the page user wanted to visit
|
|
356
|
+
|
|
357
|
+
Your API routes should provide user info to the client.
|
|
358
|
+
|
|
359
|
+
---
|
|
360
|
+
|
|
361
|
+
## Server Utilities
|
|
362
|
+
|
|
363
|
+
Import from `@tidecloak/nextjs/server`:
|
|
364
|
+
|
|
365
|
+
| Function | Description |
|
|
366
|
+
|----------|-------------|
|
|
367
|
+
| `exchangeCodeForTokens(config, authData)` | Exchange authorization code for tokens |
|
|
368
|
+
| `refreshAccessToken(config, refreshToken)` | Refresh an expired access token |
|
|
369
|
+
| `parseAuthCodeData(body)` | Parse auth code data from request body |
|
|
370
|
+
| `setSessionCookie(response, sessionId, options?)` | Set HTTP-only session cookie |
|
|
371
|
+
| `getSessionFromRequest(req, cookieName?)` | Get session ID from request cookies |
|
|
372
|
+
| `clearSessionCookie(response, cookieName?)` | Clear session cookie |
|
|
373
|
+
| `verifyTideCloakToken(config, token, roles?)` | Verify JWT and check roles |
|
|
374
|
+
| `createTideCloakProxy(options)` | Create proxy for route protection (Next.js 16+) |
|
|
375
|
+
| `createTideCloakMiddleware(options)` | Create middleware for route protection (Next.js 13.5 to 15) |
|
|
376
|
+
|
|
377
|
+
---
|
|
378
|
+
|
|
379
|
+
## When to Use Hybrid vs Front-channel
|
|
380
|
+
|
|
381
|
+
| Scenario | Mode |
|
|
382
|
+
|----------|------|
|
|
383
|
+
| Need tokens in browser for client-side API calls | Front-channel |
|
|
384
|
+
| Tokens should never be in browser | Hybrid |
|
|
385
|
+
| Server needs to make API calls on behalf of user | Hybrid |
|
|
386
|
+
| Simple SPA with public API | Front-channel |
|
|
387
|
+
| Sensitive data, high security requirements | Hybrid |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tidecloak/nextjs",
|
|
3
|
-
"version": "0.14.
|
|
3
|
+
"version": "0.14.33",
|
|
4
4
|
"description": "TideCloak nextjs SDK",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": {
|
|
@@ -18,7 +18,8 @@
|
|
|
18
18
|
}
|
|
19
19
|
},
|
|
20
20
|
"files": [
|
|
21
|
-
"dist"
|
|
21
|
+
"dist",
|
|
22
|
+
"docs"
|
|
22
23
|
],
|
|
23
24
|
"publishConfig": {
|
|
24
25
|
"access": "public"
|
|
@@ -52,8 +53,8 @@
|
|
|
52
53
|
"prepare": "npm run build"
|
|
53
54
|
},
|
|
54
55
|
"dependencies": {
|
|
55
|
-
"@tidecloak/react": "0.14.
|
|
56
|
-
"@tidecloak/verify": "0.14.
|
|
56
|
+
"@tidecloak/react": "^0.14.33",
|
|
57
|
+
"@tidecloak/verify": "^0.14.33"
|
|
57
58
|
},
|
|
58
59
|
"devDependencies": {
|
|
59
60
|
"@types/node": "^22.0.0",
|