@tidecloak/nextjs 0.14.33-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](../tidecloak-create-nextjs/README.md) to get started quickly.
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
- | Edge middleware | Yes | Yes |
30
+ | Route protection (proxy/middleware) | Yes | Yes |
31
31
 
32
32
  ---
33
33
 
34
34
  ## Requirements
35
35
 
36
- - Next.js 13.4+ (App Router) or Next.js 12+ (Pages Router)
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
- - `createTideCloakMiddleware()` - Edge middleware for route protection
49
- - `createTideCloakProxy()` - Node.js proxy for route protection (recommended on Next.js 16+)
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 — see the [`@tidecloak/js` DPoP docs](../tidecloak-js/docs/FRONT_CHANNEL.md#dpop-opt-in).
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": string;
19
- resource: string;
20
- "public-client": boolean;
21
- "confidential-port": number;
22
- jwk: {
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;IACvB,QAAQ,EAAE,MAAM,CAAC;IACjB,eAAe,EAAE,OAAO,CAAC;IACzB,mBAAmB,EAAE,MAAM,CAAC;IAC5B,GAAG,EAAE;QACH,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"}
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.33-staging",
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.33-staging",
56
- "@tidecloak/verify": "0.14.33-staging"
56
+ "@tidecloak/react": "^0.14.33",
57
+ "@tidecloak/verify": "^0.14.33"
57
58
  },
58
59
  "devDependencies": {
59
60
  "@types/node": "^22.0.0",