@strivacity/sdk-next 3.0.3 → 4.0.0-beta.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.
Files changed (90) hide show
  1. package/README.md +1983 -428
  2. package/dist/client/components.cjs +1 -0
  3. package/dist/client/components.d.ts +1 -0
  4. package/dist/client/components.mjs +1 -0
  5. package/dist/client/hooks.cjs +1 -0
  6. package/dist/client/hooks.d.ts +1 -0
  7. package/dist/client/hooks.mjs +1 -0
  8. package/dist/client/index.cjs +1 -0
  9. package/dist/client/index.d.ts +9 -0
  10. package/dist/client/index.mjs +1 -0
  11. package/dist/client/storages.cjs +1 -0
  12. package/dist/client/storages.d.ts +1 -0
  13. package/dist/client/storages.mjs +1 -0
  14. package/dist/client/types.cjs +1 -0
  15. package/dist/client/types.d.ts +1 -0
  16. package/dist/client/types.mjs +1 -0
  17. package/dist/client/utils.cjs +1 -0
  18. package/dist/client/utils.d.ts +1 -0
  19. package/dist/client/utils.mjs +1 -0
  20. package/dist/errors.cjs +1 -0
  21. package/dist/errors.d.ts +1 -0
  22. package/dist/errors.mjs +1 -0
  23. package/dist/server/handlers/api.cjs +2 -0
  24. package/dist/server/handlers/api.cjs.map +1 -0
  25. package/dist/server/handlers/api.d.ts +4 -0
  26. package/dist/server/handlers/api.mjs +2 -0
  27. package/dist/server/handlers/api.mjs.map +1 -0
  28. package/dist/server/handlers/route.cjs +2 -0
  29. package/dist/server/handlers/route.cjs.map +1 -0
  30. package/dist/server/handlers/route.d.ts +4 -0
  31. package/dist/server/handlers/route.mjs +2 -0
  32. package/dist/server/handlers/route.mjs.map +1 -0
  33. package/dist/server/index.cjs +1 -0
  34. package/dist/server/index.d.ts +8 -0
  35. package/dist/server/index.mjs +1 -0
  36. package/dist/server/sdk.cjs +2 -0
  37. package/dist/server/sdk.cjs.map +1 -0
  38. package/dist/server/sdk.d.ts +8 -0
  39. package/dist/server/sdk.mjs +2 -0
  40. package/dist/server/sdk.mjs.map +1 -0
  41. package/dist/server/storages.cjs +2 -0
  42. package/dist/server/storages.cjs.map +1 -0
  43. package/dist/server/storages.d.ts +20 -0
  44. package/dist/server/storages.mjs +2 -0
  45. package/dist/server/storages.mjs.map +1 -0
  46. package/dist/server/types.cjs +1 -0
  47. package/dist/server/types.d.ts +101 -0
  48. package/dist/server/types.mjs +1 -0
  49. package/dist/server/utils.cjs +2 -0
  50. package/dist/server/utils.cjs.map +1 -0
  51. package/dist/server/utils.d.ts +28 -0
  52. package/dist/server/utils.mjs +2 -0
  53. package/dist/server/utils.mjs.map +1 -0
  54. package/dist/types.cjs +1 -2
  55. package/dist/types.d.ts +2 -176
  56. package/dist/types.mjs +1 -2
  57. package/package.json +48 -10
  58. package/testing/tests/client/components.spec.ts +10 -0
  59. package/testing/tests/client/hooks.spec.ts +10 -0
  60. package/testing/tests/client/index.spec.ts +106 -0
  61. package/testing/tests/client/storages.spec.ts +10 -0
  62. package/testing/tests/client/utils.spec.ts +10 -0
  63. package/testing/tests/server/handlers/api.spec.ts +110 -0
  64. package/testing/tests/server/handlers/route.spec.ts +173 -0
  65. package/testing/tests/server/index.spec.ts +103 -0
  66. package/testing/tests/server/sdk.spec.ts +250 -0
  67. package/testing/tests/server/storages.spec.ts +353 -0
  68. package/testing/tests/server/utils.spec.ts +110 -0
  69. package/dist/AuthProvider.cjs +0 -2
  70. package/dist/AuthProvider.cjs.map +0 -1
  71. package/dist/AuthProvider.d.ts +0 -7
  72. package/dist/AuthProvider.mjs +0 -2
  73. package/dist/AuthProvider.mjs.map +0 -1
  74. package/dist/LoginRenderer.cjs +0 -2
  75. package/dist/LoginRenderer.cjs.map +0 -1
  76. package/dist/LoginRenderer.d.ts +0 -20
  77. package/dist/LoginRenderer.mjs +0 -2
  78. package/dist/LoginRenderer.mjs.map +0 -1
  79. package/dist/composables.cjs +0 -2
  80. package/dist/composables.cjs.map +0 -1
  81. package/dist/composables.d.ts +0 -12
  82. package/dist/composables.mjs +0 -2
  83. package/dist/composables.mjs.map +0 -1
  84. package/dist/index.cjs +0 -2
  85. package/dist/index.cjs.map +0 -1
  86. package/dist/index.d.ts +0 -13
  87. package/dist/index.mjs +0 -2
  88. package/dist/index.mjs.map +0 -1
  89. package/dist/types.cjs.map +0 -1
  90. package/dist/types.mjs.map +0 -1
package/README.md CHANGED
@@ -1,132 +1,319 @@
1
1
  # @strivacity/sdk-next
2
2
 
3
- A Next.js library that integrates Strivacity's policy-driven authentication journeys into your application using the OAuth 2.0 PKCE flow. Supports `redirect`, `popup`, `native`, and `embedded` modes.
3
+ Next.js SDK for [Strivacity](https://www.strivacity.com) - adds PKCE-protected OIDC authentication to your Next.js application. Ships with a client SDK and a backend-for-frontend ([BFF](../../README.md#bff)) Server SDK that runs entirely inside your Next.js app via Route Handlers or API Routes - no separate backend required.
4
+
5
+ Built on top of [@strivacity/sdk-core](../sdk-core) - see the [core SDK documentation](../sdk-core/README.md) for detailed information about authentication flows, configuration options, and advanced features.
6
+
7
+ **See also:**
8
+ - [Full Documentation](https://docs.strivacity.com/reference/overview) - Complete guide for all authentication modes
9
+ - [Example App](../../apps/next) - Working Next.js example covering both client-managed and server-managed sessions
10
+ - [Core SDK](../sdk-core/README.md) - Framework-agnostic SDK documentation
11
+
12
+ ## Table of contents
13
+
14
+ - [Prerequisites](#prerequisites)
15
+ - [Installation](#installation)
16
+ - [Choosing a mode](#choosing-a-mode)
17
+ - [Client-managed vs. server-managed sessions](#client-managed-vs-server-managed-sessions)
18
+ - [Quick start](#quick-start)
19
+ - [Client SDK](#client-sdk)
20
+ - [Authentication modes](#authentication-modes)
21
+ - [redirect mode](#redirect-mode)
22
+ - [popup mode](#popup-mode)
23
+ - [embedded mode](#embedded-mode)
24
+ - [native mode](#native-mode)
25
+ - [Hooks API](#hooks-api)
26
+ - [useStrivacity](#usestrivacity)
27
+ - [useNativeLogin](#usenativelogin)
28
+ - [withAuthGuard (client)](#withauthguard-client)
29
+ - [Server SDK](#server-sdk)
30
+ - [Setup](#setup)
31
+ - [Accessing the session server-side](#accessing-the-session-server-side)
32
+ - [withAuthGuard](#withauthguard)
33
+ - [withApiAuthRequired](#withapiauthrequired)
34
+ - [Storages](#server-storages)
35
+ - [Back-channel logout](#back-channel-logout)
36
+ - [Server SDK API reference](#server-sdk-api-reference)
37
+ - [Server configuration reference](#server-configuration-reference)
38
+ - [Route guards](#route-guards)
39
+ - [Shared features](#shared-features)
40
+ - [Configuration reference](#configuration-reference)
41
+ - [Migration guide](#migration-guide)
42
+ - [Vulnerability Reporting](#vulnerability-reporting)
43
+ - [License](#license)
44
+ - [Contributing](#contributing)
4
45
 
5
- See our [Developer Portal](https://www.strivacity.com/learn-support/developer-hub) to get started with developing with the Strivacity product.
46
+ ---
6
47
 
7
- ## Overview
48
+ ## Prerequisites
8
49
 
9
- This SDK allows you to integrate Strivacity's policy-driven journeys into your Next.js application. It wraps the `@strivacity/sdk-core` library as a React context provider and exposes a `useStrivacity` hook that provides authentication state and methods throughout your component tree. The SDK uses the OAuth 2.0 PKCE flow to authenticate with Strivacity. For detailed configuration options, available modes, and advanced usage refer to the [`@strivacity/sdk-core` documentation](https://github.com/Strivacity/sdk-js/blob/main/packages/sdk-core/README.md).
50
+ - Next.js 15+ (App Router or Pages Router)
51
+ - React 18+
52
+ - A Strivacity tenant with an application configured (issuer URL, client ID, redirect URI)
10
53
 
11
- ## Demo Application
54
+ ---
12
55
 
13
- - [Example app](https://github.com/Strivacity/sdk-js/tree/main/apps/next)
14
- - [Headless example app](https://github.com/Strivacity/sdk-js/tree/main/apps/next-headless)
56
+ ## Installation
15
57
 
16
- ## Requirements
58
+ ```bash
59
+ npm install @strivacity/sdk-next
60
+ ```
17
61
 
18
- - Next.js: 14+
62
+ ---
19
63
 
20
- ## Install
64
+ ## Choosing a mode
21
65
 
22
- ```bash
23
- npm install @strivacity/sdk-next
66
+ The SDK supports **four authentication modes**:
67
+
68
+ | Mode | Login UI | Best for |
69
+ | ---------- | ---------------------------------------- | -------------------------------------------- |
70
+ | `redirect` | Strivacity hosted page | Standard web apps |
71
+ | `popup` | Strivacity hosted page in a popup | SPAs that must stay on the current page |
72
+ | `embedded` | Strivacity web components in your page | Branded login inside your own layout |
73
+ | `native` | Your own components driven by flow state | Full UI control, step-by-step form rendering |
74
+
75
+ > All modes use the same PKCE-protected OIDC flow under the hood. The `mode` option only controls where the login UI lives and how the flow state is consumed.
76
+
77
+ ---
78
+
79
+ ## Client-managed vs. server-managed sessions
80
+
81
+ The SDK supports two session strategies, selectable per app via a single option:
82
+
83
+ | Strategy | Tokens live in | Best for |
84
+ | -------- | --------------- | -------- |
85
+ | **Client-managed** | Browser storage (`localStorage` by default) | Simple SPAs that don't need to hide tokens from the browser |
86
+ | **Server-managed (BFF)** | Server-side storage (encrypted http-only cookies by default) via the [Server SDK](#server-sdk) | Apps that need to keep tokens inaccessible to client-side JavaScript, sign requests server-side, or add custom server-side validation |
87
+
88
+ Set `serverSessionUri` on the shared SDK options to switch the client SDK into server-managed mode - login requests are then routed through your own server endpoint instead of the SDK talking to the IDP directly, and tokens are never read from or written to client-side storage. See [Server-side session management](../sdk-core/README.md#server-side-session-management) in the core SDK docs for how this works under the hood. Both strategies are shown side by side below.
89
+
90
+ ---
91
+
92
+ ## Quick start
93
+
94
+ ### 1. Configure shared options
95
+
96
+ Both the client and Server SDK read from the same configuration - keep it in one file and import it from both sides:
97
+
98
+ ```ts
99
+ // lib/auth/options.ts
100
+ import type { SDKInitConfig } from '@strivacity/sdk-next/client';
101
+
102
+ export const sdkOptions: SDKInitConfig = {
103
+ mode: 'redirect', // authentication mode
104
+ issuer: 'https://<YOUR_TENANT_DOMAIN>', // OIDC provider URL
105
+ clientId: 'YOUR_CLIENT_ID', // OAuth2 client ID
106
+ redirectUri: 'https://your-app.example.com/callback', // callback URL after authentication
107
+ scopes: ['openid', 'profile', 'email'], // requested user permissions/data
108
+
109
+ // Omit this line entirely for client-managed sessions
110
+ serverSessionUri: '/auth/login',
111
+ };
112
+ ```
113
+
114
+ ### 2. Set up the Server SDK
115
+
116
+ Only needed for server-managed sessions - skip this step (and step 3's catch-all route) if you're using client-managed sessions.
117
+
118
+ ```ts
119
+ // lib/auth/server.ts
120
+ import { createServerSDK } from '@strivacity/sdk-next/server';
121
+ import { sdkOptions } from './options';
122
+
123
+ export const sdk = createServerSDK({
124
+ ...sdkOptions,
125
+ secret: process.env.SECRET, // http-only cookie encryption key (random 32+ characters)
126
+ postLoginRedirectUri: '/profile',
127
+ });
128
+ ```
129
+
130
+ ### 3. Mount the catch-all route
131
+
132
+ A single Route Handler wires up every auth route automatically - `sdk.handler` inspects the request path and dispatches to the matching internal handler:
133
+
134
+ ```ts
135
+ // app/auth/[...strivacity]/route.ts
136
+ import { sdk } from '../../../lib/auth/server';
137
+
138
+ export const GET = sdk.handler;
139
+ export const POST = sdk.handler;
140
+ export const PUT = sdk.handler;
141
+ export const PATCH = sdk.handler;
142
+ export const DELETE = sdk.handler;
24
143
  ```
25
144
 
26
- ## Usage
145
+ | Method | Path | Description | Response |
146
+ | ------ | --------------------------- | ---------------------------------------------------------- | -------------------------------------------- |
147
+ | `GET` | `/auth/login` | Starts the login flow and redirects to the IDP | `302` redirect to IDP |
148
+ | `GET` | `/auth/register` | Starts the registration flow and redirects to the IDP | `302` redirect to IDP |
149
+ | `GET` | `/auth/callback` | Completes authentication (handles the IDP callback) | `302` redirect or popup close script |
150
+ | `GET` | `/auth/refresh` | Refreshes the access token | `204 No Content` or `302` redirect |
151
+ | `GET` | `/auth/revoke` | Revokes tokens and clears the session | `204 No Content` |
152
+ | `GET` | `/auth/logout` | Ends the session and redirects to the IDP logout page | `302` redirect to IDP logout |
153
+ | `GET` | `/auth/entry` | Handles external flow entry (e.g., password reset link) - embedded/native modes only | JSON with session data |
154
+ | `POST` | `/auth/backchannel-logout` | Processes back-channel logout requests from the IDP | `204 No Content` |
27
155
 
28
- ### Initialization
156
+ > The `/auth` prefix and route names come from `authUrlPrefix` - see [Server configuration reference](#server-configuration-reference).
29
157
 
30
- Wrap your application with `StyAuthProvider` in your root layout. Use the `'use client'` directive since the provider uses React context:
158
+ ### 4. Wrap your app with the client provider
31
159
 
32
160
  ```tsx
33
- // app/providers.tsx
161
+ // lib/auth/provider.tsx
34
162
  'use client';
35
163
 
36
- import { StyAuthProvider, type SDKOptions } from '@strivacity/sdk-next';
37
-
38
- const options: SDKOptions = {
39
- mode: 'redirect', // or 'popup', 'native', 'embedded'
40
- issuer: 'https://<YOUR_DOMAIN>',
41
- scopes: ['openid', 'profile'],
42
- clientId: '<YOUR_CLIENT_ID>',
43
- redirectUri: '<YOUR_REDIRECT_URI>',
44
- };
164
+ import type { ReactNode } from 'react';
165
+ import type { SessionData } from '@strivacity/sdk-next/client';
166
+ import { StyAuthProvider } from '@strivacity/sdk-next/client';
167
+ import { sdkOptions } from './options';
45
168
 
46
- export function Providers({ children }: { children: React.ReactNode }) {
47
- return <StyAuthProvider options={options}>{children}</StyAuthProvider>;
169
+ export function AuthProvider({ session, children }: { session?: SessionData | null; children: ReactNode }) {
170
+ return (
171
+ <StyAuthProvider options={{ ...sdkOptions }} session={session}>
172
+ {children}
173
+ </StyAuthProvider>
174
+ );
48
175
  }
49
176
  ```
50
177
 
178
+ For client-managed sessions, omit the `session` prop - the client SDK loads and manages the session itself:
179
+
51
180
  ```tsx
52
181
  // app/layout.tsx
53
- import { Providers } from './providers';
182
+ import type { ReactNode } from 'react';
183
+ import { AuthProvider } from '../lib/auth/provider';
54
184
 
55
- export default function RootLayout({ children }: { children: React.ReactNode }) {
185
+ export default function RootLayout({ children }: { children: ReactNode }) {
56
186
  return (
57
- <html>
187
+ <html lang="en-US">
58
188
  <body>
59
- <Providers>{children}</Providers>
189
+ <AuthProvider>{children}</AuthProvider>
60
190
  </body>
61
191
  </html>
62
192
  );
63
193
  }
64
194
  ```
65
195
 
66
- Use the `useStrivacity` hook in any client component to access authentication state:
196
+ For server-managed sessions, fetch the session once in your root layout (a Server Component) via the Server SDK's `getSession()` and pass it down:
67
197
 
68
198
  ```tsx
69
- 'use client';
199
+ // app/layout.tsx
200
+ import type { ReactNode } from 'react';
201
+ import { AuthProvider } from '../lib/auth/provider';
202
+ import { sdk } from '../lib/auth/server';
70
203
 
71
- import { useStrivacity } from '@strivacity/sdk-next';
204
+ export const dynamic = 'force-dynamic';
72
205
 
73
- export default function MyComponent() {
74
- const { loading, isAuthenticated, idTokenClaims } = useStrivacity();
206
+ export default async function RootLayout({ children }: { children: ReactNode }) {
207
+ const session = await sdk.getSession();
208
+
209
+ return (
210
+ <html lang="en-US">
211
+ <body>
212
+ <AuthProvider session={session}>{children}</AuthProvider>
213
+ </body>
214
+ </html>
215
+ );
75
216
  }
76
217
  ```
77
218
 
78
- ### Redirect / Popup mode
219
+ ---
220
+
221
+ ## Client SDK
222
+
223
+ The client SDK is [@strivacity/sdk-react](../sdk-react) re-exported under `@strivacity/sdk-next/client` - every hook, component, and type documented in the [React SDK README](../sdk-react/README.md) works exactly the same way here, just imported from `@strivacity/sdk-next/client` instead of `@strivacity/sdk-react`. Mark components that use hooks with `'use client'`; pages/layouts that only compose client components can remain Server Components (as shown for `app/layout.tsx` in [Quick start](#quick-start)).
224
+
225
+ ### Authentication modes
226
+
227
+ #### redirect mode
228
+
229
+ > For details on how this mode works, see the [hosted journey documentation](https://docs.strivacity.com/reference/hosted-journey).
79
230
 
80
- In `redirect` mode the user is taken to the identity provider in the same window; in `popup` mode authentication happens in a popup. Both are initiated the same way from code.
231
+ The current browser tab navigates to the Strivacity-hosted login page and back to the configured `redirectUri` after authentication.
81
232
 
82
- #### Login page example
233
+ ##### Login
234
+
235
+ **Client-managed sessions**:
236
+
237
+ Call this to start the login flow. It redirects the user to the Strivacity login page in the current browser tab, where they authenticate.
83
238
 
84
239
  ```tsx
240
+ // app/login/page.tsx
85
241
  'use client';
86
242
 
87
243
  import { useEffect } from 'react';
88
- import { useStrivacity } from '@strivacity/sdk-next';
244
+ import { useStrivacity } from '@strivacity/sdk-next/client';
245
+ import type { RedirectFlow } from '@strivacity/sdk-next/client';
89
246
 
90
- export default function Login() {
91
- const { login } = useStrivacity();
247
+ export default function LoginPage() {
248
+ const { loading, login } = useStrivacity<RedirectFlow>();
92
249
 
93
250
  useEffect(() => {
94
- login();
95
- }, []);
251
+ if (loading) {
252
+ return;
253
+ }
254
+
255
+ void login({
256
+ // Optional parameters
257
+ loginHint: 'user@example.com', // identifier or JWT-encoded data to hint the login flow
258
+ acrValues: ['urn:strivacity:loa:2'], // request specific authentication context
259
+ audiences: ['https://api.example.com'], // target resources for the access token
260
+ });
261
+ }, [loading, login]);
96
262
 
97
263
  return (
98
264
  <section>
99
- <h1>Redirecting...</h1>
265
+ <h1>Redirecting to login...</h1>
100
266
  </section>
101
267
  );
102
268
  }
103
269
  ```
104
270
 
105
- #### Callback page example
271
+ **Server-managed sessions**:
272
+
273
+ Skip the client SDK entirely and redirect straight to `/auth/login` - the Server SDK builds the authorization request and redirects to the IDP:
274
+
275
+ ```tsx
276
+ // app/login/page.tsx
277
+ import { redirect } from 'next/navigation';
278
+
279
+ export default function LoginPage() {
280
+ redirect('/auth/login');
281
+ }
282
+ ```
283
+
284
+ ##### Handle the callback
106
285
 
107
- The callback page handles the response from the identity provider. It calls `handleCallback()` and redirects to `/profile` on success:
286
+ **Client-managed sessions**:
287
+
288
+ Call this on your redirect URI page after the IDP sends the user back. It parses the query parameters from the callback URL, verifies the state matches what was stored during login (CSRF protection), exchanges the authorization code for tokens using PKCE, validates the ID token, and stores the session in the [configured storage](../sdk-core/README.md#storages).
108
289
 
109
290
  ```tsx
291
+ // app/callback/page.tsx
110
292
  'use client';
111
293
 
112
294
  import { useEffect } from 'react';
113
295
  import { useRouter } from 'next/navigation';
114
- import { useStrivacity } from '@strivacity/sdk-next';
296
+ import { useStrivacity } from '@strivacity/sdk-next/client';
115
297
 
116
- export default function Callback() {
298
+ export default function CallbackPage() {
299
+ const { loading, handleCallback } = useStrivacity();
117
300
  const router = useRouter();
118
- const { handleCallback } = useStrivacity();
301
+ const searchParams = new URLSearchParams(window.location.search);
119
302
 
120
303
  useEffect(() => {
121
- (async () => {
122
- try {
123
- await handleCallback();
124
- router.push('/profile');
125
- } catch (error) {
126
- console.error('Error during callback handling:', error);
127
- }
128
- })();
129
- }, []);
304
+ if (loading) {
305
+ return;
306
+ }
307
+
308
+ if (searchParams.get('error')) {
309
+ router.replace(`/error?${searchParams.toString()}`);
310
+ return;
311
+ }
312
+
313
+ handleCallback()
314
+ .then(() => router.push('/profile'))
315
+ .catch((error) => router.push(`/error?message=${encodeURIComponent(error.message)}`));
316
+ }, [loading]);
130
317
 
131
318
  return (
132
319
  <section>
@@ -136,545 +323,1913 @@ export default function Callback() {
136
323
  }
137
324
  ```
138
325
 
139
- #### Profile page example
140
-
141
- ```tsx
142
- 'use client';
326
+ **Server-managed sessions**:
143
327
 
144
- import { useStrivacity } from '@strivacity/sdk-next';
328
+ Forward the callback query string to `/auth/callback` - the Server SDK completes the code exchange and redirects to `postLoginRedirectUri`:
145
329
 
146
- export default function Profile() {
147
- const { loading, isAuthenticated, accessToken, accessTokenExpired, accessTokenExpirationDate, idTokenClaims, refreshToken } = useStrivacity();
148
-
149
- if (loading) {
150
- return <h1>Loading...</h1>;
151
- }
330
+ ```tsx
331
+ // app/callback/page.tsx
332
+ import { redirect } from 'next/navigation';
152
333
 
153
- return (
154
- <section>
155
- <dl>
156
- <dt>
157
- <strong>accessToken</strong>
158
- </dt>
159
- <dd>
160
- <pre>{JSON.stringify(accessToken)}</pre>
161
- </dd>
162
- <dt>
163
- <strong>refreshToken</strong>
164
- </dt>
165
- <dd>
166
- <pre>{JSON.stringify(refreshToken)}</pre>
167
- </dd>
168
- <dt>
169
- <strong>accessTokenExpired</strong>
170
- </dt>
171
- <dd>
172
- <pre>{JSON.stringify(accessTokenExpired)}</pre>
173
- </dd>
174
- <dt>
175
- <strong>accessTokenExpirationDate</strong>
176
- </dt>
177
- <dd>
178
- <pre>{accessTokenExpirationDate ? new Date(accessTokenExpirationDate * 1000).toLocaleString() : JSON.stringify(null)}</pre>
179
- </dd>
180
- <dt>
181
- <strong>claims</strong>
182
- </dt>
183
- <dd>
184
- <pre>{JSON.stringify(idTokenClaims, null, 2)}</pre>
185
- </dd>
186
- </dl>
187
- </section>
188
- );
334
+ export default async function CallbackPage({ searchParams }: { searchParams: Promise<Record<string, string>> }) {
335
+ const params = new URLSearchParams(await searchParams);
336
+ redirect(`/auth/callback?${params.toString()}`);
189
337
  }
190
338
  ```
191
339
 
192
- #### Logout page example
340
+ ##### Registration
193
341
 
194
- The `postLogoutRedirectUri` parameter is optional and specifies where users are redirected after logout. This URI must be configured in the Admin Console as an allowed post-logout redirect URI.
342
+ **Client-managed sessions**:
343
+
344
+ Call this to start the registration flow. It works the same way as `login()` but opens the registration form instead.
195
345
 
196
346
  ```tsx
347
+ // app/register/page.tsx
197
348
  'use client';
198
349
 
199
350
  import { useEffect } from 'react';
200
- import { useRouter } from 'next/navigation';
201
- import { useStrivacity } from '@strivacity/sdk-next';
351
+ import { useStrivacity } from '@strivacity/sdk-next/client';
352
+ import type { RedirectFlow } from '@strivacity/sdk-next/client';
202
353
 
203
- export default function Logout() {
204
- const router = useRouter();
205
- const { isAuthenticated, logout } = useStrivacity();
354
+ export default function RegisterPage() {
355
+ const { loading, register } = useStrivacity<RedirectFlow>();
206
356
 
207
357
  useEffect(() => {
208
- (async () => {
209
- if (isAuthenticated) {
210
- await logout({ postLogoutRedirectUri: location.origin });
211
- } else {
212
- router.push('/');
213
- }
214
- })();
215
- }, []);
358
+ if (loading) {
359
+ return;
360
+ }
361
+
362
+ void register({
363
+ loginHint: 'user@example.com',
364
+ });
365
+ }, [loading, register]);
216
366
 
217
367
  return (
218
368
  <section>
219
- <h1>Logging out...</h1>
369
+ <h1>Redirecting to registration...</h1>
220
370
  </section>
221
371
  );
222
372
  }
223
373
  ```
224
374
 
225
- #### Component example
375
+ **Server-managed sessions**:
376
+
377
+ Skip the client SDK entirely and redirect straight to `/auth/register` - the Server SDK builds the registration request and redirects to the IDP:
378
+
379
+ ```tsx
380
+ // app/register/page.tsx
381
+ import { redirect } from 'next/navigation';
382
+
383
+ export default function RegisterPage() {
384
+ redirect('/auth/register');
385
+ }
386
+ ```
387
+
388
+ ##### Logout
389
+
390
+ **Client-managed sessions**:
391
+
392
+ Call this to clear the session and redirect to the Strivacity end-session endpoint. After that the user is redirected back to your app at `postLogoutRedirectUri`.
226
393
 
227
394
  ```tsx
395
+ // app/logout/page.tsx
228
396
  'use client';
229
397
 
230
- import { useStrivacity } from '@strivacity/sdk-next';
398
+ import { useEffect } from 'react';
399
+ import { useStrivacity } from '@strivacity/sdk-next/client';
231
400
 
232
- export default function Nav() {
233
- const { isAuthenticated, idTokenClaims, login, logout } = useStrivacity();
234
- const name = `${idTokenClaims?.given_name} ${idTokenClaims?.family_name}`;
401
+ export default function LogoutPage() {
402
+ const { loading, logout } = useStrivacity();
235
403
 
236
- return isAuthenticated ? (
237
- <div>
238
- <div>Welcome, {name}!</div>
239
- <button onClick={() => logout()}>Logout</button>
240
- </div>
241
- ) : (
242
- <div>
243
- <div>Not logged in</div>
244
- <button onClick={() => login()}>Log in</button>
245
- </div>
246
- );
404
+ useEffect(() => {
405
+ if (loading) {
406
+ return;
407
+ }
408
+
409
+ void logout();
410
+ }, [loading, logout]);
411
+
412
+ return null;
247
413
  }
248
414
  ```
249
415
 
250
- ### Native mode
416
+ **Server-managed sessions**:
251
417
 
252
- In `native` mode the `StyLoginRenderer` component renders the authentication UI inline using your custom widget components. You can define custom components for each input type; see [Example widgets](https://github.com/Strivacity/sdk-js/tree/main/apps/next/src/components/widgets).
418
+ With `serverSessionUri` configured, redirect to `/auth/logout` instead - the Server SDK clears the session and redirects to the IDP end-session endpoint:
253
419
 
254
- The example widgets use SCSS for styling and Luxon for date handling:
420
+ ```tsx
421
+ // app/logout/page.tsx
422
+ import { redirect } from 'next/navigation';
255
423
 
256
- ```bash
257
- npm install sass luxon
258
- npm install --save-dev @types/luxon
259
- ```
260
-
261
- ```tsx
262
- import CheckboxWidget from './checkbox.widget';
263
- import DateWidget from './date.widget';
264
- import InputWidget from './input.widget';
265
- import LayoutWidget from './layout.widget';
266
- import MultiSelectWidget from './multiselect.widget';
267
- import PasscodeWidget from './passcode.widget';
268
- import LoadingWidget from './loading.widget';
269
- import PasswordWidget from './password.widget';
270
- import PhoneWidget from './phone.widget';
271
- import SelectWidget from './select.widget';
272
- import StaticWidget from './static.widget';
273
- import SubmitWidget from './submit.widget';
274
-
275
- export const widgets = {
276
- checkbox: CheckboxWidget,
277
- date: DateWidget,
278
- input: InputWidget,
279
- layout: LayoutWidget,
280
- loading: LoadingWidget,
281
- passcode: PasscodeWidget,
282
- password: PasswordWidget,
283
- phone: PhoneWidget,
284
- select: SelectWidget,
285
- multiSelect: MultiSelectWidget,
286
- static: StaticWidget,
287
- submit: SubmitWidget,
288
- };
424
+ export default function LogoutPage() {
425
+ redirect('/auth/logout');
426
+ }
289
427
  ```
290
428
 
291
- #### Login page example
429
+ ##### Token management
292
430
 
293
- The login page extracts `session_id` and optionally `language` from the URL on load, cleans up the URL, and passes them to the renderer. When a `session_id` is present the renderer calls `startSession(sessionId)` to resume the existing flow instead of starting a new one. When a `language` parameter is present it is passed to the renderer which uses it for the authentication UI and calls `onLanguageChange` with the resolved language.
431
+ **Client-managed sessions**:
432
+
433
+ Call these methods to manage the session and access token client-side.
294
434
 
295
435
  ```tsx
296
436
  'use client';
297
437
 
298
- import { useEffect, useState } from 'react';
299
- import { useRouter } from 'next/navigation';
300
- import { StyLoginRenderer, FallbackError, type LoginFlowState } from '@strivacity/sdk-next';
301
- import { widgets } from '@/components/widgets';
438
+ import { useStrivacity } from '@strivacity/sdk-next/client';
302
439
 
303
- export default function Login() {
304
- const router = useRouter();
305
- const [sessionId, setSessionId] = useState<string | null>(null);
306
- const [language, setLanguage] = useState<string | null>(null);
440
+ export function TokenPanel() {
441
+ const { loading, idTokenClaims, accessToken, refreshToken, refresh, revoke } = useStrivacity();
307
442
 
308
- useEffect(() => {
309
- if (window.location.search !== '') {
310
- const url = new URL(window.location.href);
311
- setSessionId(url.searchParams.get('session_id'));
443
+ async function onRefresh() {
444
+ // Refresh the access token using the refresh token
445
+ await refresh();
446
+ }
312
447
 
313
- if (url.searchParams.has('language')) {
314
- setLanguage(url.searchParams.get('language'));
315
- }
448
+ async function onRevoke() {
449
+ // Revoke all tokens at the authorization server and clear the local session
450
+ await revoke();
451
+ }
316
452
 
317
- url.search = '';
318
- window.history.replaceState({}, '', url.toString());
319
- }
320
- }, []);
453
+ if (loading) {
454
+ return null;
455
+ }
456
+
457
+ // idTokenClaims, accessToken, and refreshToken are plain values from context -
458
+ // they update automatically whenever the SDK's session changes
459
+ return (
460
+ <div>
461
+ <button onClick={onRefresh}>Refresh</button>
462
+ <button onClick={onRevoke}>Revoke</button>
463
+ <pre>{JSON.stringify({ idTokenClaims, accessToken, refreshToken }, null, 2)}</pre>
464
+ </div>
465
+ );
466
+ }
467
+ ```
321
468
 
322
- const onLogin = async () => {
323
- router.push('/profile');
324
- };
469
+ **Server-managed sessions**:
325
470
 
326
- const onFallback = (error: FallbackError) => {
327
- if (error.url) {
328
- window.location.href = error.url.toString();
329
- } else {
330
- alert(error);
331
- }
332
- };
471
+ With `serverSessionUri` configured, tokens are refreshed/revoked by the Server SDK - trigger it by navigating to the catch-all route, then let it redirect back:
333
472
 
334
- const onError = (error: string) => {
335
- alert(error);
336
- };
473
+ ```tsx
474
+ 'use client';
337
475
 
338
- const onGlobalMessage = (message: string) => {
339
- alert(message);
340
- };
476
+ export function TokenPanel() {
477
+ function onRefresh() {
478
+ // sdk.refreshSession() runs server-side, then redirects back to returnTo
479
+ window.location.href = '/auth/refresh?returnTo=/profile';
480
+ }
341
481
 
342
- const onBlockReady = ({ previousState, state }: { previousState: LoginFlowState; state: LoginFlowState }) => {
343
- console.log('previousState', previousState);
344
- console.log('state', state);
345
- };
482
+ function onRevoke() {
483
+ // sdk.revokeSession() runs server-side, then redirects to postLogoutRedirectUri
484
+ window.location.href = '/auth/revoke';
485
+ }
346
486
 
347
487
  return (
348
- <StyLoginRenderer
349
- widgets={widgets}
350
- sessionId={sessionId}
351
- language={language}
352
- onLanguageChange={(lang) => setLanguage(lang)}
353
- onFallback={onFallback}
354
- onLogin={() => void onLogin()}
355
- onError={onError}
356
- onGlobalMessage={onGlobalMessage}
357
- onBlockReady={onBlockReady}
358
- />
488
+ <div>
489
+ <button onClick={onRefresh}>Refresh</button>
490
+ <button onClick={onRevoke}>Revoke</button>
491
+ </div>
359
492
  );
360
493
  }
361
494
  ```
362
495
 
363
- #### Callback page example
496
+ ---
497
+
498
+ #### popup mode
499
+
500
+ > For details on how this mode works, see the [hosted journey documentation](https://docs.strivacity.com/reference/hosted-journey).
501
+
502
+ The Strivacity login page opens in a separate window or tab. After authentication the opened window or tab closes itself and the parent page receives the session - no full-page navigation required.
503
+
504
+ ##### Login
505
+
506
+ **Client-managed sessions**:
507
+
508
+ Call this to start the login flow. It opens a popup window by default with the Strivacity login page, where the user authenticates. After that the popup closes itself and the session is stored in the [configured storage](../sdk-core/README.md#storages).
364
509
 
365
- When a `session_id` is present in the URL the native flow is resumed by forwarding it to the login page. Otherwise the standard `handleCallback()` path is used:
510
+ By default a centered popup window opens. Pass `popupWindowTarget` to change where the window opens, and `popupWindowFeatures` to control its size and position:
366
511
 
367
512
  ```tsx
513
+ // app/login/page.tsx
368
514
  'use client';
369
515
 
370
- import { useEffect } from 'react';
516
+ import { useEffect, useRef } from 'react';
371
517
  import { useRouter } from 'next/navigation';
372
- import { useStrivacity } from '@strivacity/sdk-next';
518
+ import { useStrivacity } from '@strivacity/sdk-next/client';
519
+ import type { PopupFlow } from '@strivacity/sdk-next/client';
373
520
 
374
- export default function Callback() {
521
+ export default function LoginPage() {
522
+ const { loading, login } = useStrivacity<PopupFlow>();
375
523
  const router = useRouter();
376
- const { handleCallback } = useStrivacity();
524
+ const startedRef = useRef(false);
377
525
 
378
526
  useEffect(() => {
379
- (async () => {
380
- const url = new URL(location.href);
381
- const sessionId = url.searchParams.get('session_id');
382
-
383
- if (sessionId) {
384
- router.push(`/login?session_id=${sessionId}`);
385
- } else {
386
- try {
387
- await handleCallback();
388
- router.push('/profile');
389
- } catch (error) {
390
- console.error('Error during callback handling:', error);
391
- }
392
- }
393
- })();
394
- }, []);
527
+ if (loading || startedRef.current) {
528
+ return;
529
+ }
530
+
531
+ // Prevent multiple calls (React StrictMode)
532
+ startedRef.current = true;
533
+
534
+ login({
535
+ // Optional parameters
536
+ loginHint: 'user@example.com', // identifier or JWT-encoded data to hint the login flow
537
+ acrValues: ['urn:strivacity:loa:2'], // request specific authentication context
538
+ audiences: ['https://api.example.com'], // target resources for the access token
539
+ popupWindowTarget: '_blank', // any valid browsing context name
540
+ popupWindowFeatures: {
541
+ width: 500,
542
+ height: 700,
543
+ left: 100,
544
+ top: 100,
545
+ toolbar: false,
546
+ location: false,
547
+ resizable: true,
548
+ scrollbars: true,
549
+ },
550
+ })
551
+ .then(() => router.push('/profile'))
552
+ .catch((error) => router.push(`/error?message=${encodeURIComponent(error.message)}`));
553
+ }, [loading, login, router]);
395
554
 
396
555
  return (
397
556
  <section>
398
- <h1>Logging in...</h1>
557
+ <h1>Opening login popup...</h1>
399
558
  </section>
400
559
  );
401
560
  }
402
561
  ```
403
562
 
404
- #### Entry page example
563
+ **Server-managed sessions**:
405
564
 
406
- The entry page processes flows started by an external process (e.g. password reset) by calling `entry()` to extract the necessary parameters to resume the flow and forwarding them to the callback page:
565
+ Popup mode always needs client-side JavaScript to open the window, so there's no server-only alternative here.
566
+
567
+ ##### Handle the callback
568
+
569
+ **Client-managed sessions**:
570
+
571
+ The popup resolves automatically - no callback page is needed. Token exchange happens inside the popup and the result is posted back to the opener window.
572
+
573
+ **Server-managed sessions**:
574
+
575
+ Same as client managed - the popup's internal callback request is also transparently proxied through `/auth/callback`, and the result is posted back to the opener window exactly the same way.
576
+
577
+ ##### Registration
578
+
579
+ **Client-managed sessions**:
580
+
581
+ Call this to start the registration flow. It works the same way as `login()` but opens the registration form instead.
407
582
 
408
583
  ```tsx
584
+ // app/register/page.tsx
409
585
  'use client';
410
586
 
411
- import { useEffect } from 'react';
587
+ import { useEffect, useRef } from 'react';
412
588
  import { useRouter } from 'next/navigation';
413
- import { useStrivacity } from '@strivacity/sdk-next';
589
+ import { useStrivacity } from '@strivacity/sdk-next/client';
590
+ import type { PopupFlow } from '@strivacity/sdk-next/client';
414
591
 
415
- export default function Entry() {
592
+ export default function RegisterPage() {
593
+ const { loading, register } = useStrivacity<PopupFlow>();
416
594
  const router = useRouter();
417
- const { entry } = useStrivacity();
595
+ const startedRef = useRef(false);
418
596
 
419
597
  useEffect(() => {
420
- (async () => {
421
- try {
422
- const data = await entry();
423
-
424
- if (data && Object.keys(data).length > 0) {
425
- router.push(`/callback?${new URLSearchParams(data).toString()}`);
426
- } else {
427
- router.push('/');
428
- }
429
- } catch (error) {
430
- console.error('Entry failed:', error);
431
- router.push('/');
432
- }
433
- })();
434
- }, []);
598
+ if (loading || startedRef.current) {
599
+ return;
600
+ }
601
+
602
+ // Prevent multiple calls (React StrictMode)
603
+ startedRef.current = true;
604
+
605
+ register({
606
+ // Optional parameters
607
+ loginHint: 'user@example.com', // identifier or JWT-encoded data to hint the login flow
608
+ acrValues: ['urn:strivacity:loa:2'], // request specific authentication context
609
+ audiences: ['https://api.example.com'], // target resources for the access token
610
+ popupWindowTarget: '_blank', // any valid browsing context name
611
+ popupWindowFeatures: {
612
+ width: 500,
613
+ height: 700,
614
+ left: 100,
615
+ top: 100,
616
+ toolbar: false,
617
+ location: false,
618
+ resizable: true,
619
+ scrollbars: true,
620
+ },
621
+ })
622
+ .then(() => router.push('/profile'))
623
+ .catch((error) => router.push(`/error?message=${encodeURIComponent(error.message)}`));
624
+ }, [loading, register, router]);
435
625
 
436
626
  return (
437
627
  <section>
438
- <h1>Loading...</h1>
628
+ <h1>Opening registration popup...</h1>
439
629
  </section>
440
630
  );
441
631
  }
442
632
  ```
443
633
 
444
- #### Profile page example
634
+ **Server-managed sessions**:
445
635
 
446
- Same as the profile page example in redirect/popup mode.
636
+ Popup mode always needs client-side JavaScript to open the window, so there's no server-only alternative here.
447
637
 
448
- #### Logout page example
638
+ ##### Logout
449
639
 
450
- Same as the logout page example in redirect/popup mode.
640
+ **Client-managed sessions**:
451
641
 
452
- ### Embedded mode
453
-
454
- In `embedded` mode the `<sty-login>` web component (loaded via `bundle.js` from the cluster) handles rendering. Import the bundle at application startup to register the Strivacity web components:
642
+ Call this to clear the session and redirect to the Strivacity end-session endpoint. After that the user is redirected back to your app at `postLogoutRedirectUri`.
455
643
 
456
644
  ```tsx
457
- // app/providers.tsx
645
+ // app/logout/page.tsx
458
646
  'use client';
459
647
 
460
648
  import { useEffect } from 'react';
461
- import { StyAuthProvider } from '@strivacity/sdk-next';
649
+ import { useStrivacity } from '@strivacity/sdk-next/client';
650
+
651
+ export default function LogoutPage() {
652
+ const { loading, logout } = useStrivacity();
462
653
 
463
- export function Providers({ children }: { children: React.ReactNode }) {
464
654
  useEffect(() => {
465
- void import(`${process.env.NEXT_PUBLIC_ISSUER}/assets/components/bundle.js`);
466
- }, []);
655
+ if (loading) {
656
+ return;
657
+ }
467
658
 
468
- return (
469
- <StyAuthProvider
470
- options={{
471
- mode: 'embedded',
472
- issuer: 'https://<YOUR_DOMAIN>',
473
- scopes: ['openid', 'profile'],
474
- clientId: '<YOUR_CLIENT_ID>',
475
- redirectUri: '<YOUR_REDIRECT_URI>',
476
- }}
477
- >
478
- {children}
479
- </StyAuthProvider>
480
- );
659
+ void logout();
660
+ }, [loading, logout]);
661
+
662
+ return null;
481
663
  }
482
664
  ```
483
665
 
484
- ## Logging
485
-
486
- The SDK supports optional logging to help you debug authentication flows and monitor SDK behavior. You can enable the built-in console logger or provide your own custom logger implementation.
666
+ **Server-managed sessions**:
487
667
 
488
- ### Using the Default Logger
489
-
490
- Enable the default console logger by adding the `logging` option:
668
+ With `serverSessionUri` configured, redirect to `/auth/logout` instead - the Server SDK clears the session and redirects to the IDP end-session endpoint:
491
669
 
492
670
  ```tsx
493
- import { StyAuthProvider, DefaultLogging, type SDKOptions } from '@strivacity/sdk-next';
671
+ // app/logout/page.tsx
672
+ import { redirect } from 'next/navigation';
494
673
 
495
- const options: SDKOptions = {
496
- mode: 'redirect',
497
- issuer: 'https://<YOUR_DOMAIN>',
498
- scopes: ['openid', 'profile'],
499
- clientId: '<YOUR_CLIENT_ID>',
500
- redirectUri: '<YOUR_REDIRECT_URI>',
501
- logging: DefaultLogging,
502
- };
674
+ export default function LogoutPage() {
675
+ redirect('/auth/logout');
676
+ }
503
677
  ```
504
678
 
505
- ### Creating a Custom Logger
679
+ ##### Token management
506
680
 
507
- Implement the `SDKLogging` interface and pass your class to the `logging` option:
681
+ **Client-managed sessions**:
508
682
 
509
- ```typescript
510
- import type { SDKLogging } from '@strivacity/sdk-next';
683
+ Call these methods to manage the session and access token client-side.
511
684
 
512
- export class MyLogger implements SDKLogging {
513
- xEventId?: string;
685
+ ```tsx
686
+ 'use client';
514
687
 
515
- debug(message: string): void {
516
- console.debug(this.xEventId ? `[${this.xEventId}] ${message}` : message);
517
- }
688
+ import { useStrivacity } from '@strivacity/sdk-next/client';
518
689
 
519
- info(message: string): void {
520
- console.info(this.xEventId ? `[${this.xEventId}] ${message}` : message);
690
+ export function TokenPanel() {
691
+ const { loading, idTokenClaims, accessToken, refreshToken, refresh, revoke } = useStrivacity();
692
+
693
+ async function onRefresh() {
694
+ // Refresh the access token using the refresh token
695
+ await refresh();
521
696
  }
522
697
 
523
- warn(message: string): void {
524
- console.warn(this.xEventId ? `[${this.xEventId}] ${message}` : message);
698
+ async function onRevoke() {
699
+ // Revoke all tokens at the authorization server and clear the local session
700
+ await revoke();
525
701
  }
526
702
 
527
- error(message: string, error: Error): void {
528
- console.error(this.xEventId ? `[${this.xEventId}] ${message}` : message, error);
703
+ if (loading) {
704
+ return null;
529
705
  }
706
+
707
+ // idTokenClaims, accessToken, and refreshToken are plain values from context -
708
+ // they update automatically whenever the SDK's session changes
709
+ return (
710
+ <div>
711
+ <button onClick={onRefresh}>Refresh</button>
712
+ <button onClick={onRevoke}>Revoke</button>
713
+ <pre>{JSON.stringify({ idTokenClaims, accessToken, refreshToken }, null, 2)}</pre>
714
+ </div>
715
+ );
530
716
  }
531
717
  ```
532
718
 
533
- The `SDKLogging` interface requires `debug`, `info`, `warn`, and `error` methods. The optional `xEventId` property, when set by the SDK, provides a correlation ID to trace related log messages across the authentication flow.
534
-
535
- ## HTTP Client
719
+ **Server-managed sessions**:
536
720
 
537
- The SDK uses a built-in `fetch`-based HTTP client for all requests. You can replace it with your own implementation by extending `SDKHttpClient` and passing your class via the `httpClient` option. This is useful when you need to attach custom headers (e.g. `x-sty-app-id`) to every outgoing request or route traffic through a proxy.
538
-
539
- ### Adding custom headers to every request
721
+ With `serverSessionUri` configured, tokens are refreshed/revoked by the Server SDK - trigger it by navigating to the catch-all route, then let it redirect back:
540
722
 
541
723
  ```tsx
542
- import { StyAuthProvider, SDKHttpClient, type HttpClientResponse, type SDKOptions } from '@strivacity/sdk-next';
543
-
544
- class CustomHttpClient extends SDKHttpClient {
545
- async request<T>(url: string, options?: RequestInit): Promise<HttpClientResponse<T>> {
546
- const mergedOptions: RequestInit = {
547
- ...options,
548
- headers: {
549
- 'x-sty-app-id': 'my-app',
550
- ...(options?.headers as Record<string, string>),
551
- },
552
- };
724
+ 'use client';
553
725
 
554
- const response = await fetch(url, mergedOptions);
726
+ export function TokenPanel() {
727
+ function onRefresh() {
728
+ // sdk.refreshSession() runs server-side, then redirects back to returnTo
729
+ window.location.href = '/auth/refresh?returnTo=/profile';
730
+ }
555
731
 
556
- return {
557
- headers: response.headers,
558
- ok: response.ok,
559
- status: response.status,
560
- statusText: response.statusText,
561
- url: response.url,
562
- json: async () => (await response.json()) as T,
563
- text: async () => await response.text(),
564
- };
732
+ function onRevoke() {
733
+ // sdk.revokeSession() runs server-side, then redirects to postLogoutRedirectUri
734
+ window.location.href = '/auth/revoke';
565
735
  }
566
- }
567
736
 
568
- const options: SDKOptions = {
569
- // ...other options
570
- httpClient: CustomHttpClient,
571
- };
737
+ return (
738
+ <div>
739
+ <button onClick={onRefresh}>Refresh</button>
740
+ <button onClick={onRevoke}>Revoke</button>
741
+ </div>
742
+ );
743
+ }
572
744
  ```
573
745
 
574
- Any header you add inside `request()` is automatically included in every SDK request
746
+ ---
575
747
 
576
- ### CORS configuration
748
+ #### embedded mode
577
749
 
578
- For custom request headers to reach the Strivacity cluster, the cluster must be configured to explicitly allow them. Add the header name(s) to the **Access-Control-Allow-Headers** list in the cluster settings. Without this, browsers will block the preflight `OPTIONS` request and the SDK call will fail with a CORS error.
750
+ > For details on how this mode works, see the [embedded journey documentation](https://docs.strivacity.com/reference/embedded-journey).
579
751
 
580
- ```
581
- Access-Control-Allow-Headers: x-sty-app-id, <any other custom headers>
582
- ```
752
+ The login UI renders inside your own page using Strivacity web components (`<sty-login>`, `<sty-notifications>`, `<sty-language-selector>`). The component bundle isn't an npm package - load it dynamically from your Strivacity tenant cluster once, on the login page:
583
753
 
584
- ## API Documentation
754
+ ##### Login / Register
585
755
 
586
- ### `useStrivacity` hook
756
+ **Client-managed sessions**:
587
757
 
588
- ```typescript
589
- useStrivacity<T extends PopupContext | RedirectContext | NativeContext>(): T;
590
- ```
758
+ ```tsx
759
+ // app/login/page.tsx
760
+ 'use client';
591
761
 
592
- The hook returns a different context type depending on the `mode` configured in `StyAuthProvider`.
762
+ import { useEffect, useRef } from 'react';
763
+ import { useRouter } from 'next/navigation';
764
+ import { useStrivacity, injectScript } from '@strivacity/sdk-next/client';
765
+ import type { LoginComponent } from '@strivacity/sdk-next/client/types';
593
766
 
594
- **Shared properties (all modes)**
767
+ export default function LoginPage() {
768
+ const router = useRouter();
769
+ const { sdk } = useStrivacity();
770
+ const searchParams = new URLSearchParams(window.location.search);
595
771
 
596
- - **`sdk: RedirectFlow | PopupFlow | NativeFlow`**: The underlying SDK flow instance.
597
- - **`loading: boolean`**: `true` while the session is being initialized.
598
- - **`options: SDKOptions`**: The configured SDK options.
599
- - **`isAuthenticated: boolean`**: `true` when the user has a valid session.
600
- - **`idTokenClaims: IdTokenClaims | null`**: Claims from the ID token, or `null` if not authenticated.
601
- - **`accessToken: string | null`**: The current access token.
602
- - **`refreshToken: string | null`**: The current refresh token.
603
- - **`accessTokenExpired: boolean`**: `true` when the access token has expired.
604
- - **`accessTokenExpirationDate: number | null`**: Expiration timestamp (Unix seconds) of the access token.
772
+ // Optional: Resume a session started from an entry URL (e.g., password reset)
773
+ const sessionId = searchParams.get('session_id');
774
+ const shortAppId = searchParams.get('short_app_id');
775
+ const lang = searchParams.get('language') ?? navigator.language;
776
+ const loginRef = useRef<LoginComponent>(null);
605
777
 
606
- ---
778
+ useEffect(() => {
779
+ // injectScript loads the <sty-login>/<sty-notifications>/<sty-language-selector>
780
+ // custom element definitions from the auth server
781
+ injectScript('sty-components', `${sdk.options.issuer}/assets/components/bundle.js`);
782
+ }, [sdk]);
607
783
 
608
- **Type: `RedirectContext`**
784
+ useEffect(() => {
785
+ const element = loginRef.current;
609
786
 
610
- - **`login(options?: LoginOptions): Promise<void>`**: Initiates login by redirecting to the identity provider.
611
- - **`register(options?: RegisterOptions): Promise<void>`**: Initiates registration using a redirect flow.
612
- - **`refresh(): Promise<void>`**: Refreshes the user's session.
613
- - **`revoke(): Promise<void>`**: Revokes the current session tokens.
614
- - **`logout(options?: LogoutOptions): Promise<void>`**: Logs the user out via redirect.
615
- - **`handleCallback(url?: string): Promise<void>`**: Processes the authorization callback after redirect.
616
- - **`entry(): Promise<Record<string, string>>`**: Processes an externally-initiated flow URL and returns the parameters needed to resume the flow.
787
+ if (!element) {
788
+ return;
789
+ }
617
790
 
618
- ---
791
+ element.params = {
792
+ loginHint: 'user@example.com', // identifier or JWT-encoded data to hint the login flow
793
+ acrValues: ['urn:strivacity:loa:2'], // request specific authentication context
794
+ audiences: ['https://api.example.com'], // target resources for the access token
795
+ language: 'en-US', // set the UI language (BCP 47 language tag)
796
+ prompt: 'login', // use 'create' to open the registration flow instead
797
+ };
798
+ element.sessionId = sessionId;
799
+ element.shortAppId = shortAppId;
800
+ element.lang = lang;
801
+
802
+ const onLogin = () => router.push('/profile');
803
+ const onClose = () => window.location.reload();
804
+ const onError = (event: Event) => router.push(`/error?message=${encodeURIComponent((event as CustomEvent<string>).detail)}`);
805
+
806
+ element.addEventListener('login', onLogin);
807
+ element.addEventListener('close', onClose);
808
+ element.addEventListener('error', onError);
809
+
810
+ return () => {
811
+ element.removeEventListener('login', onLogin);
812
+ element.removeEventListener('close', onClose);
813
+ element.removeEventListener('error', onError);
814
+ };
815
+ }, [router, sessionId, shortAppId, lang]);
619
816
 
620
- **Type: `PopupContext`**
817
+ return (
818
+ <section>
819
+ <sty-notifications></sty-notifications>
820
+ <sty-login ref={loginRef}></sty-login>
821
+ <sty-language-selector></sty-language-selector>
822
+ </section>
823
+ );
824
+ }
825
+ ```
621
826
 
622
- - **`login(options?: LoginOptions): Promise<void>`**: Initiates login using a popup window.
623
- - **`register(options?: RegisterOptions): Promise<void>`**: Initiates registration using a popup.
624
- - **`refresh(): Promise<void>`**: Refreshes the user's session.
625
- - **`revoke(): Promise<void>`**: Revokes the current session tokens.
626
- - **`logout(options?: LogoutOptions): Promise<void>`**: Logs the user out via popup.
627
- - **`handleCallback(url?: string): Promise<void>`**: Processes the authorization callback.
628
- - **`entry(): Promise<Record<string, string>>`**: Processes an externally-initiated flow URL.
827
+ **Server-managed sessions**:
629
828
 
630
- ---
829
+ With `serverSessionUri` configured the code above is unchanged - the `<sty-login>` component's internal requests are transparently proxied through `/auth/login`/`/auth/register` instead of going straight to the IDP.
631
830
 
632
- **Type: `NativeContext`**
831
+ ##### Controlling when the flow starts
633
832
 
634
- - **`login(options?: LoginOptions): Promise<NativeFlowHandler>`**: Initiates login using the native flow.
635
- - **`register(options?: RegisterOptions): Promise<NativeFlowHandler>`**: Initiates registration using the native flow.
636
- - **`refresh(): Promise<void>`**: Refreshes the user's session.
637
- - **`revoke(): Promise<void>`**: Revokes the current session tokens.
638
- - **`logout(options?: LogoutOptions): Promise<void>`**: Logs the user out via redirect.
639
- - **`handleCallback(url?: string): Promise<void>`**: Processes the authorization callback.
640
- - **`entry(): Promise<Record<string, string>>`**: Processes an externally-initiated flow URL.
833
+ By default the login flow starts automatically as soon as `<sty-login>` connects to the DOM. Add the `lazy` attribute to take manual control, then call `start()` when ready. `start()` accepts an optional params object forwarded to the authorization request, or you can set params via the `params` property before calling it:
641
834
 
642
- ---
835
+ ```tsx
836
+ // app/login/page.tsx
837
+ 'use client';
643
838
 
644
- ### `StyLoginRenderer` component
839
+ import { useRef } from 'react';
840
+ import { useRouter } from 'next/navigation';
841
+ import type { LoginComponent } from '@strivacity/sdk-next/client/types';
645
842
 
646
- Used in `native` mode to render the authentication UI with your own widget components.
843
+ export function LazyLogin() {
844
+ const router = useRouter();
845
+ const loginRef = useRef<LoginComponent>(null);
846
+
847
+ async function onStartClick() {
848
+ await loginRef.current?.start({
849
+ loginHint: 'user@example.com', // identifier or JWT-encoded data to hint the login flow
850
+ acrValues: ['urn:strivacity:loa:2'], // request specific authentication context
851
+ audiences: ['https://api.example.com'], // target resources for the access token
852
+ language: 'en-US', // set the UI language (BCP 47 language tag)
853
+ prompt: 'login', // use 'create' to open the registration flow instead
854
+ });
855
+ }
647
856
 
648
- **Props**
857
+ return (
858
+ <section>
859
+ <sty-notifications></sty-notifications>
860
+ <button onClick={onStartClick}>Continue to login</button>
861
+ <sty-login ref={loginRef} lazy onLogin={() => router.push('/profile')}></sty-login>
862
+ <sty-language-selector></sty-language-selector>
863
+ </section>
864
+ );
865
+ }
866
+ ```
649
867
 
650
- - **`params?: NativeParams`**: Additional parameters for the native login flow.
651
- - **`widgets?: PartialRecord<WidgetType, React.ComponentType>`**: Custom React components for each widget type used in the flow.
652
- - **`sessionId?: string | null`**: Session ID for resuming an existing authentication session.
653
- - **`language?: string | null`**: Language tag (e.g. `"en-US"`) for the authentication UI. Defaults to `navigator.language`. After the session starts the component calls `onLanguageChange` with the resolved language. See the [Translations](https://docs.strivacity.com/docs/translations) page to learn about language precedence implemented by the product.
868
+ You can also set params via the `params` property before calling `start()`:
654
869
 
655
- **Event callbacks**
870
+ ```tsx
871
+ // app/login/page.tsx
872
+ 'use client';
656
873
 
657
- - **`onLogin`**: Called on successful authentication. Receives `IdTokenClaims | null`.
658
- - **`onFallback`**: Called when the native flow needs to fall back to redirect. Receives `FallbackError` with a fallback URL.
659
- - **`onError`**: Called when an error occurs during authentication.
660
- - **`onGlobalMessage`**: Called when the flow wants to display a global message (e.g. account lockout warning).
661
- - **`onBlockReady`**: Called on flow state transitions. Receives `{ previousState: LoginFlowState; state: LoginFlowState }`. Useful for analytics and custom logging.
662
- - **`onLanguageChange`**: Called after the session starts with the resolved language string.
874
+ import { useRef } from 'react';
875
+ import { useRouter } from 'next/navigation';
876
+ import type { LoginComponent } from '@strivacity/sdk-next/client/types';
663
877
 
664
- ## Vulnerability Reporting
878
+ export function LazyLogin() {
879
+ const router = useRouter();
880
+ const loginRef = useRef<LoginComponent>(null);
665
881
 
666
- The [Guidelines for responsible disclosure](https://www.strivacity.com/report-a-security-issue) details the procedure for disclosing security issues. Please do not report security vulnerabilities on the public issue tracker.
882
+ async function onStartClick() {
883
+ if (!loginRef.current) {
884
+ return;
885
+ }
667
886
 
668
- ## License
887
+ loginRef.current.params = {
888
+ loginHint: 'user@example.com', // identifier or JWT-encoded data to hint the login flow
889
+ acrValues: ['urn:strivacity:loa:2'], // request specific authentication context
890
+ audiences: ['https://api.example.com'], // target resources for the access token
891
+ language: 'en-US', // set the UI language (BCP 47 language tag)
892
+ prompt: 'login', // use 'create' to open the registration flow instead
893
+ };
894
+ await loginRef.current.start();
895
+ }
896
+
897
+ return (
898
+ <>
899
+ <sty-login ref={loginRef} lazy></sty-login>
900
+ <button onClick={onStartClick}>Start Login</button>
901
+ </>
902
+ );
903
+ }
904
+ ```
905
+
906
+ ##### Login events
907
+
908
+ The `<sty-login>` element dispatches `login`, `close`, and `error` custom events. Attach listeners on the element ref inside `useEffect`:
909
+
910
+ ```tsx
911
+ // app/login/page.tsx
912
+ 'use client';
913
+
914
+ import { useRef } from 'react';
915
+ import { useRouter } from 'next/navigation';
916
+ import type { LoginComponent } from '@strivacity/sdk-next/client/types';
917
+
918
+ export function LazyLogin() {
919
+ const router = useRouter();
920
+ const loginRef = useRef<LoginComponent>(null);
921
+
922
+ useEffect(() => {
923
+ const element = loginRef.current;
924
+
925
+ if (!element) {
926
+ return;
927
+ }
928
+
929
+ const onLogin = () => {
930
+ // User authenticated - navigate to a protected page
931
+ navigate('/profile');
932
+ };
933
+ const onClose = () => {
934
+ // User cancelled or closed the login flow
935
+ window.location.reload();
936
+ };
937
+ const onError = (event: Event) => {
938
+ // A fatal error occurred - the message is available in event.detail
939
+ navigate(`/error?message=${encodeURIComponent((event as CustomEvent<string>).detail)}`);
940
+ };
941
+
942
+ element.addEventListener('login', onLogin);
943
+ element.addEventListener('close', onClose);
944
+ element.addEventListener('error', onError);
945
+
946
+ return () => {
947
+ element.removeEventListener('login', onLogin);
948
+ element.removeEventListener('close', onClose);
949
+ element.removeEventListener('error', onError);
950
+ };
951
+ }, [navigate]);
952
+
953
+ return (
954
+ <section>
955
+ <sty-notifications></sty-notifications>
956
+ <sty-login ref={loginRef}></sty-login>
957
+ <sty-language-selector></sty-language-selector>
958
+ </section>
959
+ );
960
+ }
961
+ ```
962
+
963
+ ##### Notification events
964
+
965
+ The components dispatch `notification` events on the `document` that the `<sty-notifications>` component automatically displays. If you don't want to use `<sty-notifications>`, you can listen to these events and handle them yourself:
966
+
967
+ ```tsx
968
+ 'use client';
969
+
970
+ import { useEffect } from 'react';
971
+
972
+ export function CustomNotifications() {
973
+ useEffect(() => {
974
+ function onNotification(event: Event) {
975
+ const customEvent = event as CustomEvent;
976
+
977
+ if (customEvent.detail.action === 'show') {
978
+ // Add new notification to your custom notification system
979
+ console.log('New notification:', customEvent.detail.notification);
980
+ } else if (customEvent.detail.action === 'clear') {
981
+ console.log('Clear all notifications');
982
+ }
983
+ }
984
+
985
+ document.addEventListener('notification', onNotification);
986
+
987
+ return () => document.removeEventListener('notification', onNotification);
988
+ }, []);
989
+
990
+ return null;
991
+ }
992
+ ```
993
+
994
+ ##### Dynamic language switching
995
+
996
+ The `<sty-language-selector>` component provides a built-in UI for language switching. If you don't want to use it, you can change the UI language dynamically by updating the `lang` property on the `<sty-login>` component:
997
+
998
+ ```tsx
999
+ 'use client';
1000
+
1001
+ import { useRef, useState } from 'react';
1002
+
1003
+ export function LanguageSwitcher() {
1004
+ const loginRef = useRef<HTMLElement & { lang: string }>(null);
1005
+ const [currentLang, setCurrentLang] = useState('en-US');
1006
+
1007
+ function changeLanguage(lang: string) {
1008
+ setCurrentLang(lang);
1009
+
1010
+ if (loginRef.current) {
1011
+ loginRef.current.lang = lang;
1012
+ }
1013
+ }
1014
+
1015
+ return (
1016
+ <section>
1017
+ <div>
1018
+ <button onClick={() => changeLanguage('en-US')}>English</button>
1019
+ <button onClick={() => changeLanguage('fr-FR')}>Français</button>
1020
+ <button onClick={() => changeLanguage('de-DE')}>Deutsch</button>
1021
+ </div>
1022
+ <sty-login ref={loginRef} lang={currentLang}></sty-login>
1023
+ </section>
1024
+ );
1025
+ }
1026
+ ```
1027
+
1028
+ ##### Handle the callback
1029
+
1030
+ **Client-managed sessions**:
1031
+
1032
+ No separate callback page is needed. The `<sty-login>` component handles the entire authentication flow automatically, including token exchange, and dispatches a `login` event when authentication completes successfully.
1033
+
1034
+ **Server-managed sessions**:
1035
+
1036
+ Same as client managed - the component's internal callback request is also transparently proxied through `/auth/callback`, with no separate page needed either way.
1037
+
1038
+ #### Externally-initiated flows (entry)
1039
+
1040
+ For flows started externally (e.g. a password reset email link), the user lands on the entry URL you configured in your Strivacity application native client settings. Call `entry()` on that landing page to resolve the flow parameters from the IDP (`session_id`, `short_app_id`, `language`).
1041
+
1042
+ You have two options:
1043
+
1044
+ **Option 1: Redirect to a separate login page**
1045
+
1046
+ Forward the parameters as query params to your login page:
1047
+
1048
+ ```tsx
1049
+ // app/entry/page.tsx
1050
+ 'use client';
1051
+
1052
+ import { useEffect } from 'react';
1053
+ import { useRouter } from 'next/navigation';
1054
+ import { useStrivacity } from '@strivacity/sdk-next/client';
1055
+ import type { EmbeddedFlow } from '@strivacity/sdk-next/client';
1056
+
1057
+ export default function EntryPage() {
1058
+ const { loading, entry } = useStrivacity<EmbeddedFlow>();
1059
+ const router = useRouter();
1060
+
1061
+ useEffect(() => {
1062
+ if (loading) {
1063
+ return;
1064
+ }
1065
+
1066
+ entry()
1067
+ .then((data) => {
1068
+ // Redirect to login page with flow parameters
1069
+ const params = new URLSearchParams({
1070
+ session_id: data.session_id,
1071
+ short_app_id: data.short_app_id,
1072
+ language: data.language,
1073
+ });
1074
+ window.location.href = `/login?${params}`;
1075
+ })
1076
+ .catch((error) => router.push(`/error?message=${encodeURIComponent(error.message)}`));
1077
+ }, [loading, entry, router]);
1078
+
1079
+ return (
1080
+ <section>
1081
+ <h1>Loading...</h1>
1082
+ </section>
1083
+ );
1084
+ }
1085
+ ```
1086
+
1087
+ Then on your login page, read the parameters and pass them to `<sty-login>`:
1088
+
1089
+ ```tsx
1090
+ // app/login/page.tsx
1091
+ 'use client';
1092
+
1093
+ import { useEffect, useRef } from 'react';
1094
+ import { useRouter } from 'next/navigation';
1095
+ import type { LoginComponent } from '@strivacity/sdk-next/client/types';
1096
+
1097
+ export default function LoginPage() {
1098
+ const router = useRouter();
1099
+ const searchParams = new URLSearchParams(window.location.search);
1100
+
1101
+ // Read parameters from URL
1102
+ const sessionId = searchParams.get('session_id');
1103
+ const shortAppId = searchParams.get('short_app_id');
1104
+ const language = searchParams.get('language');
1105
+ const loginRef = useRef<LoginComponent>(null);
1106
+
1107
+ useEffect(() => {
1108
+ const element = loginRef.current;
1109
+
1110
+ if (!element) {
1111
+ return;
1112
+ }
1113
+
1114
+ element.sessionId = sessionId;
1115
+ element.shortAppId = shortAppId;
1116
+ element.lang = language;
1117
+
1118
+ const onLogin = () => router.push('/profile');
1119
+ const onError = (event: Event) => router.push(`/error?message=${encodeURIComponent((event as CustomEvent<string>).detail)}`);
1120
+
1121
+ element.addEventListener('login', onLogin);
1122
+ element.addEventListener('error', onError);
1123
+
1124
+ return () => {
1125
+ element.removeEventListener('login', onLogin);
1126
+ element.removeEventListener('error', onError);
1127
+ };
1128
+ }, [router, sessionId, shortAppId, language]);
1129
+
1130
+ return (
1131
+ <section>
1132
+ <sty-notifications></sty-notifications>
1133
+ <sty-login ref={loginRef}></sty-login>
1134
+ <sty-language-selector></sty-language-selector>
1135
+ </section>
1136
+ );
1137
+ }
1138
+ ```
1139
+
1140
+ **Option 2: Render login on the entry page**
1141
+
1142
+ Pass the parameters directly to `<sty-login>` on the same page:
1143
+
1144
+ ```tsx
1145
+ // app/entry/page.tsx
1146
+ 'use client';
1147
+
1148
+ import { useEffect, useRef, useState } from 'react';
1149
+ import { useRouter } from 'next/navigation';
1150
+ import { useStrivacity } from '@strivacity/sdk-next/client';
1151
+ import type { LoginComponent } from '@strivacity/sdk-next/client/types';
1152
+ import type { EmbeddedFlow } from '@strivacity/sdk-next/client';
1153
+
1154
+ export default function EntryPage() {
1155
+ const { loading, entry } = useStrivacity<EmbeddedFlow>();
1156
+ const router = useRouter();
1157
+ const loginRef = useRef<LoginComponent>(null);
1158
+
1159
+ const [sessionId, setSessionId] = useState<string | null>(null);
1160
+ const [shortAppId, setShortAppId] = useState<string | null>(null);
1161
+ const [language, setLanguage] = useState<string | null>(null);
1162
+
1163
+ useEffect(() => {
1164
+ if (loading) {
1165
+ return;
1166
+ }
1167
+
1168
+ entry()
1169
+ .then((data) => {
1170
+ // Set state for sty-login component
1171
+ setSessionId(data.session_id);
1172
+ setShortAppId(data.short_app_id);
1173
+ setLanguage(data.language);
1174
+ })
1175
+ .catch((error) => router.push(`/error?message=${encodeURIComponent(error.message)}`));
1176
+ }, [loading, entry, router]);
1177
+
1178
+ useEffect(() => {
1179
+ const element = loginRef.current;
1180
+
1181
+ if (!element || !sessionId) {
1182
+ return;
1183
+ }
1184
+
1185
+ element.sessionId = sessionId;
1186
+ element.shortAppId = shortAppId;
1187
+ element.lang = language;
1188
+
1189
+ const onLogin = () => router.push('/profile');
1190
+ const onError = (event: Event) => router.push(`/error?message=${encodeURIComponent((event as CustomEvent<string>).detail)}`);
1191
+
1192
+ element.addEventListener('login', onLogin);
1193
+ element.addEventListener('error', onError);
1194
+
1195
+ return () => {
1196
+ element.removeEventListener('login', onLogin);
1197
+ element.removeEventListener('error', onError);
1198
+ };
1199
+ }, [router, sessionId, shortAppId, language]);
1200
+
1201
+ if (!sessionId) {
1202
+ return (
1203
+ <section>
1204
+ <h1>Loading...</h1>
1205
+ </section>
1206
+ );
1207
+ }
1208
+
1209
+ return (
1210
+ <section>
1211
+ <sty-notifications></sty-notifications>
1212
+ <sty-login ref={loginRef}></sty-login>
1213
+ <sty-language-selector></sty-language-selector>
1214
+ </section>
1215
+ );
1216
+ }
1217
+ ```
1218
+
1219
+ ##### Logout
1220
+
1221
+ **Client-managed sessions**:
1222
+
1223
+ Call this to clear the session and redirect to the Strivacity end-session endpoint. After that the user is redirected back to your app at `postLogoutRedirectUri`.
1224
+
1225
+ ```tsx
1226
+ // app/logout/page.tsx
1227
+ 'use client';
1228
+
1229
+ import { useEffect } from 'react';
1230
+ import { useStrivacity } from '@strivacity/sdk-next/client';
1231
+
1232
+ export default function LogoutPage() {
1233
+ const { loading, logout } = useStrivacity();
1234
+
1235
+ useEffect(() => {
1236
+ if (loading) {
1237
+ return;
1238
+ }
1239
+
1240
+ void logout();
1241
+ }, [loading, logout]);
1242
+
1243
+ return null;
1244
+ }
1245
+ ```
1246
+
1247
+ **Server-managed sessions**:
1248
+
1249
+ With `serverSessionUri` configured, redirect to `/auth/logout` instead - the Server SDK clears the session and redirects to the IDP end-session endpoint:
1250
+
1251
+ ```tsx
1252
+ // app/logout/page.tsx
1253
+ import { redirect } from 'next/navigation';
1254
+
1255
+ export default function LogoutPage() {
1256
+ redirect('/auth/logout');
1257
+ }
1258
+ ```
1259
+
1260
+ ##### Token management
1261
+
1262
+ **Client-managed sessions**:
1263
+
1264
+ Call these methods to manage the session and access token client-side.
1265
+
1266
+ ```tsx
1267
+ 'use client';
1268
+
1269
+ import { useStrivacity } from '@strivacity/sdk-next/client';
1270
+
1271
+ export function TokenPanel() {
1272
+ const { loading, idTokenClaims, accessToken, refreshToken, refresh, revoke } = useStrivacity();
1273
+
1274
+ async function onRefresh() {
1275
+ // Refresh the access token using the refresh token
1276
+ await refresh();
1277
+ }
1278
+
1279
+ async function onRevoke() {
1280
+ // Revoke all tokens at the authorization server and clear the local session
1281
+ await revoke();
1282
+ }
1283
+
1284
+ if (loading) {
1285
+ return null;
1286
+ }
1287
+
1288
+ // idTokenClaims, accessToken, and refreshToken are plain values from context -
1289
+ // they update automatically whenever the SDK's session changes
1290
+ return (
1291
+ <div>
1292
+ <button onClick={onRefresh}>Refresh</button>
1293
+ <button onClick={onRevoke}>Revoke</button>
1294
+ <pre>{JSON.stringify({ idTokenClaims, accessToken, refreshToken }, null, 2)}</pre>
1295
+ </div>
1296
+ );
1297
+ }
1298
+ ```
1299
+
1300
+ **Server-managed sessions**:
1301
+
1302
+ With `serverSessionUri` configured, tokens are refreshed/revoked by the Server SDK - trigger it by navigating to the catch-all route, then let it redirect back:
1303
+
1304
+ ```tsx
1305
+ 'use client';
1306
+
1307
+ export function TokenPanel() {
1308
+ function onRefresh() {
1309
+ // sdk.refreshSession() runs server-side, then redirects back to returnTo
1310
+ window.location.href = '/auth/refresh?returnTo=/profile';
1311
+ }
1312
+
1313
+ function onRevoke() {
1314
+ // sdk.revokeSession() runs server-side, then redirects to postLogoutRedirectUri
1315
+ window.location.href = '/auth/revoke';
1316
+ }
1317
+
1318
+ return (
1319
+ <div>
1320
+ <button onClick={onRefresh}>Refresh</button>
1321
+ <button onClick={onRevoke}>Revoke</button>
1322
+ </div>
1323
+ );
1324
+ }
1325
+ ```
1326
+
1327
+ ---
1328
+
1329
+ #### native mode
1330
+
1331
+ > For details on how this mode works, see the [native journey documentation](https://docs.strivacity.com/reference/native-journey).
1332
+
1333
+ You build the entire login UI with your own components. `useNativeLogin()` drives a "headless" auth flow: instead of redirecting to a hosted page, the SDK returns a JSON description of the current screen that you render yourself, submit each form step with `submitForm()`, and repeat until the flow finalizes automatically.
1334
+
1335
+ > The example below shows a simplified custom implementation. For a complete native renderer with all widget types, see the [example app](../../apps/next/components/auth/native/NativeLogin.tsx).
1336
+
1337
+ ##### Login / Register
1338
+
1339
+ **Client-managed sessions**:
1340
+
1341
+ ```tsx
1342
+ // app/login/page.tsx
1343
+ 'use client';
1344
+
1345
+ import { useRouter } from 'next/navigation';
1346
+ import { useNativeLogin } from '@strivacity/sdk-next/client';
1347
+
1348
+ export default function LoginPage() {
1349
+ const router = useRouter();
1350
+ const searchParams = new URLSearchParams(window.location.search);
1351
+
1352
+ const { state, forms, messages, loading, submitForm, setFormValue } = useNativeLogin({
1353
+ params: {
1354
+ prompt: 'login', // use 'create' to open the registration flow instead
1355
+ language: 'en-US', // set the UI language (BCP 47 language tag)
1356
+ sdk: 'web-minimal', // rendering mode: 'web-minimal' for simplified rendering, 'web' (default) for full rendering hints and branding
1357
+ sessionId: searchParams.get('session_id'), // pass a session ID to resume an existing flow
1358
+ },
1359
+ onLogin: async () => {
1360
+ router.push('/profile');
1361
+ },
1362
+ onClose: () => {
1363
+ window.location.reload();
1364
+ },
1365
+ onError: async (error) => {
1366
+ router.push(`/error?message=${encodeURIComponent(error.message)}`);
1367
+ },
1368
+ onFallback: (error) => {
1369
+ // Fallback to hosted journey if native widget not supported
1370
+ window.location.href = error.url.toString();
1371
+ },
1372
+ onGlobalMessage: (message) => {
1373
+ alert(message.text);
1374
+ },
1375
+ });
1376
+
1377
+ if (loading || !state.screen) {
1378
+ return (
1379
+ <section>
1380
+ <h1>Loading...</h1>
1381
+ </section>
1382
+ );
1383
+ }
1384
+
1385
+ if (state.screen === 'identifier') {
1386
+ return (
1387
+ <section>
1388
+ <h2>Sign In</h2>
1389
+ <form
1390
+ onSubmit={async (event) => {
1391
+ event.preventDefault();
1392
+ await submitForm('identifier');
1393
+ }}
1394
+ >
1395
+ <input
1396
+ type="text"
1397
+ placeholder="Email"
1398
+ value={(forms['identifier']?.identifier as string) ?? ''}
1399
+ onChange={(event) => setFormValue('identifier', 'identifier', event.target.value)}
1400
+ />
1401
+ {messages['identifier']?.identifier && <div className="error">{messages['identifier'].identifier.text}</div>}
1402
+ <button type="submit">Continue</button>
1403
+ </form>
1404
+ </section>
1405
+ );
1406
+ }
1407
+
1408
+ if (state.screen === 'password') {
1409
+ return (
1410
+ <section>
1411
+ <h2>Enter Password</h2>
1412
+ <form
1413
+ onSubmit={async (event) => {
1414
+ event.preventDefault();
1415
+ await submitForm('password');
1416
+ }}
1417
+ >
1418
+ <input
1419
+ type="password"
1420
+ placeholder="Password"
1421
+ value={(forms['password']?.password as string) ?? ''}
1422
+ onChange={(event) => setFormValue('password', 'password', event.target.value)}
1423
+ />
1424
+ {messages['password']?.password && <div className="error">{messages['password'].password.text}</div>}
1425
+ <button type="submit">Sign In</button>
1426
+ </form>
1427
+ </section>
1428
+ );
1429
+ }
1430
+
1431
+ return null;
1432
+ }
1433
+ ```
1434
+
1435
+ **Server-managed sessions**:
1436
+
1437
+ With `serverSessionUri` configured the code above is unchanged - `useNativeLogin`'s internal requests are transparently proxied through your server instead of going straight to the IDP.
1438
+
1439
+ ##### Handle the callback
1440
+
1441
+ **Client-managed sessions**:
1442
+
1443
+ No separate callback page is needed. Once `state.finalizeUrl` is set, `submitForm()` automatically finalizes the session internally to exchange the authorization code for tokens and store it.
1444
+
1445
+ **Server-managed sessions**:
1446
+
1447
+ Same as client managed - finalizing the session also transparently proxies through your server, with no separate page needed either way.
1448
+
1449
+ #### Externally-initiated flows (entry)
1450
+
1451
+ For flows started externally (e.g. a password reset email link), the user lands on the entry URL you configured in your Strivacity application native client settings. Call `entry()` on that landing page to resolve the flow parameters from the IDP (`session_id`, `short_app_id`, `language`).
1452
+
1453
+ You have two options:
1454
+
1455
+ **Option 1: Redirect to a separate login page**
1456
+
1457
+ ```tsx
1458
+ // app/entry/page.tsx
1459
+ 'use client';
1460
+
1461
+ import { useEffect } from 'react';
1462
+ import { useRouter } from 'next/navigation';
1463
+ import { useStrivacity } from '@strivacity/sdk-next/client';
1464
+ import type { NativeFlow } from '@strivacity/sdk-next/client';
1465
+
1466
+ export default function EntryPage() {
1467
+ const { loading, entry } = useStrivacity<NativeFlow>();
1468
+ const router = useRouter();
1469
+
1470
+ useEffect(() => {
1471
+ if (loading) {
1472
+ return;
1473
+ }
1474
+
1475
+ entry()
1476
+ .then((data) => {
1477
+ const params = new URLSearchParams({
1478
+ session_id: data.session_id,
1479
+ short_app_id: data.short_app_id,
1480
+ language: data.language,
1481
+ });
1482
+ window.location.href = `/login?${params}`;
1483
+ })
1484
+ .catch((error) => router.push(`/error?message=${encodeURIComponent(error.message)}`));
1485
+ }, [loading, entry, router]);
1486
+
1487
+ return (
1488
+ <section>
1489
+ <h1>Loading...</h1>
1490
+ </section>
1491
+ );
1492
+ }
1493
+ ```
1494
+
1495
+ Then on your login page, read query parameters from the URL and pass it to `useNativeLogin` to resume the flow, exactly as shown in the Login / Register example above:
1496
+
1497
+ ```tsx
1498
+ // app/login/page.tsx
1499
+ 'use client';
1500
+
1501
+ import { useNativeLogin } from '@strivacity/sdk-next/client';
1502
+
1503
+ export default function LoginPage() {
1504
+ const searchParams = new URLSearchParams(window.location.search);
1505
+
1506
+ const { state, loading, forms, messages, submitForm, setFormValue } = useNativeLogin({
1507
+ params: {
1508
+ sessionId: searchParams.get('session_id'),
1509
+ language: searchParams.get('language'),
1510
+ },
1511
+ onLogin: async () => {
1512
+ window.location.href = '/profile';
1513
+ },
1514
+ });
1515
+
1516
+ // ...render based on `state.screen` as shown in the Login / Register example above
1517
+ }
1518
+ ```
1519
+
1520
+ **Option 2: Render login on the entry page**
1521
+
1522
+ Call `useNativeLogin` directly inside `app/entry/page.tsx`, feeding it the `session_id` resolved from `entry()` - no redirect needed:
1523
+
1524
+ ```tsx
1525
+ // app/entry/page.tsx
1526
+ 'use client';
1527
+
1528
+ import { useEffect, useState } from 'react';
1529
+ import { useRouter } from 'next/navigation';
1530
+ import { useStrivacity, useNativeLogin } from '@strivacity/sdk-next/client';
1531
+ import type { NativeFlow } from '@strivacity/sdk-next/client';
1532
+
1533
+ export default function EntryPage() {
1534
+ const { loading: sdkLoading, entry } = useStrivacity<NativeFlow>();
1535
+ const router = useRouter();
1536
+ const [sessionId, setSessionId] = useState<string | null>(null);
1537
+ const [language, setLanguage] = useState<string | null>(null);
1538
+ const [ready, setReady] = useState(false);
669
1539
 
670
- @strivacity/sdk-next is available under the MIT License. See the [LICENSE](https://github.com/Strivacity/sdk-js/blob/main/LICENSE) file for more info.
1540
+ useEffect(() => {
1541
+ if (sdkLoading) {
1542
+ return;
1543
+ }
671
1544
 
672
- ## Contributing
1545
+ entry()
1546
+ .then((data) => {
1547
+ setSessionId(data.session_id);
1548
+ setLanguage(data.language);
1549
+ setReady(true);
1550
+ })
1551
+ .catch((error) => router.push(`/error?message=${encodeURIComponent(error.message)}`));
1552
+ }, [sdkLoading, entry, router]);
1553
+
1554
+ const { state, loading, forms, messages, submitForm, setFormValue } = useNativeLogin({
1555
+ params: { sessionId, language },
1556
+ onLogin: async () => {
1557
+ router.push('/profile');
1558
+ },
1559
+ onError: async (error) => {
1560
+ router.push(`/error?message=${encodeURIComponent(error.message)}`);
1561
+ },
1562
+ });
1563
+
1564
+ if (!ready || loading || !state.screen) {
1565
+ return (
1566
+ <section>
1567
+ <h1>Loading...</h1>
1568
+ </section>
1569
+ );
1570
+ }
673
1571
 
674
- Please see our [contributing guide](https://github.com/Strivacity/sdk-js/blob/main/CONTRIBUTING.md).
1572
+ // ...render based on `state.screen` as shown in the Login / Register example above
1573
+ return null;
1574
+ }
1575
+ ```
1576
+
1577
+ ##### Logout
1578
+
1579
+ **Client-managed sessions**:
1580
+
1581
+ Call this to clear the session and redirect to the Strivacity end-session endpoint. After that the user is redirected back to your app at `postLogoutRedirectUri`.
1582
+
1583
+ ```tsx
1584
+ // app/logout/page.tsx
1585
+ 'use client';
1586
+
1587
+ import { useEffect } from 'react';
1588
+ import { useStrivacity } from '@strivacity/sdk-next/client';
1589
+
1590
+ export default function LogoutPage() {
1591
+ const { loading, logout } = useStrivacity();
1592
+
1593
+ useEffect(() => {
1594
+ if (loading) {
1595
+ return;
1596
+ }
1597
+
1598
+ void logout();
1599
+ }, [loading, logout]);
1600
+
1601
+ return null;
1602
+ }
1603
+ ```
1604
+
1605
+ **Server-managed sessions**:
1606
+
1607
+ With `serverSessionUri` configured, redirect to `/auth/logout` instead - the Server SDK clears the session and redirects to the IDP end-session endpoint:
1608
+
1609
+ ```tsx
1610
+ // app/logout/page.tsx
1611
+ import { redirect } from 'next/navigation';
1612
+
1613
+ export default function LogoutPage() {
1614
+ redirect('/auth/logout');
1615
+ }
1616
+ ```
1617
+
1618
+ ##### Token management
1619
+
1620
+ **Client-managed sessions**:
1621
+
1622
+ Call these methods to manage the session and access token client-side.
1623
+
1624
+ ```tsx
1625
+ 'use client';
1626
+
1627
+ import { useStrivacity } from '@strivacity/sdk-next/client';
1628
+
1629
+ export function TokenPanel() {
1630
+ const { loading, idTokenClaims, accessToken, refreshToken, refresh, revoke } = useStrivacity();
1631
+
1632
+ async function onRefresh() {
1633
+ // Refresh the access token using the refresh token
1634
+ await refresh();
1635
+ }
1636
+
1637
+ async function onRevoke() {
1638
+ // Revoke all tokens at the authorization server and clear the local session
1639
+ await revoke();
1640
+ }
1641
+
1642
+ if (loading) {
1643
+ return null;
1644
+ }
1645
+
1646
+ // idTokenClaims, accessToken, and refreshToken are plain values from context -
1647
+ // they update automatically whenever the SDK's session changes
1648
+ return (
1649
+ <div>
1650
+ <button onClick={onRefresh}>Refresh</button>
1651
+ <button onClick={onRevoke}>Revoke</button>
1652
+ <pre>{JSON.stringify({ idTokenClaims, accessToken, refreshToken }, null, 2)}</pre>
1653
+ </div>
1654
+ );
1655
+ }
1656
+ ```
1657
+
1658
+ **Server-managed sessions**:
1659
+
1660
+ With `serverSessionUri` configured, tokens are refreshed/revoked by the Server SDK - trigger it by navigating to the catch-all route, then let it redirect back:
1661
+
1662
+ ```tsx
1663
+ 'use client';
1664
+
1665
+ export function TokenPanel() {
1666
+ function onRefresh() {
1667
+ // sdk.refreshSession() runs server-side, then redirects back to returnTo
1668
+ window.location.href = '/auth/refresh?returnTo=/profile';
1669
+ }
1670
+
1671
+ function onRevoke() {
1672
+ // sdk.revokeSession() runs server-side, then redirects to postLogoutRedirectUri
1673
+ window.location.href = '/auth/revoke';
1674
+ }
1675
+
1676
+ return (
1677
+ <div>
1678
+ <button onClick={onRefresh}>Refresh</button>
1679
+ <button onClick={onRevoke}>Revoke</button>
1680
+ </div>
1681
+ );
1682
+ }
1683
+ ```
1684
+
1685
+ ---
1686
+
1687
+ ### Hooks API
1688
+
1689
+ #### useStrivacity
1690
+
1691
+ The main hook for accessing the SDK instance and authentication state.
1692
+
1693
+ ```ts
1694
+ import { useStrivacity } from '@strivacity/sdk-next/client';
1695
+ import type { RedirectFlow } from '@strivacity/sdk-next/client';
1696
+
1697
+ const ctx = useStrivacity<RedirectFlow>();
1698
+ ```
1699
+
1700
+ ##### Returns
1701
+
1702
+ ```ts
1703
+ {
1704
+ // SDK instance (access any SDK method)
1705
+ sdk: RedirectFlow | PopupFlow | EmbeddedFlow | NativeFlow;
1706
+
1707
+ // Reactive state (plain values - the component re-renders when they change)
1708
+ loading: boolean; // True during initialization
1709
+ isAuthenticated: boolean; // True if user has valid session
1710
+ idTokenClaims: IdTokenClaims | null; // Decoded ID token claims
1711
+ accessToken: string | null; // Current access token
1712
+ refreshToken: string | null; // Current refresh token
1713
+
1714
+ // Methods (all are async)
1715
+ login(params?: LoginParams): Promise<void>; // Start login flow
1716
+ register(params?: LoginParams): Promise<void>; // Start registration flow
1717
+ handleCallback(url?: string): Promise<void>; // Handle OAuth callback
1718
+ logout(params?: LogoutParams): Promise<void>; // End session
1719
+ refresh(): Promise<void>; // Refresh access token
1720
+ revoke(): Promise<void>; // Revoke tokens
1721
+ entry(): Promise<EntryData>; // Handle external entry (embedded/native only)
1722
+ }
1723
+ ```
1724
+
1725
+ #### useNativeLogin
1726
+
1727
+ Hook for managing native login flow state. Only available in `native` mode.
1728
+
1729
+ ```ts
1730
+ import { useNativeLogin } from '@strivacity/sdk-next/client';
1731
+ import type { NativeParams } from '@strivacity/sdk-next/client';
1732
+
1733
+ const ctx = useNativeLogin({
1734
+ params: { /* login params */ },
1735
+ onLogin: (session) => { /* handle login */ },
1736
+ onError: (error) => { /* handle error */ },
1737
+ // ... other callbacks
1738
+ });
1739
+ ```
1740
+
1741
+ ##### Options
1742
+
1743
+ ```ts
1744
+ {
1745
+ params?: NativeParams; // Initial flow parameters
1746
+ onLogin?: (session: SessionData) => void | Promise<void>; // Called on successful login
1747
+ onClose?: () => void; // Called when user closes the flow
1748
+ onError?: (error: unknown) => void; // Called on error
1749
+ onFallback?: (error: FallbackError) => void; // Called when fallback needed
1750
+ onGlobalMessage?: (message: NativeFlowMessage) => void; // Called for global messages
1751
+ }
1752
+ ```
1753
+
1754
+ ##### Returns
1755
+
1756
+ ```ts
1757
+ {
1758
+ // Reactive state (plain values - the component re-renders when they change)
1759
+ loading: boolean; // True while fetching next screen
1760
+ state: Partial<NativeFlowState>; // Current flow state (screen, forms, layout, etc.)
1761
+ forms: Record<string, Record<string, unknown>>; // Form data by form ID
1762
+ messages: Record<string, Record<string, NativeFlowMessage>>; // Validation messages
1763
+
1764
+ // Methods
1765
+ submitForm(formId: string, customBody?: Record<string, unknown>): Promise<void>; // Submit a form and advance to next screen
1766
+ setFormValue(formId: string, widgetId: string, value: unknown): void; // Update a single field value before submission
1767
+ setMessage(formId: string, widgetId: string, value: NativeFlowMessage): void; // Set a validation/info message on a widget
1768
+ triggerFallback(message?: string): void; // Manually trigger fallback to hosted journey
1769
+ triggerClose(): void; // Signal that the login flow was closed by the user
1770
+ }
1771
+ ```
1772
+
1773
+ #### withAuthGuard (client)
1774
+
1775
+ Higher-order component that guards a component with authentication. Waits for the SDK to finish loading, checks the session, and redirects to the login page if the user is not authenticated. Not to be confused with the Server SDK's [`withAuthGuard`](#withauthguard), which guards a page/route server-side.
1776
+
1777
+ ```tsx
1778
+ 'use client';
1779
+
1780
+ import { withAuthGuard } from '@strivacity/sdk-next/client';
1781
+
1782
+ export default withAuthGuard(
1783
+ function Profile() {
1784
+ return <section>Protected content</section>;
1785
+ },
1786
+ { loginUri: '/login' },
1787
+ );
1788
+ ```
1789
+
1790
+ ##### Options
1791
+
1792
+ ```ts
1793
+ {
1794
+ loginUri?: string; // URI to redirect to if the user is not authenticated (default: '/login')
1795
+ onLoading?: () => ReactNode; // Optional render function shown while loading/checking authentication
1796
+ }
1797
+ ```
1798
+
1799
+ ---
1800
+
1801
+ ## Server SDK
1802
+
1803
+
1804
+ This is the same backend-for-frontend ([BFF](../../README.md#bff)) server implementation as the [core Server SDK](../sdk-core/README.md#server-sdk), pre-wired for Next.js: `createServerSDK` provides a Next.js `ServerAdapter`, a default encrypted-cookie storage backed by `next/headers`' `cookies()`.
1805
+
1806
+ ### Setup
1807
+
1808
+ ```ts
1809
+ // lib/auth/server.ts
1810
+ import { createServerSDK } from '@strivacity/sdk-next/server';
1811
+ import { sdkOptions } from './options';
1812
+
1813
+ export const sdk = createServerSDK({
1814
+ ...sdkOptions,
1815
+ secret: process.env.SECRET, // required unless you provide a custom `storage`
1816
+ postLoginRedirectUri: '/profile',
1817
+ });
1818
+ ```
1819
+
1820
+ Works with both routers. Mount the catch-all route once:
1821
+
1822
+ ```ts
1823
+ // App Router: app/auth/[...strivacity]/route.ts
1824
+ import { sdk } from '../../../lib/auth/server';
1825
+
1826
+ export const GET = sdk.handler;
1827
+ export const POST = sdk.handler;
1828
+ export const PUT = sdk.handler;
1829
+ export const PATCH = sdk.handler;
1830
+ export const DELETE = sdk.handler;
1831
+ ```
1832
+
1833
+ ```ts
1834
+ // Pages Router: pages/api/auth/[...strivacity].ts
1835
+ import type { NextApiRequest, NextApiResponse } from 'next';
1836
+ import { sdk } from '../../../lib/auth/server';
1837
+ import { toWebRequest, applyResponse } from '../../../lib/auth/adapter';
1838
+
1839
+ export default async function handler(req: NextApiRequest, res: NextApiResponse) {
1840
+ const response = await sdk.handler(toWebRequest(req));
1841
+ await applyResponse(response, res);
1842
+ }
1843
+ ```
1844
+
1845
+ > The Pages Router hands you a Node.js request/response pair rather than a Web `Request`/`Response` - see the core SDK's [Express adapter example](../sdk-core/README.md#server-usage) for `toWebRequest`/`applyResponse` implementations you can adapt.
1846
+
1847
+ ### Accessing the session server-side
1848
+
1849
+ Call `getSession()` from any Server Component, Route Handler, or `getServerSideProps` to read the current session without going through the client SDK:
1850
+
1851
+ ```tsx
1852
+ // app/profile/page.tsx (Server Component)
1853
+ import { redirect } from 'next/navigation';
1854
+ import { sdk } from '../../lib/auth/server';
1855
+
1856
+ export default async function ProfilePage() {
1857
+ const session = await sdk.getSession();
1858
+
1859
+ if (!session) {
1860
+ redirect('/login');
1861
+ }
1862
+
1863
+ return <h1>Hello, {session.claims?.given_name}</h1>;
1864
+ }
1865
+ ```
1866
+
1867
+ ### withAuthGuard
1868
+
1869
+ Guards a page and injects a `session` prop, redirecting to `options.loginUri` (default `/login`) when there's no active session.
1870
+
1871
+ **App Router:**
1872
+
1873
+ ```tsx
1874
+ // app/profile/page.tsx
1875
+ import { sdk } from '../../lib/auth/server';
1876
+
1877
+ export default sdk.withAuthGuard(async function ProfilePage({ session }) {
1878
+ return <h1>Hello, {session.claims?.given_name}</h1>;
1879
+ });
1880
+ ```
1881
+
1882
+ **Pages Router:**
1883
+
1884
+ ```tsx
1885
+ // pages/profile.tsx
1886
+ import type { SessionData } from '@strivacity/sdk-next/server';
1887
+ import { sdk } from '../lib/auth/server';
1888
+
1889
+ export const getServerSideProps = sdk.withAuthGuard<{ session: SessionData }>();
1890
+
1891
+ export default function ProfilePage({ session }: { session: SessionData }) {
1892
+ return <h1>Hello, {session.claims?.given_name}</h1>;
1893
+ }
1894
+ ```
1895
+
1896
+ Pass `returnTo` to redirect back to a specific page after login, or (Pages Router only) `getServerSideProps` to run your own data fetching alongside the guard:
1897
+
1898
+ ```ts
1899
+ export const getServerSideProps = sdk.withAuthGuard({
1900
+ returnTo: '/profile',
1901
+ getServerSideProps: async () => ({ props: { extra: 'data' } }),
1902
+ });
1903
+ ```
1904
+
1905
+ ### withApiAuthRequired
1906
+
1907
+ Guards an API route; responds `401 { error: 'not_authenticated' }` when there's no active session.
1908
+
1909
+ **App Router:**
1910
+
1911
+ ```ts
1912
+ // app/api/me/route.ts
1913
+ import { sdk } from '../../../lib/auth/server';
1914
+
1915
+ export const GET = sdk.withApiAuthRequired(async (req) => {
1916
+ const session = await sdk.getSession(req);
1917
+ return Response.json({ claims: session?.claims });
1918
+ });
1919
+ ```
1920
+
1921
+ **Pages Router:**
1922
+
1923
+ ```ts
1924
+ // pages/api/me.ts
1925
+ import { sdk } from '../../lib/auth/server';
1926
+
1927
+ export default sdk.withApiAuthRequired(async (req, res) => {
1928
+ const session = await sdk.getSession(req);
1929
+ res.json({ claims: session?.claims });
1930
+ });
1931
+ ```
1932
+
1933
+ <a id="server-storages"></a>
1934
+ ### Storages
1935
+
1936
+ By default the Server SDK stores tokens encrypted in http-only cookies and login state in a global in-memory `Map`. Provide `storage`/`stateStorage` to use something else.
1937
+
1938
+ #### Built-in session storages
1939
+
1940
+ - **`createEncryptedCookieStorage(secret, options?)`** - default storage that keeps the session encrypted in an http-only cookie.
1941
+ - **`createSessionIdCookieStorage(storage, options?)`** - puts only a small, random session-id cookie on the client and keeps the actual session payload in the `storage` you provide. This supports back-channel logout out of the box.
1942
+
1943
+ ```ts
1944
+ // lib/auth/storage.ts
1945
+ import { createSessionIdCookieStorage, createServerMemoryStorage } from '@strivacity/sdk-next/server';
1946
+
1947
+ export const sessionStorage = createSessionIdCookieStorage(
1948
+ createServerMemoryStorage(),
1949
+ {
1950
+ // maxAge: 30 * 24 * 60 * 60 // Without maxAge this is a browser-session cookie that gets cleared when the browser closes
1951
+ },
1952
+ );
1953
+ ```
1954
+
1955
+ ```ts
1956
+ // lib/auth/server.ts
1957
+ import { createServerSDK } from '@strivacity/sdk-next/server';
1958
+ import { sdkOptions } from './options';
1959
+ import { sessionStorage } from './storage';
1960
+
1961
+ export const sdk = createServerSDK({
1962
+ ...sdkOptions,
1963
+ storage: sessionStorage,
1964
+ postLoginRedirectUri: '/profile',
1965
+ });
1966
+ ```
1967
+
1968
+ #### Custom storage
1969
+
1970
+ For example you can use Redis via [unstorage](https://npmjs.com/package/unstorage):
1971
+
1972
+ ```ts
1973
+ // lib/auth/storage.ts
1974
+ import { createStorage } from 'unstorage';
1975
+ import redisDriver from 'unstorage/drivers/redis';
1976
+ import type { NextServerStorage, SDKStorage } from '@strivacity/sdk-next/server';
1977
+
1978
+ const unstorageInstance = createStorage({ driver: redisDriver({ url: process.env.REDIS_URL }) });
1979
+
1980
+ // Custom session storage for tokens
1981
+ export const sessionStorage: NextServerStorage = {
1982
+ async get(key) {
1983
+ return unstorageInstance.getItem<string>(key);
1984
+ },
1985
+ async set(key, value) {
1986
+ await unstorageInstance.setItem(key, value);
1987
+ },
1988
+ async delete(key) {
1989
+ await unstorageInstance.removeItem(key);
1990
+ },
1991
+ // Required for back-channel logout support - see below.
1992
+ // Scans all stored sessions and removes those matching the logout token's sid or sub claim.
1993
+ async deleteByLogoutToken(logoutToken) {
1994
+ const keys = await unstorageInstance.getKeys();
1995
+ await Promise.all(
1996
+ keys.map(async (key) => {
1997
+ const raw = await unstorageInstance.getItem<string>(key);
1998
+ if (!raw) return;
1999
+ const session = JSON.parse(raw);
2000
+ if ((logoutToken.sid && session.sid === logoutToken.sid) || (logoutToken.sub && session.sub === logoutToken.sub)) {
2001
+ await unstorageInstance.removeItem(key);
2002
+ }
2003
+ }),
2004
+ );
2005
+ },
2006
+ };
2007
+
2008
+ // Custom state storage for the OAuth2 state parameter
2009
+ export const stateStorage: SDKStorage = {
2010
+ async get(key) {
2011
+ return unstorageInstance.getItem<string>(key);
2012
+ },
2013
+ async set(key, value) {
2014
+ await unstorageInstance.setItem(key, value);
2015
+ },
2016
+ async delete(key) {
2017
+ await unstorageInstance.removeItem(key);
2018
+ },
2019
+ };
2020
+ ```
2021
+
2022
+ ```ts
2023
+ // lib/auth/server.ts
2024
+ import { createServerSDK } from '@strivacity/sdk-next/server';
2025
+ import { sdkOptions } from './options';
2026
+ import { sessionStorage, stateStorage } from './storage';
2027
+
2028
+ export const sdk = createServerSDK({
2029
+ ...sdkOptions,
2030
+ storage: sessionStorage, // Custom Redis-backed session storage
2031
+ stateStorage, // Custom Redis-backed state storage
2032
+ });
2033
+ ```
2034
+
2035
+ > For more details on the storage interfaces, see the core SDK's [Custom storage](../sdk-core/README.md#server-storages) section.
2036
+
2037
+ ### Back-channel logout
2038
+
2039
+ OIDC back-channel logout lets the authorization server terminate sessions server-to-server, without involving the browser. When the IDP sends a logout event (e.g. an admin terminates a session, or the user logs out from a different device), it POSTs a signed `logout_token` JWT to `/auth/backchannel-logout` - already wired up by the catch-all route from [Setup](#setup) - which routes it to `sdk.handleBackChannelLogout(req)`.
2040
+
2041
+ The handler verifies the token's signature against the IDP's JWKS, validates the `iss`, `aud`, `iat` (freshness), and `jti` (replay protection) claims, requires the `http://schemas.openid.net/event/backchannel-logout` event and a `sid` or `sub` claim, then calls `storage.deleteByLogoutToken({ sid?, sub? })` to remove the matching session(s). It responds `200` on success, `400` for an invalid or malformed `logout_token`, and `501` if the configured storage doesn't implement `deleteByLogoutToken`.
2042
+
2043
+ > **The default encrypted-cookie storage does not support back-channel logout** because each cookie is bound to a single browser session - there is no server-side index to look up by `sid` or `sub`. To support back-channel logout, use [`createSessionIdCookieStorage`](#server-storages) with a `storage` that implements `deleteByLogoutToken` (e.g. `createServerMemoryStorage()` for local testing), or a fully custom server storage as shown in the [Custom storage](#server-storages) example above.
2044
+
2045
+ Configure the **Back-channel logout URI** in your Strivacity application settings to:
2046
+
2047
+ ```
2048
+ https://your-app.example.com/auth/backchannel-logout
2049
+ ```
2050
+
2051
+ For a complete explanation of the handshake and validation performed, see the core SDK's [Back-channel logout](../sdk-core/README.md#server-backchannel-logout) documentation.
2052
+
2053
+ ### Server SDK API reference
2054
+
2055
+ ```ts
2056
+ {
2057
+ options: ServerSDKOptions; // resolved server SDK configuration
2058
+
2059
+ // Session management
2060
+ getSession(req?): Promise<SessionData | null>; // read the current session
2061
+ updateSession(session, req?): Promise<void>; // persist new session data
2062
+ refreshSession(req?): Promise<SessionData>; // refresh tokens using the refresh token
2063
+ revokeSession(req?): Promise<void>; // revoke tokens and clear the session
2064
+ getEntrySession(entryUrl): Promise<Record<string, string>>; // resolve an externally-initiated (embedded/native) entry URL
2065
+ completeLogin(params, req?): Promise<SessionData>; // exchange an authorization code for tokens
2066
+ logout(postLogoutRedirectUri, req?): Promise<URL>; // clear the session, returns the IDP end-session URL
2067
+
2068
+ // Route handlers - each returns a Response; `handler` dispatches to the one matching the request path
2069
+ handleLogin(req): Promise<Response>;
2070
+ handleRegister(req): Promise<Response>;
2071
+ handleCallback(req): Promise<Response>;
2072
+ handleRefresh(req): Promise<Response>;
2073
+ handleRevoke(req): Promise<Response>;
2074
+ handleEntry(req): Promise<Response>;
2075
+ handleLogout(req): Promise<Response>;
2076
+ handleBackChannelLogout(req): Promise<Response>;
2077
+ handler(req): Promise<Response | null>; // dispatches based on `authUrlPrefix`, or null if no route matched
2078
+
2079
+ // Next.js-specific
2080
+ withAuthGuard(...): ...; // see withAuthGuard
2081
+ withApiAuthRequired(...): ...; // see withApiAuthRequired
2082
+ }
2083
+ ```
2084
+
2085
+ ### Server configuration reference
2086
+
2087
+ The Server SDK accepts the same configuration as the client SDK (see [Configuration reference](#configuration-reference)), plus:
2088
+
2089
+ | Option | Type | Required | Default | Description |
2090
+ | ------ | ---- | -------- | ------- | ----------- |
2091
+ | `secret` | `string` | Only if using default storage | - | Encryption key (32+ random characters) for the http-only cookie session storage |
2092
+ | `storage` | `NextServerStorage` | No | Encrypted cookie storage | Custom session storage; see [Storages](#server-storages) |
2093
+ | `stateStorage` | `SDKStorage` | No | In-memory `Map` | Custom OAuth2 state storage |
2094
+ | `authUrlPrefix` | `string` | No | `'/auth'` | URL prefix matched by `handler`/the catch-all route |
2095
+ | `loginUri` | `string` | No | `'/login'` | Page `withAuthGuard` redirects to when there's no session |
2096
+ | `postLoginRedirectUri` | `string` | No | - | Default redirect after login when no `?returnTo=` is given |
2097
+ | `postLogoutRedirectUri` | `string` | No | - | Default redirect after logout |
2098
+ | `cookieMaxAge` | `number` | No | `2592000` (30 days) | Max age of the session cookie in seconds |
2099
+
2100
+ ---
2101
+
2102
+ ## Route guards
2103
+
2104
+ - **Client-side**: wrap a `'use client'` component with `withAuthGuard` from `@strivacity/sdk-next/client` - see [withAuthGuard](#withauthguard-client) above.
2105
+ - **Server-side**: wrap a page (App Router) or `getServerSideProps` (Pages Router) with `sdk.withAuthGuard` from the Server SDK - see [withAuthGuard](#withauthguard) above.
2106
+
2107
+ ---
2108
+
2109
+ ## Shared features
2110
+
2111
+ The Next.js SDK is built on top of the core SDK and supports all its features, on both the client and server:
2112
+
2113
+ - **[Storages](../sdk-core/README.md#storages)** - localStorage, sessionStorage, IndexedDB, Cache API, Memory, Worker (client), encrypted cookies, in-memory (server)
2114
+ - **[SDK events](../sdk-core/README.md#sdk-events)** - Subscribe to authentication lifecycle events
2115
+ - **[Logging](../sdk-core/README.md#logging)** - Built-in and custom logger support
2116
+ - **[HTTP client](../sdk-core/README.md#http-client)** - Custom HTTP client integration
2117
+ - **[Error handling](../sdk-core/README.md#error-handling)** - Typed error classes for different failure scenarios
2118
+ - **[Utility functions](../sdk-core/README.md#utility-functions)** - Base64URL, JWT decoding, encryption, etc.
2119
+ - **[Caching](../sdk-core/README.md#caching)** - OIDC metadata and JWKS caching
2120
+
2121
+ ---
2122
+
2123
+ ## Configuration reference
2124
+
2125
+ The client SDK accepts the same configuration as the core SDK - see the [core SDK configuration reference](../sdk-core/README.md#configuration-reference). See [Server configuration reference](#server-configuration-reference) above for the additional server-specific options.
2126
+
2127
+ ---
2128
+
2129
+ ## Migration guide
2130
+
2131
+ ### Migrating to v4.0
2132
+
2133
+ v4 replaces the SDK's class-based flow architecture with function-based architecture, and adds a first-class Server SDK for server-managed (BFF) sessions. `StyAuthProvider`, `useStrivacity()`, `useNativeLogin()`, and the built-in `redirect`/`popup`/`embedded`/`native` modes are unchanged - only apps that used `mode: 'custom'` or drove `native` mode through the old `NativeFlowHandler` need to update their code.
2134
+
2135
+ #### Class-based flows replaced by functions
2136
+
2137
+ In v3, flows were classes (`RedirectFlow`, `PopupFlow`, `NativeFlow`, `EmbeddedFlow`), and the only way to customize behavior beyond the built-in modes - for example, to proxy authentication through your own backend in a bespoke way - was `mode: 'custom'` with a `customFlow` class that extended one of them and override its methods:
2138
+
2139
+ ```ts
2140
+ // v3
2141
+ import { NativeFlow } from '@strivacity/sdk-core/flows/NativeFlow';
2142
+
2143
+ export class CustomNativeFlow extends NativeFlow {
2144
+ override async refresh(): Promise<void> {
2145
+ // ...
2146
+ }
2147
+ }
2148
+ ```
2149
+
2150
+ v4 removes `mode: 'custom'`, the `customFlow` option, and the flow classes entirely. In their place, `createBaseFlow` (from `@strivacity/sdk-core/flows/base`) is a factory function that returns a plain object of methods closing over shared state - build your own flow by composing it, without extending anything:
2151
+
2152
+ ```ts
2153
+ // v4
2154
+ import { createBaseFlow } from '@strivacity/sdk-core/flows/base';
2155
+ import { getDefaultFlowState, getSDKOptions } from '@strivacity/sdk-core/utils';
2156
+ import type { SDKInitConfig, SDKOptions } from '@strivacity/sdk-core/types';
2157
+
2158
+ export function createCustomFlow(initConfig: SDKInitConfig) {
2159
+ const state = getDefaultFlowState();
2160
+ const options = getSDKOptions<SDKOptions>(state, initConfig);
2161
+ const base = createBaseFlow(state, options);
2162
+
2163
+ async function refresh(): Promise<void> {
2164
+ // ...
2165
+ }
2166
+
2167
+ return { ...base, refresh };
2168
+ }
2169
+ ```
2170
+
2171
+ This is a low-level `@strivacity/sdk-core` primitive - it's used the same way no matter which framework package you build on top of it. Wire it up by adding `factory: createCustomFlow` to your shared `sdkOptions` - see [Custom flow](../sdk-core/README.md#custom-flow) in the core SDK README for the full pattern and usage example.
2172
+
2173
+ #### Server-managed sessions ([BFF](../../README.md#bff)) are now built in
2174
+
2175
+ In v3, routing authentication through your own backend meant writing a custom flow class like the one above yourself: manually calling `fetch()` against hand-written endpoints, and reimplementing PKCE/state handling, CSRF protection, and server-side token storage on your own.
2176
+
2177
+ v4 replaces that with the Server SDK shown in [Quick start](#quick-start) above: add `serverSessionUri` to your shared `sdkOptions`, create the server side with `createServerSDK` from `@strivacity/sdk-next/server`, and export its `handler` from a single catch-all Route Handler - PKCE, state, and session storage are all handled by the Server SDK:
2178
+
2179
+ ```ts
2180
+ // lib/auth/server.ts
2181
+ import { createServerSDK } from '@strivacity/sdk-next/server';
2182
+ import { sdkOptions } from './options';
2183
+
2184
+ export const sdk = createServerSDK({
2185
+ ...sdkOptions,
2186
+ secret: process.env.SECRET, // http-only cookie encryption key (random 32+ characters)
2187
+ });
2188
+ ```
2189
+
2190
+ #### Native mode: no more `NativeFlowHandler`
2191
+
2192
+ In v3, `native` mode's `login()`/`register()` returned a separate `NativeFlowHandler` instance, and the flow was driven through that handler:
2193
+
2194
+ ```ts
2195
+ // v3
2196
+ const handler = await sdk.login();
2197
+ const state = await handler.startSession(sessionId);
2198
+ const nextState = await handler.submitForm('formId', { identifier: 'user@example.com' });
2199
+ await handler.finalizeSession(nextState.finalizeUrl);
2200
+ ```
2201
+
2202
+ v4 moves `startSession()`, `submitForm()`, and `finalizeSession()` directly onto the flow itself - in `@strivacity/sdk-next` this is wrapped for you by the [`useNativeLogin()`](#usenativelogin) hook:
2203
+
2204
+ ```ts
2205
+ import { useNativeLogin } from '@strivacity/sdk-next/client';
2206
+
2207
+ // v4
2208
+ const { state, forms, messages, submitForm } = useNativeLogin({
2209
+ params: { sessionId },
2210
+ });
2211
+
2212
+ await submitForm('formId');
2213
+ ```
2214
+
2215
+ Update any code that calls `login()`/`register()` and drives the returned handler in `native` mode to use `useNativeLogin()` instead.
675
2216
 
676
2217
  ## Migrating to v3.0
677
2218
 
678
2219
  ### Entry API Major Changes
679
2220
 
680
2221
  Strivacity SDK's `entry()` API now returns a structured object instead of a plain string. Check the example above in the usage section for more details.
2222
+
2223
+ ---
2224
+
2225
+ ## Vulnerability Reporting
2226
+
2227
+ The [Guidelines for responsible disclosure](https://www.strivacity.com/report-a-security-issue) details the procedure for disclosing security issues. Please do not report security vulnerabilities on the public issue tracker.
2228
+
2229
+ ## License
2230
+
2231
+ This package is available under the MIT License. See the [LICENSE](https://github.com/Strivacity/sdk-js/blob/main/LICENSE) file for more info.
2232
+
2233
+ ## Contributing
2234
+
2235
+ Please see our [contributing guide](https://github.com/Strivacity/sdk-js/blob/main/CONTRIBUTING.md).