@strivacity/sdk-next 3.0.2 → 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.
- package/CHANGELOG.md +6 -0
- package/README.md +1983 -428
- package/dist/client/components.cjs +1 -0
- package/dist/client/components.d.ts +1 -0
- package/dist/client/components.mjs +1 -0
- package/dist/client/hooks.cjs +1 -0
- package/dist/client/hooks.d.ts +1 -0
- package/dist/client/hooks.mjs +1 -0
- package/dist/client/index.cjs +1 -0
- package/dist/client/index.d.ts +9 -0
- package/dist/client/index.mjs +1 -0
- package/dist/client/storages.cjs +1 -0
- package/dist/client/storages.d.ts +1 -0
- package/dist/client/storages.mjs +1 -0
- package/dist/client/types.cjs +1 -0
- package/dist/client/types.d.ts +1 -0
- package/dist/client/types.mjs +1 -0
- package/dist/client/utils.cjs +1 -0
- package/dist/client/utils.d.ts +1 -0
- package/dist/client/utils.mjs +1 -0
- package/dist/errors.cjs +1 -0
- package/dist/errors.d.ts +1 -0
- package/dist/errors.mjs +1 -0
- package/dist/server/handlers/api.cjs +2 -0
- package/dist/server/handlers/api.cjs.map +1 -0
- package/dist/server/handlers/api.d.ts +4 -0
- package/dist/server/handlers/api.mjs +2 -0
- package/dist/server/handlers/api.mjs.map +1 -0
- package/dist/server/handlers/route.cjs +2 -0
- package/dist/server/handlers/route.cjs.map +1 -0
- package/dist/server/handlers/route.d.ts +4 -0
- package/dist/server/handlers/route.mjs +2 -0
- package/dist/server/handlers/route.mjs.map +1 -0
- package/dist/server/index.cjs +1 -0
- package/dist/server/index.d.ts +8 -0
- package/dist/server/index.mjs +1 -0
- package/dist/server/sdk.cjs +2 -0
- package/dist/server/sdk.cjs.map +1 -0
- package/dist/server/sdk.d.ts +8 -0
- package/dist/server/sdk.mjs +2 -0
- package/dist/server/sdk.mjs.map +1 -0
- package/dist/server/storages.cjs +2 -0
- package/dist/server/storages.cjs.map +1 -0
- package/dist/server/storages.d.ts +20 -0
- package/dist/server/storages.mjs +2 -0
- package/dist/server/storages.mjs.map +1 -0
- package/dist/server/types.cjs +1 -0
- package/dist/server/types.d.ts +101 -0
- package/dist/server/types.mjs +1 -0
- package/dist/server/utils.cjs +2 -0
- package/dist/server/utils.cjs.map +1 -0
- package/dist/server/utils.d.ts +28 -0
- package/dist/server/utils.mjs +2 -0
- package/dist/server/utils.mjs.map +1 -0
- package/dist/types.cjs +1 -2
- package/dist/types.d.ts +2 -176
- package/dist/types.mjs +1 -2
- package/package.json +48 -10
- package/testing/tests/client/components.spec.ts +10 -0
- package/testing/tests/client/hooks.spec.ts +10 -0
- package/testing/tests/client/index.spec.ts +106 -0
- package/testing/tests/client/storages.spec.ts +10 -0
- package/testing/tests/client/utils.spec.ts +10 -0
- package/testing/tests/server/handlers/api.spec.ts +110 -0
- package/testing/tests/server/handlers/route.spec.ts +173 -0
- package/testing/tests/server/index.spec.ts +103 -0
- package/testing/tests/server/sdk.spec.ts +250 -0
- package/testing/tests/server/storages.spec.ts +353 -0
- package/testing/tests/server/utils.spec.ts +110 -0
- package/dist/AuthProvider.cjs +0 -2
- package/dist/AuthProvider.cjs.map +0 -1
- package/dist/AuthProvider.d.ts +0 -7
- package/dist/AuthProvider.mjs +0 -2
- package/dist/AuthProvider.mjs.map +0 -1
- package/dist/LoginRenderer.cjs +0 -2
- package/dist/LoginRenderer.cjs.map +0 -1
- package/dist/LoginRenderer.d.ts +0 -20
- package/dist/LoginRenderer.mjs +0 -2
- package/dist/LoginRenderer.mjs.map +0 -1
- package/dist/composables.cjs +0 -2
- package/dist/composables.cjs.map +0 -1
- package/dist/composables.d.ts +0 -12
- package/dist/composables.mjs +0 -2
- package/dist/composables.mjs.map +0 -1
- package/dist/index.cjs +0 -2
- package/dist/index.cjs.map +0 -1
- package/dist/index.d.ts +0 -13
- package/dist/index.mjs +0 -2
- package/dist/index.mjs.map +0 -1
- package/dist/types.cjs.map +0 -1
- package/dist/types.mjs.map +0 -1
package/README.md
CHANGED
|
@@ -1,132 +1,319 @@
|
|
|
1
1
|
# @strivacity/sdk-next
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
46
|
+
---
|
|
6
47
|
|
|
7
|
-
##
|
|
48
|
+
## Prerequisites
|
|
8
49
|
|
|
9
|
-
|
|
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
|
-
|
|
54
|
+
---
|
|
12
55
|
|
|
13
|
-
|
|
14
|
-
- [Headless example app](https://github.com/Strivacity/sdk-js/tree/main/apps/next-headless)
|
|
56
|
+
## Installation
|
|
15
57
|
|
|
16
|
-
|
|
58
|
+
```bash
|
|
59
|
+
npm install @strivacity/sdk-next
|
|
60
|
+
```
|
|
17
61
|
|
|
18
|
-
|
|
62
|
+
---
|
|
19
63
|
|
|
20
|
-
##
|
|
64
|
+
## Choosing a mode
|
|
21
65
|
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
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
|
-
|
|
156
|
+
> The `/auth` prefix and route names come from `authUrlPrefix` - see [Server configuration reference](#server-configuration-reference).
|
|
29
157
|
|
|
30
|
-
Wrap your
|
|
158
|
+
### 4. Wrap your app with the client provider
|
|
31
159
|
|
|
32
160
|
```tsx
|
|
33
|
-
//
|
|
161
|
+
// lib/auth/provider.tsx
|
|
34
162
|
'use client';
|
|
35
163
|
|
|
36
|
-
import {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
|
47
|
-
return
|
|
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 {
|
|
182
|
+
import type { ReactNode } from 'react';
|
|
183
|
+
import { AuthProvider } from '../lib/auth/provider';
|
|
54
184
|
|
|
55
|
-
export default function RootLayout({ children }: { children:
|
|
185
|
+
export default function RootLayout({ children }: { children: ReactNode }) {
|
|
56
186
|
return (
|
|
57
|
-
<html>
|
|
187
|
+
<html lang="en-US">
|
|
58
188
|
<body>
|
|
59
|
-
<
|
|
189
|
+
<AuthProvider>{children}</AuthProvider>
|
|
60
190
|
</body>
|
|
61
191
|
</html>
|
|
62
192
|
);
|
|
63
193
|
}
|
|
64
194
|
```
|
|
65
195
|
|
|
66
|
-
|
|
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
|
-
|
|
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
|
-
|
|
204
|
+
export const dynamic = 'force-dynamic';
|
|
72
205
|
|
|
73
|
-
export default function
|
|
74
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
231
|
+
The current browser tab navigates to the Strivacity-hosted login page and back to the configured `redirectUri` after authentication.
|
|
81
232
|
|
|
82
|
-
|
|
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
|
|
91
|
-
const { login } = useStrivacity();
|
|
247
|
+
export default function LoginPage() {
|
|
248
|
+
const { loading, login } = useStrivacity<RedirectFlow>();
|
|
92
249
|
|
|
93
250
|
useEffect(() => {
|
|
94
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
298
|
+
export default function CallbackPage() {
|
|
299
|
+
const { loading, handleCallback } = useStrivacity();
|
|
117
300
|
const router = useRouter();
|
|
118
|
-
const
|
|
301
|
+
const searchParams = new URLSearchParams(window.location.search);
|
|
119
302
|
|
|
120
303
|
useEffect(() => {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
|
|
140
|
-
|
|
141
|
-
```tsx
|
|
142
|
-
'use client';
|
|
326
|
+
**Server-managed sessions**:
|
|
143
327
|
|
|
144
|
-
|
|
328
|
+
Forward the callback query string to `/auth/callback` - the Server SDK completes the code exchange and redirects to `postLoginRedirectUri`:
|
|
145
329
|
|
|
146
|
-
|
|
147
|
-
|
|
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
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
-
|
|
340
|
+
##### Registration
|
|
193
341
|
|
|
194
|
-
|
|
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 {
|
|
201
|
-
import {
|
|
351
|
+
import { useStrivacity } from '@strivacity/sdk-next/client';
|
|
352
|
+
import type { RedirectFlow } from '@strivacity/sdk-next/client';
|
|
202
353
|
|
|
203
|
-
export default function
|
|
204
|
-
const
|
|
205
|
-
const { isAuthenticated, logout } = useStrivacity();
|
|
354
|
+
export default function RegisterPage() {
|
|
355
|
+
const { loading, register } = useStrivacity<RedirectFlow>();
|
|
206
356
|
|
|
207
357
|
useEffect(() => {
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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>
|
|
369
|
+
<h1>Redirecting to registration...</h1>
|
|
220
370
|
</section>
|
|
221
371
|
);
|
|
222
372
|
}
|
|
223
373
|
```
|
|
224
374
|
|
|
225
|
-
|
|
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 {
|
|
398
|
+
import { useEffect } from 'react';
|
|
399
|
+
import { useStrivacity } from '@strivacity/sdk-next/client';
|
|
231
400
|
|
|
232
|
-
export default function
|
|
233
|
-
const {
|
|
234
|
-
const name = `${idTokenClaims?.given_name} ${idTokenClaims?.family_name}`;
|
|
401
|
+
export default function LogoutPage() {
|
|
402
|
+
const { loading, logout } = useStrivacity();
|
|
235
403
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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
|
-
|
|
416
|
+
**Server-managed sessions**:
|
|
251
417
|
|
|
252
|
-
|
|
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
|
-
|
|
420
|
+
```tsx
|
|
421
|
+
// app/logout/page.tsx
|
|
422
|
+
import { redirect } from 'next/navigation';
|
|
255
423
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
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
|
-
|
|
429
|
+
##### Token management
|
|
292
430
|
|
|
293
|
-
|
|
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 {
|
|
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
|
|
304
|
-
const
|
|
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
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
443
|
+
async function onRefresh() {
|
|
444
|
+
// Refresh the access token using the refresh token
|
|
445
|
+
await refresh();
|
|
446
|
+
}
|
|
312
447
|
|
|
313
|
-
|
|
314
|
-
|
|
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
|
-
|
|
318
|
-
|
|
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
|
-
|
|
323
|
-
router.push('/profile');
|
|
324
|
-
};
|
|
469
|
+
**Server-managed sessions**:
|
|
325
470
|
|
|
326
|
-
|
|
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
|
-
|
|
335
|
-
|
|
336
|
-
};
|
|
473
|
+
```tsx
|
|
474
|
+
'use client';
|
|
337
475
|
|
|
338
|
-
|
|
339
|
-
|
|
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
|
-
|
|
343
|
-
|
|
344
|
-
|
|
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
|
-
<
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
521
|
+
export default function LoginPage() {
|
|
522
|
+
const { loading, login } = useStrivacity<PopupFlow>();
|
|
375
523
|
const router = useRouter();
|
|
376
|
-
const
|
|
524
|
+
const startedRef = useRef(false);
|
|
377
525
|
|
|
378
526
|
useEffect(() => {
|
|
379
|
-
(
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
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>
|
|
557
|
+
<h1>Opening login popup...</h1>
|
|
399
558
|
</section>
|
|
400
559
|
);
|
|
401
560
|
}
|
|
402
561
|
```
|
|
403
562
|
|
|
404
|
-
|
|
563
|
+
**Server-managed sessions**:
|
|
405
564
|
|
|
406
|
-
|
|
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
|
|
592
|
+
export default function RegisterPage() {
|
|
593
|
+
const { loading, register } = useStrivacity<PopupFlow>();
|
|
416
594
|
const router = useRouter();
|
|
417
|
-
const
|
|
595
|
+
const startedRef = useRef(false);
|
|
418
596
|
|
|
419
597
|
useEffect(() => {
|
|
420
|
-
(
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
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>
|
|
628
|
+
<h1>Opening registration popup...</h1>
|
|
439
629
|
</section>
|
|
440
630
|
);
|
|
441
631
|
}
|
|
442
632
|
```
|
|
443
633
|
|
|
444
|
-
|
|
634
|
+
**Server-managed sessions**:
|
|
445
635
|
|
|
446
|
-
|
|
636
|
+
Popup mode always needs client-side JavaScript to open the window, so there's no server-only alternative here.
|
|
447
637
|
|
|
448
|
-
|
|
638
|
+
##### Logout
|
|
449
639
|
|
|
450
|
-
|
|
640
|
+
**Client-managed sessions**:
|
|
451
641
|
|
|
452
|
-
|
|
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/
|
|
645
|
+
// app/logout/page.tsx
|
|
458
646
|
'use client';
|
|
459
647
|
|
|
460
648
|
import { useEffect } from 'react';
|
|
461
|
-
import {
|
|
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
|
-
|
|
466
|
-
|
|
655
|
+
if (loading) {
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
467
658
|
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
671
|
+
// app/logout/page.tsx
|
|
672
|
+
import { redirect } from 'next/navigation';
|
|
494
673
|
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
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
|
-
|
|
679
|
+
##### Token management
|
|
506
680
|
|
|
507
|
-
|
|
681
|
+
**Client-managed sessions**:
|
|
508
682
|
|
|
509
|
-
|
|
510
|
-
import type { SDKLogging } from '@strivacity/sdk-next';
|
|
683
|
+
Call these methods to manage the session and access token client-side.
|
|
511
684
|
|
|
512
|
-
|
|
513
|
-
|
|
685
|
+
```tsx
|
|
686
|
+
'use client';
|
|
514
687
|
|
|
515
|
-
|
|
516
|
-
console.debug(this.xEventId ? `[${this.xEventId}] ${message}` : message);
|
|
517
|
-
}
|
|
688
|
+
import { useStrivacity } from '@strivacity/sdk-next/client';
|
|
518
689
|
|
|
519
|
-
|
|
520
|
-
|
|
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
|
-
|
|
524
|
-
|
|
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
|
-
|
|
528
|
-
|
|
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
|
-
|
|
534
|
-
|
|
535
|
-
## HTTP Client
|
|
719
|
+
**Server-managed sessions**:
|
|
536
720
|
|
|
537
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
557
|
-
|
|
558
|
-
|
|
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
|
-
|
|
569
|
-
|
|
570
|
-
|
|
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
|
-
|
|
746
|
+
---
|
|
575
747
|
|
|
576
|
-
|
|
748
|
+
#### embedded mode
|
|
577
749
|
|
|
578
|
-
For
|
|
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
|
-
|
|
754
|
+
##### Login / Register
|
|
585
755
|
|
|
586
|
-
|
|
756
|
+
**Client-managed sessions**:
|
|
587
757
|
|
|
588
|
-
```
|
|
589
|
-
|
|
590
|
-
|
|
758
|
+
```tsx
|
|
759
|
+
// app/login/page.tsx
|
|
760
|
+
'use client';
|
|
591
761
|
|
|
592
|
-
|
|
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
|
-
|
|
767
|
+
export default function LoginPage() {
|
|
768
|
+
const router = useRouter();
|
|
769
|
+
const { sdk } = useStrivacity();
|
|
770
|
+
const searchParams = new URLSearchParams(window.location.search);
|
|
595
771
|
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
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
|
-
|
|
784
|
+
useEffect(() => {
|
|
785
|
+
const element = loginRef.current;
|
|
609
786
|
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
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
|
-
|
|
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
|
-
-
|
|
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
|
-
|
|
831
|
+
##### Controlling when the flow starts
|
|
633
832
|
|
|
634
|
-
-
|
|
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
|
-
|
|
839
|
+
import { useRef } from 'react';
|
|
840
|
+
import { useRouter } from 'next/navigation';
|
|
841
|
+
import type { LoginComponent } from '@strivacity/sdk-next/client/types';
|
|
645
842
|
|
|
646
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
870
|
+
```tsx
|
|
871
|
+
// app/login/page.tsx
|
|
872
|
+
'use client';
|
|
656
873
|
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
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
|
-
|
|
878
|
+
export function LazyLogin() {
|
|
879
|
+
const router = useRouter();
|
|
880
|
+
const loginRef = useRef<LoginComponent>(null);
|
|
665
881
|
|
|
666
|
-
|
|
882
|
+
async function onStartClick() {
|
|
883
|
+
if (!loginRef.current) {
|
|
884
|
+
return;
|
|
885
|
+
}
|
|
667
886
|
|
|
668
|
-
|
|
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
|
-
|
|
1540
|
+
useEffect(() => {
|
|
1541
|
+
if (sdkLoading) {
|
|
1542
|
+
return;
|
|
1543
|
+
}
|
|
671
1544
|
|
|
672
|
-
|
|
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
|
-
|
|
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).
|