@strivacity/sdk-react 3.0.3 → 4.0.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1 -1
- package/README.md +1562 -406
- package/dist/assets/rolldown-runtime.cjs +1 -0
- package/dist/assets/rolldown-runtime.mjs +1 -0
- package/dist/components.cjs +2 -0
- package/dist/components.cjs.map +1 -0
- package/dist/components.d.ts +11 -0
- package/dist/components.mjs +2 -0
- package/dist/components.mjs.map +1 -0
- package/dist/errors.cjs +2 -0
- package/dist/errors.cjs.map +1 -0
- package/dist/errors.d.ts +1 -0
- package/dist/errors.mjs +2 -0
- package/dist/errors.mjs.map +1 -0
- package/dist/hooks.cjs +2 -0
- package/dist/hooks.cjs.map +1 -0
- package/dist/hooks.d.ts +33 -0
- package/dist/hooks.mjs +2 -0
- package/dist/hooks.mjs.map +1 -0
- package/dist/index.cjs +2 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +9 -13
- package/dist/index.mjs +2 -2
- package/dist/index.mjs.map +1 -1
- package/dist/storages.cjs +1 -0
- package/dist/storages.d.ts +1 -0
- package/dist/storages.mjs +1 -0
- package/dist/types.cjs +1 -2
- package/dist/types.d.ts +147 -110
- package/dist/types.mjs +1 -2
- package/dist/utils.cjs +1 -0
- package/dist/utils.d.ts +1 -0
- package/dist/utils.mjs +1 -0
- package/package.json +51 -7
- package/testing/tests/components.spec.tsx +146 -0
- package/testing/tests/errors.spec.ts +10 -0
- package/testing/tests/guard.spec.tsx +119 -0
- package/testing/tests/hooks.spec.tsx +418 -0
- package/testing/tests/index.spec.ts +105 -0
- package/testing/tests/storages.spec.ts +10 -0
- package/testing/tests/utils.spec.ts +10 -0
- package/testing/utils/common.tsx +63 -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/types.cjs.map +0 -1
- package/dist/types.mjs.map +0 -1
package/README.md
CHANGED
|
@@ -1,116 +1,222 @@
|
|
|
1
1
|
# @strivacity/sdk-react
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
React SDK for [Strivacity](https://www.strivacity.com) - adds PKCE-protected OIDC authentication to your React application with hooks.
|
|
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/react) - Working React example covering all four login modes
|
|
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
|
+
- [Quick start](#quick-start)
|
|
18
|
+
- [Authentication modes](#authentication-modes)
|
|
19
|
+
- [redirect mode](#redirect-mode)
|
|
20
|
+
- [popup mode](#popup-mode)
|
|
21
|
+
- [embedded mode](#embedded-mode)
|
|
22
|
+
- [native mode](#native-mode)
|
|
23
|
+
- [Hooks API](#hooks-api)
|
|
24
|
+
- [useStrivacity](#usestrivacity)
|
|
25
|
+
- [useNativeLogin](#usenativelogin)
|
|
26
|
+
- [withAuthGuard](#withauthguard)
|
|
27
|
+
- [Route guards](#route-guards)
|
|
28
|
+
- [Token management](#token-management)
|
|
29
|
+
- [Shared features](#shared-features)
|
|
30
|
+
- [Configuration reference](#configuration-reference)
|
|
31
|
+
- [Migration guide](#migration-guide)
|
|
32
|
+
- [Vulnerability Reporting](#vulnerability-reporting)
|
|
33
|
+
- [License](#license)
|
|
34
|
+
- [Contributing](#contributing)
|
|
4
35
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
## Overview
|
|
8
|
-
|
|
9
|
-
This SDK allows you to integrate Strivacity's policy-driven journeys into your React application. It wraps the `@strivacity/sdk-core` library as a React context provider and exposes a `useStrivacity` hook that provides authentication state and methods throughout your component tree. The SDK uses the OAuth 2.0 PKCE flow to authenticate with Strivacity. For detailed configuration options, available modes, and advanced usage refer to the [`@strivacity/sdk-core` documentation](https://github.com/Strivacity/sdk-js/blob/main/packages/sdk-core/README.md).
|
|
10
|
-
|
|
11
|
-
## Demo Application
|
|
36
|
+
---
|
|
12
37
|
|
|
13
|
-
|
|
14
|
-
- [Ionic Example app](https://github.com/Strivacity/sdk-js/tree/main/apps/ionic-react)
|
|
38
|
+
## Prerequisites
|
|
15
39
|
|
|
16
|
-
|
|
40
|
+
- React 18+
|
|
41
|
+
- A Strivacity tenant with an application configured (issuer URL, client ID, redirect URI)
|
|
17
42
|
|
|
18
|
-
|
|
43
|
+
---
|
|
19
44
|
|
|
20
|
-
##
|
|
45
|
+
## Installation
|
|
21
46
|
|
|
22
47
|
```bash
|
|
23
48
|
npm install @strivacity/sdk-react
|
|
24
49
|
```
|
|
25
50
|
|
|
26
|
-
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## Choosing a mode
|
|
54
|
+
|
|
55
|
+
The SDK supports **four authentication modes**:
|
|
27
56
|
|
|
28
|
-
|
|
57
|
+
| Mode | Login UI | Best for |
|
|
58
|
+
| ---------- | ---------------------------------------- | -------------------------------------------- |
|
|
59
|
+
| `redirect` | Strivacity hosted page | Standard web apps |
|
|
60
|
+
| `popup` | Strivacity hosted page in a popup | SPAs that must stay on the current page |
|
|
61
|
+
| `embedded` | Strivacity web components in your page | Branded login inside your own layout |
|
|
62
|
+
| `native` | Your own components driven by flow state | Full UI control, step-by-step form rendering |
|
|
29
63
|
|
|
30
|
-
|
|
64
|
+
> 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.
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## Quick start
|
|
69
|
+
|
|
70
|
+
### 1. Wrap your app with the provider
|
|
71
|
+
|
|
72
|
+
`StyAuthProvider` is a React context provider - wrap it around your root component once. It initializes the SDK and provides the auth context to every component in the tree via React context.
|
|
31
73
|
|
|
32
74
|
```tsx
|
|
75
|
+
// main.tsx
|
|
33
76
|
import { createRoot } from 'react-dom/client';
|
|
34
|
-
import {
|
|
35
|
-
import {
|
|
36
|
-
|
|
37
|
-
const options: SDKOptions = {
|
|
38
|
-
mode: 'redirect', // or 'popup', 'native', 'embedded'
|
|
39
|
-
issuer: 'https://<YOUR_DOMAIN>',
|
|
40
|
-
scopes: ['openid', 'profile'],
|
|
41
|
-
clientId: '<YOUR_CLIENT_ID>',
|
|
42
|
-
redirectUri: '<YOUR_REDIRECT_URI>',
|
|
43
|
-
};
|
|
77
|
+
import { StyAuthProvider } from '@strivacity/sdk-react';
|
|
78
|
+
import { App } from './App';
|
|
44
79
|
|
|
45
80
|
createRoot(document.getElementById('app')!).render(
|
|
46
|
-
<
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
81
|
+
<StyAuthProvider
|
|
82
|
+
options={{
|
|
83
|
+
mode: 'redirect', // authentication mode
|
|
84
|
+
issuer: 'https://<YOUR_TENANT_DOMAIN>', // OIDC provider URL
|
|
85
|
+
clientId: 'YOUR_CLIENT_ID', // OAuth2 client ID
|
|
86
|
+
redirectUri: 'https://your-app.example.com/callback', // callback URL after authentication
|
|
87
|
+
scopes: ['openid', 'profile', 'email'], // requested user permissions/data
|
|
88
|
+
}}
|
|
89
|
+
>
|
|
90
|
+
<App />
|
|
91
|
+
</StyAuthProvider>,
|
|
53
92
|
);
|
|
54
93
|
```
|
|
55
94
|
|
|
56
|
-
Use the
|
|
95
|
+
### 2. Use the auth state
|
|
96
|
+
|
|
97
|
+
Call `useStrivacity()` in any component to read the current authentication state and trigger login or logout.
|
|
57
98
|
|
|
58
99
|
```tsx
|
|
59
100
|
import { useStrivacity } from '@strivacity/sdk-react';
|
|
60
101
|
|
|
61
|
-
export
|
|
62
|
-
const { loading, isAuthenticated, idTokenClaims } = useStrivacity();
|
|
102
|
+
export function Header() {
|
|
103
|
+
const { loading, isAuthenticated, idTokenClaims, login, logout } = useStrivacity();
|
|
104
|
+
|
|
105
|
+
if (loading) {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return (
|
|
110
|
+
<div>
|
|
111
|
+
{isAuthenticated ? (
|
|
112
|
+
<>
|
|
113
|
+
<span>Hello, {idTokenClaims?.given_name}</span>
|
|
114
|
+
<button onClick={() => logout()}>Log out</button>
|
|
115
|
+
</>
|
|
116
|
+
) : (
|
|
117
|
+
<button onClick={() => login()}>Log in</button>
|
|
118
|
+
)}
|
|
119
|
+
</div>
|
|
120
|
+
);
|
|
63
121
|
}
|
|
64
122
|
```
|
|
65
123
|
|
|
66
|
-
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
## Authentication modes
|
|
67
127
|
|
|
68
|
-
|
|
128
|
+
### redirect mode
|
|
69
129
|
|
|
70
|
-
|
|
130
|
+
> For details on how this mode works, see the [hosted journey documentation](https://docs.strivacity.com/reference/hosted-journey).
|
|
131
|
+
|
|
132
|
+
The current browser tab navigates to the Strivacity-hosted login page and back to the configured `redirectUri` after authentication.
|
|
133
|
+
|
|
134
|
+
```tsx
|
|
135
|
+
// main.tsx
|
|
136
|
+
import { createRoot } from 'react-dom/client';
|
|
137
|
+
import { StyAuthProvider } from '@strivacity/sdk-react';
|
|
138
|
+
import { App } from './App';
|
|
139
|
+
|
|
140
|
+
createRoot(document.getElementById('app')!).render(
|
|
141
|
+
<StyAuthProvider
|
|
142
|
+
options={{
|
|
143
|
+
mode: 'redirect', // authentication mode
|
|
144
|
+
issuer: 'https://<YOUR_TENANT_DOMAIN>', // OIDC provider URL
|
|
145
|
+
clientId: 'YOUR_CLIENT_ID', // OAuth2 client ID
|
|
146
|
+
redirectUri: 'https://your-app.example.com/callback', // callback URL after authentication
|
|
147
|
+
scopes: ['openid', 'profile', 'email'], // requested user permissions/data
|
|
148
|
+
}}
|
|
149
|
+
>
|
|
150
|
+
<App />
|
|
151
|
+
</StyAuthProvider>,
|
|
152
|
+
);
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
#### Login
|
|
156
|
+
|
|
157
|
+
Call this to start the login flow. It redirects the user to the Strivacity login page in current browser tab, where they authenticate.
|
|
71
158
|
|
|
72
159
|
```tsx
|
|
160
|
+
// pages/Login.tsx
|
|
73
161
|
import { useEffect } from 'react';
|
|
74
162
|
import { useStrivacity } from '@strivacity/sdk-react';
|
|
163
|
+
import type { RedirectFlow } from '@strivacity/sdk-react';
|
|
75
164
|
|
|
76
165
|
export default function Login() {
|
|
77
|
-
const { login } = useStrivacity();
|
|
166
|
+
const { loading, login } = useStrivacity<RedirectFlow>();
|
|
78
167
|
|
|
79
168
|
useEffect(() => {
|
|
80
|
-
|
|
81
|
-
|
|
169
|
+
if (loading) {
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
void login({
|
|
174
|
+
// Optional parameters
|
|
175
|
+
loginHint: 'user@example.com', // identifier or JWT-encoded data to hint the login flow
|
|
176
|
+
acrValues: ['urn:strivacity:loa:2'], // request specific authentication context
|
|
177
|
+
audiences: ['https://api.example.com'], // target resources for the access token
|
|
178
|
+
locationMethod: 'assign', // window.location method: 'assign' adds to browser history, 'replace' doesn't (default: 'assign')
|
|
179
|
+
targetWindow: 'self', // 'self' redirects current window, 'top' redirects top-level window (default: 'self')
|
|
180
|
+
});
|
|
181
|
+
}, [loading, login]);
|
|
82
182
|
|
|
83
183
|
return (
|
|
84
184
|
<section>
|
|
85
|
-
<h1>Redirecting...</h1>
|
|
185
|
+
<h1>Redirecting to login...</h1>
|
|
86
186
|
</section>
|
|
87
187
|
);
|
|
88
188
|
}
|
|
89
189
|
```
|
|
90
190
|
|
|
91
|
-
####
|
|
191
|
+
#### Handle the callback
|
|
92
192
|
|
|
93
|
-
|
|
193
|
+
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](#storages). After that you can redirect to a protected page or render your app.
|
|
94
194
|
|
|
95
195
|
```tsx
|
|
196
|
+
// pages/Callback.tsx
|
|
197
|
+
import { useStrivacity } from '@strivacity/sdk-react';
|
|
96
198
|
import { useEffect } from 'react';
|
|
97
199
|
import { useNavigate } from 'react-router';
|
|
98
|
-
import { useStrivacity } from '@strivacity/sdk-react';
|
|
99
200
|
|
|
100
201
|
export default function Callback() {
|
|
202
|
+
const { loading, handleCallback } = useStrivacity();
|
|
101
203
|
const navigate = useNavigate();
|
|
102
|
-
const
|
|
204
|
+
const searchParams = new URLSearchParams(window.location.search);
|
|
103
205
|
|
|
104
206
|
useEffect(() => {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
}
|
|
113
|
-
|
|
207
|
+
if (loading) {
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (searchParams.get('error')) {
|
|
212
|
+
void navigate(`/error?${searchParams.toString()}`, { replace: true });
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
handleCallback()
|
|
217
|
+
.then(() => navigate('/profile'))
|
|
218
|
+
.catch((error) => navigate(`/error?message=${encodeURIComponent(error.message)}`));
|
|
219
|
+
}, [loading]);
|
|
114
220
|
|
|
115
221
|
return (
|
|
116
222
|
<section>
|
|
@@ -120,325 +226,329 @@ export default function Callback() {
|
|
|
120
226
|
}
|
|
121
227
|
```
|
|
122
228
|
|
|
123
|
-
|
|
229
|
+
> The callback URL is automatically read from `window.location.href` if not provided. You can pass a custom URL as the first parameter: `await sdk.handleCallback(customUrl)`.
|
|
230
|
+
|
|
231
|
+
#### Registration
|
|
232
|
+
|
|
233
|
+
Call this to start the registration flow. It works the same way as `login()` but opens the registration form instead.
|
|
124
234
|
|
|
125
235
|
```tsx
|
|
236
|
+
// pages/Register.tsx
|
|
237
|
+
import { useEffect } from 'react';
|
|
126
238
|
import { useStrivacity } from '@strivacity/sdk-react';
|
|
239
|
+
import type { RedirectFlow } from '@strivacity/sdk-react';
|
|
127
240
|
|
|
128
|
-
export default function
|
|
129
|
-
const { loading,
|
|
241
|
+
export default function Register() {
|
|
242
|
+
const { loading, register } = useStrivacity<RedirectFlow>();
|
|
130
243
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
244
|
+
useEffect(() => {
|
|
245
|
+
if (loading) {
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
void register({
|
|
250
|
+
loginHint: 'user@example.com',
|
|
251
|
+
});
|
|
252
|
+
}, [loading, register]);
|
|
134
253
|
|
|
135
254
|
return (
|
|
136
255
|
<section>
|
|
137
|
-
<
|
|
138
|
-
<dt>
|
|
139
|
-
<strong>accessToken</strong>
|
|
140
|
-
</dt>
|
|
141
|
-
<dd>
|
|
142
|
-
<pre>{JSON.stringify(accessToken)}</pre>
|
|
143
|
-
</dd>
|
|
144
|
-
<dt>
|
|
145
|
-
<strong>refreshToken</strong>
|
|
146
|
-
</dt>
|
|
147
|
-
<dd>
|
|
148
|
-
<pre>{JSON.stringify(refreshToken)}</pre>
|
|
149
|
-
</dd>
|
|
150
|
-
<dt>
|
|
151
|
-
<strong>accessTokenExpired</strong>
|
|
152
|
-
</dt>
|
|
153
|
-
<dd>
|
|
154
|
-
<pre>{JSON.stringify(accessTokenExpired)}</pre>
|
|
155
|
-
</dd>
|
|
156
|
-
<dt>
|
|
157
|
-
<strong>accessTokenExpirationDate</strong>
|
|
158
|
-
</dt>
|
|
159
|
-
<dd>
|
|
160
|
-
<pre>{accessTokenExpirationDate ? new Date(accessTokenExpirationDate * 1000).toLocaleString() : JSON.stringify(null)}</pre>
|
|
161
|
-
</dd>
|
|
162
|
-
<dt>
|
|
163
|
-
<strong>claims</strong>
|
|
164
|
-
</dt>
|
|
165
|
-
<dd>
|
|
166
|
-
<pre>{JSON.stringify(idTokenClaims, null, 2)}</pre>
|
|
167
|
-
</dd>
|
|
168
|
-
</dl>
|
|
256
|
+
<h1>Redirecting to registration...</h1>
|
|
169
257
|
</section>
|
|
170
258
|
);
|
|
171
259
|
}
|
|
172
260
|
```
|
|
173
261
|
|
|
174
|
-
#### Logout
|
|
262
|
+
#### Logout
|
|
175
263
|
|
|
176
|
-
|
|
264
|
+
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`.
|
|
177
265
|
|
|
178
266
|
```tsx
|
|
179
|
-
|
|
180
|
-
import { useNavigate } from 'react-router';
|
|
267
|
+
// pages/Logout.tsx
|
|
181
268
|
import { useStrivacity } from '@strivacity/sdk-react';
|
|
269
|
+
import { useEffect } from 'react';
|
|
182
270
|
|
|
183
271
|
export default function Logout() {
|
|
184
|
-
const
|
|
185
|
-
const { isAuthenticated, logout } = useStrivacity();
|
|
272
|
+
const { loading, logout } = useStrivacity();
|
|
186
273
|
|
|
187
274
|
useEffect(() => {
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
} else {
|
|
192
|
-
await navigate('/');
|
|
193
|
-
}
|
|
194
|
-
})();
|
|
195
|
-
}, []);
|
|
275
|
+
if (loading) {
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
196
278
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
);
|
|
279
|
+
void logout();
|
|
280
|
+
}, [loading, logout]);
|
|
281
|
+
|
|
282
|
+
return null;
|
|
202
283
|
}
|
|
203
284
|
```
|
|
204
285
|
|
|
205
|
-
|
|
286
|
+
> The `postLogoutRedirectUri` must be configured in your Strivacity application client settings as an allowed post-logout redirect URI. If the URL is invalid or not provided, the user remains on the Strivacity-hosted logged out page.
|
|
287
|
+
|
|
288
|
+
#### Token management
|
|
289
|
+
|
|
290
|
+
Call these methods to manage the session and access token.
|
|
206
291
|
|
|
207
292
|
```tsx
|
|
208
293
|
import { useStrivacity } from '@strivacity/sdk-react';
|
|
209
294
|
|
|
210
|
-
export
|
|
211
|
-
const {
|
|
212
|
-
const name = `${idTokenClaims?.given_name} ${idTokenClaims?.family_name}`;
|
|
295
|
+
export function TokenPanel() {
|
|
296
|
+
const { loading, idTokenClaims, accessToken, refreshToken, refresh, revoke } = useStrivacity();
|
|
213
297
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
298
|
+
async function onRefresh() {
|
|
299
|
+
// Refresh the access token using the refresh token
|
|
300
|
+
await refresh();
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
async function onRevoke() {
|
|
304
|
+
// Revoke all tokens at the authorization server and clear the local session
|
|
305
|
+
await revoke();
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (loading) {
|
|
309
|
+
return null;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// idTokenClaims, accessToken, and refreshToken are plain values from context -
|
|
313
|
+
// they update automatically whenever the SDK's session changes
|
|
314
|
+
return (
|
|
220
315
|
<div>
|
|
221
|
-
<
|
|
222
|
-
<button onClick={
|
|
316
|
+
<button onClick={onRefresh}>Refresh</button>
|
|
317
|
+
<button onClick={onRevoke}>Revoke</button>
|
|
318
|
+
<pre>{JSON.stringify({ idTokenClaims, accessToken, refreshToken }, null, 2)}</pre>
|
|
223
319
|
</div>
|
|
224
320
|
);
|
|
225
321
|
}
|
|
226
322
|
```
|
|
227
323
|
|
|
228
|
-
|
|
324
|
+
---
|
|
229
325
|
|
|
230
|
-
|
|
326
|
+
### popup mode
|
|
231
327
|
|
|
232
|
-
|
|
328
|
+
> For details on how this mode works, see the [hosted journey documentation](https://docs.strivacity.com/reference/hosted-journey).
|
|
233
329
|
|
|
234
|
-
|
|
235
|
-
npm install sass luxon
|
|
236
|
-
npm install --save-dev @types/luxon
|
|
237
|
-
```
|
|
330
|
+
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.
|
|
238
331
|
|
|
239
332
|
```tsx
|
|
240
|
-
|
|
241
|
-
import
|
|
242
|
-
import
|
|
243
|
-
import
|
|
244
|
-
import MultiSelectWidget from './multiselect.widget';
|
|
245
|
-
import PasscodeWidget from './passcode.widget';
|
|
246
|
-
import LoadingWidget from './loading.widget';
|
|
247
|
-
import PasswordWidget from './password.widget';
|
|
248
|
-
import PhoneWidget from './phone.widget';
|
|
249
|
-
import SelectWidget from './select.widget';
|
|
250
|
-
import StaticWidget from './static.widget';
|
|
251
|
-
import SubmitWidget from './submit.widget';
|
|
333
|
+
// main.tsx
|
|
334
|
+
import { createRoot } from 'react-dom/client';
|
|
335
|
+
import { StyAuthProvider } from '@strivacity/sdk-react';
|
|
336
|
+
import { App } from './App';
|
|
252
337
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
};
|
|
338
|
+
createRoot(document.getElementById('app')!).render(
|
|
339
|
+
<StyAuthProvider
|
|
340
|
+
options={{
|
|
341
|
+
mode: 'popup', // authentication mode
|
|
342
|
+
issuer: 'https://<YOUR_TENANT_DOMAIN>', // OIDC provider URL
|
|
343
|
+
clientId: 'YOUR_CLIENT_ID', // OAuth2 client ID
|
|
344
|
+
redirectUri: 'https://your-app.example.com/callback', // callback URL after authentication
|
|
345
|
+
scopes: ['openid', 'profile', 'email'], // requested user permissions/data
|
|
346
|
+
}}
|
|
347
|
+
>
|
|
348
|
+
<App />
|
|
349
|
+
</StyAuthProvider>,
|
|
350
|
+
);
|
|
267
351
|
```
|
|
268
352
|
|
|
269
|
-
#### Login
|
|
353
|
+
#### Login
|
|
354
|
+
|
|
355
|
+
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](#storages).
|
|
270
356
|
|
|
271
|
-
|
|
357
|
+
By default a centered popup window opens. Pass `popupWindowTarget` to change where the window opens, and `popupWindowFeatures` to control its size and position:
|
|
272
358
|
|
|
273
359
|
```tsx
|
|
274
|
-
|
|
360
|
+
// pages/Login.tsx
|
|
361
|
+
import { useEffect, useRef } from 'react';
|
|
362
|
+
import { useStrivacity } from '@strivacity/sdk-react';
|
|
275
363
|
import { useNavigate } from 'react-router';
|
|
276
|
-
import {
|
|
277
|
-
import { widgets } from '@/components/widgets';
|
|
364
|
+
import type { PopupFlow } from '@strivacity/sdk-react';
|
|
278
365
|
|
|
279
366
|
export default function Login() {
|
|
367
|
+
const { loading, login } = useStrivacity<PopupFlow>();
|
|
280
368
|
const navigate = useNavigate();
|
|
281
|
-
const
|
|
282
|
-
const [language, setLanguage] = useState<string | null>(null);
|
|
369
|
+
const startedRef = useRef(false);
|
|
283
370
|
|
|
284
371
|
useEffect(() => {
|
|
285
|
-
if (
|
|
286
|
-
|
|
287
|
-
setSessionId(url.searchParams.get('session_id'));
|
|
288
|
-
|
|
289
|
-
if (url.searchParams.has('language')) {
|
|
290
|
-
setLanguage(url.searchParams.get('language'));
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
url.search = '';
|
|
294
|
-
window.history.replaceState({}, '', url.toString());
|
|
295
|
-
}
|
|
296
|
-
}, []);
|
|
297
|
-
|
|
298
|
-
const onLogin = async () => {
|
|
299
|
-
await navigate('/profile');
|
|
300
|
-
};
|
|
301
|
-
|
|
302
|
-
const onFallback = (error: FallbackError) => {
|
|
303
|
-
if (error.url) {
|
|
304
|
-
window.location.href = error.url.toString();
|
|
305
|
-
} else {
|
|
306
|
-
alert(error);
|
|
372
|
+
if (loading || startedRef.current) {
|
|
373
|
+
return;
|
|
307
374
|
}
|
|
308
|
-
};
|
|
309
|
-
|
|
310
|
-
const onError = (error: string) => {
|
|
311
|
-
alert(error);
|
|
312
|
-
};
|
|
313
|
-
|
|
314
|
-
const onGlobalMessage = (message: string) => {
|
|
315
|
-
alert(message);
|
|
316
|
-
};
|
|
317
375
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
376
|
+
// Prevent multiple calls (React StrictMode)
|
|
377
|
+
startedRef.current = true;
|
|
378
|
+
|
|
379
|
+
login({
|
|
380
|
+
// Optional parameters
|
|
381
|
+
loginHint: 'user@example.com', // identifier or JWT-encoded data to hint the login flow
|
|
382
|
+
acrValues: ['urn:strivacity:loa:2'], // request specific authentication context
|
|
383
|
+
audiences: ['https://api.example.com'], // target resources for the access token
|
|
384
|
+
popupWindowTarget: '_blank', // any valid browsing context name
|
|
385
|
+
popupWindowFeatures: {
|
|
386
|
+
width: 500,
|
|
387
|
+
height: 700,
|
|
388
|
+
left: 100,
|
|
389
|
+
top: 100,
|
|
390
|
+
toolbar: false,
|
|
391
|
+
location: false,
|
|
392
|
+
resizable: true,
|
|
393
|
+
scrollbars: true,
|
|
394
|
+
},
|
|
395
|
+
})
|
|
396
|
+
.then(() => navigate('/profile'))
|
|
397
|
+
.catch((error) => navigate(`/error?message=${encodeURIComponent(error.message)}`));
|
|
398
|
+
}, [loading, login, navigate]);
|
|
322
399
|
|
|
323
400
|
return (
|
|
324
|
-
<
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
language={language}
|
|
328
|
-
onLanguageChange={(lang) => setLanguage(lang)}
|
|
329
|
-
onFallback={onFallback}
|
|
330
|
-
onLogin={() => void onLogin()}
|
|
331
|
-
onError={onError}
|
|
332
|
-
onGlobalMessage={onGlobalMessage}
|
|
333
|
-
onBlockReady={onBlockReady}
|
|
334
|
-
/>
|
|
401
|
+
<section>
|
|
402
|
+
<h1>Opening login popup...</h1>
|
|
403
|
+
</section>
|
|
335
404
|
);
|
|
336
405
|
}
|
|
337
406
|
```
|
|
338
407
|
|
|
339
|
-
####
|
|
408
|
+
#### Handle the callback
|
|
409
|
+
|
|
410
|
+
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.
|
|
340
411
|
|
|
341
|
-
|
|
412
|
+
#### Registration
|
|
413
|
+
|
|
414
|
+
Call this to start the registration flow. It works the same way as `login()` but opens the registration form instead.
|
|
342
415
|
|
|
343
416
|
```tsx
|
|
344
|
-
|
|
345
|
-
import {
|
|
417
|
+
// pages/Register.tsx
|
|
418
|
+
import { useEffect, useRef } from 'react';
|
|
346
419
|
import { useStrivacity } from '@strivacity/sdk-react';
|
|
420
|
+
import { useNavigate } from 'react-router';
|
|
421
|
+
import type { PopupFlow } from '@strivacity/sdk-react';
|
|
347
422
|
|
|
348
|
-
export default function
|
|
423
|
+
export default function Register() {
|
|
424
|
+
const { loading, register } = useStrivacity<PopupFlow>();
|
|
349
425
|
const navigate = useNavigate();
|
|
350
|
-
const
|
|
426
|
+
const startedRef = useRef(false);
|
|
351
427
|
|
|
352
428
|
useEffect(() => {
|
|
353
|
-
(
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
429
|
+
if (loading || startedRef.current) {
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// Prevent multiple calls (React StrictMode)
|
|
434
|
+
startedRef.current = true;
|
|
435
|
+
|
|
436
|
+
register({
|
|
437
|
+
// Optional parameters
|
|
438
|
+
loginHint: 'user@example.com', // identifier or JWT-encoded data to hint the login flow
|
|
439
|
+
acrValues: ['urn:strivacity:loa:2'], // request specific authentication context
|
|
440
|
+
audiences: ['https://api.example.com'], // target resources for the access token
|
|
441
|
+
popupWindowTarget: '_blank', // any valid browsing context name
|
|
442
|
+
popupWindowFeatures: {
|
|
443
|
+
width: 500,
|
|
444
|
+
height: 700,
|
|
445
|
+
left: 100,
|
|
446
|
+
top: 100,
|
|
447
|
+
toolbar: false,
|
|
448
|
+
location: false,
|
|
449
|
+
resizable: true,
|
|
450
|
+
scrollbars: true,
|
|
451
|
+
},
|
|
452
|
+
})
|
|
453
|
+
.then(() => navigate('/profile'))
|
|
454
|
+
.catch((error) => navigate(`/error?message=${encodeURIComponent(error.message)}`));
|
|
455
|
+
}, [loading, register, navigate]);
|
|
369
456
|
|
|
370
457
|
return (
|
|
371
458
|
<section>
|
|
372
|
-
<h1>
|
|
459
|
+
<h1>Opening registration popup...</h1>
|
|
373
460
|
</section>
|
|
374
461
|
);
|
|
375
462
|
}
|
|
376
463
|
```
|
|
377
464
|
|
|
378
|
-
####
|
|
465
|
+
#### Logout
|
|
379
466
|
|
|
380
|
-
|
|
467
|
+
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`.
|
|
381
468
|
|
|
382
469
|
```tsx
|
|
383
|
-
|
|
384
|
-
import { useNavigate } from 'react-router';
|
|
470
|
+
// pages/Logout.tsx
|
|
385
471
|
import { useStrivacity } from '@strivacity/sdk-react';
|
|
472
|
+
import { useEffect } from 'react';
|
|
386
473
|
|
|
387
|
-
export default function
|
|
388
|
-
const
|
|
389
|
-
const { entry } = useStrivacity();
|
|
474
|
+
export default function Logout() {
|
|
475
|
+
const { loading, logout } = useStrivacity();
|
|
390
476
|
|
|
391
477
|
useEffect(() => {
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
478
|
+
if (loading) {
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
395
481
|
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
} else {
|
|
399
|
-
await navigate('/');
|
|
400
|
-
}
|
|
401
|
-
} catch (error) {
|
|
402
|
-
console.error('Entry failed:', error);
|
|
403
|
-
await navigate('/');
|
|
404
|
-
}
|
|
405
|
-
})();
|
|
406
|
-
}, []);
|
|
482
|
+
void logout();
|
|
483
|
+
}, [loading, logout]);
|
|
407
484
|
|
|
408
|
-
return
|
|
409
|
-
<section>
|
|
410
|
-
<h1>Loading...</h1>
|
|
411
|
-
</section>
|
|
412
|
-
);
|
|
485
|
+
return null;
|
|
413
486
|
}
|
|
414
487
|
```
|
|
415
488
|
|
|
416
|
-
|
|
489
|
+
> The `postLogoutRedirectUri` must be configured in your Strivacity application client settings as an allowed post-logout redirect URI. If the URL is invalid or not provided, the user remains on the Strivacity-hosted logged out page.
|
|
490
|
+
|
|
491
|
+
#### Token management
|
|
492
|
+
|
|
493
|
+
Call these methods to manage the session and access token.
|
|
494
|
+
|
|
495
|
+
```tsx
|
|
496
|
+
import { useStrivacity } from '@strivacity/sdk-react';
|
|
497
|
+
|
|
498
|
+
export function TokenPanel() {
|
|
499
|
+
const { loading, idTokenClaims, accessToken, refreshToken, refresh, revoke } = useStrivacity();
|
|
500
|
+
|
|
501
|
+
async function onRefresh() {
|
|
502
|
+
// Refresh the access token using the refresh token
|
|
503
|
+
await refresh();
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
async function onRevoke() {
|
|
507
|
+
// Revoke all tokens at the authorization server and clear the local session
|
|
508
|
+
await revoke();
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
if (loading) {
|
|
512
|
+
return null;
|
|
513
|
+
}
|
|
417
514
|
|
|
418
|
-
|
|
515
|
+
// idTokenClaims, accessToken, and refreshToken are plain values from context -
|
|
516
|
+
// they update automatically whenever the SDK's session changes
|
|
517
|
+
return (
|
|
518
|
+
<div>
|
|
519
|
+
<button onClick={onRefresh}>Refresh</button>
|
|
520
|
+
<button onClick={onRevoke}>Revoke</button>
|
|
521
|
+
<pre>{JSON.stringify({ idTokenClaims, accessToken, refreshToken }, null, 2)}</pre>
|
|
522
|
+
</div>
|
|
523
|
+
);
|
|
524
|
+
}
|
|
525
|
+
```
|
|
419
526
|
|
|
420
|
-
|
|
527
|
+
---
|
|
421
528
|
|
|
422
|
-
|
|
529
|
+
### embedded mode
|
|
423
530
|
|
|
424
|
-
|
|
531
|
+
> For details on how this mode works, see the [embedded journey documentation](https://docs.strivacity.com/reference/embedded-journey).
|
|
425
532
|
|
|
426
|
-
|
|
533
|
+
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 during application bootstrap, alongside SDK initialization:
|
|
427
534
|
|
|
428
535
|
```tsx
|
|
536
|
+
// main.tsx
|
|
429
537
|
import { createRoot } from 'react-dom/client';
|
|
430
|
-
import { StyAuthProvider } from '@strivacity/sdk-react';
|
|
538
|
+
import { StyAuthProvider, injectScript } from '@strivacity/sdk-react';
|
|
539
|
+
import { App } from './App';
|
|
431
540
|
|
|
432
|
-
|
|
541
|
+
// Load web components bundle
|
|
542
|
+
injectScript('sty-components', 'https://<YOUR_TENANT_DOMAIN>/assets/components/bundle.js');
|
|
433
543
|
|
|
434
544
|
createRoot(document.getElementById('app')!).render(
|
|
435
545
|
<StyAuthProvider
|
|
436
546
|
options={{
|
|
437
|
-
mode: 'embedded',
|
|
438
|
-
issuer: 'https://<
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
547
|
+
mode: 'embedded', // authentication mode
|
|
548
|
+
issuer: 'https://<YOUR_TENANT_DOMAIN>', // OIDC provider URL
|
|
549
|
+
clientId: 'YOUR_CLIENT_ID', // OAuth2 client ID
|
|
550
|
+
redirectUri: 'https://your-app.example.com/callback', // callback URL after authentication
|
|
551
|
+
scopes: ['openid', 'profile', 'email'], // requested user permissions/data
|
|
442
552
|
}}
|
|
443
553
|
>
|
|
444
554
|
<App />
|
|
@@ -446,200 +556,1246 @@ createRoot(document.getElementById('app')!).render(
|
|
|
446
556
|
);
|
|
447
557
|
```
|
|
448
558
|
|
|
449
|
-
|
|
559
|
+
#### Login / Register
|
|
450
560
|
|
|
451
|
-
|
|
561
|
+
In embedded mode `<sty-login>` web component does not take `issuer`, `clientId`, or `redirectUri` as attributes - those come from the SDK configuration above. Place it on your login page together with `sty-notifications` (toast-style system notifications) and `sty-language-selector` (a language switcher for the login flow). Since `<sty-login>` is a plain custom element, its properties (`params`, `sessionId`, `shortAppId`, `lang`) and DOM events are wired up through a ref instead of JSX props:
|
|
452
562
|
|
|
453
|
-
|
|
563
|
+
```tsx
|
|
564
|
+
// pages/Login.tsx
|
|
565
|
+
import { useEffect, useRef } from 'react';
|
|
566
|
+
import { useNavigate } from 'react-router';
|
|
567
|
+
import type { LoginComponent } from '@strivacity/sdk-react/types';
|
|
454
568
|
|
|
455
|
-
|
|
569
|
+
export default function Login() {
|
|
570
|
+
const navigate = useNavigate();
|
|
571
|
+
const searchParams = new URLSearchParams(window.location.search);
|
|
456
572
|
|
|
457
|
-
|
|
458
|
-
|
|
573
|
+
// Optional: Resume a session started from an entry URL (e.g., password reset)
|
|
574
|
+
const sessionId = searchParams.get('session_id');
|
|
575
|
+
const shortAppId = searchParams.get('short_app_id');
|
|
576
|
+
const lang = searchParams.get('language') ?? navigator.language;
|
|
577
|
+
const loginRef = useRef<LoginComponent>(null);
|
|
459
578
|
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
issuer: 'https://<YOUR_DOMAIN>',
|
|
463
|
-
scopes: ['openid', 'profile'],
|
|
464
|
-
clientId: '<YOUR_CLIENT_ID>',
|
|
465
|
-
redirectUri: '<YOUR_REDIRECT_URI>',
|
|
466
|
-
logging: DefaultLogging,
|
|
467
|
-
};
|
|
468
|
-
```
|
|
579
|
+
useEffect(() => {
|
|
580
|
+
const element = loginRef.current;
|
|
469
581
|
|
|
470
|
-
|
|
582
|
+
if (!element) {
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
471
585
|
|
|
472
|
-
|
|
586
|
+
element.params = {
|
|
587
|
+
loginHint: 'user@example.com', // identifier or JWT-encoded data to hint the login flow
|
|
588
|
+
acrValues: ['urn:strivacity:loa:2'], // request specific authentication context
|
|
589
|
+
audiences: ['https://api.example.com'], // target resources for the access token
|
|
590
|
+
language: 'en-US', // set the UI language (BCP 47 language tag)
|
|
591
|
+
prompt: 'login', // use 'create' to open the registration flow instead
|
|
592
|
+
};
|
|
593
|
+
element.sessionId = sessionId;
|
|
594
|
+
element.shortAppId = shortAppId;
|
|
595
|
+
element.lang = lang;
|
|
596
|
+
|
|
597
|
+
const onLogin = () => navigate('/profile');
|
|
598
|
+
const onClose = () => window.location.reload();
|
|
599
|
+
const onError = (event: Event) => navigate(`/error?message=${encodeURIComponent((event as CustomEvent<string>).detail)}`);
|
|
600
|
+
|
|
601
|
+
element.addEventListener('login', onLogin);
|
|
602
|
+
element.addEventListener('close', onClose);
|
|
603
|
+
element.addEventListener('error', onError);
|
|
604
|
+
|
|
605
|
+
return () => {
|
|
606
|
+
element.removeEventListener('login', onLogin);
|
|
607
|
+
element.removeEventListener('close', onClose);
|
|
608
|
+
element.removeEventListener('error', onError);
|
|
609
|
+
};
|
|
610
|
+
}, [navigate, sessionId, shortAppId, lang]);
|
|
473
611
|
|
|
474
|
-
|
|
475
|
-
|
|
612
|
+
return (
|
|
613
|
+
<section>
|
|
614
|
+
<sty-notifications></sty-notifications>
|
|
615
|
+
<sty-login ref={loginRef}></sty-login>
|
|
616
|
+
<sty-language-selector></sty-language-selector>
|
|
617
|
+
</section>
|
|
618
|
+
);
|
|
619
|
+
}
|
|
620
|
+
```
|
|
476
621
|
|
|
477
|
-
|
|
478
|
-
xEventId?: string;
|
|
622
|
+
##### Controlling when the flow starts
|
|
479
623
|
|
|
480
|
-
|
|
481
|
-
console.debug(this.xEventId ? `[${this.xEventId}] ${message}` : message);
|
|
482
|
-
}
|
|
624
|
+
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:
|
|
483
625
|
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
626
|
+
```tsx
|
|
627
|
+
// pages/Login.tsx
|
|
628
|
+
import { useRef } from 'react';
|
|
629
|
+
import { useNavigate } from 'react-router';
|
|
630
|
+
import type { LoginComponent } from '@strivacity/sdk-react/types';
|
|
487
631
|
|
|
488
|
-
|
|
489
|
-
|
|
632
|
+
export default function Login() {
|
|
633
|
+
const navigate = useNavigate();
|
|
634
|
+
const loginRef = useRef<LoginComponent>(null);
|
|
635
|
+
|
|
636
|
+
async function onStartClick() {
|
|
637
|
+
await loginRef.current?.start({
|
|
638
|
+
loginHint: 'user@example.com', // identifier or JWT-encoded data to hint the login flow
|
|
639
|
+
acrValues: ['urn:strivacity:loa:2'], // request specific authentication context
|
|
640
|
+
audiences: ['https://api.example.com'], // target resources for the access token
|
|
641
|
+
language: 'en-US', // set the UI language (BCP 47 language tag)
|
|
642
|
+
prompt: 'login', // use 'create' to open the registration flow instead
|
|
643
|
+
});
|
|
490
644
|
}
|
|
491
645
|
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
646
|
+
return (
|
|
647
|
+
<section>
|
|
648
|
+
<sty-notifications></sty-notifications>
|
|
649
|
+
<button onClick={onStartClick}>Continue to login</button>
|
|
650
|
+
<sty-login ref={loginRef} lazy onLogin={() => navigate('/profile')}></sty-login>
|
|
651
|
+
<sty-language-selector></sty-language-selector>
|
|
652
|
+
</section>
|
|
653
|
+
);
|
|
495
654
|
}
|
|
496
655
|
```
|
|
497
656
|
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
## HTTP Client
|
|
501
|
-
|
|
502
|
-
The SDK uses a built-in `fetch`-based HTTP client for all requests. You can replace it with your own implementation by extending `SDKHttpClient` and passing your class via the `httpClient` option. This is useful when you need to attach custom headers (e.g. `x-sty-app-id`) to every outgoing request or route traffic through a proxy.
|
|
503
|
-
|
|
504
|
-
### Adding custom headers to every request
|
|
657
|
+
You can also set params via the `params` property before calling `start()`:
|
|
505
658
|
|
|
506
659
|
```tsx
|
|
507
|
-
import {
|
|
660
|
+
import { useRef } from 'react';
|
|
661
|
+
import type { LoginComponent } from '@strivacity/sdk-react/types';
|
|
508
662
|
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
const mergedOptions: RequestInit = {
|
|
512
|
-
...options,
|
|
513
|
-
headers: {
|
|
514
|
-
'x-sty-app-id': 'my-app',
|
|
515
|
-
...(options?.headers as Record<string, string>),
|
|
516
|
-
},
|
|
517
|
-
};
|
|
663
|
+
export function LazyLogin() {
|
|
664
|
+
const loginRef = useRef<LoginComponent>(null);
|
|
518
665
|
|
|
519
|
-
|
|
666
|
+
async function onStartClick() {
|
|
667
|
+
if (!loginRef.current) {
|
|
668
|
+
return;
|
|
669
|
+
}
|
|
520
670
|
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
json: async () => (await response.json()) as T,
|
|
528
|
-
text: async () => await response.text(),
|
|
671
|
+
loginRef.current.params = {
|
|
672
|
+
loginHint: 'user@example.com', // identifier or JWT-encoded data to hint the login flow
|
|
673
|
+
acrValues: ['urn:strivacity:loa:2'], // request specific authentication context
|
|
674
|
+
audiences: ['https://api.example.com'], // target resources for the access token
|
|
675
|
+
language: 'en-US', // set the UI language (BCP 47 language tag)
|
|
676
|
+
prompt: 'login', // use 'create' to open the registration flow instead
|
|
529
677
|
};
|
|
678
|
+
await loginRef.current.start();
|
|
530
679
|
}
|
|
531
|
-
}
|
|
532
680
|
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
}
|
|
681
|
+
return (
|
|
682
|
+
<>
|
|
683
|
+
<sty-login ref={loginRef} lazy></sty-login>
|
|
684
|
+
<button onClick={onStartClick}>Start Login</button>
|
|
685
|
+
</>
|
|
686
|
+
);
|
|
687
|
+
}
|
|
537
688
|
```
|
|
538
689
|
|
|
539
|
-
|
|
690
|
+
##### Login events
|
|
540
691
|
|
|
541
|
-
|
|
692
|
+
The `<sty-login>` element dispatches `login`, `close`, and `error` custom events. Attach listeners on the element ref inside `useEffect`:
|
|
542
693
|
|
|
543
|
-
|
|
694
|
+
```tsx
|
|
695
|
+
// pages/Login.tsx
|
|
696
|
+
import { useEffect, useRef } from 'react';
|
|
697
|
+
import { useNavigate } from 'react-router';
|
|
698
|
+
import type { LoginComponent } from '@strivacity/sdk-react/types';
|
|
544
699
|
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
700
|
+
export default function Login() {
|
|
701
|
+
const navigate = useNavigate();
|
|
702
|
+
const loginRef = useRef<LoginComponent>(null);
|
|
548
703
|
|
|
549
|
-
|
|
704
|
+
useEffect(() => {
|
|
705
|
+
const element = loginRef.current;
|
|
550
706
|
|
|
551
|
-
|
|
707
|
+
if (!element) {
|
|
708
|
+
return;
|
|
709
|
+
}
|
|
552
710
|
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
711
|
+
const onLogin = () => {
|
|
712
|
+
// User authenticated - navigate to a protected page
|
|
713
|
+
navigate('/profile');
|
|
714
|
+
};
|
|
715
|
+
const onClose = () => {
|
|
716
|
+
// User cancelled or closed the login flow
|
|
717
|
+
window.location.reload();
|
|
718
|
+
};
|
|
719
|
+
const onError = (event: Event) => {
|
|
720
|
+
// A fatal error occurred - the message is available in event.detail
|
|
721
|
+
navigate(`/error?message=${encodeURIComponent((event as CustomEvent<string>).detail)}`);
|
|
722
|
+
};
|
|
556
723
|
|
|
557
|
-
|
|
724
|
+
element.addEventListener('login', onLogin);
|
|
725
|
+
element.addEventListener('close', onClose);
|
|
726
|
+
element.addEventListener('error', onError);
|
|
558
727
|
|
|
559
|
-
|
|
728
|
+
return () => {
|
|
729
|
+
element.removeEventListener('login', onLogin);
|
|
730
|
+
element.removeEventListener('close', onClose);
|
|
731
|
+
element.removeEventListener('error', onError);
|
|
732
|
+
};
|
|
733
|
+
}, [navigate]);
|
|
560
734
|
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
-
|
|
564
|
-
-
|
|
565
|
-
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
735
|
+
return (
|
|
736
|
+
<section>
|
|
737
|
+
<sty-notifications></sty-notifications>
|
|
738
|
+
<sty-login ref={loginRef}></sty-login>
|
|
739
|
+
<sty-language-selector></sty-language-selector>
|
|
740
|
+
</section>
|
|
741
|
+
);
|
|
742
|
+
}
|
|
743
|
+
```
|
|
570
744
|
|
|
571
|
-
|
|
745
|
+
##### Notification events
|
|
572
746
|
|
|
573
|
-
|
|
747
|
+
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:
|
|
574
748
|
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
- **`refresh(): Promise<void>`**: Refreshes the user's session.
|
|
578
|
-
- **`revoke(): Promise<void>`**: Revokes the current session tokens.
|
|
579
|
-
- **`logout(options?: LogoutOptions): Promise<void>`**: Logs the user out via redirect.
|
|
580
|
-
- **`handleCallback(url?: string): Promise<void>`**: Processes the authorization callback after redirect.
|
|
581
|
-
- **`entry(): Promise<Record<string, string>>`**: Processes an externally-initiated flow URL and returns the parameters needed to resume the flow.
|
|
749
|
+
```tsx
|
|
750
|
+
import { useEffect } from 'react';
|
|
582
751
|
|
|
583
|
-
|
|
752
|
+
export function CustomNotifications() {
|
|
753
|
+
useEffect(() => {
|
|
754
|
+
function onNotification(event: Event) {
|
|
755
|
+
const customEvent = event as CustomEvent;
|
|
756
|
+
|
|
757
|
+
if (customEvent.detail.action === 'show') {
|
|
758
|
+
// Add new notification to your custom notification system
|
|
759
|
+
const notification = customEvent.detail.notification;
|
|
760
|
+
console.log('New notification:', notification);
|
|
761
|
+
// Handle the notification display in your own UI
|
|
762
|
+
} else if (customEvent.detail.action === 'clear') {
|
|
763
|
+
// Clear all notifications
|
|
764
|
+
console.log('Clear all notifications');
|
|
765
|
+
}
|
|
766
|
+
}
|
|
584
767
|
|
|
585
|
-
|
|
768
|
+
document.addEventListener('notification', onNotification);
|
|
586
769
|
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
- **`refresh(): Promise<void>`**: Refreshes the user's session.
|
|
590
|
-
- **`revoke(): Promise<void>`**: Revokes the current session tokens.
|
|
591
|
-
- **`logout(options?: LogoutOptions): Promise<void>`**: Logs the user out via popup.
|
|
592
|
-
- **`handleCallback(url?: string): Promise<void>`**: Processes the authorization callback.
|
|
593
|
-
- **`entry(): Promise<Record<string, string>>`**: Processes an externally-initiated flow URL.
|
|
770
|
+
return () => document.removeEventListener('notification', onNotification);
|
|
771
|
+
}, []);
|
|
594
772
|
|
|
595
|
-
|
|
773
|
+
return null;
|
|
774
|
+
}
|
|
775
|
+
```
|
|
596
776
|
|
|
597
|
-
|
|
777
|
+
##### Dynamic language switching
|
|
598
778
|
|
|
599
|
-
-
|
|
600
|
-
- **`register(options?: RegisterOptions): Promise<NativeFlowHandler>`**: Initiates registration using the native flow.
|
|
601
|
-
- **`refresh(): Promise<void>`**: Refreshes the user's session.
|
|
602
|
-
- **`revoke(): Promise<void>`**: Revokes the current session tokens.
|
|
603
|
-
- **`logout(options?: LogoutOptions): Promise<void>`**: Logs the user out via redirect.
|
|
604
|
-
- **`handleCallback(url?: string): Promise<void>`**: Processes the authorization callback.
|
|
605
|
-
- **`entry(): Promise<Record<string, string>>`**: Processes an externally-initiated flow URL.
|
|
779
|
+
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:
|
|
606
780
|
|
|
607
|
-
|
|
781
|
+
```tsx
|
|
782
|
+
import { useRef, useState } from 'react';
|
|
608
783
|
|
|
609
|
-
|
|
784
|
+
export function LanguageSwitcher() {
|
|
785
|
+
const loginRef = useRef<HTMLElement & { lang: string }>(null);
|
|
786
|
+
const [currentLang, setCurrentLang] = useState('en-US');
|
|
610
787
|
|
|
611
|
-
|
|
788
|
+
function changeLanguage(lang: string) {
|
|
789
|
+
setCurrentLang(lang);
|
|
612
790
|
|
|
613
|
-
|
|
791
|
+
if (loginRef.current) {
|
|
792
|
+
loginRef.current.lang = lang;
|
|
793
|
+
}
|
|
794
|
+
}
|
|
614
795
|
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
796
|
+
return (
|
|
797
|
+
<section>
|
|
798
|
+
<div>
|
|
799
|
+
<button onClick={() => changeLanguage('en-US')}>English</button>
|
|
800
|
+
<button onClick={() => changeLanguage('fr-FR')}>Français</button>
|
|
801
|
+
<button onClick={() => changeLanguage('de-DE')}>Deutsch</button>
|
|
802
|
+
</div>
|
|
803
|
+
<sty-login ref={loginRef} lang={currentLang}></sty-login>
|
|
804
|
+
</section>
|
|
805
|
+
);
|
|
806
|
+
}
|
|
807
|
+
```
|
|
619
808
|
|
|
620
|
-
|
|
809
|
+
#### Handle the callback
|
|
621
810
|
|
|
622
|
-
-
|
|
623
|
-
- **`onFallback`**: Called when the native flow needs to fall back to redirect. Receives `FallbackError` with a fallback URL.
|
|
624
|
-
- **`onError`**: Called when an error occurs during authentication.
|
|
625
|
-
- **`onGlobalMessage`**: Called when the flow wants to display a global message (e.g. account lockout warning).
|
|
626
|
-
- **`onBlockReady`**: Called on flow state transitions. Receives `{ previousState: LoginFlowState; state: LoginFlowState }`. Useful for analytics and custom logging.
|
|
627
|
-
- **`onLanguageChange`**: Called after the session starts with the resolved language string.
|
|
811
|
+
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.
|
|
628
812
|
|
|
629
|
-
|
|
813
|
+
#### Externally-initiated flows (entry)
|
|
630
814
|
|
|
631
|
-
|
|
815
|
+
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`).
|
|
632
816
|
|
|
633
|
-
|
|
817
|
+
You have two options:
|
|
634
818
|
|
|
635
|
-
|
|
819
|
+
**Option 1: Redirect to a separate login page**
|
|
636
820
|
|
|
637
|
-
|
|
821
|
+
Forward the parameters as query params to your login page:
|
|
638
822
|
|
|
639
|
-
|
|
823
|
+
```tsx
|
|
824
|
+
// pages/Entry.tsx
|
|
825
|
+
import { useStrivacity } from '@strivacity/sdk-react';
|
|
826
|
+
import { useEffect } from 'react';
|
|
827
|
+
import { useNavigate } from 'react-router';
|
|
828
|
+
import type { EmbeddedFlow } from '@strivacity/sdk-react';
|
|
829
|
+
|
|
830
|
+
export default function Entry() {
|
|
831
|
+
const { loading, entry } = useStrivacity<EmbeddedFlow>();
|
|
832
|
+
const navigate = useNavigate();
|
|
833
|
+
|
|
834
|
+
useEffect(() => {
|
|
835
|
+
if (loading) {
|
|
836
|
+
return;
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
entry()
|
|
840
|
+
.then((data) => {
|
|
841
|
+
// Redirect to login page with flow parameters
|
|
842
|
+
const params = new URLSearchParams({
|
|
843
|
+
session_id: data.session_id,
|
|
844
|
+
short_app_id: data.short_app_id,
|
|
845
|
+
language: data.language,
|
|
846
|
+
});
|
|
847
|
+
window.location.href = `/login?${params}`;
|
|
848
|
+
})
|
|
849
|
+
.catch((error) => navigate(`/error?message=${encodeURIComponent(error.message)}`));
|
|
850
|
+
}, [loading, entry, navigate]);
|
|
851
|
+
|
|
852
|
+
return (
|
|
853
|
+
<section>
|
|
854
|
+
<h1>Loading...</h1>
|
|
855
|
+
</section>
|
|
856
|
+
);
|
|
857
|
+
}
|
|
858
|
+
```
|
|
859
|
+
|
|
860
|
+
Then on your login page, read the parameters and pass them to `<sty-login>`:
|
|
861
|
+
|
|
862
|
+
```tsx
|
|
863
|
+
// pages/Login.tsx
|
|
864
|
+
import { useEffect, useRef } from 'react';
|
|
865
|
+
import { useNavigate } from 'react-router';
|
|
866
|
+
import type { LoginComponent } from '@strivacity/sdk-react/types';
|
|
867
|
+
|
|
868
|
+
export default function Login() {
|
|
869
|
+
const navigate = useNavigate();
|
|
870
|
+
const searchParams = new URLSearchParams(window.location.search);
|
|
871
|
+
|
|
872
|
+
// Read parameters from URL
|
|
873
|
+
const sessionId = searchParams.get('session_id');
|
|
874
|
+
const shortAppId = searchParams.get('short_app_id');
|
|
875
|
+
const language = searchParams.get('language');
|
|
876
|
+
const loginRef = useRef<LoginComponent>(null);
|
|
877
|
+
|
|
878
|
+
useEffect(() => {
|
|
879
|
+
const element = loginRef.current;
|
|
880
|
+
|
|
881
|
+
if (!element) {
|
|
882
|
+
return;
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
element.sessionId = sessionId;
|
|
886
|
+
element.shortAppId = shortAppId;
|
|
887
|
+
element.lang = language;
|
|
888
|
+
|
|
889
|
+
const onLogin = () => navigate('/profile');
|
|
890
|
+
const onError = (event: Event) => navigate(`/error?message=${encodeURIComponent((event as CustomEvent<string>).detail)}`);
|
|
891
|
+
|
|
892
|
+
element.addEventListener('login', onLogin);
|
|
893
|
+
element.addEventListener('error', onError);
|
|
894
|
+
|
|
895
|
+
return () => {
|
|
896
|
+
element.removeEventListener('login', onLogin);
|
|
897
|
+
element.removeEventListener('error', onError);
|
|
898
|
+
};
|
|
899
|
+
}, [navigate, sessionId, shortAppId, language]);
|
|
900
|
+
|
|
901
|
+
return (
|
|
902
|
+
<section>
|
|
903
|
+
<sty-notifications></sty-notifications>
|
|
904
|
+
<sty-login ref={loginRef}></sty-login>
|
|
905
|
+
<sty-language-selector></sty-language-selector>
|
|
906
|
+
</section>
|
|
907
|
+
);
|
|
908
|
+
}
|
|
909
|
+
```
|
|
910
|
+
|
|
911
|
+
**Option 2: Render login on the entry page**
|
|
912
|
+
|
|
913
|
+
Pass the parameters directly to `<sty-login>` on the same page:
|
|
914
|
+
|
|
915
|
+
```tsx
|
|
916
|
+
// pages/Entry.tsx
|
|
917
|
+
import { useStrivacity } from '@strivacity/sdk-react';
|
|
918
|
+
import { useEffect, useRef, useState } from 'react';
|
|
919
|
+
import { useNavigate } from 'react-router';
|
|
920
|
+
import type { LoginComponent } from '@strivacity/sdk-react/types';
|
|
921
|
+
import type { EmbeddedFlow } from '@strivacity/sdk-react';
|
|
922
|
+
|
|
923
|
+
export default function Entry() {
|
|
924
|
+
const { loading, entry } = useStrivacity<EmbeddedFlow>();
|
|
925
|
+
const navigate = useNavigate();
|
|
926
|
+
const loginRef = useRef<LoginComponent>(null);
|
|
927
|
+
|
|
928
|
+
const [sessionId, setSessionId] = useState<string | null>(null);
|
|
929
|
+
const [shortAppId, setShortAppId] = useState<string | null>(null);
|
|
930
|
+
const [language, setLanguage] = useState<string | null>(null);
|
|
931
|
+
|
|
932
|
+
useEffect(() => {
|
|
933
|
+
if (loading) {
|
|
934
|
+
return;
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
entry()
|
|
938
|
+
.then((data) => {
|
|
939
|
+
// Set state for sty-login component
|
|
940
|
+
setSessionId(data.session_id);
|
|
941
|
+
setShortAppId(data.short_app_id);
|
|
942
|
+
setLanguage(data.language);
|
|
943
|
+
})
|
|
944
|
+
.catch((error) => navigate(`/error?message=${encodeURIComponent(error.message)}`));
|
|
945
|
+
}, [loading, entry, navigate]);
|
|
946
|
+
|
|
947
|
+
useEffect(() => {
|
|
948
|
+
const element = loginRef.current;
|
|
949
|
+
|
|
950
|
+
if (!element || !sessionId) {
|
|
951
|
+
return;
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
element.sessionId = sessionId;
|
|
955
|
+
element.shortAppId = shortAppId;
|
|
956
|
+
element.lang = language;
|
|
957
|
+
|
|
958
|
+
const onLogin = () => navigate('/profile');
|
|
959
|
+
const onError = (event: Event) => navigate(`/error?message=${encodeURIComponent((event as CustomEvent<string>).detail)}`);
|
|
960
|
+
|
|
961
|
+
element.addEventListener('login', onLogin);
|
|
962
|
+
element.addEventListener('error', onError);
|
|
963
|
+
|
|
964
|
+
return () => {
|
|
965
|
+
element.removeEventListener('login', onLogin);
|
|
966
|
+
element.removeEventListener('error', onError);
|
|
967
|
+
};
|
|
968
|
+
}, [navigate, sessionId, shortAppId, language]);
|
|
969
|
+
|
|
970
|
+
if (!sessionId) {
|
|
971
|
+
return (
|
|
972
|
+
<section>
|
|
973
|
+
<h1>Loading...</h1>
|
|
974
|
+
</section>
|
|
975
|
+
);
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
return (
|
|
979
|
+
<section>
|
|
980
|
+
<sty-notifications></sty-notifications>
|
|
981
|
+
<sty-login ref={loginRef}></sty-login>
|
|
982
|
+
<sty-language-selector></sty-language-selector>
|
|
983
|
+
</section>
|
|
984
|
+
);
|
|
985
|
+
}
|
|
986
|
+
```
|
|
987
|
+
|
|
988
|
+
#### Logout
|
|
989
|
+
|
|
990
|
+
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`.
|
|
991
|
+
|
|
992
|
+
```tsx
|
|
993
|
+
// pages/Logout.tsx
|
|
994
|
+
import { useStrivacity } from '@strivacity/sdk-react';
|
|
995
|
+
import { useEffect } from 'react';
|
|
996
|
+
|
|
997
|
+
export default function Logout() {
|
|
998
|
+
const { loading, logout } = useStrivacity();
|
|
999
|
+
|
|
1000
|
+
useEffect(() => {
|
|
1001
|
+
if (loading) {
|
|
1002
|
+
return;
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
void logout();
|
|
1006
|
+
}, [loading, logout]);
|
|
1007
|
+
|
|
1008
|
+
return null;
|
|
1009
|
+
}
|
|
1010
|
+
```
|
|
1011
|
+
|
|
1012
|
+
> The `postLogoutRedirectUri` must be configured in your Strivacity application client settings as an allowed post-logout redirect URI. If the URL is invalid or not provided, the user remains on the Strivacity-hosted logged out page.
|
|
1013
|
+
|
|
1014
|
+
#### Token management
|
|
1015
|
+
|
|
1016
|
+
Call these methods to manage the session and access token.
|
|
1017
|
+
|
|
1018
|
+
```tsx
|
|
1019
|
+
import { useStrivacity } from '@strivacity/sdk-react';
|
|
1020
|
+
|
|
1021
|
+
export function TokenPanel() {
|
|
1022
|
+
const { loading, idTokenClaims, accessToken, refreshToken, refresh, revoke } = useStrivacity();
|
|
1023
|
+
|
|
1024
|
+
async function onRefresh() {
|
|
1025
|
+
// Refresh the access token using the refresh token
|
|
1026
|
+
await refresh();
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
async function onRevoke() {
|
|
1030
|
+
// Revoke all tokens at the authorization server and clear the local session
|
|
1031
|
+
await revoke();
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
if (loading) {
|
|
1035
|
+
return null;
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
// idTokenClaims, accessToken, and refreshToken are plain values from context -
|
|
1039
|
+
// they update automatically whenever the SDK's session changes
|
|
1040
|
+
return (
|
|
1041
|
+
<div>
|
|
1042
|
+
<button onClick={onRefresh}>Refresh</button>
|
|
1043
|
+
<button onClick={onRevoke}>Revoke</button>
|
|
1044
|
+
<pre>{JSON.stringify({ idTokenClaims, accessToken, refreshToken }, null, 2)}</pre>
|
|
1045
|
+
</div>
|
|
1046
|
+
);
|
|
1047
|
+
}
|
|
1048
|
+
```
|
|
1049
|
+
|
|
1050
|
+
---
|
|
1051
|
+
|
|
1052
|
+
### native mode
|
|
1053
|
+
|
|
1054
|
+
> For details on how this mode works, see the [native journey documentation](https://docs.strivacity.com/reference/native-journey).
|
|
1055
|
+
|
|
1056
|
+
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.
|
|
1057
|
+
|
|
1058
|
+
> The example below shows a simplified custom implementation. For a complete native renderer with all widget types, see the [example app](../../apps/react/src/components/auth/native/NativeLogin.tsx).
|
|
1059
|
+
|
|
1060
|
+
#### Login / Register
|
|
1061
|
+
|
|
1062
|
+
Create a login page using `useNativeLogin`:
|
|
1063
|
+
|
|
1064
|
+
```tsx
|
|
1065
|
+
// pages/Login.tsx
|
|
1066
|
+
import { useNativeLogin } from '@strivacity/sdk-react';
|
|
1067
|
+
|
|
1068
|
+
export default function Login() {
|
|
1069
|
+
const searchParams = new URLSearchParams(window.location.search);
|
|
1070
|
+
|
|
1071
|
+
const { state, forms, messages, loading, submitForm, setFormValue } = useNativeLogin({
|
|
1072
|
+
params: {
|
|
1073
|
+
prompt: 'login', // use 'create' to open the registration flow instead
|
|
1074
|
+
language: 'en-US', // set the UI language (BCP 47 language tag)
|
|
1075
|
+
sdk: 'web-minimal', // rendering mode: 'web-minimal' for simplified rendering (see below), 'web' (default) for full rendering hints and branding
|
|
1076
|
+
sessionId: searchParams.get('session_id'), // pass a session ID to resume an existing flow
|
|
1077
|
+
},
|
|
1078
|
+
onLogin: async () => {
|
|
1079
|
+
window.location.href = '/profile';
|
|
1080
|
+
},
|
|
1081
|
+
onClose: () => {
|
|
1082
|
+
window.location.reload();
|
|
1083
|
+
},
|
|
1084
|
+
onError: async (error) => {
|
|
1085
|
+
window.location.href = `/error?message=${encodeURIComponent(error.message)}`;
|
|
1086
|
+
},
|
|
1087
|
+
onFallback: (error) => {
|
|
1088
|
+
// Fallback to hosted journey if native widget not supported
|
|
1089
|
+
window.location.href = error.url.toString();
|
|
1090
|
+
},
|
|
1091
|
+
onGlobalMessage: (message) => {
|
|
1092
|
+
alert(message.text);
|
|
1093
|
+
},
|
|
1094
|
+
});
|
|
1095
|
+
|
|
1096
|
+
if (loading || !state.screen) {
|
|
1097
|
+
return (
|
|
1098
|
+
<section>
|
|
1099
|
+
<h1>Loading...</h1>
|
|
1100
|
+
</section>
|
|
1101
|
+
);
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
if (state.screen === 'identifier') {
|
|
1105
|
+
return (
|
|
1106
|
+
<section>
|
|
1107
|
+
<h2>Sign In</h2>
|
|
1108
|
+
<form
|
|
1109
|
+
onSubmit={async (event) => {
|
|
1110
|
+
event.preventDefault();
|
|
1111
|
+
await submitForm('identifier');
|
|
1112
|
+
}}
|
|
1113
|
+
>
|
|
1114
|
+
<input
|
|
1115
|
+
type="text"
|
|
1116
|
+
placeholder="Email"
|
|
1117
|
+
value={(forms['identifier']?.identifier as string) ?? ''}
|
|
1118
|
+
onChange={(event) => setFormValue('identifier', 'identifier', event.target.value)}
|
|
1119
|
+
/>
|
|
1120
|
+
{messages['identifier']?.identifier && <div className="error">{messages['identifier'].identifier.text}</div>}
|
|
1121
|
+
<button type="submit">Continue</button>
|
|
1122
|
+
</form>
|
|
1123
|
+
</section>
|
|
1124
|
+
);
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
if (state.screen === 'password') {
|
|
1128
|
+
return (
|
|
1129
|
+
<section>
|
|
1130
|
+
<h2>Enter Password</h2>
|
|
1131
|
+
<form
|
|
1132
|
+
onSubmit={async (event) => {
|
|
1133
|
+
event.preventDefault();
|
|
1134
|
+
await submitForm('password');
|
|
1135
|
+
}}
|
|
1136
|
+
>
|
|
1137
|
+
<input
|
|
1138
|
+
type="password"
|
|
1139
|
+
placeholder="Password"
|
|
1140
|
+
value={(forms['password']?.password as string) ?? ''}
|
|
1141
|
+
onChange={(event) => setFormValue('password', 'password', event.target.value)}
|
|
1142
|
+
/>
|
|
1143
|
+
{messages['password']?.password && <div className="error">{messages['password'].password.text}</div>}
|
|
1144
|
+
<button type="submit">Sign In</button>
|
|
1145
|
+
</form>
|
|
1146
|
+
</section>
|
|
1147
|
+
);
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
return null;
|
|
1151
|
+
}
|
|
1152
|
+
```
|
|
1153
|
+
|
|
1154
|
+
#### Handle the callback
|
|
1155
|
+
|
|
1156
|
+
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.
|
|
1157
|
+
|
|
1158
|
+
#### Externally-initiated flows (entry)
|
|
1159
|
+
|
|
1160
|
+
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`).
|
|
1161
|
+
|
|
1162
|
+
You have two options:
|
|
1163
|
+
|
|
1164
|
+
**Option 1: Redirect to a separate login page**
|
|
1165
|
+
|
|
1166
|
+
```tsx
|
|
1167
|
+
// pages/Entry.tsx
|
|
1168
|
+
import { useStrivacity } from '@strivacity/sdk-react';
|
|
1169
|
+
import { useEffect } from 'react';
|
|
1170
|
+
import { useNavigate } from 'react-router';
|
|
1171
|
+
import type { NativeFlow } from '@strivacity/sdk-react';
|
|
1172
|
+
|
|
1173
|
+
export default function Entry() {
|
|
1174
|
+
const { loading, entry } = useStrivacity<NativeFlow>();
|
|
1175
|
+
const navigate = useNavigate();
|
|
1176
|
+
|
|
1177
|
+
useEffect(() => {
|
|
1178
|
+
if (loading) {
|
|
1179
|
+
return;
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
entry()
|
|
1183
|
+
.then((data) => {
|
|
1184
|
+
const params = new URLSearchParams({
|
|
1185
|
+
session_id: data.session_id,
|
|
1186
|
+
short_app_id: data.short_app_id,
|
|
1187
|
+
language: data.language,
|
|
1188
|
+
});
|
|
1189
|
+
window.location.href = `/login?${params}`;
|
|
1190
|
+
})
|
|
1191
|
+
.catch((error) => navigate(`/error?message=${encodeURIComponent(error.message)}`));
|
|
1192
|
+
}, [loading, entry, navigate]);
|
|
1193
|
+
|
|
1194
|
+
return (
|
|
1195
|
+
<section>
|
|
1196
|
+
<h1>Loading...</h1>
|
|
1197
|
+
</section>
|
|
1198
|
+
);
|
|
1199
|
+
}
|
|
1200
|
+
```
|
|
1201
|
+
|
|
1202
|
+
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:
|
|
1203
|
+
|
|
1204
|
+
```tsx
|
|
1205
|
+
// pages/Login.tsx
|
|
1206
|
+
import { useNativeLogin } from '@strivacity/sdk-react';
|
|
1207
|
+
|
|
1208
|
+
export default function Login() {
|
|
1209
|
+
const searchParams = new URLSearchParams(window.location.search);
|
|
1210
|
+
|
|
1211
|
+
const { state, loading, forms, messages, submitForm, setFormValue } = useNativeLogin({
|
|
1212
|
+
params: {
|
|
1213
|
+
sessionId: searchParams.get('session_id'),
|
|
1214
|
+
language: searchParams.get('language'),
|
|
1215
|
+
},
|
|
1216
|
+
onLogin: async () => {
|
|
1217
|
+
window.location.href = '/profile';
|
|
1218
|
+
},
|
|
1219
|
+
});
|
|
1220
|
+
|
|
1221
|
+
// ...render based on `state.screen` as shown in the Login / Register example above
|
|
1222
|
+
}
|
|
1223
|
+
```
|
|
1224
|
+
|
|
1225
|
+
**Option 2: Render login on the entry page**
|
|
1226
|
+
|
|
1227
|
+
Call `useNativeLogin` directly inside `Entry.tsx`, feeding it the `session_id` resolved from `entry()` - no redirect needed:
|
|
1228
|
+
|
|
1229
|
+
```tsx
|
|
1230
|
+
// pages/Entry.tsx
|
|
1231
|
+
import { useStrivacity, useNativeLogin } from '@strivacity/sdk-react';
|
|
1232
|
+
import { useEffect, useState } from 'react';
|
|
1233
|
+
import { useNavigate } from 'react-router';
|
|
1234
|
+
import type { NativeFlow } from '@strivacity/sdk-react';
|
|
1235
|
+
|
|
1236
|
+
export default function Entry() {
|
|
1237
|
+
const { loading: sdkLoading, entry } = useStrivacity<NativeFlow>();
|
|
1238
|
+
const navigate = useNavigate();
|
|
1239
|
+
const [sessionId, setSessionId] = useState<string | null>(null);
|
|
1240
|
+
const [language, setLanguage] = useState<string | null>(null);
|
|
1241
|
+
const [ready, setReady] = useState(false);
|
|
1242
|
+
|
|
1243
|
+
useEffect(() => {
|
|
1244
|
+
if (sdkLoading) {
|
|
1245
|
+
return;
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
entry()
|
|
1249
|
+
.then((data) => {
|
|
1250
|
+
setSessionId(data.session_id);
|
|
1251
|
+
setLanguage(data.language);
|
|
1252
|
+
setReady(true);
|
|
1253
|
+
})
|
|
1254
|
+
.catch((error) => navigate(`/error?message=${encodeURIComponent(error.message)}`));
|
|
1255
|
+
}, [sdkLoading, entry, navigate]);
|
|
1256
|
+
|
|
1257
|
+
const { state, loading, forms, messages, submitForm, setFormValue } = useNativeLogin({
|
|
1258
|
+
params: { sessionId, language },
|
|
1259
|
+
onLogin: async () => {
|
|
1260
|
+
await navigate('/profile');
|
|
1261
|
+
},
|
|
1262
|
+
onError: async (error) => {
|
|
1263
|
+
await navigate(`/error?message=${encodeURIComponent(error.message)}`);
|
|
1264
|
+
},
|
|
1265
|
+
});
|
|
1266
|
+
|
|
1267
|
+
if (!ready || loading || !state.screen) {
|
|
1268
|
+
return (
|
|
1269
|
+
<section>
|
|
1270
|
+
<h1>Loading...</h1>
|
|
1271
|
+
</section>
|
|
1272
|
+
);
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
// ...render based on `state.screen` as shown in the Login / Register example above
|
|
1276
|
+
return null;
|
|
1277
|
+
}
|
|
1278
|
+
```
|
|
1279
|
+
|
|
1280
|
+
#### Logout
|
|
1281
|
+
|
|
1282
|
+
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`.
|
|
1283
|
+
|
|
1284
|
+
```tsx
|
|
1285
|
+
// pages/Logout.tsx
|
|
1286
|
+
import { useStrivacity } from '@strivacity/sdk-react';
|
|
1287
|
+
import { useEffect } from 'react';
|
|
1288
|
+
|
|
1289
|
+
export default function Logout() {
|
|
1290
|
+
const { loading, logout } = useStrivacity();
|
|
1291
|
+
|
|
1292
|
+
useEffect(() => {
|
|
1293
|
+
if (loading) {
|
|
1294
|
+
return;
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
void logout();
|
|
1298
|
+
}, [loading, logout]);
|
|
1299
|
+
|
|
1300
|
+
return null;
|
|
1301
|
+
}
|
|
1302
|
+
```
|
|
1303
|
+
|
|
1304
|
+
> The `postLogoutRedirectUri` must be configured in your Strivacity application client settings as an allowed post-logout redirect URI. If the URL is invalid or not provided, the user remains on the Strivacity-hosted logged out page.
|
|
1305
|
+
|
|
1306
|
+
#### Token management
|
|
1307
|
+
|
|
1308
|
+
Call these methods to manage the session and access token.
|
|
1309
|
+
|
|
1310
|
+
```tsx
|
|
1311
|
+
import { useStrivacity } from '@strivacity/sdk-react';
|
|
1312
|
+
|
|
1313
|
+
export function TokenPanel() {
|
|
1314
|
+
const { loading, idTokenClaims, accessToken, refreshToken, refresh, revoke } = useStrivacity();
|
|
1315
|
+
|
|
1316
|
+
async function onRefresh() {
|
|
1317
|
+
// Refresh the access token using the refresh token
|
|
1318
|
+
await refresh();
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
async function onRevoke() {
|
|
1322
|
+
// Revoke all tokens at the authorization server and clear the local session
|
|
1323
|
+
await revoke();
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
if (loading) {
|
|
1327
|
+
return null;
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
// idTokenClaims, accessToken, and refreshToken are plain values from context -
|
|
1331
|
+
// they update automatically whenever the SDK's session changes
|
|
1332
|
+
return (
|
|
1333
|
+
<div>
|
|
1334
|
+
<button onClick={onRefresh}>Refresh</button>
|
|
1335
|
+
<button onClick={onRevoke}>Revoke</button>
|
|
1336
|
+
<pre>{JSON.stringify({ idTokenClaims, accessToken, refreshToken }, null, 2)}</pre>
|
|
1337
|
+
</div>
|
|
1338
|
+
);
|
|
1339
|
+
}
|
|
1340
|
+
```
|
|
1341
|
+
|
|
1342
|
+
---
|
|
1343
|
+
|
|
1344
|
+
## Hooks API
|
|
1345
|
+
|
|
1346
|
+
### useStrivacity
|
|
1347
|
+
|
|
1348
|
+
The main hook for accessing the SDK instance and authentication state.
|
|
1349
|
+
|
|
1350
|
+
```ts
|
|
1351
|
+
import { useStrivacity } from '@strivacity/sdk-react';
|
|
1352
|
+
import type { RedirectFlow } from '@strivacity/sdk-react';
|
|
1353
|
+
|
|
1354
|
+
const ctx = useStrivacity<RedirectFlow>();
|
|
1355
|
+
```
|
|
1356
|
+
|
|
1357
|
+
#### Returns
|
|
1358
|
+
|
|
1359
|
+
```ts
|
|
1360
|
+
{
|
|
1361
|
+
// SDK instance (access any SDK method)
|
|
1362
|
+
sdk: RedirectFlow | PopupFlow | EmbeddedFlow | NativeFlow;
|
|
1363
|
+
|
|
1364
|
+
// Reactive state (plain values - the component re-renders when they change)
|
|
1365
|
+
loading: boolean; // True during initialization
|
|
1366
|
+
isAuthenticated: boolean; // True if user has valid session
|
|
1367
|
+
idTokenClaims: IdTokenClaims | null; // Decoded ID token claims
|
|
1368
|
+
accessToken: string | null; // Current access token
|
|
1369
|
+
refreshToken: string | null; // Current refresh token
|
|
1370
|
+
|
|
1371
|
+
// Methods (all are async)
|
|
1372
|
+
login(params?: LoginParams): Promise<void>; // Start login flow
|
|
1373
|
+
register(params?: LoginParams): Promise<void>; // Start registration flow
|
|
1374
|
+
handleCallback(url?: string): Promise<void>; // Handle OAuth callback
|
|
1375
|
+
logout(params?: LogoutParams): Promise<void>; // End session
|
|
1376
|
+
refresh(): Promise<void>; // Refresh access token
|
|
1377
|
+
revoke(): Promise<void>; // Revoke tokens
|
|
1378
|
+
entry(): Promise<EntryData>; // Handle external entry (embedded/native only)
|
|
1379
|
+
}
|
|
1380
|
+
```
|
|
1381
|
+
|
|
1382
|
+
### useNativeLogin
|
|
1383
|
+
|
|
1384
|
+
Hook for managing native login flow state. Only available in `native` mode.
|
|
1385
|
+
|
|
1386
|
+
```ts
|
|
1387
|
+
import { useNativeLogin } from '@strivacity/sdk-react';
|
|
1388
|
+
import type { NativeParams } from '@strivacity/sdk-react';
|
|
1389
|
+
|
|
1390
|
+
const ctx = useNativeLogin({
|
|
1391
|
+
params: { /* login params */ },
|
|
1392
|
+
onLogin: (session) => { /* handle login */ },
|
|
1393
|
+
onError: (error) => { /* handle error */ },
|
|
1394
|
+
// ... other callbacks
|
|
1395
|
+
});
|
|
1396
|
+
```
|
|
1397
|
+
|
|
1398
|
+
#### Options
|
|
1399
|
+
|
|
1400
|
+
```ts
|
|
1401
|
+
{
|
|
1402
|
+
params?: NativeParams; // Initial flow parameters
|
|
1403
|
+
onLogin?: (session: SessionData) => void | Promise<void>; // Called on successful login
|
|
1404
|
+
onClose?: () => void; // Called when user closes the flow
|
|
1405
|
+
onError?: (error: unknown) => void; // Called on error
|
|
1406
|
+
onFallback?: (error: FallbackError) => void; // Called when fallback needed
|
|
1407
|
+
onGlobalMessage?: (message: NativeFlowMessage) => void; // Called for global messages
|
|
1408
|
+
}
|
|
1409
|
+
```
|
|
1410
|
+
|
|
1411
|
+
#### Returns
|
|
1412
|
+
|
|
1413
|
+
```ts
|
|
1414
|
+
{
|
|
1415
|
+
// Reactive state (plain values - the component re-renders when they change)
|
|
1416
|
+
loading: boolean; // True while fetching next screen
|
|
1417
|
+
state: Partial<NativeFlowState>; // Current flow state (screen, forms, layout, etc.)
|
|
1418
|
+
forms: Record<string, Record<string, unknown>>; // Form data by form ID
|
|
1419
|
+
messages: Record<string, Record<string, NativeFlowMessage>>; // Validation messages
|
|
1420
|
+
|
|
1421
|
+
// Methods
|
|
1422
|
+
submitForm(formId: string, customBody?: Record<string, unknown>): Promise<void>; // Submit a form and advance to next screen
|
|
1423
|
+
setFormValue(formId: string, widgetId: string, value: unknown): void; // Update a single field value before submission
|
|
1424
|
+
setMessage(formId: string, widgetId: string, value: NativeFlowMessage): void; // Set a validation/info message on a widget
|
|
1425
|
+
triggerFallback(message?: string): void; // Manually trigger fallback to hosted journey
|
|
1426
|
+
triggerClose(): void; // Signal that the login flow was closed by the user
|
|
1427
|
+
}
|
|
1428
|
+
```
|
|
1429
|
+
|
|
1430
|
+
### withAuthGuard
|
|
1431
|
+
|
|
1432
|
+
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.
|
|
1433
|
+
|
|
1434
|
+
```tsx
|
|
1435
|
+
import { withAuthGuard } from '@strivacity/sdk-react';
|
|
1436
|
+
|
|
1437
|
+
export default withAuthGuard(
|
|
1438
|
+
function Profile() {
|
|
1439
|
+
return <section>Protected content</section>;
|
|
1440
|
+
},
|
|
1441
|
+
{ loginUri: '/login' },
|
|
1442
|
+
);
|
|
1443
|
+
```
|
|
1444
|
+
|
|
1445
|
+
#### Options
|
|
1446
|
+
|
|
1447
|
+
```ts
|
|
1448
|
+
{
|
|
1449
|
+
loginUri?: string; // URI to redirect to if the user is not authenticated (default: '/login')
|
|
1450
|
+
onLoading?: () => ReactNode; // Optional render function shown while loading/checking authentication
|
|
1451
|
+
}
|
|
1452
|
+
```
|
|
1453
|
+
|
|
1454
|
+
---
|
|
1455
|
+
|
|
1456
|
+
## Route guards
|
|
1457
|
+
|
|
1458
|
+
Protect routes that require authentication with the `withAuthGuard` higher-order component. It waits for the SDK to finish loading, verifies the session, and redirects to the login page if the user is not authenticated:
|
|
1459
|
+
|
|
1460
|
+
```tsx
|
|
1461
|
+
// pages/Profile.tsx
|
|
1462
|
+
import { withAuthGuard, useStrivacity } from '@strivacity/sdk-react';
|
|
1463
|
+
|
|
1464
|
+
export default withAuthGuard(
|
|
1465
|
+
function Profile() {
|
|
1466
|
+
const { idTokenClaims } = useStrivacity();
|
|
1467
|
+
|
|
1468
|
+
return <section>Hello, {idTokenClaims?.given_name}</section>;
|
|
1469
|
+
},
|
|
1470
|
+
{ loginUri: '/login' },
|
|
1471
|
+
);
|
|
1472
|
+
```
|
|
1473
|
+
|
|
1474
|
+
Then register the guarded component as a regular route in your router setup:
|
|
1475
|
+
|
|
1476
|
+
```tsx
|
|
1477
|
+
// main.tsx
|
|
1478
|
+
import { Routes, Route } from 'react-router';
|
|
1479
|
+
import Profile from './pages/Profile';
|
|
1480
|
+
|
|
1481
|
+
<Routes>
|
|
1482
|
+
<Route path="/profile" element={<Profile />} />
|
|
1483
|
+
</Routes>;
|
|
1484
|
+
```
|
|
1485
|
+
|
|
1486
|
+
---
|
|
1487
|
+
|
|
1488
|
+
## Shared features
|
|
1489
|
+
|
|
1490
|
+
The React SDK is built on top of the core SDK and supports all its features:
|
|
1491
|
+
|
|
1492
|
+
- **[Storages](../sdk-core/README.md#storages)** - localStorage, sessionStorage, IndexedDB, Cache API, Memory, Worker
|
|
1493
|
+
- **[SDK events](../sdk-core/README.md#sdk-events)** - Subscribe to authentication lifecycle events
|
|
1494
|
+
- **[Logging](../sdk-core/README.md#logging)** - Built-in and custom logger support
|
|
1495
|
+
- **[HTTP client](../sdk-core/README.md#http-client)** - Custom HTTP client integration
|
|
1496
|
+
- **[Error handling](../sdk-core/README.md#error-handling)** - Typed error classes for different failure scenarios
|
|
1497
|
+
- **[Utility functions](../sdk-core/README.md#utility-functions)** - Base64URL, JWT decoding, encryption, etc.
|
|
1498
|
+
- **[Caching](../sdk-core/README.md#caching)** - OIDC metadata and JWKS caching
|
|
1499
|
+
|
|
1500
|
+
---
|
|
1501
|
+
|
|
1502
|
+
## Configuration reference
|
|
1503
|
+
|
|
1504
|
+
The React SDK accepts the same configuration as the core SDK. For detailed information about each option, see the [core SDK configuration reference](../sdk-core/README.md#configuration-reference).
|
|
1505
|
+
|
|
1506
|
+
---
|
|
1507
|
+
|
|
1508
|
+
## Advanced
|
|
1509
|
+
|
|
1510
|
+
### Ionic / Capacitor support
|
|
1511
|
+
|
|
1512
|
+
The SDK works with Ionic and Capacitor applications. For native mobile platforms (iOS/Android), you need to override the default `urlHandler`, `callbackHandler`, and `storage` implementations to integrate with Capacitor plugins.
|
|
1513
|
+
|
|
1514
|
+
```tsx
|
|
1515
|
+
// main.tsx
|
|
1516
|
+
import { createRoot } from 'react-dom/client';
|
|
1517
|
+
import { StyAuthProvider, SDKStorage, SDKHttpClient, LocalStorage } from '@strivacity/sdk-react';
|
|
1518
|
+
import { Capacitor, CapacitorHttp } from '@capacitor/core';
|
|
1519
|
+
import { Preferences } from '@capacitor/preferences';
|
|
1520
|
+
import { InAppBrowser } from '@capacitor/inappbrowser';
|
|
1521
|
+
import { redirectUrlHandler, redirectCallbackHandler } from '@strivacity/sdk-core/utils/handlers';
|
|
1522
|
+
import type { PluginListenerHandle } from '@capacitor/core';
|
|
1523
|
+
import type { HttpClientResponse } from '@strivacity/sdk-react';
|
|
1524
|
+
import { App } from './App';
|
|
1525
|
+
|
|
1526
|
+
// Custom HTTP client using Capacitor's HTTP plugin
|
|
1527
|
+
class CapacitorHttpClient extends SDKHttpClient {
|
|
1528
|
+
async request<T>(url: string, options?: RequestInit): Promise<HttpClientResponse<T>> {
|
|
1529
|
+
const response = await CapacitorHttp.request({
|
|
1530
|
+
url,
|
|
1531
|
+
method: options?.method || 'GET',
|
|
1532
|
+
headers: (options?.headers as Record<string, string>) || {},
|
|
1533
|
+
data: options?.body,
|
|
1534
|
+
webFetchExtra: options,
|
|
1535
|
+
});
|
|
1536
|
+
|
|
1537
|
+
return {
|
|
1538
|
+
headers: new Headers(response.headers),
|
|
1539
|
+
ok: response.status >= 200 && response.status < 300,
|
|
1540
|
+
status: response.status,
|
|
1541
|
+
statusText: '',
|
|
1542
|
+
url: response.url,
|
|
1543
|
+
json: () => Promise.resolve(response.data),
|
|
1544
|
+
text: () => Promise.resolve(response.data),
|
|
1545
|
+
};
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
// Custom storage using Capacitor Preferences
|
|
1550
|
+
class CapacitorStorage extends SDKStorage {
|
|
1551
|
+
async get(key: string): Promise<string | null> {
|
|
1552
|
+
const { value } = await Preferences.get({ key });
|
|
1553
|
+
return value;
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
async set(key: string, value: string): Promise<void> {
|
|
1557
|
+
await Preferences.set({ key, value });
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1560
|
+
async delete(key: string): Promise<void> {
|
|
1561
|
+
await Preferences.remove({ key });
|
|
1562
|
+
}
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
createRoot(document.getElementById('app')!).render(
|
|
1566
|
+
<StyAuthProvider
|
|
1567
|
+
options={{
|
|
1568
|
+
mode: 'redirect',
|
|
1569
|
+
issuer: 'https://<YOUR_TENANT_DOMAIN>',
|
|
1570
|
+
clientId: 'YOUR_CLIENT_ID',
|
|
1571
|
+
redirectUri: 'https://your-app.example.com/callback',
|
|
1572
|
+
scopes: ['openid', 'profile', 'email'],
|
|
1573
|
+
|
|
1574
|
+
// Use Capacitor HTTP on native platforms
|
|
1575
|
+
httpClient: CapacitorHttpClient,
|
|
1576
|
+
|
|
1577
|
+
// Use Capacitor Preferences on native platforms, localStorage on web
|
|
1578
|
+
storage: Capacitor.getPlatform() === 'web' ? LocalStorage : CapacitorStorage,
|
|
1579
|
+
|
|
1580
|
+
// Handle URL redirects with InAppBrowser on native platforms
|
|
1581
|
+
async urlHandler(url, responseMode) {
|
|
1582
|
+
if (Capacitor.getPlatform() === 'web') {
|
|
1583
|
+
return redirectUrlHandler(url, responseMode);
|
|
1584
|
+
} else {
|
|
1585
|
+
await InAppBrowser.openInWebView({
|
|
1586
|
+
url,
|
|
1587
|
+
options: { /* Configure InAppBrowser options */ },
|
|
1588
|
+
});
|
|
1589
|
+
}
|
|
1590
|
+
},
|
|
1591
|
+
|
|
1592
|
+
// Handle OAuth callback with InAppBrowser listeners on native platforms
|
|
1593
|
+
async callbackHandler(url, responseMode) {
|
|
1594
|
+
if (Capacitor.getPlatform() === 'web') {
|
|
1595
|
+
return redirectCallbackHandler(url, responseMode);
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
return new Promise(async (resolve, reject) => {
|
|
1599
|
+
let navigationListener: PluginListenerHandle | null = null;
|
|
1600
|
+
let finishListener: PluginListenerHandle | null = null;
|
|
1601
|
+
let userCancelled = true;
|
|
1602
|
+
|
|
1603
|
+
const cleanupListeners = async () => {
|
|
1604
|
+
if (navigationListener) {
|
|
1605
|
+
await navigationListener.remove();
|
|
1606
|
+
navigationListener = null;
|
|
1607
|
+
}
|
|
1608
|
+
if (finishListener) {
|
|
1609
|
+
await finishListener.remove();
|
|
1610
|
+
finishListener = null;
|
|
1611
|
+
}
|
|
1612
|
+
};
|
|
1613
|
+
|
|
1614
|
+
try {
|
|
1615
|
+
// Listen for page navigation in InAppBrowser
|
|
1616
|
+
navigationListener = await InAppBrowser.addListener(
|
|
1617
|
+
'browserPageNavigationCompleted',
|
|
1618
|
+
async (event) => {
|
|
1619
|
+
const navigatedUrl = event.url;
|
|
1620
|
+
|
|
1621
|
+
// Check if the navigated URL matches our callback URL
|
|
1622
|
+
if (navigatedUrl && navigatedUrl.startsWith(url)) {
|
|
1623
|
+
try {
|
|
1624
|
+
const urlInstance = new URL(navigatedUrl);
|
|
1625
|
+
const dataString = responseMode === 'query'
|
|
1626
|
+
? urlInstance.search
|
|
1627
|
+
: urlInstance.hash;
|
|
1628
|
+
const params = Object.fromEntries(
|
|
1629
|
+
new URLSearchParams(dataString.slice(1))
|
|
1630
|
+
);
|
|
1631
|
+
|
|
1632
|
+
userCancelled = false;
|
|
1633
|
+
await InAppBrowser.close();
|
|
1634
|
+
resolve(params);
|
|
1635
|
+
} catch (error) {
|
|
1636
|
+
await InAppBrowser.close();
|
|
1637
|
+
reject(error);
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
);
|
|
1642
|
+
|
|
1643
|
+
// Listen for browser close event
|
|
1644
|
+
finishListener = await InAppBrowser.addListener('browserClosed', async () => {
|
|
1645
|
+
await cleanupListeners();
|
|
1646
|
+
|
|
1647
|
+
if (userCancelled) {
|
|
1648
|
+
reject(new Error('InAppBrowser flow cancelled by user.'));
|
|
1649
|
+
}
|
|
1650
|
+
});
|
|
1651
|
+
} catch (error) {
|
|
1652
|
+
await cleanupListeners();
|
|
1653
|
+
reject(error);
|
|
1654
|
+
}
|
|
1655
|
+
});
|
|
1656
|
+
},
|
|
1657
|
+
}}
|
|
1658
|
+
>
|
|
1659
|
+
<App />
|
|
1660
|
+
</StyAuthProvider>,
|
|
1661
|
+
);
|
|
1662
|
+
```
|
|
1663
|
+
|
|
1664
|
+
> For more details on custom handlers and storage implementations, see the [core SDK advanced section](../sdk-core/README.md#advanced).
|
|
1665
|
+
|
|
1666
|
+
### Server-side session (BFF) mode
|
|
1667
|
+
|
|
1668
|
+
Set `serverSessionUri` to route login through your own server instead of talking to the identity provider directly from the browser - tokens are then stored server-side and never reach client-side JavaScript. See [Server-side session management](../sdk-core/README.md#server-side-session-management) in the core SDK README for the full explanation and how to read the session on the client.
|
|
1669
|
+
|
|
1670
|
+
```tsx
|
|
1671
|
+
// main.tsx
|
|
1672
|
+
createRoot(document.getElementById('app')!).render(
|
|
1673
|
+
<StyAuthProvider
|
|
1674
|
+
options={{
|
|
1675
|
+
mode: 'redirect',
|
|
1676
|
+
issuer: 'https://<YOUR_TENANT_DOMAIN>',
|
|
1677
|
+
clientId: 'YOUR_CLIENT_ID',
|
|
1678
|
+
redirectUri: 'https://your-app.example.com/callback',
|
|
1679
|
+
scopes: ['openid', 'profile', 'email'],
|
|
1680
|
+
serverSessionUri: '/api/auth/login', // Requests are routed through your server; tokens are NOT written to client storage
|
|
1681
|
+
}}
|
|
1682
|
+
>
|
|
1683
|
+
<App />
|
|
1684
|
+
</StyAuthProvider>,
|
|
1685
|
+
);
|
|
1686
|
+
```
|
|
1687
|
+
|
|
1688
|
+
See [`apps/react`](../../apps/react) paired with [`apps/backend`](../../apps/backend) for a working example. See also [Using the backend app with SPA apps](../../README.md#using-the-backend-app-with-spa-apps) in the root README.
|
|
1689
|
+
|
|
1690
|
+
---
|
|
1691
|
+
|
|
1692
|
+
## Migration guide
|
|
1693
|
+
|
|
1694
|
+
### Migrating to v4.0
|
|
1695
|
+
|
|
1696
|
+
v4 replaces the SDK's class-based flow architecture with function-based architecture, and adds first-class support 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.
|
|
1697
|
+
|
|
1698
|
+
#### Class-based flows replaced by functions
|
|
1699
|
+
|
|
1700
|
+
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:
|
|
1701
|
+
|
|
1702
|
+
```ts
|
|
1703
|
+
// v3
|
|
1704
|
+
import { NativeFlow } from '@strivacity/sdk-core/flows/NativeFlow';
|
|
1705
|
+
|
|
1706
|
+
export class CustomNativeFlow extends NativeFlow {
|
|
1707
|
+
override async refresh(): Promise<void> {
|
|
1708
|
+
// ...
|
|
1709
|
+
}
|
|
1710
|
+
}
|
|
1711
|
+
```
|
|
1712
|
+
|
|
1713
|
+
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:
|
|
1714
|
+
|
|
1715
|
+
```ts
|
|
1716
|
+
// v4
|
|
1717
|
+
import { createBaseFlow } from '@strivacity/sdk-core/flows/base';
|
|
1718
|
+
import { getDefaultFlowState, getSDKOptions } from '@strivacity/sdk-core/utils';
|
|
1719
|
+
import type { SDKInitConfig, SDKOptions } from '@strivacity/sdk-core/types';
|
|
1720
|
+
|
|
1721
|
+
export function createCustomFlow(initConfig: SDKInitConfig) {
|
|
1722
|
+
const state = getDefaultFlowState();
|
|
1723
|
+
const options = getSDKOptions<SDKOptions>(state, initConfig);
|
|
1724
|
+
const base = createBaseFlow(state, options);
|
|
1725
|
+
|
|
1726
|
+
async function refresh(): Promise<void> {
|
|
1727
|
+
// ...
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1730
|
+
return { ...base, refresh };
|
|
1731
|
+
}
|
|
1732
|
+
```
|
|
1733
|
+
|
|
1734
|
+
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 the `options` prop passed to `StyAuthProvider` - see [Custom flow](../sdk-core/README.md#custom-flow) in the core SDK README for the full pattern and usage example.
|
|
1735
|
+
|
|
1736
|
+
#### Server-managed sessions (BFF) are now built in
|
|
1737
|
+
|
|
1738
|
+
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.
|
|
1739
|
+
|
|
1740
|
+
`@strivacity/sdk-react` doesn't ship its own server - it's a client SDK for SPAs, meant to pair with a separate backend. v4 replaces the old hand-rolled approach with a single option, `serverSessionUri`, on `StyAuthProvider`, paired with the Server SDK (`createBaseServerSDK` from `@strivacity/sdk-core/server`) running on that backend - PKCE, state, and session storage are all handled by the Server SDK:
|
|
1741
|
+
|
|
1742
|
+
```tsx
|
|
1743
|
+
// v4
|
|
1744
|
+
<StyAuthProvider
|
|
1745
|
+
options={{
|
|
1746
|
+
// ...
|
|
1747
|
+
serverSessionUri: '/api/auth/login', // requests are routed through your server; tokens are never written to client storage
|
|
1748
|
+
}}
|
|
1749
|
+
>
|
|
1750
|
+
<App />
|
|
1751
|
+
</StyAuthProvider>
|
|
1752
|
+
```
|
|
1753
|
+
|
|
1754
|
+
See [`apps/react`](../../apps/react) paired with [`apps/backend`](../../apps/backend) for a working example.
|
|
1755
|
+
|
|
1756
|
+
#### Native mode: no more `NativeFlowHandler`
|
|
1757
|
+
|
|
1758
|
+
In v3, `native` mode's `login()`/`register()` returned a separate `NativeFlowHandler` instance, and the flow was driven through that handler:
|
|
1759
|
+
|
|
1760
|
+
```ts
|
|
1761
|
+
// v3
|
|
1762
|
+
const handler = await sdk.login();
|
|
1763
|
+
const state = await handler.startSession(sessionId);
|
|
1764
|
+
const nextState = await handler.submitForm('formId', { identifier: 'user@example.com' });
|
|
1765
|
+
await handler.finalizeSession(nextState.finalizeUrl);
|
|
1766
|
+
```
|
|
1767
|
+
|
|
1768
|
+
v4 moves `startSession()`, `submitForm()`, and `finalizeSession()` directly onto the flow itself - in `@strivacity/sdk-react` this is wrapped for you by the [`useNativeLogin()`](#usenativelogin) hook:
|
|
1769
|
+
|
|
1770
|
+
```ts
|
|
1771
|
+
import { useNativeLogin } from '@strivacity/sdk-react';
|
|
1772
|
+
|
|
1773
|
+
// v4
|
|
1774
|
+
const { state, forms, messages, submitForm } = useNativeLogin({
|
|
1775
|
+
params: { sessionId },
|
|
1776
|
+
});
|
|
1777
|
+
|
|
1778
|
+
await submitForm('formId');
|
|
1779
|
+
```
|
|
1780
|
+
|
|
1781
|
+
Update any code that calls `login()`/`register()` and drives the returned handler in `native` mode to use `useNativeLogin()` instead.
|
|
640
1782
|
|
|
641
1783
|
## Migrating to v3.0
|
|
642
1784
|
|
|
643
1785
|
### Entry API Major Changes
|
|
644
1786
|
|
|
645
1787
|
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.
|
|
1788
|
+
|
|
1789
|
+
---
|
|
1790
|
+
|
|
1791
|
+
## Vulnerability Reporting
|
|
1792
|
+
|
|
1793
|
+
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.
|
|
1794
|
+
|
|
1795
|
+
## License
|
|
1796
|
+
|
|
1797
|
+
This package is available under the MIT License. See the [LICENSE](https://github.com/Strivacity/sdk-js/blob/main/LICENSE) file for more info.
|
|
1798
|
+
|
|
1799
|
+
## Contributing
|
|
1800
|
+
|
|
1801
|
+
Please see our [contributing guide](https://github.com/Strivacity/sdk-js/blob/main/CONTRIBUTING.md).
|