@strivacity/sdk-core 4.0.0-beta.0 → 4.0.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +228 -86
- package/dist/assets/oidc.cjs +1 -1
- package/dist/assets/oidc.cjs.map +1 -1
- package/dist/assets/oidc.mjs +1 -1
- package/dist/assets/oidc.mjs.map +1 -1
- package/dist/assets/server.cjs +1 -1
- package/dist/assets/server.cjs.map +1 -1
- package/dist/assets/server.mjs +1 -1
- package/dist/assets/server.mjs.map +1 -1
- package/dist/flows/base.cjs +1 -1
- package/dist/flows/base.cjs.map +1 -1
- package/dist/flows/base.d.ts +4 -2
- package/dist/flows/base.mjs +1 -1
- package/dist/flows/base.mjs.map +1 -1
- package/dist/flows/popup.cjs +1 -1
- package/dist/flows/popup.cjs.map +1 -1
- package/dist/flows/popup.mjs +1 -1
- package/dist/flows/popup.mjs.map +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +6 -2
- package/dist/index.mjs +1 -1
- package/dist/index.mjs.map +1 -1
- package/dist/server/base.cjs +1 -1
- package/dist/server/base.cjs.map +1 -1
- package/dist/server/base.d.ts +2 -4
- package/dist/server/base.mjs +1 -1
- package/dist/server/base.mjs.map +1 -1
- package/dist/server/index.cjs +1 -1
- package/dist/server/index.mjs +1 -1
- package/dist/storages/cache.cjs.map +1 -1
- package/dist/storages/cache.d.ts +1 -1
- package/dist/storages/cache.mjs.map +1 -1
- package/dist/storages/index.cjs +1 -1
- package/dist/storages/index.mjs +1 -1
- package/dist/storages/server.cjs +1 -1
- package/dist/storages/server.d.ts +29 -1
- package/dist/storages/server.mjs +1 -1
- package/dist/storages/worker.cjs.map +1 -1
- package/dist/storages/worker.d.ts +1 -1
- package/dist/storages/worker.mjs.map +1 -1
- package/dist/types/native.d.ts +67 -2
- package/dist/types/oidc.cjs.map +1 -1
- package/dist/types/oidc.d.ts +47 -6
- package/dist/types/oidc.mjs.map +1 -1
- package/dist/types/popup.d.ts +1 -1
- package/dist/types/server.d.ts +21 -9
- package/dist/utils/index.cjs +1 -1
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.mjs +1 -1
- package/dist/utils/native-login.cjs +2 -0
- package/dist/utils/native-login.cjs.map +1 -0
- package/dist/utils/native-login.d.ts +13 -0
- package/dist/utils/native-login.mjs +2 -0
- package/dist/utils/native-login.mjs.map +1 -0
- package/dist/utils/oidc.d.ts +3 -4
- package/dist/utils/path.cjs +2 -0
- package/dist/utils/path.cjs.map +1 -0
- package/dist/utils/path.d.ts +16 -0
- package/dist/utils/path.mjs +2 -0
- package/dist/utils/path.mjs.map +1 -0
- package/dist/utils/server.d.ts +5 -4
- package/dist/utils/session.cjs +1 -1
- package/dist/utils/session.cjs.map +1 -1
- package/dist/utils/session.d.ts +10 -1
- package/dist/utils/session.mjs +1 -1
- package/dist/utils/session.mjs.map +1 -1
- package/package.json +1 -1
- package/testing/tests/flows/base.spec.ts +0 -1030
- package/testing/tests/flows/embedded.spec.ts +0 -443
- package/testing/tests/flows/index.spec.ts +0 -8
- package/testing/tests/flows/native.spec.ts +0 -487
- package/testing/tests/flows/popup.spec.ts +0 -400
- package/testing/tests/flows/redirect.spec.ts +0 -369
- package/testing/tests/handlers/embedded.spec.ts +0 -199
- package/testing/tests/handlers/index.spec.ts +0 -8
- package/testing/tests/handlers/native.spec.ts +0 -283
- package/testing/tests/handlers/popup.spec.ts +0 -206
- package/testing/tests/handlers/redirect.spec.ts +0 -70
- package/testing/tests/index.spec.ts +0 -78
- package/testing/tests/server/base.spec.ts +0 -846
- package/testing/tests/server/index.spec.ts +0 -15
- package/testing/tests/storages/cache.spec.ts +0 -107
- package/testing/tests/storages/index.spec.ts +0 -22
- package/testing/tests/storages/indexeddb.spec.ts +0 -113
- package/testing/tests/storages/local.spec.ts +0 -46
- package/testing/tests/storages/memory.spec.ts +0 -51
- package/testing/tests/storages/server.spec.ts +0 -435
- package/testing/tests/storages/session.spec.ts +0 -46
- package/testing/tests/storages/worker.spec.ts +0 -106
- package/testing/tests/utils/common.spec.ts +0 -77
- package/testing/tests/utils/credentials.spec.ts +0 -195
- package/testing/tests/utils/dom.spec.ts +0 -37
- package/testing/tests/utils/index.spec.ts +0 -71
- package/testing/tests/utils/logging.spec.ts +0 -82
- package/testing/tests/utils/oidc.spec.ts +0 -744
- package/testing/tests/utils/server.spec.ts +0 -235
package/README.md
CHANGED
|
@@ -22,6 +22,7 @@ Use it directly in vanilla JS/TS projects or use one of the [framework-specific
|
|
|
22
22
|
- [Server SDK](#server-sdk)
|
|
23
23
|
- [Usage](#server-usage)
|
|
24
24
|
- [Storages](#server-storages)
|
|
25
|
+
- [Session helpers](#server-session-helpers)
|
|
25
26
|
- [Back-channel logout](#server-backchannel-logout)
|
|
26
27
|
- [Configuration Options](#server-configuration-options)
|
|
27
28
|
- [Shared features](#shared-features)
|
|
@@ -130,10 +131,12 @@ Call this on your redirect URI page after the IDP sends the user back. It parses
|
|
|
130
131
|
// callback.ts
|
|
131
132
|
import { sdk } from './sdk';
|
|
132
133
|
|
|
133
|
-
await sdk.handleCallback();
|
|
134
|
-
window.location.href = '/profile';
|
|
134
|
+
const { returnTo } = await sdk.handleCallback();
|
|
135
|
+
window.location.href = returnTo ?? '/profile';
|
|
135
136
|
```
|
|
136
137
|
|
|
138
|
+
To come back to the page that started the login (for example the one a route guard protected), pass it to `login({ returnTo: '/orders' })`. It must be a path or a URL on the current origin, otherwise `login()` throws. The SDK keeps it with the login state, not in the URL, and `handleCallback()` resolves to it. With `serverSessionUri` it is forwarded to the Server SDK login route, which redirects there after its callback.
|
|
139
|
+
|
|
137
140
|
> 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)`.
|
|
138
141
|
|
|
139
142
|
#### Registration
|
|
@@ -169,6 +172,8 @@ await sdk.logout({
|
|
|
169
172
|
|
|
170
173
|
> 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.
|
|
171
174
|
|
|
175
|
+
With [server-managed sessions](#logging-out), `logout()` doesn't end the server session - send the browser to your server's logout route instead.
|
|
176
|
+
|
|
172
177
|
#### Token management
|
|
173
178
|
|
|
174
179
|
Call these methods to manage the session and access token.
|
|
@@ -218,7 +223,7 @@ const sdk = initFlow({
|
|
|
218
223
|
|
|
219
224
|
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).
|
|
220
225
|
|
|
221
|
-
By default a centered popup window opens. Pass `popupWindowTarget` to change where the window opens, and `popupWindowFeatures` to control its size and
|
|
226
|
+
By default a centered popup window opens. Pass `popupWindowTarget` to change where the window opens, and `popupWindowFeatures` to control its size and other window features (`left`/`top` are ignored - the SDK always recalculates them to center the popup over the current window):
|
|
222
227
|
|
|
223
228
|
```ts
|
|
224
229
|
// login.ts
|
|
@@ -233,8 +238,6 @@ await sdk.login({
|
|
|
233
238
|
popupWindowFeatures: {
|
|
234
239
|
width: 500,
|
|
235
240
|
height: 700,
|
|
236
|
-
left: 100,
|
|
237
|
-
top: 100,
|
|
238
241
|
toolbar: false,
|
|
239
242
|
location: false,
|
|
240
243
|
resizable: true,
|
|
@@ -250,7 +253,16 @@ if (await sdk.isAuthenticated) {
|
|
|
250
253
|
|
|
251
254
|
#### Handle the callback
|
|
252
255
|
|
|
253
|
-
The
|
|
256
|
+
The pop-up still navigates to your `redirectUri`, so you still need a callback page there - without it nothing posts the result back to the opener. Call `handleCallback()` on that page; in popup mode it never resolves (the opener closes the pop-up once it receives the result), so don't rely on code after it running:
|
|
257
|
+
|
|
258
|
+
```ts
|
|
259
|
+
// callback.ts
|
|
260
|
+
import { sdk } from './sdk';
|
|
261
|
+
|
|
262
|
+
void sdk.handleCallback();
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
`login()` resolves in the opener once the result has been received and, for client-managed sessions, the tokens have been exchanged.
|
|
254
266
|
|
|
255
267
|
#### Registration
|
|
256
268
|
|
|
@@ -269,8 +281,6 @@ await sdk.register({
|
|
|
269
281
|
popupWindowFeatures: {
|
|
270
282
|
width: 500,
|
|
271
283
|
height: 700,
|
|
272
|
-
left: 100,
|
|
273
|
-
top: 100,
|
|
274
284
|
toolbar: false,
|
|
275
285
|
location: false,
|
|
276
286
|
resizable: true,
|
|
@@ -299,6 +309,8 @@ await sdk.logout({
|
|
|
299
309
|
|
|
300
310
|
> 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.
|
|
301
311
|
|
|
312
|
+
With [server-managed sessions](#logging-out), `logout()` doesn't end the server session - send the browser to your server's logout route instead.
|
|
313
|
+
|
|
302
314
|
#### Token management
|
|
303
315
|
|
|
304
316
|
Call these methods to manage the session and access token.
|
|
@@ -456,7 +468,9 @@ login.lang = 'en-US'; // Switch to English
|
|
|
456
468
|
|
|
457
469
|
#### Handle the callback
|
|
458
470
|
|
|
459
|
-
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.
|
|
471
|
+
No separate callback page is needed for client-managed sessions. The `<sty-login>` component handles the entire authentication flow automatically, including token exchange, and dispatches a `login` event when authentication completes successfully.
|
|
472
|
+
|
|
473
|
+
With [server-managed sessions](#server-side-session-management) (`serverSessionUri` set), the SDK instead redirects the browser to your `redirectUri` with `?code=…`, so you need a callback page there that forwards the browser to your server's callback route (e.g. `/auth/callback`).
|
|
460
474
|
|
|
461
475
|
#### Externally-initiated flows (entry)
|
|
462
476
|
|
|
@@ -497,7 +511,7 @@ const params = new URLSearchParams(window.location.search);
|
|
|
497
511
|
const login = document.querySelector('sty-login[lazy]');
|
|
498
512
|
login.sessionId = params.get('session_id');
|
|
499
513
|
login.shortAppId = params.get('short_app_id');
|
|
500
|
-
login.
|
|
514
|
+
login.lang = params.get('language') ?? navigator.language;
|
|
501
515
|
login.start();
|
|
502
516
|
|
|
503
517
|
login.addEventListener('login', () => {
|
|
@@ -523,7 +537,7 @@ const data = await sdk.entry();
|
|
|
523
537
|
const login = document.querySelector('sty-login[lazy]');
|
|
524
538
|
login.sessionId = data.session_id;
|
|
525
539
|
login.shortAppId = data.short_app_id;
|
|
526
|
-
login.
|
|
540
|
+
login.lang = data.language;
|
|
527
541
|
login.start();
|
|
528
542
|
|
|
529
543
|
login.addEventListener('login', () => {
|
|
@@ -547,6 +561,8 @@ await sdk.logout({
|
|
|
547
561
|
|
|
548
562
|
> 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.
|
|
549
563
|
|
|
564
|
+
With [server-managed sessions](#logging-out), `logout()` doesn't end the server session - send the browser to your server's logout route instead.
|
|
565
|
+
|
|
550
566
|
#### Token management
|
|
551
567
|
|
|
552
568
|
Call these methods to manage the session and access token.
|
|
@@ -576,7 +592,7 @@ const refreshToken = sdk.refreshToken; // read-only property - returns the curre
|
|
|
576
592
|
|
|
577
593
|
> For details on how this mode works, see the [native journey documentation](https://docs.strivacity.com/reference/native-journey).
|
|
578
594
|
|
|
579
|
-
You build the entire login UI with your own components. Call `startSession()` on the SDK instance to get the initial `NativeFlowState`, render the widgets, submit each form step with `submitForm()`, and repeat until `state.finalizeUrl` is set -
|
|
595
|
+
You build the entire login UI with your own components. Call `startSession()` on the SDK instance to get the initial `NativeFlowState`, render the widgets, submit each form step with `submitForm()`, and repeat until `state.finalizeUrl` is set. When a `submitForm()` (or `startSession()`) response contains `finalizeUrl`, the SDK finalizes the session automatically - you don't need to call `finalizeSession()` yourself.
|
|
580
596
|
|
|
581
597
|
> [Each example app](https://github.com/Strivacity/sdk-js/tree/main/apps) in this repository includes a framework-specific renderer implementation that you can use as a reference for your own integration.
|
|
582
598
|
|
|
@@ -590,19 +606,20 @@ const nativeFlowState = await sdk.startSession({
|
|
|
590
606
|
// Optional parameters
|
|
591
607
|
prompt: 'login', // use 'create' to open the registration flow instead
|
|
592
608
|
language: 'en-US', // set the UI language (BCP 47 language tag)
|
|
593
|
-
sdk: 'web-minimal', // rendering mode: 'web-minimal' for simplified rendering (see below), 'web' (default) for full rendering hints and branding
|
|
594
609
|
sessionId: null, // pass a session ID to resume an existing flow
|
|
595
610
|
});
|
|
596
611
|
|
|
597
612
|
// NativeFlowState structure:
|
|
598
613
|
// - web mode (default): Full response with all features
|
|
599
614
|
// - web-minimal mode: Minimal response with layout, branding, and render hints excluded
|
|
615
|
+
// The client SDK always requests `web` mode; `web-minimal` is only available through the Server SDK (BFF),
|
|
616
|
+
// whose login endpoint (`handleLogin`) forwards an `?sdk=web-minimal` query parameter in embedded and native modes
|
|
600
617
|
|
|
601
618
|
// nativeFlowState.branding - branding information (logos, etc.) (`web` mode only)
|
|
602
619
|
// nativeFlowState.forms - array of form definitions with widgets
|
|
603
620
|
// nativeFlowState.layout - tree structure defining widget rendering order (`web` mode only)
|
|
604
621
|
// nativeFlowState.screen - current step identifier (e.g., 'identifier', 'password')
|
|
605
|
-
// nativeFlowState.finalizeUrl - set when authentication completes
|
|
622
|
+
// nativeFlowState.finalizeUrl - set when authentication completes (the SDK has already finalized the session by then)
|
|
606
623
|
|
|
607
624
|
// Render the form based on nativeFlowState.screen, collect user input, then submit:
|
|
608
625
|
const nextNativeFlowState = await sdk.submitForm('identifier', {
|
|
@@ -610,8 +627,7 @@ const nextNativeFlowState = await sdk.submitForm('identifier', {
|
|
|
610
627
|
});
|
|
611
628
|
|
|
612
629
|
if (nextNativeFlowState.finalizeUrl) {
|
|
613
|
-
//
|
|
614
|
-
await sdk.finalizeSession(nextNativeFlowState.finalizeUrl);
|
|
630
|
+
// submitForm() has already finalized the session at the IDP, exchanged the authorization code for tokens and stored them in the configured storage
|
|
615
631
|
|
|
616
632
|
// Redirect to a protected page after successful authentication
|
|
617
633
|
if (await sdk.isAuthenticated) {
|
|
@@ -624,7 +640,9 @@ if (nextNativeFlowState.finalizeUrl) {
|
|
|
624
640
|
|
|
625
641
|
#### Handle the callback
|
|
626
642
|
|
|
627
|
-
No separate callback page is needed. Once `
|
|
643
|
+
No separate callback page is needed for client-managed sessions. Once a response contains `finalizeUrl`, the SDK finalizes the session automatically - it exchanges the authorization code for tokens and stores the session (see the Login / Register example above).
|
|
644
|
+
|
|
645
|
+
With [server-managed sessions](#server-side-session-management) (`serverSessionUri` set), the SDK instead redirects the browser to your `redirectUri` with `?code=…`, so you need a callback page there that forwards the browser to your server's callback route (e.g. `/auth/callback`).
|
|
628
646
|
|
|
629
647
|
#### Externally-initiated flows (entry)
|
|
630
648
|
|
|
@@ -698,6 +716,33 @@ await sdk.logout({
|
|
|
698
716
|
|
|
699
717
|
> 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.
|
|
700
718
|
|
|
719
|
+
With [server-managed sessions](#logging-out), `logout()` doesn't end the server session - send the browser to your server's logout route instead.
|
|
720
|
+
|
|
721
|
+
#### Native login controller
|
|
722
|
+
|
|
723
|
+
To build a native login integration for a framework that has no Strivacity package, use `createNativeLoginController`. It holds the state the framework packages expose through `useNativeLogin` (`loading`, `forms`, `messages` and `state`) and publishes every change as a new immutable snapshot. Bind the snapshots to your framework's reactivity, and call `start()` once when the login UI mounts.
|
|
724
|
+
|
|
725
|
+
```ts
|
|
726
|
+
import { createNativeLoginController } from '@strivacity/sdk-core/utils';
|
|
727
|
+
import { sdk } from './sdk';
|
|
728
|
+
|
|
729
|
+
// The SDK is passed as a getter: it is resolved on every use, so it may be created after the controller
|
|
730
|
+
const controller = createNativeLoginController(() => sdk, {
|
|
731
|
+
onLogin: () => (window.location.href = '/profile'),
|
|
732
|
+
onFallback: (error) => (window.location.href = error.url.toString()),
|
|
733
|
+
onError: (error) => console.error(error),
|
|
734
|
+
});
|
|
735
|
+
|
|
736
|
+
const unsubscribe = controller.subscribe(({ loading, state, forms, messages }) => render({ loading, state, forms, messages }));
|
|
737
|
+
|
|
738
|
+
await controller.start(); // sdk.init() + startSession(options.params)
|
|
739
|
+
|
|
740
|
+
controller.setFormValue('identifier', 'identifier', 'user@example.com');
|
|
741
|
+
await controller.submitForm('identifier');
|
|
742
|
+
```
|
|
743
|
+
|
|
744
|
+
`setOptions(options)` replaces the callbacks, for example on every render. `triggerFallback()`, `triggerClose()` and `setMessage()` work the same way as in `useNativeLogin`.
|
|
745
|
+
|
|
701
746
|
#### Token management
|
|
702
747
|
|
|
703
748
|
Call these methods to manage the session and access token.
|
|
@@ -749,11 +794,40 @@ In this mode, the SDK relies on your backend to:
|
|
|
749
794
|
- Store tokens in server-side storage (e.g., Redis, encrypted cookies)
|
|
750
795
|
- Complete the OAuth2 code exchange and redirect the browser back to your app
|
|
751
796
|
|
|
797
|
+
#### Logging out
|
|
798
|
+
|
|
799
|
+
`logout()` doesn't end a server-managed session: it clears the local client state and redirects to the IDP end-session endpoint only if the client session holds an ID token, but the server session is left in place. Send the browser to your server's logout route (`handleLogout`, such as `/api/auth/logout`) instead, which clears the server session and redirects to the IDP end-session endpoint (or straight to `returnTo` / `postLogoutRedirectUri` when the session holds no ID token):
|
|
800
|
+
|
|
801
|
+
```ts
|
|
802
|
+
// logout.ts
|
|
803
|
+
window.location.assign('/api/auth/logout?returnTo=/goodbye');
|
|
804
|
+
```
|
|
805
|
+
|
|
806
|
+
The logout route accepts an optional `returnTo` query param, and only a same-origin one. A missing, invalid, or cross-origin value falls back to the server's `postLogoutRedirectUri`. Whichever URL is used must also be registered as an allowed post-logout redirect URI in your Strivacity application client settings.
|
|
807
|
+
|
|
808
|
+
#### Refreshing the session
|
|
809
|
+
|
|
810
|
+
The browser never refreshes tokens in this mode - the refresh token stays on the server, and `autoRefresh` is ignored. Refresh the session on the server instead: during server rendering with the Server SDK's [`getSession(request)`](#server-session-helpers), or by sending the browser to your server's refresh route (`handleRefresh`, such as `/api/auth/refresh`), which rotates the session cookie and redirects back to a same-origin `returnTo`. On the Server SDK, `autoRefresh: false` turns off the refresh in `getSession` (and in the integrations built on it).
|
|
811
|
+
|
|
812
|
+
`refresh()` does the navigation for you: it sends the browser (as a full-page navigation, also in `popup` mode) to `${authUrlPrefix}/refresh?returnTo=<url>`, where `returnTo` defaults to the current URL. `authUrlPrefix` defaults to `/auth`, the Server SDK's own default, so set it on the client only when your server uses another prefix.
|
|
813
|
+
|
|
814
|
+
```ts
|
|
815
|
+
// sdk.ts
|
|
816
|
+
const sdk = initFlow({
|
|
817
|
+
// ...
|
|
818
|
+
serverSessionUri: '/api/auth/login',
|
|
819
|
+
authUrlPrefix: '/api/auth', // Where your server serves the auth routes: refresh() and the guards use `${authUrlPrefix}/refresh`
|
|
820
|
+
});
|
|
821
|
+
|
|
822
|
+
// Navigates to /api/auth/refresh?returnTo=/profile, then the server redirects back to /profile
|
|
823
|
+
await sdk.refresh({ returnTo: '/profile' });
|
|
824
|
+
```
|
|
825
|
+
|
|
752
826
|
#### Accessing the session on the client-side
|
|
753
827
|
|
|
754
828
|
Tokens never reach the client SDK in this mode, so expose only what the UI needs - typically the decoded `claims`, not the raw tokens - through your own backend:
|
|
755
829
|
|
|
756
|
-
- **Server-rendered pages** - fetch the session once per request in your server-side loader/layout via the Server SDK's `getSession(request)`, then pass the safe subset down to your client-rendered components. Framework-specific SDK wrappers already handle this for you internally.
|
|
830
|
+
- **Server-rendered pages** - fetch the session once per request in your server-side loader/layout via the Server SDK's `getSession(request)` (it refreshes a near-expiry session first with `autoRefresh`, so call it where the response cookies can still be written), then pass the safe subset down to your client-rendered components. Framework-specific SDK wrappers already handle this for you internally.
|
|
757
831
|
- **Client-rendered pages** - same idea, but triggered from the client: expose a small endpoint (e.g. `/api/auth/session`) that calls `getSession(req)` server-side and returns the safe subset as JSON, then fetch it on the client and store it wherever your client-side session state lives.
|
|
758
832
|
|
|
759
833
|
```ts
|
|
@@ -766,14 +840,37 @@ export async function GET(req: Request) {
|
|
|
766
840
|
}
|
|
767
841
|
```
|
|
768
842
|
|
|
843
|
+
```ts
|
|
844
|
+
// main.ts (client)
|
|
845
|
+
const response = await fetch('/api/auth/session', { credentials: 'include' });
|
|
846
|
+
const { claims } = (await response.json()) ?? {};
|
|
847
|
+
|
|
848
|
+
// Keep `claims` in your own client-side state (store, context, ...) and render from it
|
|
849
|
+
```
|
|
850
|
+
|
|
851
|
+
Note that the client SDK's `isAuthenticated` / `isAuthenticatedSync` only report `true` for a session that has an `access_token` and a future `expires_at`, so loading a claims-only object into the SDK won't mark the user as authenticated. If you want the client SDK state (`isAuthenticated`, `idTokenClaims`, events) to reflect the server session - as the [example apps](../../apps) do - return the full session from your endpoint and load it with `updateSession()`. This exposes the access token to the browser (it's still never written to client-side storage), so only do it when that trade-off is acceptable. Strip the refresh and ID tokens on the server with `toClientSession(session)` (from `@strivacity/sdk-core/utils`), which returns a copy without `refresh_token` and `id_token`:
|
|
852
|
+
|
|
853
|
+
```ts
|
|
854
|
+
// api/auth/session.ts (server route)
|
|
855
|
+
import { toClientSession } from '@strivacity/sdk-core/utils';
|
|
856
|
+
import { sdk } from './server';
|
|
857
|
+
|
|
858
|
+
export async function GET(req: Request) {
|
|
859
|
+
return Response.json(toClientSession(await sdk.getSession(req)));
|
|
860
|
+
}
|
|
861
|
+
```
|
|
862
|
+
|
|
769
863
|
```ts
|
|
770
864
|
// main.ts (client)
|
|
771
865
|
import { sdk } from './client';
|
|
772
866
|
|
|
773
|
-
const response = await fetch('/api/auth/session');
|
|
774
|
-
const session = await response.json();
|
|
867
|
+
const response = await fetch('/api/auth/session', { credentials: 'include' });
|
|
775
868
|
|
|
776
|
-
|
|
869
|
+
const session = response.ok ? await response.json() : null; // endpoint returns `toClientSession(session)`, or null
|
|
870
|
+
|
|
871
|
+
if (session?.access_token) {
|
|
872
|
+
await sdk.updateSession(session);
|
|
873
|
+
}
|
|
777
874
|
```
|
|
778
875
|
|
|
779
876
|
---
|
|
@@ -798,7 +895,7 @@ const sdk = createBaseServerSDK(
|
|
|
798
895
|
// Server adapter
|
|
799
896
|
{
|
|
800
897
|
toRequest: (req) => toWebRequest(req), // converts framework-specific request to standard Web Request
|
|
801
|
-
redirect: (
|
|
898
|
+
redirect: (url, status) => Response.redirect(url, status), // optional - (url, status, event?) => Response; override default redirect behavior
|
|
802
899
|
},
|
|
803
900
|
|
|
804
901
|
// Server configuration
|
|
@@ -835,7 +932,7 @@ mount('get', '/api/auth/register', (req) => sdk.handleRegister(req));
|
|
|
835
932
|
/**
|
|
836
933
|
* Handles external flow entry (e.g., password reset link).
|
|
837
934
|
* @modes embedded, native
|
|
838
|
-
* @returns
|
|
935
|
+
* @returns 302 redirect to `loginUri`
|
|
839
936
|
*/
|
|
840
937
|
mount('get', '/api/auth/entry', (req) => sdk.handleEntry(req));
|
|
841
938
|
|
|
@@ -856,21 +953,22 @@ mount('get', '/api/auth/refresh', (req) => sdk.handleRefresh(req));
|
|
|
856
953
|
/**
|
|
857
954
|
* Revokes tokens and clears the session.
|
|
858
955
|
* @modes all
|
|
859
|
-
* @returns
|
|
956
|
+
* @returns 302 redirect to `postLogoutRedirectUri`
|
|
860
957
|
*/
|
|
861
958
|
mount('get', '/api/auth/revoke', (req) => sdk.handleRevoke(req));
|
|
862
959
|
|
|
863
960
|
/**
|
|
864
961
|
* Ends the session and redirects to the IDP logout page.
|
|
962
|
+
* Accepts an optional same-origin `?returnTo=` (falls back to `postLogoutRedirectUri`).
|
|
865
963
|
* @modes all
|
|
866
|
-
* @returns 302 redirect to IDP logout
|
|
964
|
+
* @returns 302 redirect to IDP logout (or straight to `returnTo` when the session has no ID token)
|
|
867
965
|
*/
|
|
868
966
|
mount('get', '/api/auth/logout', (req) => sdk.handleLogout(req));
|
|
869
967
|
|
|
870
968
|
/**
|
|
871
969
|
* Processes back-channel logout requests from the IDP.
|
|
872
970
|
* @modes all
|
|
873
|
-
* @returns
|
|
971
|
+
* @returns 200 OK (400 invalid token, 501 storage without deleteByLogoutToken, 500 delete failed)
|
|
874
972
|
*/
|
|
875
973
|
mount('post', '/api/auth/backchannel-logout', (req) => sdk.handleBackChannelLogout(req));
|
|
876
974
|
```
|
|
@@ -957,8 +1055,8 @@ By default, the server SDK stores:
|
|
|
957
1055
|
|
|
958
1056
|
#### Built-in session storages
|
|
959
1057
|
|
|
960
|
-
- **`createEncryptedCookieStorage(secret, options?)`** - default storage that keeps the session encrypted in an http-only cookie.
|
|
961
|
-
- **`createSessionIdCookieStorage(adapter, storage, options?)`** - puts only a small, random session-id cookie on the client and keeps the actual session payload in the `storage` you provide.
|
|
1058
|
+
- **`createEncryptedCookieStorage(secret, adapter, options?)`** - default storage that keeps the session encrypted in an http-only cookie.
|
|
1059
|
+
- **`createSessionIdCookieStorage(adapter, storage, options?)`** - puts only a small, random session-id cookie on the client and keeps the actual session payload in the `storage` you provide. It supports back-channel logout when `storage` implements `deleteByLogoutToken`.
|
|
962
1060
|
|
|
963
1061
|
```ts
|
|
964
1062
|
// server.ts
|
|
@@ -982,7 +1080,9 @@ const sdk = createBaseServerSDK(adapter, {
|
|
|
982
1080
|
#### Custom storage
|
|
983
1081
|
|
|
984
1082
|
You can customize storage by providing `storage` and `stateStorage` options.
|
|
985
|
-
For example you can use [unstorage](https://npmjs.com/package/unstorage) which provides a unified async key-value API with dozens of built-in drivers (Redis, Cloudflare KV, filesystem, memory, and more)
|
|
1083
|
+
For example you can use [unstorage](https://npmjs.com/package/unstorage) which provides a unified async key-value API with dozens of built-in drivers (Redis, Cloudflare KV, filesystem, memory, and more).
|
|
1084
|
+
|
|
1085
|
+
The `get` / `set` / `delete` methods of a `ServerStorage` receive the incoming request event as their last argument, while the SDK always calls it with the same key (`storageTokenName`). A storage that ignores the event would therefore share a single session across all users - key the data per browser session instead. The easiest way is to wrap your key-value store with `createSessionIdCookieStorage`, which puts a random session id in a cookie and calls your store with that id as the key:
|
|
986
1086
|
|
|
987
1087
|
```ts
|
|
988
1088
|
// storage.ts
|
|
@@ -994,29 +1094,30 @@ const unstorageInstance = createStorage({
|
|
|
994
1094
|
driver: redisDriver({ url: process.env.REDIS_URL }),
|
|
995
1095
|
});
|
|
996
1096
|
|
|
997
|
-
//
|
|
998
|
-
|
|
1097
|
+
// Session payload store - `key` is the per-browser session id provided by `createSessionIdCookieStorage`
|
|
1098
|
+
// (raw get/set keep the serialized session string as-is)
|
|
1099
|
+
export const sessionStore: ServerStorage = {
|
|
999
1100
|
async get(key) {
|
|
1000
|
-
return unstorageInstance.
|
|
1101
|
+
return unstorageInstance.getItemRaw<string>(`session:${key}`);
|
|
1001
1102
|
},
|
|
1002
1103
|
async set(key, value) {
|
|
1003
|
-
await unstorageInstance.
|
|
1104
|
+
await unstorageInstance.setItemRaw(`session:${key}`, value);
|
|
1004
1105
|
},
|
|
1005
1106
|
async delete(key) {
|
|
1006
|
-
await unstorageInstance.removeItem(key);
|
|
1107
|
+
await unstorageInstance.removeItem(`session:${key}`);
|
|
1007
1108
|
},
|
|
1008
1109
|
// Required for OIDC back-channel logout support.
|
|
1009
|
-
// Scans all stored sessions and removes those
|
|
1110
|
+
// Scans all stored sessions and removes those whose ID token claims match the logout token's sid or sub claim.
|
|
1010
1111
|
async deleteByLogoutToken(logoutToken) {
|
|
1011
|
-
const keys = await unstorageInstance.getKeys();
|
|
1112
|
+
const keys = await unstorageInstance.getKeys('session');
|
|
1012
1113
|
await Promise.all(
|
|
1013
1114
|
keys.map(async (key) => {
|
|
1014
|
-
const raw = await unstorageInstance.
|
|
1115
|
+
const raw = await unstorageInstance.getItemRaw<string>(key);
|
|
1015
1116
|
if (!raw) return;
|
|
1016
1117
|
const session = JSON.parse(raw);
|
|
1017
1118
|
if (
|
|
1018
|
-
(logoutToken.sid && session.sid === logoutToken.sid) ||
|
|
1019
|
-
(logoutToken.sub && session.sub === logoutToken.sub)
|
|
1119
|
+
(logoutToken.sid && session.claims?.sid === logoutToken.sid) ||
|
|
1120
|
+
(logoutToken.sub && session.claims?.sub === logoutToken.sub)
|
|
1020
1121
|
) {
|
|
1021
1122
|
await unstorageInstance.removeItem(key);
|
|
1022
1123
|
}
|
|
@@ -1042,19 +1143,42 @@ export const stateStorage: SDKStorage = {
|
|
|
1042
1143
|
```ts
|
|
1043
1144
|
// server.ts
|
|
1044
1145
|
import { createBaseServerSDK } from '@strivacity/sdk-core/server';
|
|
1146
|
+
import { createSessionIdCookieStorage } from '@strivacity/sdk-core/storages';
|
|
1045
1147
|
import { toWebRequest } from './utils';
|
|
1046
|
-
import {
|
|
1148
|
+
import { sessionStore, stateStorage } from './storage';
|
|
1047
1149
|
|
|
1048
|
-
const
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1150
|
+
const adapter = { toRequest: (req) => toWebRequest(req) };
|
|
1151
|
+
|
|
1152
|
+
const sdk = createBaseServerSDK(adapter, {
|
|
1153
|
+
// other configuration options
|
|
1154
|
+
storage: createSessionIdCookieStorage(adapter, sessionStore), // Custom Redis-backed session storage, keyed by a session-id cookie
|
|
1155
|
+
stateStorage: stateStorage, // Custom Redis-backed state storage
|
|
1156
|
+
});
|
|
1157
|
+
```
|
|
1158
|
+
|
|
1159
|
+
<a id="server-session-helpers"></a>
|
|
1160
|
+
### Session helpers
|
|
1161
|
+
|
|
1162
|
+
Besides the route handlers, the Server SDK exposes the session directly, for your own routes, loaders and middleware. Each method takes the framework-native request event:
|
|
1163
|
+
|
|
1164
|
+
| Method | Description |
|
|
1165
|
+
| -------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
1166
|
+
| `getSession(event)` | Returns the session, or `null`. With `autoRefresh` (the default) it refreshes the session first when the access token is expired or within `refreshSkew` of expiry and a refresh token exists, like the client SDK. Refreshes at most once per request event - concurrent and later calls with the same event share the result. A failed refresh resolves to `null`. With `autoRefresh: false` - the option, or `getSession(event, { autoRefresh: false })` for one call - it returns the stored session |
|
|
1167
|
+
| `updateSession(session, event)` | Stores a session |
|
|
1168
|
+
| `refreshSession(event)` | Refreshes the session with its refresh token (throws on failure and clears the session) |
|
|
1169
|
+
| `revokeSession(event)` | Revokes the refresh token (or the access token when there is none) and clears the session |
|
|
1170
|
+
|
|
1171
|
+
`getSession` queues the rotated session cookie on the event instead of writing it to a response, so write the queued cookies to the response yourself with `flushSetCookies(event, response)` (from `@strivacity/sdk-core/utils`). The framework SDKs do this for you in their middleware and SSR integrations.
|
|
1172
|
+
|
|
1173
|
+
```ts
|
|
1174
|
+
// api/me.ts (server route)
|
|
1175
|
+
import { flushSetCookies } from '@strivacity/sdk-core/utils';
|
|
1176
|
+
import { sdk } from './server';
|
|
1177
|
+
|
|
1178
|
+
export async function GET(req: Request) {
|
|
1179
|
+
const session = await sdk.getSession(req);
|
|
1180
|
+
return flushSetCookies(req, Response.json(session ? { claims: session.claims } : null, { status: session ? 200 : 401 }));
|
|
1181
|
+
}
|
|
1058
1182
|
```
|
|
1059
1183
|
|
|
1060
1184
|
<a id="server-backchannel-logout"></a>
|
|
@@ -1062,7 +1186,7 @@ const sdk = createBaseServerSDK(
|
|
|
1062
1186
|
|
|
1063
1187
|
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 the `/api/auth/backchannel-logout` route you mounted earlier, which the SDK routes to `sdk.handleBackChannelLogout(req)`.
|
|
1064
1188
|
|
|
1065
|
-
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`,
|
|
1189
|
+
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`, `501` if the configured storage doesn't implement `deleteByLogoutToken`, and `500` if deleting the session fails.
|
|
1066
1190
|
|
|
1067
1191
|
> **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`, or a fully custom server storage as shown in the [Custom storage](#server-storages) example above.
|
|
1068
1192
|
|
|
@@ -1086,15 +1210,16 @@ The server SDK accepts the same configuration as the client SDK, plus server-spe
|
|
|
1086
1210
|
scopes: string[],
|
|
1087
1211
|
storage: ServerStorage, // Custom session storage
|
|
1088
1212
|
stateStorage: SDKStorage, // Custom OAuth2 state storage
|
|
1089
|
-
serverSessionUri: string, // Optional -
|
|
1213
|
+
serverSessionUri: string, // Optional - defaults to `${authUrlPrefix}/login`
|
|
1090
1214
|
// ... other client SDK options
|
|
1091
1215
|
|
|
1092
1216
|
// server-specific options
|
|
1093
1217
|
secret: '<your-encryption-key>', // encrypts http-only cookies
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1218
|
+
authUrlPrefix: '/auth', // Prefix for auth routes (default: '/auth')
|
|
1219
|
+
loginUri: '/login', // App's login page; the entry route (`handleEntry`) redirects there with `session_id`, `short_app_id` and `language` (default: '/login')
|
|
1220
|
+
cookieMaxAge: 2592000, // Max age for session cookies in seconds (default: 30 days)
|
|
1221
|
+
postLoginRedirectUri: '/profile', // Redirect after login when no `?returnTo=` is given (default: origin of `redirectUri`)
|
|
1222
|
+
postLogoutRedirectUri: '/', // Redirect after logout (default: origin of `redirectUri`)
|
|
1098
1223
|
}
|
|
1099
1224
|
```
|
|
1100
1225
|
|
|
@@ -1130,7 +1255,7 @@ The SDK stores login state parameters separately from session tokens. Override `
|
|
|
1130
1255
|
|
|
1131
1256
|
```ts
|
|
1132
1257
|
import { initFlow } from '@strivacity/sdk-core';
|
|
1133
|
-
import { createSessionStorage } from '@strivacity/sdk-core/storages';
|
|
1258
|
+
import { createLocalStorage, createSessionStorage } from '@strivacity/sdk-core/storages';
|
|
1134
1259
|
|
|
1135
1260
|
const sdk = initFlow({
|
|
1136
1261
|
mode: 'redirect',
|
|
@@ -1175,7 +1300,7 @@ const sdk = createBaseServerSDK(adapter, {
|
|
|
1175
1300
|
|
|
1176
1301
|
##### Worker Storage
|
|
1177
1302
|
|
|
1178
|
-
For enhanced security isolation, you can offload all storage operations to a Web Worker via `postMessage`,
|
|
1303
|
+
For enhanced security isolation, you can offload all storage operations to a Web Worker via `postMessage`, so the persisted session lives in the worker's storage (e.g. IndexedDB) instead of `localStorage`. Note that the SDK still keeps the active session in main-thread memory while the page is running. Simply instantiate the Worker and bridge it using `handleWorkerStorageRequests(storage)` inside the worker context:
|
|
1179
1304
|
|
|
1180
1305
|
```ts
|
|
1181
1306
|
// storage.worker.ts
|
|
@@ -1204,7 +1329,7 @@ const sdk = initFlow({
|
|
|
1204
1329
|
All flow instances expose the same set of getters:
|
|
1205
1330
|
|
|
1206
1331
|
```ts
|
|
1207
|
-
// Async - waits for init, optionally auto-refreshes an expired token
|
|
1332
|
+
// Async - waits for init, optionally auto-refreshes an expired token (never when serverSessionUri is set)
|
|
1208
1333
|
const isAuthenticated: boolean = await sdk.isAuthenticated;
|
|
1209
1334
|
|
|
1210
1335
|
// Sync - true only if a non-expired session is already in memory
|
|
@@ -1224,12 +1349,14 @@ const sessionId: string | null = sdk.sessionId; // current session identifier
|
|
|
1224
1349
|
const shortAppId: string | null = sdk.shortAppId; // short application identifier
|
|
1225
1350
|
const language: string = sdk.language; // current UI language code
|
|
1226
1351
|
|
|
1227
|
-
// Verify authentication status (auto-refreshes an expired token if a refresh token is available)
|
|
1352
|
+
// Verify authentication status (auto-refreshes an expired token if a refresh token is available, unless serverSessionUri is set)
|
|
1228
1353
|
const ok: boolean = await sdk.checkAuthentication();
|
|
1229
1354
|
// Verify authentication status without attempting a refresh
|
|
1230
1355
|
const ok: boolean = await sdk.checkAuthentication({ autoRefresh: false });
|
|
1231
1356
|
```
|
|
1232
1357
|
|
|
1358
|
+
Tabs that share the session storage (`localStorage`, IndexedDB, ...) never refresh at the same time: `refresh()` (also the automatic one) runs under a [Web Lock](https://developer.mozilla.org/docs/Web/API/Web_Locks_API) per client and storage key. A tab that waited for the lock first re-reads the storage and uses the session another tab has just refreshed, instead of sending the old refresh token again, which would fail with refresh token rotation and clear the session. Without the Web Locks API, refresh runs directly.
|
|
1359
|
+
|
|
1233
1360
|
---
|
|
1234
1361
|
|
|
1235
1362
|
### SDK events
|
|
@@ -1243,7 +1370,7 @@ Subscribe to authentication lifecycle events via `subscribeToEvent` (one specifi
|
|
|
1243
1370
|
| `sessionUpdated` | `{ idToken, accessToken, refreshToken, claims }` | The session was updated (e.g., after a token refresh) |
|
|
1244
1371
|
| `sessionCleared` | - | The session was cleared (e.g., after logout or token revocation) |
|
|
1245
1372
|
| `loginInitiated` | - | A login or registration redirect / popup has started |
|
|
1246
|
-
| `flowInitiated` |
|
|
1373
|
+
| `flowInitiated` | - | A native or embedded flow has started |
|
|
1247
1374
|
| `loggedIn` | `{ idToken, accessToken, refreshToken, claims }` | Tokens were received and stored after a successful login |
|
|
1248
1375
|
| `logoutInitiated` | `{ idToken, claims }` | Logout was initiated, before the redirect to the IDP end-session endpoint |
|
|
1249
1376
|
| `tokenRefreshed` | `{ idToken, accessToken, refreshToken, claims }` | Access token was silently refreshed |
|
|
@@ -1378,25 +1505,25 @@ For cases where none of the built-in modes fit-for example, when all authenticat
|
|
|
1378
1505
|
|
|
1379
1506
|
`createBaseFlow` is the base factory function used by all built-in flows. It gives you the full shared method set (`init`, `checkAuthentication`, `refresh`, `revoke`, `logout`, `handleCallback`, `getSession`, `updateSession`, `cleanupSession`, `subscribeToEvent`, `subscribeToAllEvents`, `tokenExchange`) and lets you add your own login logic on top.
|
|
1380
1507
|
|
|
1381
|
-
Wire it up through `initFlow` by passing your factory function as the `factory` option - when present, `initFlow` calls it with the
|
|
1508
|
+
Wire it up through `initFlow` by passing your factory function as the `factory` option - when present, `initFlow` calls it with the raw options you passed to `initFlow` (defaults not applied - call `getSDKOptions()` yourself, as below) instead of dispatching to the built-in `redirect`/`popup`/`embedded`/`native` flows.
|
|
1382
1509
|
|
|
1383
1510
|
```ts
|
|
1384
1511
|
import type { SDKInitConfig, SDKOptions } from '@strivacity/sdk-core/types';
|
|
1385
1512
|
import { getDefaultFlowState, getSDKOptions } from '@strivacity/sdk-core/utils';
|
|
1386
1513
|
import { createBaseFlow } from '@strivacity/sdk-core/flows/base';
|
|
1387
1514
|
|
|
1388
|
-
type CustomFlow = {
|
|
1389
|
-
init: ReturnType<typeof createBaseFlow>['init']
|
|
1390
|
-
login (params?: Record<string, unknown>) => Promise<void
|
|
1391
|
-
refresh () => Promise<void
|
|
1392
|
-
revoke () => Promise<void
|
|
1393
|
-
logout: () => Promise<void
|
|
1515
|
+
export type CustomFlow = {
|
|
1516
|
+
init: ReturnType<typeof createBaseFlow>['init'];
|
|
1517
|
+
login: (params?: Record<string, unknown>) => Promise<void>;
|
|
1518
|
+
refresh: () => Promise<void>;
|
|
1519
|
+
revoke: () => Promise<void>;
|
|
1520
|
+
logout: () => Promise<void>;
|
|
1394
1521
|
// Add your own methods here
|
|
1395
|
-
}
|
|
1522
|
+
};
|
|
1396
1523
|
|
|
1397
1524
|
export function createCustomFlow(initConfig: SDKInitConfig): CustomFlow {
|
|
1398
1525
|
const state = getDefaultFlowState();
|
|
1399
|
-
const options = getSDKOptions<SDKOptions>(
|
|
1526
|
+
const options = getSDKOptions<SDKOptions>(initConfig);
|
|
1400
1527
|
const base = createBaseFlow(state, options);
|
|
1401
1528
|
|
|
1402
1529
|
async function login(params: Record<string, unknown> = {}): Promise<void> {
|
|
@@ -1421,8 +1548,9 @@ export function createCustomFlow(initConfig: SDKInitConfig): CustomFlow {
|
|
|
1421
1548
|
});
|
|
1422
1549
|
|
|
1423
1550
|
if (!response.ok) {
|
|
1551
|
+
const failedRefreshToken = state.session?.refresh_token;
|
|
1424
1552
|
await base.cleanupSession();
|
|
1425
|
-
base.dispatchEvent('tokenRefreshFailed', [{}]);
|
|
1553
|
+
base.dispatchEvent('tokenRefreshFailed', [{ refreshToken: failedRefreshToken }]);
|
|
1426
1554
|
return;
|
|
1427
1555
|
}
|
|
1428
1556
|
|
|
@@ -1437,15 +1565,16 @@ export function createCustomFlow(initConfig: SDKInitConfig): CustomFlow {
|
|
|
1437
1565
|
credentials: 'include',
|
|
1438
1566
|
});
|
|
1439
1567
|
|
|
1568
|
+
const token = state.session?.refresh_token ?? state.session?.access_token;
|
|
1569
|
+
|
|
1440
1570
|
if (!response.ok) {
|
|
1441
1571
|
await base.cleanupSession();
|
|
1442
|
-
base.dispatchEvent('tokenRevokeFailed', [{}]);
|
|
1572
|
+
base.dispatchEvent('tokenRevokeFailed', [{ token }]);
|
|
1443
1573
|
return;
|
|
1444
1574
|
}
|
|
1445
1575
|
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
base.dispatchEvent('tokenRefreshed', [{ accessToken: session.access_token, claims: session.claims }]);
|
|
1576
|
+
await base.cleanupSession();
|
|
1577
|
+
base.dispatchEvent('tokenRevoked', [{ token }]);
|
|
1449
1578
|
}
|
|
1450
1579
|
|
|
1451
1580
|
async function logout(): Promise<void> {
|
|
@@ -1486,7 +1615,7 @@ Or wire it through `initFlow` via the `factory` option:
|
|
|
1486
1615
|
|
|
1487
1616
|
```ts
|
|
1488
1617
|
import { initFlow } from '@strivacity/sdk-core';
|
|
1489
|
-
import { createCustomFlow } from './custom-flow';
|
|
1618
|
+
import { createCustomFlow, type CustomFlow } from './custom-flow';
|
|
1490
1619
|
|
|
1491
1620
|
const sdk = initFlow<CustomFlow>({
|
|
1492
1621
|
mode: 'native',
|
|
@@ -1582,7 +1711,7 @@ try {
|
|
|
1582
1711
|
// error.recoverable === true - retry might work
|
|
1583
1712
|
console.error('Network error - retrying...', error.message);
|
|
1584
1713
|
} else if (error instanceof ServerError) {
|
|
1585
|
-
// Server returned 5xx error
|
|
1714
|
+
// Server returned a 5xx or 429 error, or the discovery document / JWKS request returned a non-OK response
|
|
1586
1715
|
// error.recoverable === true - temporary server issue
|
|
1587
1716
|
// error.status contains HTTP status code
|
|
1588
1717
|
console.error(`Server error ${error.status}:`, error.message);
|
|
@@ -1622,10 +1751,10 @@ try {
|
|
|
1622
1751
|
|
|
1623
1752
|
**Error categories:**
|
|
1624
1753
|
- `Network` - Network connectivity issues (recoverable)
|
|
1625
|
-
- `Server` - Server errors 5xx (recoverable)
|
|
1754
|
+
- `Server` - Server errors: 5xx and 429 responses, and any non-OK discovery document / JWKS response (recoverable)
|
|
1626
1755
|
- `Oidc` - OIDC protocol errors from authorization server (not recoverable)
|
|
1627
1756
|
- `Protocol` - Protocol violations or invalid responses (not recoverable)
|
|
1628
|
-
- `
|
|
1757
|
+
- `InvalidRequest` - Invalid SDK configuration, thrown as `ConfigurationError` (not recoverable)
|
|
1629
1758
|
- `Internal` - Unexpected internal SDK errors (not recoverable)
|
|
1630
1759
|
|
|
1631
1760
|
---
|
|
@@ -1736,7 +1865,7 @@ try {
|
|
|
1736
1865
|
- Verifying tokens received from external sources (e.g., webhooks, API callbacks)
|
|
1737
1866
|
- Server-side token verification when building custom server flows
|
|
1738
1867
|
|
|
1739
|
-
> **Note:** The SDK automatically verifies ID tokens during login and refresh flows. You only need `verifyJwt` when handling tokens from external sources.
|
|
1868
|
+
> **Note:** The SDK automatically verifies ID tokens during login and refresh flows. The ID token's `exp` claim is checked with a 60-second clock-skew tolerance, so a device clock that runs slightly fast doesn't reject a freshly issued token with `ProtocolError('Token has expired')`. You only need `verifyJwt` when handling tokens from external sources.
|
|
1740
1869
|
|
|
1741
1870
|
#### PKCE code verifier generation
|
|
1742
1871
|
|
|
@@ -1781,12 +1910,14 @@ injectScript('my-script', 'https://example.com/script.js');
|
|
|
1781
1910
|
| `responseType` | `'code'` | No | `'code'` | OAuth2 response type |
|
|
1782
1911
|
| `responseMode` | `'query' \| 'fragment'` | No | `'query'` | OAuth2 response mode |
|
|
1783
1912
|
| `storage` | `SDKStorage` | No | `localStorage` | [Session storage](#storages) |
|
|
1784
|
-
| `stateStorage` | `SDKStorage` | No |
|
|
1913
|
+
| `stateStorage` | `SDKStorage` | No | `createSessionStorage()` | OAuth2 PKCE / state storage |
|
|
1785
1914
|
| `storageTokenName` | `string` | No | `'sty.session'` | Key under which the session is stored in the storage |
|
|
1786
|
-
| `autoRefresh` | `boolean` | No | `true` |
|
|
1915
|
+
| `autoRefresh` | `boolean` | No | `true` | Refreshes an expired (or about-to-expire, see `refreshSkew`) access token when authentication is checked (`isAuthenticated`, `checkAuthentication()`, route guards). There is no background timer. Set to `false` to manage refresh manually. Ignored in the browser with `serverSessionUri` - the server refreshes the session instead. On the Server SDK, `false` turns off the refresh in `getSession`. Browser tabs that share the session storage refresh, revoke and log out one at a time (Web Locks API) |
|
|
1916
|
+
| `refreshSkew` | `number` | No | `60` | Number of seconds before the access token's actual expiration that it is treated as expired, so it gets refreshed early (capped at half of the token lifetime, so short-lived tokens are not refreshed on every check) |
|
|
1917
|
+
| `clockSkew` | `number` | No | `60` | Number of seconds of clock difference tolerated between the device and the authorization server when validating the ID token's expiration |
|
|
1787
1918
|
| `lazyLoad` | `boolean` | No | `false` | When `true`, defers initialization until the first method call and returns the instance synchronously |
|
|
1788
|
-
| `serverSessionUri` | `string`
|
|
1789
|
-
| `
|
|
1919
|
+
| `serverSessionUri` | `string \| boolean \| null` | No | `null` | Routes login requests through your server and enables server-managed sessions; `false` / `null` keeps client-managed sessions. The Server SDK defaults it to `` `${authUrlPrefix}/login` ``; see [Server-side session management](#server-side-session-management) |
|
|
1920
|
+
| `authUrlPrefix` | `string` | No | `'/auth'` | URL prefix of the Server SDK auth routes. With server-managed sessions `refresh()` navigates the browser to `${authUrlPrefix}/refresh` with `returnTo` set to the current URL (or `refresh({ returnTo })`), so the server rotates the session cookie and redirects back; see [Refreshing the session](#refreshing-the-session) |
|
|
1790
1921
|
| `logging` | `SDKLogging` | No | - | Logging adapter; see [Logging](#logging) |
|
|
1791
1922
|
| `httpClient` | `SDKHttpClient` | No | fetch | Custom HTTP client adapter; see [HTTP client](#http-client) |
|
|
1792
1923
|
| `discoveryDocumentCacheTTL` | `number` | No | `3600` | OIDC metadata cache duration in seconds (1 hour by default) |
|
|
@@ -1995,7 +2126,7 @@ import { getDefaultFlowState, getSDKOptions } from '@strivacity/sdk-core/utils';
|
|
|
1995
2126
|
|
|
1996
2127
|
export function createCustomFlow(initConfig: SDKInitConfig) {
|
|
1997
2128
|
const state = getDefaultFlowState();
|
|
1998
|
-
const options = getSDKOptions(
|
|
2129
|
+
const options = getSDKOptions(initConfig);
|
|
1999
2130
|
const base = createBaseFlow(state, options);
|
|
2000
2131
|
|
|
2001
2132
|
async function refresh(): Promise<void> {
|
|
@@ -2046,11 +2177,22 @@ v4 moves `startSession()`, `submitForm()`, and `finalizeSession()` directly onto
|
|
|
2046
2177
|
// v4
|
|
2047
2178
|
const state = await sdk.startSession({ sessionId });
|
|
2048
2179
|
const nextState = await sdk.submitForm('formId', { identifier: 'user@example.com' });
|
|
2049
|
-
|
|
2180
|
+
// When nextState.finalizeUrl is set, submitForm() has already finalized the session - no finalizeSession() call needed
|
|
2050
2181
|
```
|
|
2051
2182
|
|
|
2052
2183
|
See [native mode](#client-native-mode) above for the full rendering loop. Update any code that calls `login()`/`register()` and drives the returned handler to call `sdk.startSession()`/`sdk.submitForm()`/`sdk.finalizeSession()` directly instead. Framework-specific wrappers already wrap this for you.
|
|
2053
2184
|
|
|
2185
|
+
#### Other breaking changes
|
|
2186
|
+
|
|
2187
|
+
- **`mode` is required.** v3 defaulted to `redirect` when `mode` was omitted; v4 throws `UnsupportedFlowError` (see [Unsupported flow error](#unsupported-flow-error)).
|
|
2188
|
+
- **`storage`, `logging`, and `httpClient` take plain objects instead of classes.** v3 expected constructors (`storage: LocalStorage`, `logging: MyLogger`) that it instantiated itself; v4 expects instances, e.g. `storage: createLocalStorage()`, `logging: createDefaultLogging()`. The `SDKStorage`/`SDKLogging`/`SDKHttpClient` abstract classes are now plain types, and `SDKStorageType`/`SDKLoggingType`/`SDKHttpClientType` were removed.
|
|
2189
|
+
- **Custom HTTP clients must implement `sendTokenRequest`** in addition to `request`, and the optional logger property was renamed from `logging` to `logger` (see [HTTP client](#http-client)).
|
|
2190
|
+
- **Error classes are no longer exported from the package root.** Import them from `@strivacity/sdk-core/utils` instead of `@strivacity/sdk-core`.
|
|
2191
|
+
- **Events are scoped per SDK instance.** In v3, event subscriptions were shared across all flow instances; in v4 each instance has its own subscribers.
|
|
2192
|
+
- **PKCE / login state is stored in `stateStorage`** (default: `createSessionStorage()`), separately from the session `storage`. v3 stored it in the session storage.
|
|
2193
|
+
- **`waitToInitialize()` was replaced by `init()`.**
|
|
2194
|
+
- **Type renames:** `LoginFlowState` → `NativeFlowState`, `LoginFlowMessage` → `NativeFlowMessage`, `MetadataOptions` → `DiscoveryDocument`.
|
|
2195
|
+
|
|
2054
2196
|
### Migrating to v3.0
|
|
2055
2197
|
|
|
2056
2198
|
##### Entry API Major Changes
|