@strivacity/sdk-core 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/README.md +1873 -340
- package/dist/assets/oidc.cjs +2 -0
- package/dist/assets/oidc.cjs.map +1 -0
- package/dist/assets/oidc.mjs +2 -0
- package/dist/assets/oidc.mjs.map +1 -0
- package/dist/assets/server.cjs +2 -0
- package/dist/assets/server.cjs.map +1 -0
- package/dist/assets/server.mjs +2 -0
- package/dist/assets/server.mjs.map +1 -0
- package/dist/flows/base.cjs +2 -0
- package/dist/flows/base.cjs.map +1 -0
- package/dist/flows/base.d.ts +26 -0
- package/dist/flows/base.mjs +2 -0
- package/dist/flows/base.mjs.map +1 -0
- package/dist/flows/embedded.cjs +2 -0
- package/dist/flows/embedded.cjs.map +1 -0
- package/dist/flows/embedded.d.ts +9 -0
- package/dist/flows/embedded.mjs +2 -0
- package/dist/flows/embedded.mjs.map +1 -0
- package/dist/flows/index.cjs +1 -0
- package/dist/flows/index.d.ts +5 -0
- package/dist/flows/index.mjs +1 -0
- package/dist/flows/native.cjs +2 -0
- package/dist/flows/native.cjs.map +1 -0
- package/dist/flows/native.d.ts +9 -0
- package/dist/flows/native.mjs +2 -0
- package/dist/flows/native.mjs.map +1 -0
- package/dist/flows/popup.cjs +2 -0
- package/dist/flows/popup.cjs.map +1 -0
- package/dist/flows/popup.d.ts +9 -0
- package/dist/flows/popup.mjs +2 -0
- package/dist/flows/popup.mjs.map +1 -0
- package/dist/flows/redirect.cjs +2 -0
- package/dist/flows/redirect.cjs.map +1 -0
- package/dist/flows/redirect.d.ts +9 -0
- package/dist/flows/redirect.mjs +2 -0
- package/dist/flows/redirect.mjs.map +1 -0
- package/dist/handlers/embedded.cjs +2 -0
- package/dist/handlers/embedded.cjs.map +1 -0
- package/dist/handlers/embedded.d.ts +37 -0
- package/dist/handlers/embedded.mjs +2 -0
- package/dist/handlers/embedded.mjs.map +1 -0
- package/dist/handlers/index.cjs +1 -0
- package/dist/handlers/index.d.ts +2 -0
- package/dist/handlers/index.mjs +1 -0
- package/dist/handlers/native.cjs +2 -0
- package/dist/handlers/native.cjs.map +1 -0
- package/dist/handlers/native.d.ts +56 -0
- package/dist/handlers/native.mjs +2 -0
- package/dist/handlers/native.mjs.map +1 -0
- package/dist/handlers/popup.cjs +2 -0
- package/dist/handlers/popup.cjs.map +1 -0
- package/dist/handlers/popup.d.ts +21 -0
- package/dist/handlers/popup.mjs +2 -0
- package/dist/handlers/popup.mjs.map +1 -0
- package/dist/handlers/redirect.cjs +2 -0
- package/dist/handlers/redirect.cjs.map +1 -0
- package/dist/handlers/redirect.d.ts +18 -0
- package/dist/handlers/redirect.mjs +2 -0
- package/dist/handlers/redirect.mjs.map +1 -0
- package/dist/index.cjs +2 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +16 -21
- package/dist/index.mjs +2 -2
- package/dist/index.mjs.map +1 -1
- package/dist/server/base.cjs +2 -0
- package/dist/server/base.cjs.map +1 -0
- package/dist/server/base.d.ts +15 -0
- package/dist/server/base.mjs +2 -0
- package/dist/server/base.mjs.map +1 -0
- package/dist/server/index.cjs +1 -0
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.mjs +1 -0
- package/dist/storages/cache.cjs +2 -0
- package/dist/storages/cache.cjs.map +1 -0
- package/dist/storages/cache.d.ts +11 -0
- package/dist/storages/cache.mjs +2 -0
- package/dist/storages/cache.mjs.map +1 -0
- package/dist/storages/index.cjs +1 -0
- package/dist/storages/index.d.ts +7 -0
- package/dist/storages/index.mjs +1 -0
- package/dist/storages/indexeddb.cjs +2 -0
- package/dist/storages/indexeddb.cjs.map +1 -0
- package/dist/storages/indexeddb.d.ts +10 -0
- package/dist/storages/indexeddb.mjs +2 -0
- package/dist/storages/indexeddb.mjs.map +1 -0
- package/dist/storages/local.cjs +2 -0
- package/dist/storages/local.cjs.map +1 -0
- package/dist/storages/local.d.ts +7 -0
- package/dist/storages/local.mjs +2 -0
- package/dist/storages/local.mjs.map +1 -0
- package/dist/storages/memory.cjs +2 -0
- package/dist/storages/memory.cjs.map +1 -0
- package/dist/storages/memory.d.ts +8 -0
- package/dist/storages/memory.mjs +2 -0
- package/dist/storages/memory.mjs.map +1 -0
- package/dist/storages/server.cjs +1 -0
- package/dist/storages/server.d.ts +39 -0
- package/dist/storages/server.mjs +1 -0
- package/dist/storages/session.cjs +2 -0
- package/dist/storages/session.cjs.map +1 -0
- package/dist/storages/session.d.ts +7 -0
- package/dist/storages/session.mjs +2 -0
- package/dist/storages/session.mjs.map +1 -0
- package/dist/storages/worker.cjs +2 -0
- package/dist/storages/worker.cjs.map +1 -0
- package/dist/storages/worker.d.ts +46 -0
- package/dist/storages/worker.mjs +2 -0
- package/dist/storages/worker.mjs.map +1 -0
- package/dist/types/common.cjs +0 -0
- package/dist/types/common.d.ts +145 -0
- package/dist/types/common.mjs +0 -0
- package/dist/types/embedded.cjs +0 -0
- package/dist/types/embedded.d.ts +103 -0
- package/dist/types/embedded.mjs +0 -0
- package/dist/types/index.cjs +1 -0
- package/dist/types/index.d.ts +7 -0
- package/dist/types/index.mjs +1 -0
- package/dist/types/native.cjs +0 -0
- package/dist/types/native.d.ts +385 -0
- package/dist/types/native.mjs +0 -0
- package/dist/types/oidc.cjs +2 -0
- package/dist/types/oidc.cjs.map +1 -0
- package/dist/types/oidc.d.ts +768 -0
- package/dist/types/oidc.mjs +2 -0
- package/dist/types/oidc.mjs.map +1 -0
- package/dist/types/popup.cjs +0 -0
- package/dist/types/popup.d.ts +69 -0
- package/dist/types/popup.mjs +0 -0
- package/dist/types/redirect.cjs +0 -0
- package/dist/types/redirect.d.ts +21 -0
- package/dist/types/redirect.mjs +0 -0
- package/dist/types/server.cjs +0 -0
- package/dist/types/server.d.ts +200 -0
- package/dist/types/server.mjs +0 -0
- package/dist/utils/base64url.cjs +2 -0
- package/dist/utils/base64url.cjs.map +1 -0
- package/dist/utils/base64url.d.ts +14 -0
- package/dist/utils/base64url.mjs +2 -0
- package/dist/utils/base64url.mjs.map +1 -0
- package/dist/utils/common.cjs +2 -0
- package/dist/utils/common.cjs.map +1 -0
- package/dist/utils/common.d.ts +23 -0
- package/dist/utils/common.mjs +2 -0
- package/dist/utils/common.mjs.map +1 -0
- package/dist/utils/credentials.cjs +2 -2
- package/dist/utils/credentials.cjs.map +1 -1
- package/dist/utils/credentials.d.ts +19 -3
- package/dist/utils/credentials.mjs +2 -2
- package/dist/utils/credentials.mjs.map +1 -1
- package/dist/utils/crypto.cjs +2 -2
- package/dist/utils/crypto.cjs.map +1 -1
- package/dist/utils/crypto.d.ts +16 -26
- package/dist/utils/crypto.mjs +2 -2
- package/dist/utils/crypto.mjs.map +1 -1
- package/dist/utils/dom.cjs +2 -0
- package/dist/utils/dom.cjs.map +1 -0
- package/dist/utils/dom.d.ts +7 -0
- package/dist/utils/dom.mjs +2 -0
- package/dist/utils/dom.mjs.map +1 -0
- package/dist/utils/errors.cjs +2 -2
- package/dist/utils/errors.cjs.map +1 -1
- package/dist/utils/errors.d.ts +62 -1
- package/dist/utils/errors.mjs +2 -2
- package/dist/utils/errors.mjs.map +1 -1
- package/dist/utils/httpClient.cjs +2 -0
- package/dist/utils/httpClient.cjs.map +1 -0
- package/dist/utils/httpClient.d.ts +2 -0
- package/dist/utils/httpClient.mjs +2 -0
- package/dist/utils/httpClient.mjs.map +1 -0
- package/dist/utils/index.cjs +1 -0
- package/dist/utils/index.d.ts +12 -0
- package/dist/utils/index.mjs +1 -0
- package/dist/utils/logging.cjs +2 -0
- package/dist/utils/logging.cjs.map +1 -0
- package/dist/utils/logging.d.ts +2 -0
- package/dist/utils/logging.mjs +2 -0
- package/dist/utils/logging.mjs.map +1 -0
- package/dist/utils/oidc.cjs +1 -0
- package/dist/utils/oidc.d.ts +264 -0
- package/dist/utils/oidc.mjs +1 -0
- package/dist/utils/server.cjs +1 -0
- package/dist/utils/server.d.ts +72 -0
- package/dist/utils/server.mjs +1 -0
- package/dist/utils/session.cjs +2 -0
- package/dist/utils/session.cjs.map +1 -0
- package/dist/utils/session.d.ts +29 -0
- package/dist/utils/session.mjs +2 -0
- package/dist/utils/session.mjs.map +1 -0
- package/dist/utils/state.cjs +1 -0
- package/dist/utils/state.d.ts +23 -0
- package/dist/utils/state.mjs +1 -0
- package/package.json +53 -1
- package/testing/tests/flows/base.spec.ts +1030 -0
- package/testing/tests/flows/embedded.spec.ts +443 -0
- package/testing/tests/flows/index.spec.ts +8 -0
- package/testing/tests/flows/native.spec.ts +487 -0
- package/testing/tests/flows/popup.spec.ts +400 -0
- package/testing/tests/flows/redirect.spec.ts +369 -0
- package/testing/tests/handlers/embedded.spec.ts +199 -0
- package/testing/tests/handlers/index.spec.ts +8 -0
- package/testing/tests/handlers/native.spec.ts +283 -0
- package/testing/tests/handlers/popup.spec.ts +206 -0
- package/testing/tests/handlers/redirect.spec.ts +70 -0
- package/testing/tests/index.spec.ts +78 -0
- package/testing/tests/server/base.spec.ts +846 -0
- package/testing/tests/server/index.spec.ts +15 -0
- package/testing/tests/storages/cache.spec.ts +107 -0
- package/testing/tests/storages/index.spec.ts +22 -0
- package/testing/tests/storages/indexeddb.spec.ts +113 -0
- package/testing/tests/storages/local.spec.ts +46 -0
- package/testing/tests/storages/memory.spec.ts +51 -0
- package/testing/tests/storages/server.spec.ts +435 -0
- package/testing/tests/storages/session.spec.ts +46 -0
- package/testing/tests/storages/worker.spec.ts +106 -0
- package/testing/tests/utils/common.spec.ts +77 -0
- package/testing/tests/utils/credentials.spec.ts +195 -0
- package/testing/tests/utils/dom.spec.ts +37 -0
- package/testing/tests/utils/index.spec.ts +71 -0
- package/testing/tests/utils/logging.spec.ts +82 -0
- package/testing/tests/utils/oidc.spec.ts +744 -0
- package/testing/tests/utils/server.spec.ts +235 -0
- package/dist/flows/BaseFlow.cjs +0 -2
- package/dist/flows/BaseFlow.cjs.map +0 -1
- package/dist/flows/BaseFlow.d.ts +0 -191
- package/dist/flows/BaseFlow.mjs +0 -2
- package/dist/flows/BaseFlow.mjs.map +0 -1
- package/dist/flows/EmbeddedFlow.cjs +0 -2
- package/dist/flows/EmbeddedFlow.cjs.map +0 -1
- package/dist/flows/EmbeddedFlow.d.ts +0 -26
- package/dist/flows/EmbeddedFlow.mjs +0 -2
- package/dist/flows/EmbeddedFlow.mjs.map +0 -1
- package/dist/flows/NativeFlow.cjs +0 -2
- package/dist/flows/NativeFlow.cjs.map +0 -1
- package/dist/flows/NativeFlow.d.ts +0 -34
- package/dist/flows/NativeFlow.mjs +0 -2
- package/dist/flows/NativeFlow.mjs.map +0 -1
- package/dist/flows/PopupFlow.cjs +0 -2
- package/dist/flows/PopupFlow.cjs.map +0 -1
- package/dist/flows/PopupFlow.d.ts +0 -36
- package/dist/flows/PopupFlow.mjs +0 -2
- package/dist/flows/PopupFlow.mjs.map +0 -1
- package/dist/flows/RedirectFlow.cjs +0 -2
- package/dist/flows/RedirectFlow.cjs.map +0 -1
- package/dist/flows/RedirectFlow.d.ts +0 -38
- package/dist/flows/RedirectFlow.mjs +0 -2
- package/dist/flows/RedirectFlow.mjs.map +0 -1
- package/dist/handlers/BaseFlowHandler.cjs +0 -2
- package/dist/handlers/BaseFlowHandler.cjs.map +0 -1
- package/dist/handlers/BaseFlowHandler.d.ts +0 -61
- package/dist/handlers/BaseFlowHandler.mjs +0 -2
- package/dist/handlers/BaseFlowHandler.mjs.map +0 -1
- package/dist/handlers/EmbeddedFlowHandler.cjs +0 -2
- package/dist/handlers/EmbeddedFlowHandler.cjs.map +0 -1
- package/dist/handlers/EmbeddedFlowHandler.d.ts +0 -28
- package/dist/handlers/EmbeddedFlowHandler.mjs +0 -2
- package/dist/handlers/EmbeddedFlowHandler.mjs.map +0 -1
- package/dist/handlers/NativeFlowHandler.cjs +0 -2
- package/dist/handlers/NativeFlowHandler.cjs.map +0 -1
- package/dist/handlers/NativeFlowHandler.d.ts +0 -31
- package/dist/handlers/NativeFlowHandler.mjs +0 -2
- package/dist/handlers/NativeFlowHandler.mjs.map +0 -1
- package/dist/storages/LocalStorage.cjs +0 -2
- package/dist/storages/LocalStorage.cjs.map +0 -1
- package/dist/storages/LocalStorage.d.ts +0 -23
- package/dist/storages/LocalStorage.mjs +0 -2
- package/dist/storages/LocalStorage.mjs.map +0 -1
- package/dist/storages/SessionStorage.cjs +0 -2
- package/dist/storages/SessionStorage.cjs.map +0 -1
- package/dist/storages/SessionStorage.d.ts +0 -23
- package/dist/storages/SessionStorage.mjs +0 -2
- package/dist/storages/SessionStorage.mjs.map +0 -1
- package/dist/types.cjs +0 -2
- package/dist/types.cjs.map +0 -1
- package/dist/types.d.ts +0 -1263
- package/dist/types.mjs +0 -2
- package/dist/types.mjs.map +0 -1
- package/dist/utils/HttpClient.cjs +0 -2
- package/dist/utils/HttpClient.cjs.map +0 -1
- package/dist/utils/HttpClient.d.ts +0 -4
- package/dist/utils/HttpClient.mjs +0 -2
- package/dist/utils/HttpClient.mjs.map +0 -1
- package/dist/utils/Logging.cjs +0 -2
- package/dist/utils/Logging.cjs.map +0 -1
- package/dist/utils/Logging.d.ts +0 -8
- package/dist/utils/Logging.mjs +0 -2
- package/dist/utils/Logging.mjs.map +0 -1
- package/dist/utils/Metadata.cjs +0 -2
- package/dist/utils/Metadata.cjs.map +0 -1
- package/dist/utils/Metadata.d.ts +0 -98
- package/dist/utils/Metadata.mjs +0 -2
- package/dist/utils/Metadata.mjs.map +0 -1
- package/dist/utils/Session.cjs +0 -2
- package/dist/utils/Session.cjs.map +0 -1
- package/dist/utils/Session.d.ts +0 -82
- package/dist/utils/Session.mjs +0 -2
- package/dist/utils/Session.mjs.map +0 -1
- package/dist/utils/State.cjs +0 -2
- package/dist/utils/State.cjs.map +0 -1
- package/dist/utils/State.d.ts +0 -43
- package/dist/utils/State.mjs +0 -2
- package/dist/utils/State.mjs.map +0 -1
- package/dist/utils/base64Url.cjs +0 -2
- package/dist/utils/base64Url.cjs.map +0 -1
- package/dist/utils/base64Url.d.ts +0 -27
- package/dist/utils/base64Url.mjs +0 -2
- package/dist/utils/base64Url.mjs.map +0 -1
- package/dist/utils/date.cjs +0 -2
- package/dist/utils/date.cjs.map +0 -1
- package/dist/utils/date.d.ts +0 -6
- package/dist/utils/date.mjs +0 -2
- package/dist/utils/date.mjs.map +0 -1
- package/dist/utils/handlers.cjs +0 -2
- package/dist/utils/handlers.cjs.map +0 -1
- package/dist/utils/handlers.d.ts +0 -35
- package/dist/utils/handlers.mjs +0 -2
- package/dist/utils/handlers.mjs.map +0 -1
- package/dist/utils/jwt.cjs +0 -2
- package/dist/utils/jwt.cjs.map +0 -1
- package/dist/utils/jwt.d.ts +0 -34
- package/dist/utils/jwt.mjs +0 -2
- package/dist/utils/jwt.mjs.map +0 -1
- package/dist/utils/object.cjs +0 -2
- package/dist/utils/object.cjs.map +0 -1
- package/dist/utils/object.d.ts +0 -1
- package/dist/utils/object.mjs +0 -2
- package/dist/utils/object.mjs.map +0 -1
package/README.md
CHANGED
|
@@ -1,538 +1,2071 @@
|
|
|
1
1
|
# @strivacity/sdk-core
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Framework-agnostic JavaScript/TypeScript SDK for [Strivacity](https://www.strivacity.com) - adds PKCE-protected OIDC authentication to any web application with no framework dependencies.
|
|
4
|
+
|
|
5
|
+
Use it directly in vanilla JS/TS projects or use one of the [framework-specific wrappers](https://docs.strivacity.com/reference/javascript-sdks).
|
|
6
|
+
|
|
7
|
+
**See also:**
|
|
8
|
+
- [Full Documentation](https://docs.strivacity.com/reference/overview) - Complete guide for all authentication modes
|
|
9
|
+
- [Example Apps](../../apps/) - Working examples for different frameworks
|
|
10
|
+
|
|
11
|
+
## Table of contents
|
|
12
|
+
|
|
13
|
+
- [Prerequisites](#prerequisites)
|
|
14
|
+
- [Installation](#installation)
|
|
15
|
+
- [Choosing a mode](#choosing-a-mode)
|
|
16
|
+
- [Client SDK](#client-sdk)
|
|
17
|
+
- [redirect mode](#client-redirect-mode)
|
|
18
|
+
- [popup mode](#client-popup-mode)
|
|
19
|
+
- [embedded mode](#client-embedded-mode)
|
|
20
|
+
- [native mode](#client-native-mode)
|
|
21
|
+
- [Server-side session management](#server-side-session-management)
|
|
22
|
+
- [Server SDK](#server-sdk)
|
|
23
|
+
- [Usage](#server-usage)
|
|
24
|
+
- [Storages](#server-storages)
|
|
25
|
+
- [Back-channel logout](#server-backchannel-logout)
|
|
26
|
+
- [Configuration Options](#server-configuration-options)
|
|
27
|
+
- [Shared features](#shared-features)
|
|
28
|
+
- [Storages](#storages)
|
|
29
|
+
- [SDK events](#sdk-events)
|
|
30
|
+
- [Logging](#logging)
|
|
31
|
+
- [HTTP client](#http-client)
|
|
32
|
+
- [Session state](#session-state)
|
|
33
|
+
- [Advanced](#advanced)
|
|
34
|
+
- [Custom flow](#custom-flow)
|
|
35
|
+
- [Error handling](#error-handling)
|
|
36
|
+
- [WebAuthn support](#webauthn-support)
|
|
37
|
+
- [Utility functions](#utility-functions)
|
|
38
|
+
- [Configuration reference](#configuration-reference)
|
|
39
|
+
- [Configuration examples](#configuration-examples)
|
|
40
|
+
- [Caching](#caching)
|
|
41
|
+
- [Migration guide](#migration-guide)
|
|
42
|
+
- [Vulnerability Reporting](#vulnerability-reporting)
|
|
43
|
+
- [License](#license)
|
|
44
|
+
- [Contributing](#contributing)
|
|
4
45
|
|
|
5
|
-
See our [Developer Portal](https://www.strivacity.com/learn-support/developer-hub) to get started with developing with the Strivacity product.
|
|
6
46
|
|
|
7
|
-
|
|
47
|
+
---
|
|
8
48
|
|
|
9
|
-
|
|
49
|
+
## Prerequisites
|
|
10
50
|
|
|
11
|
-
|
|
51
|
+
- A Strivacity tenant with an application configured (issuer URL, client ID, redirect URI)
|
|
12
52
|
|
|
13
|
-
|
|
53
|
+
---
|
|
14
54
|
|
|
15
|
-
##
|
|
55
|
+
## Installation
|
|
16
56
|
|
|
17
57
|
```bash
|
|
18
58
|
npm install @strivacity/sdk-core
|
|
19
59
|
```
|
|
20
60
|
|
|
21
|
-
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## Choosing a mode
|
|
64
|
+
|
|
65
|
+
The SDK can be used in **two environments**:
|
|
66
|
+
- **[Client SDK](#client-sdk)** - runs in the browser, handles authentication flow directly
|
|
67
|
+
- **[Server SDK](#server-sdk)** - runs on your backend, proxies authentication requests to the Strivacity IDP, and manages session state server-side
|
|
68
|
+
|
|
69
|
+
Both support the same **four authentication modes**:
|
|
70
|
+
|
|
71
|
+
| Mode | Login UI | Best for |
|
|
72
|
+
| ---------- | ---------------------------------------- | -------------------------------------------- |
|
|
73
|
+
| `redirect` | Strivacity hosted page | Standard web apps |
|
|
74
|
+
| `popup` | Strivacity hosted page in a popup | Web apps that must stay on the current page |
|
|
75
|
+
| `embedded` | Strivacity web components in your page | Branded login inside your own layout |
|
|
76
|
+
| `native` | Your own components driven by flow state | Full UI control, step-by-step form rendering |
|
|
77
|
+
|
|
78
|
+
> 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.
|
|
22
79
|
|
|
23
|
-
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## Client SDK
|
|
83
|
+
|
|
84
|
+
The client SDK (`@strivacity/sdk-core`) runs directly in the browser and handles authentication flows, token management, and session storage client-side.
|
|
85
|
+
|
|
86
|
+
<a id="client-redirect-mode"></a>
|
|
87
|
+
### redirect mode
|
|
24
88
|
|
|
25
|
-
|
|
89
|
+
> For details on how this mode works, see the [hosted journey documentation](https://docs.strivacity.com/reference/hosted-journey).
|
|
26
90
|
|
|
27
|
-
|
|
91
|
+
The current browser tab navigates to the Strivacity-hosted login page and back to the configured `redirectUri` after authentication.
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
// sdk.ts
|
|
28
95
|
import { initFlow } from '@strivacity/sdk-core';
|
|
29
96
|
|
|
97
|
+
// Default - session loading starts in the background immediately
|
|
30
98
|
const sdk = initFlow({
|
|
31
|
-
mode: 'redirect', //
|
|
32
|
-
issuer: 'https://<
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
99
|
+
mode: 'redirect', // authentication mode
|
|
100
|
+
issuer: 'https://<YOUR_TENANT_DOMAIN>', // OIDC provider URL
|
|
101
|
+
clientId: 'YOUR_CLIENT_ID', // OAuth2 client ID
|
|
102
|
+
redirectUri: 'https://your-app.example.com/callback', // callback URL after authentication
|
|
103
|
+
scopes: ['openid', 'profile', 'email'], // requested user permissions/data
|
|
36
104
|
});
|
|
37
105
|
```
|
|
38
106
|
|
|
39
|
-
### Redirect / Popup mode
|
|
40
|
-
|
|
41
|
-
In `redirect` mode the user is taken to the identity provider in the same window; in `popup` mode authentication happens in a popup. Both are initiated the same way from code.
|
|
42
|
-
|
|
43
107
|
#### Login
|
|
44
108
|
|
|
45
|
-
|
|
46
|
-
|
|
109
|
+
Call this to start the login flow. It redirects the user to the Strivacity login page in current browser tab, where they authenticate.
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
// login.ts
|
|
113
|
+
import { sdk } from './sdk';
|
|
114
|
+
|
|
115
|
+
await sdk.login({
|
|
116
|
+
// Optional parameters
|
|
117
|
+
loginHint: 'user@example.com', // identifier or JWT-encoded data to hint the login flow
|
|
118
|
+
acrValues: ['urn:strivacity:loa:2'], // request specific authentication context
|
|
119
|
+
audiences: ['https://api.example.com'], // target resources for the access token
|
|
120
|
+
locationMethod: 'assign', // window.location method: 'assign' adds to browser history, 'replace' doesn't (default: 'assign')
|
|
121
|
+
targetWindow: 'self', // 'self' redirects current window, 'top' redirects top-level window (default: 'self')
|
|
122
|
+
});
|
|
47
123
|
```
|
|
48
124
|
|
|
49
|
-
####
|
|
125
|
+
#### Handle the callback
|
|
126
|
+
|
|
127
|
+
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.
|
|
50
128
|
|
|
51
|
-
|
|
129
|
+
```ts
|
|
130
|
+
// callback.ts
|
|
131
|
+
import { sdk } from './sdk';
|
|
52
132
|
|
|
53
|
-
```js
|
|
54
133
|
await sdk.handleCallback();
|
|
134
|
+
window.location.href = '/profile';
|
|
55
135
|
```
|
|
56
136
|
|
|
57
|
-
|
|
137
|
+
> 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)`.
|
|
58
138
|
|
|
59
|
-
|
|
139
|
+
#### Registration
|
|
60
140
|
|
|
61
|
-
|
|
62
|
-
|
|
141
|
+
Call this to start the registration flow. It works the same way as `login()` but opens the registration form instead.
|
|
142
|
+
|
|
143
|
+
```ts
|
|
144
|
+
// register.ts
|
|
145
|
+
import { sdk } from './sdk';
|
|
146
|
+
|
|
147
|
+
await sdk.register({
|
|
148
|
+
// Optional parameters
|
|
149
|
+
loginHint: 'user@example.com', // identifier or JWT-encoded data to hint the login flow
|
|
150
|
+
acrValues: ['urn:strivacity:loa:2'], // request specific authentication context
|
|
151
|
+
audiences: ['https://api.example.com'], // target resources for the access token
|
|
152
|
+
locationMethod: 'assign', // window.location method: 'assign' adds to browser history, 'replace' doesn't (default: 'assign')
|
|
153
|
+
targetWindow: 'self', // 'self' redirects current window, 'top' redirects top-level window (default: 'self')
|
|
154
|
+
});
|
|
63
155
|
```
|
|
64
156
|
|
|
65
|
-
####
|
|
157
|
+
#### Logout
|
|
158
|
+
|
|
159
|
+
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`.
|
|
66
160
|
|
|
67
|
-
|
|
161
|
+
```ts
|
|
162
|
+
// logout.ts
|
|
163
|
+
import { sdk } from './sdk';
|
|
68
164
|
|
|
69
|
-
|
|
70
|
-
|
|
165
|
+
await sdk.logout({
|
|
166
|
+
postLogoutRedirectUri: window.location.origin,
|
|
167
|
+
});
|
|
71
168
|
```
|
|
72
169
|
|
|
73
|
-
|
|
170
|
+
> 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.
|
|
74
171
|
|
|
75
|
-
|
|
172
|
+
#### Token management
|
|
76
173
|
|
|
77
|
-
|
|
174
|
+
Call these methods to manage the session and access token.
|
|
175
|
+
|
|
176
|
+
```ts
|
|
177
|
+
import { sdk } from './sdk';
|
|
178
|
+
|
|
179
|
+
// Refresh the access token using the refresh token
|
|
180
|
+
// (automatically waits for SDK initialization and session loading)
|
|
181
|
+
await sdk.refresh();
|
|
182
|
+
|
|
183
|
+
// Revoke all tokens at the authorization server and clear the local session
|
|
184
|
+
// (automatically waits for SDK initialization and session loading)
|
|
78
185
|
await sdk.revoke();
|
|
186
|
+
|
|
187
|
+
// Get the current access and refresh tokens (synchronous property - ensure SDK is initialized first)
|
|
188
|
+
await sdk.init();
|
|
189
|
+
const idToken = sdk.idTokenClaims // read-only property - returns the current ID token claims or null if not authenticated
|
|
190
|
+
const accessToken = sdk.accessToken; // read-only property - returns the current access token or null if not authenticated
|
|
191
|
+
const refreshToken = sdk.refreshToken; // read-only property - returns the current refresh token or null if not authenticated
|
|
79
192
|
```
|
|
80
193
|
|
|
81
|
-
|
|
194
|
+
---
|
|
82
195
|
|
|
83
|
-
|
|
196
|
+
<a id="client-popup-mode"></a>
|
|
197
|
+
### popup mode
|
|
84
198
|
|
|
85
|
-
|
|
199
|
+
> For details on how this mode works, see the [hosted journey documentation](https://docs.strivacity.com/reference/hosted-journey).
|
|
86
200
|
|
|
87
|
-
The login page
|
|
201
|
+
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.
|
|
88
202
|
|
|
89
|
-
```
|
|
203
|
+
```ts
|
|
204
|
+
// sdk.ts
|
|
90
205
|
import { initFlow } from '@strivacity/sdk-core';
|
|
91
206
|
|
|
207
|
+
// Default - session loading starts in the background immediately
|
|
92
208
|
const sdk = initFlow({
|
|
93
|
-
mode: '
|
|
94
|
-
issuer: 'https://<
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
209
|
+
mode: 'popup', // authentication mode
|
|
210
|
+
issuer: 'https://<YOUR_TENANT_DOMAIN>', // OIDC provider URL
|
|
211
|
+
clientId: 'YOUR_CLIENT_ID', // OAuth2 client ID
|
|
212
|
+
redirectUri: 'https://your-app.example.com/callback', // callback URL after authentication
|
|
213
|
+
scopes: ['openid', 'profile', 'email'], // requested user permissions/data
|
|
98
214
|
});
|
|
215
|
+
```
|
|
99
216
|
|
|
100
|
-
|
|
101
|
-
const handler = await sdk.login();
|
|
217
|
+
#### Login
|
|
102
218
|
|
|
103
|
-
|
|
104
|
-
const url = new URL(window.location.href);
|
|
105
|
-
const sessionId = url.searchParams.get('session_id');
|
|
219
|
+
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).
|
|
106
220
|
|
|
107
|
-
|
|
108
|
-
const state = await handler.startSession(sessionId);
|
|
221
|
+
By default a centered popup window opens. Pass `popupWindowTarget` to change where the window opens, and `popupWindowFeatures` to control its size and position:
|
|
109
222
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
223
|
+
```ts
|
|
224
|
+
// login.ts
|
|
225
|
+
import { sdk } from './sdk';
|
|
226
|
+
|
|
227
|
+
await sdk.login({
|
|
228
|
+
// Optional parameters
|
|
229
|
+
loginHint: 'user@example.com', // identifier or JWT-encoded data to hint the login flow
|
|
230
|
+
acrValues: ['urn:strivacity:loa:2'], // request specific authentication context
|
|
231
|
+
audiences: ['https://api.example.com'], // target resources for the access token
|
|
232
|
+
popupWindowTarget: '_blank', // any valid browsing context name
|
|
233
|
+
popupWindowFeatures: {
|
|
234
|
+
width: 500,
|
|
235
|
+
height: 700,
|
|
236
|
+
left: 100,
|
|
237
|
+
top: 100,
|
|
238
|
+
toolbar: false,
|
|
239
|
+
location: false,
|
|
240
|
+
resizable: true,
|
|
241
|
+
scrollbars: true,
|
|
242
|
+
},
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
if (await sdk.isAuthenticated) {
|
|
246
|
+
// User authenticated - navigate to a protected page
|
|
247
|
+
window.location.href = '/profile';
|
|
116
248
|
}
|
|
117
249
|
```
|
|
118
250
|
|
|
119
|
-
####
|
|
251
|
+
#### Handle the callback
|
|
252
|
+
|
|
253
|
+
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.
|
|
254
|
+
|
|
255
|
+
#### Registration
|
|
120
256
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
257
|
+
Call this to start the registration flow. It works the same way as `login()` but opens the registration form instead.
|
|
258
|
+
|
|
259
|
+
```ts
|
|
260
|
+
// register.ts
|
|
261
|
+
import { sdk } from './sdk';
|
|
262
|
+
|
|
263
|
+
await sdk.register({
|
|
264
|
+
// Optional parameters
|
|
265
|
+
loginHint: 'user@example.com', // identifier or JWT-encoded data to hint the login flow
|
|
266
|
+
acrValues: ['urn:strivacity:loa:2'], // request specific authentication context
|
|
267
|
+
audiences: ['https://api.example.com'], // target resources for the access token
|
|
268
|
+
popupWindowTarget: '_blank', // any valid browsing context name
|
|
269
|
+
popupWindowFeatures: {
|
|
270
|
+
width: 500,
|
|
271
|
+
height: 700,
|
|
272
|
+
left: 100,
|
|
273
|
+
top: 100,
|
|
274
|
+
toolbar: false,
|
|
275
|
+
location: false,
|
|
276
|
+
resizable: true,
|
|
277
|
+
scrollbars: true,
|
|
278
|
+
},
|
|
125
279
|
});
|
|
126
280
|
|
|
127
|
-
if (
|
|
128
|
-
|
|
281
|
+
if (await sdk.isAuthenticated) {
|
|
282
|
+
// User authenticated - navigate to a protected page
|
|
283
|
+
window.location.href = '/profile';
|
|
129
284
|
}
|
|
130
285
|
```
|
|
131
286
|
|
|
132
|
-
####
|
|
287
|
+
#### Logout
|
|
133
288
|
|
|
134
|
-
|
|
289
|
+
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`.
|
|
135
290
|
|
|
136
|
-
```
|
|
137
|
-
|
|
138
|
-
|
|
291
|
+
```ts
|
|
292
|
+
// logout.ts
|
|
293
|
+
import { sdk } from './sdk';
|
|
139
294
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
} else {
|
|
144
|
-
try {
|
|
145
|
-
await sdk.handleCallback();
|
|
146
|
-
window.location.href = '/profile';
|
|
147
|
-
} catch (error) {
|
|
148
|
-
console.error('Error during callback handling:', error);
|
|
149
|
-
}
|
|
150
|
-
}
|
|
295
|
+
await sdk.logout({
|
|
296
|
+
postLogoutRedirectUri: window.location.origin,
|
|
297
|
+
});
|
|
151
298
|
```
|
|
152
299
|
|
|
153
|
-
|
|
300
|
+
> 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.
|
|
154
301
|
|
|
155
|
-
|
|
302
|
+
#### Token management
|
|
156
303
|
|
|
157
|
-
|
|
158
|
-
try {
|
|
159
|
-
const data = await sdk.entry();
|
|
304
|
+
Call these methods to manage the session and access token.
|
|
160
305
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
306
|
+
```ts
|
|
307
|
+
import { sdk } from './sdk';
|
|
308
|
+
|
|
309
|
+
// Refresh the access token using the refresh token
|
|
310
|
+
// (automatically waits for SDK initialization and session loading)
|
|
311
|
+
await sdk.refresh();
|
|
312
|
+
|
|
313
|
+
// Revoke all tokens at the authorization server and clear the local session
|
|
314
|
+
// (automatically waits for SDK initialization and session loading)
|
|
315
|
+
await sdk.revoke();
|
|
316
|
+
|
|
317
|
+
// Get the current access and refresh tokens (synchronous property - ensure SDK is initialized first)
|
|
318
|
+
await sdk.init();
|
|
319
|
+
const idToken = sdk.idTokenClaims // read-only property - returns the current ID token claims or null if not authenticated
|
|
320
|
+
const accessToken = sdk.accessToken; // read-only property - returns the current access token or null if not authenticated
|
|
321
|
+
const refreshToken = sdk.refreshToken; // read-only property - returns the current refresh token or null if not authenticated
|
|
170
322
|
```
|
|
171
323
|
|
|
172
|
-
|
|
324
|
+
---
|
|
325
|
+
|
|
326
|
+
<a id="client-embedded-mode"></a>
|
|
327
|
+
### embedded mode
|
|
173
328
|
|
|
174
|
-
|
|
329
|
+
> For details on how this mode works, see the [embedded journey documentation](https://docs.strivacity.com/reference/embedded-journey).
|
|
175
330
|
|
|
176
|
-
|
|
331
|
+
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:
|
|
177
332
|
|
|
178
333
|
```ts
|
|
179
334
|
import { initFlow } from '@strivacity/sdk-core';
|
|
180
|
-
import {
|
|
335
|
+
import { injectScript } from '@strivacity/sdk-core/utils';
|
|
336
|
+
|
|
337
|
+
// Load web components bundle
|
|
338
|
+
injectScript('sty-components', 'https://<YOUR_TENANT_DOMAIN>/assets/components/bundle.js');
|
|
181
339
|
|
|
182
340
|
const sdk = initFlow({
|
|
183
|
-
mode: '
|
|
184
|
-
issuer: 'https://<
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
customFlow: CustomNativeFlow,
|
|
341
|
+
mode: 'embedded', // 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
|
|
189
346
|
});
|
|
190
347
|
```
|
|
191
348
|
|
|
192
|
-
####
|
|
349
|
+
#### Login / Register
|
|
193
350
|
|
|
194
|
-
|
|
351
|
+
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):
|
|
195
352
|
|
|
196
353
|
```ts
|
|
197
|
-
|
|
198
|
-
import {
|
|
199
|
-
import { Session } from '@strivacity/sdk-core/utils/Session';
|
|
200
|
-
import { CustomNativeFlowHandler } from './CustomNativeFlowHandler';
|
|
354
|
+
// login.ts
|
|
355
|
+
import { sdk } from './sdk';
|
|
201
356
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
357
|
+
// Mount your web components - they drive themselves from here
|
|
358
|
+
// <sty-notifications></sty-notifications>
|
|
359
|
+
// <sty-login></sty-login>
|
|
360
|
+
// <sty-language-selector></sty-language-selector>
|
|
361
|
+
```
|
|
207
362
|
|
|
208
|
-
|
|
209
|
-
params.prompt = 'create';
|
|
210
|
-
return this.login(params);
|
|
211
|
-
}
|
|
363
|
+
##### Controlling when the flow starts
|
|
212
364
|
|
|
213
|
-
|
|
214
|
-
const response = await this.httpClient.request(new URL('/api/session/info', location.origin).toString(), { method: 'GET', credentials: 'include' });
|
|
365
|
+
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 the component mounts:
|
|
215
366
|
|
|
216
|
-
|
|
367
|
+
```ts
|
|
368
|
+
// Mount your web component with lazy attribute
|
|
369
|
+
// <sty-login id="login" lazy></sty-login>
|
|
217
370
|
|
|
218
|
-
|
|
219
|
-
this.dispatchEvent('loggedIn', [{ claims: this.idTokenClaims! }]);
|
|
220
|
-
}
|
|
221
|
-
}
|
|
371
|
+
const login = document.querySelector('sty-login[lazy]');
|
|
222
372
|
|
|
223
|
-
|
|
224
|
-
|
|
373
|
+
// Option 1: Start without params (basic usage)
|
|
374
|
+
await login.start();
|
|
225
375
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
376
|
+
// Option 2: Pass params to start()
|
|
377
|
+
await login.start({
|
|
378
|
+
loginHint: 'user@example.com', // identifier or JWT-encoded data to hint the login flow
|
|
379
|
+
acrValues: ['urn:strivacity:loa:2'], // request specific authentication context
|
|
380
|
+
audiences: ['https://api.example.com'], // target resources for the access token
|
|
381
|
+
language: 'en-US', // set the UI language (BCP 47 language tag)
|
|
382
|
+
prompt: 'login', // use 'create' to open the registration flow instead
|
|
383
|
+
});
|
|
231
384
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
385
|
+
// Option 3: Set params property before calling start()
|
|
386
|
+
login.params = {
|
|
387
|
+
loginHint: 'user@example.com', // identifier or JWT-encoded data to hint the login flow
|
|
388
|
+
acrValues: ['urn:strivacity:loa:2'], // request specific authentication context
|
|
389
|
+
audiences: ['https://api.example.com'], // target resources for the access token
|
|
390
|
+
language: 'en-US', // set the UI language (BCP 47 language tag)
|
|
391
|
+
prompt: 'login', // use 'create' to open the registration flow instead
|
|
392
|
+
};
|
|
393
|
+
await login.start();
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
The web components communicate via custom events:
|
|
397
|
+
|
|
398
|
+
**Login events:**
|
|
399
|
+
|
|
400
|
+
The `<sty-login>` element dispatches `login`, `close`, and `error` custom events. Listen to them and react accordingly:
|
|
401
|
+
|
|
402
|
+
```ts
|
|
403
|
+
const login = document.querySelector('sty-login');
|
|
404
|
+
|
|
405
|
+
login.addEventListener('login', () => {
|
|
406
|
+
// User authenticated - navigate to a protected page
|
|
407
|
+
window.location.href = '/profile';
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
login.addEventListener('close', () => {
|
|
411
|
+
// User cancelled or closed the login flow
|
|
412
|
+
location.reload();
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
login.addEventListener('error', (event) => {
|
|
416
|
+
// A fatal error occurred - the message is available in event.detail
|
|
417
|
+
console.error(event.detail);
|
|
418
|
+
});
|
|
419
|
+
```
|
|
235
420
|
|
|
236
|
-
|
|
237
|
-
this.dispatchEvent('logoutInitiated', []);
|
|
421
|
+
**Notification events:**
|
|
238
422
|
|
|
239
|
-
|
|
423
|
+
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:
|
|
240
424
|
|
|
241
|
-
|
|
425
|
+
```ts
|
|
426
|
+
function onNotification(event: CustomEvent) {
|
|
427
|
+
if (event.detail.action === 'show') {
|
|
428
|
+
// Add new notification to your custom notification system
|
|
429
|
+
const notification = event.detail.notification;
|
|
430
|
+
console.log('New notification:', notification);
|
|
431
|
+
// Handle the notification display in your own UI
|
|
432
|
+
} else if (event.detail.action === 'clear') {
|
|
433
|
+
// Clear all notifications
|
|
434
|
+
console.log('Clear all notifications');
|
|
242
435
|
}
|
|
243
436
|
}
|
|
437
|
+
|
|
438
|
+
document.addEventListener('notification', onNotification);
|
|
439
|
+
|
|
440
|
+
// Don't forget to clean up when unmounting
|
|
441
|
+
document.removeEventListener('notification', onNotification);
|
|
244
442
|
```
|
|
245
443
|
|
|
246
|
-
|
|
444
|
+
**Dynamic language switching:**
|
|
247
445
|
|
|
248
|
-
|
|
446
|
+
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:
|
|
249
447
|
|
|
250
448
|
```ts
|
|
251
|
-
|
|
252
|
-
import { NativeFlowHandler } from '@strivacity/sdk-core/handlers/NativeFlowHandler';
|
|
253
|
-
import { CustomNativeFlow } from './CustomNativeFlow';
|
|
449
|
+
const login = document.querySelector('sty-login');
|
|
254
450
|
|
|
255
|
-
|
|
256
|
-
|
|
451
|
+
// Change language programmatically (e.g., from your own language selector)
|
|
452
|
+
login.lang = 'fr-FR'; // Switch to French
|
|
453
|
+
login.lang = 'de-DE'; // Switch to German
|
|
454
|
+
login.lang = 'en-US'; // Switch to English
|
|
455
|
+
```
|
|
257
456
|
|
|
258
|
-
|
|
259
|
-
if (sessionId) {
|
|
260
|
-
this.sessionId = sessionId;
|
|
261
|
-
return this.submitForm();
|
|
262
|
-
}
|
|
457
|
+
#### Handle the callback
|
|
263
458
|
|
|
264
|
-
|
|
265
|
-
this.language = language;
|
|
266
|
-
}
|
|
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.
|
|
267
460
|
|
|
268
|
-
|
|
269
|
-
method: 'POST',
|
|
270
|
-
credentials: 'include',
|
|
271
|
-
headers: { 'Content-Type': 'application/json' },
|
|
272
|
-
body: JSON.stringify(this.params),
|
|
273
|
-
});
|
|
461
|
+
#### Externally-initiated flows (entry)
|
|
274
462
|
|
|
275
|
-
|
|
276
|
-
this.sessionId = data.session_id;
|
|
463
|
+
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`).
|
|
277
464
|
|
|
278
|
-
|
|
279
|
-
}
|
|
465
|
+
You have two options:
|
|
280
466
|
|
|
281
|
-
|
|
282
|
-
const finalizeResponse = await this.sdk.httpClient.request(finalizeUrl, {
|
|
283
|
-
method: 'GET',
|
|
284
|
-
headers: { Authorization: `Bearer ${this.sessionId}` },
|
|
285
|
-
credentials: 'include',
|
|
286
|
-
});
|
|
467
|
+
**Option 1: Redirect to a separate login page**
|
|
287
468
|
|
|
288
|
-
|
|
469
|
+
Forward the parameters as query params to your login page:
|
|
289
470
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
'Content-Type': 'application/json',
|
|
294
|
-
Authorization: `Bearer ${this.sessionId}`,
|
|
295
|
-
},
|
|
296
|
-
body: JSON.stringify(Object.fromEntries(redirectUri.searchParams)),
|
|
297
|
-
credentials: 'include',
|
|
298
|
-
});
|
|
471
|
+
```ts
|
|
472
|
+
// entry.ts
|
|
473
|
+
import { sdk } from './sdk';
|
|
299
474
|
|
|
300
|
-
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
```
|
|
475
|
+
const data = await sdk.entry();
|
|
304
476
|
|
|
305
|
-
|
|
477
|
+
// Redirect to login page with flow parameters
|
|
478
|
+
const params = new URLSearchParams({
|
|
479
|
+
session_id: data.session_id,
|
|
480
|
+
short_app_id: data.short_app_id,
|
|
481
|
+
language: data.language,
|
|
482
|
+
});
|
|
483
|
+
window.location.href = `/login?${params}`;
|
|
484
|
+
```
|
|
306
485
|
|
|
307
|
-
|
|
486
|
+
Then on your login page, read the parameters and pass them to `sty-login`:
|
|
308
487
|
|
|
309
|
-
```
|
|
310
|
-
|
|
311
|
-
|
|
488
|
+
```ts
|
|
489
|
+
// login.ts
|
|
490
|
+
import { sdk } from './sdk';
|
|
491
|
+
|
|
492
|
+
// Mount your web component
|
|
493
|
+
// <sty-login id="login" lazy></sty-login>
|
|
494
|
+
|
|
495
|
+
// Read parameters from URL
|
|
496
|
+
const params = new URLSearchParams(window.location.search);
|
|
497
|
+
const login = document.querySelector('sty-login[lazy]');
|
|
498
|
+
login.sessionId = params.get('session_id');
|
|
499
|
+
login.shortAppId = params.get('short_app_id');
|
|
500
|
+
login.language = params.get('language');
|
|
501
|
+
login.start();
|
|
502
|
+
|
|
503
|
+
login.addEventListener('login', () => {
|
|
504
|
+
// User authenticated - navigate to a protected page
|
|
505
|
+
window.location.href = '/profile';
|
|
312
506
|
});
|
|
313
|
-
|
|
314
|
-
// Later: unsubscribe
|
|
315
|
-
dispose();
|
|
316
507
|
```
|
|
317
508
|
|
|
318
|
-
|
|
509
|
+
**Option 2: Render login on the entry page**
|
|
510
|
+
|
|
511
|
+
Pass the parameters directly to `sty-login` on the same page:
|
|
512
|
+
|
|
513
|
+
```ts
|
|
514
|
+
// entry.ts
|
|
515
|
+
import { sdk } from './sdk';
|
|
319
516
|
|
|
320
|
-
|
|
321
|
-
- **`loggedIn`**: Fired when a user successfully logs in.
|
|
322
|
-
- **`loginInitiated`**: Fired when a login flow is initiated.
|
|
323
|
-
- **`sessionLoaded`**: Fired when an existing session is loaded.
|
|
324
|
-
- **`accessTokenExpired`**: Fired when the access token expires.
|
|
325
|
-
- **`tokenRefreshed`**: Fired when access tokens are successfully refreshed.
|
|
326
|
-
- **`tokenRefreshFailed`**: Fired when token refresh fails.
|
|
327
|
-
- **`logoutInitiated`**: Fired when logout is initiated.
|
|
328
|
-
- **`tokenRevoked`**: Fired when tokens are successfully revoked.
|
|
329
|
-
- **`tokenRevokeFailed`**: Fired when token revocation fails.
|
|
517
|
+
const data = await sdk.entry();
|
|
330
518
|
|
|
331
|
-
|
|
519
|
+
// Mount your web component
|
|
520
|
+
// <sty-login id="login" lazy></sty-login>
|
|
332
521
|
|
|
333
|
-
|
|
522
|
+
// Set properties on the existing sty-login component
|
|
523
|
+
const login = document.querySelector('sty-login[lazy]');
|
|
524
|
+
login.sessionId = data.session_id;
|
|
525
|
+
login.shortAppId = data.short_app_id;
|
|
526
|
+
login.language = data.language;
|
|
527
|
+
login.start();
|
|
334
528
|
|
|
335
|
-
|
|
529
|
+
login.addEventListener('login', () => {
|
|
530
|
+
// User authenticated - navigate to a protected page
|
|
531
|
+
window.location.href = window.location.origin;
|
|
532
|
+
});
|
|
533
|
+
```
|
|
336
534
|
|
|
337
|
-
|
|
535
|
+
#### Logout
|
|
338
536
|
|
|
339
|
-
|
|
340
|
-
import { initFlow, DefaultLogging } from '@strivacity/sdk-core';
|
|
537
|
+
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`.
|
|
341
538
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
logging: DefaultLogging,
|
|
539
|
+
```ts
|
|
540
|
+
// logout.ts
|
|
541
|
+
import { sdk } from './sdk';
|
|
542
|
+
|
|
543
|
+
await sdk.logout({
|
|
544
|
+
postLogoutRedirectUri: window.location.origin,
|
|
349
545
|
});
|
|
350
546
|
```
|
|
351
547
|
|
|
352
|
-
The
|
|
548
|
+
> 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.
|
|
353
549
|
|
|
354
|
-
|
|
550
|
+
#### Token management
|
|
355
551
|
|
|
356
|
-
|
|
552
|
+
Call these methods to manage the session and access token.
|
|
357
553
|
|
|
358
|
-
```
|
|
359
|
-
import
|
|
554
|
+
```ts
|
|
555
|
+
import { sdk } from './sdk';
|
|
360
556
|
|
|
361
|
-
|
|
362
|
-
|
|
557
|
+
// Refresh the access token using the refresh token
|
|
558
|
+
// (automatically waits for SDK initialization and session loading)
|
|
559
|
+
await sdk.refresh();
|
|
363
560
|
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
561
|
+
// Revoke all tokens at the authorization server and clear the local session
|
|
562
|
+
// (automatically waits for SDK initialization and session loading)
|
|
563
|
+
await sdk.revoke();
|
|
367
564
|
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
565
|
+
// Get the current access and refresh tokens (synchronous property - ensure SDK is initialized first)
|
|
566
|
+
await sdk.init();
|
|
567
|
+
const idToken = sdk.idTokenClaims // read-only property - returns the current ID token claims or null if not authenticated
|
|
568
|
+
const accessToken = sdk.accessToken; // read-only property - returns the current access token or null if not authenticated
|
|
569
|
+
const refreshToken = sdk.refreshToken; // read-only property - returns the current refresh token or null if not authenticated
|
|
570
|
+
```
|
|
371
571
|
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
572
|
+
---
|
|
573
|
+
|
|
574
|
+
<a id="client-native-mode"></a>
|
|
575
|
+
### native mode
|
|
576
|
+
|
|
577
|
+
> For details on how this mode works, see the [native journey documentation](https://docs.strivacity.com/reference/native-journey).
|
|
578
|
+
|
|
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 - then call `finalizeSession()`.
|
|
580
|
+
|
|
581
|
+
> [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
|
+
|
|
583
|
+
#### Login / Register
|
|
584
|
+
|
|
585
|
+
```ts
|
|
586
|
+
// login.ts
|
|
587
|
+
import { sdk } from './sdk';
|
|
588
|
+
|
|
589
|
+
const nativeFlowState = await sdk.startSession({
|
|
590
|
+
// Optional parameters
|
|
591
|
+
prompt: 'login', // use 'create' to open the registration flow instead
|
|
592
|
+
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
|
+
sessionId: null, // pass a session ID to resume an existing flow
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
// NativeFlowState structure:
|
|
598
|
+
// - web mode (default): Full response with all features
|
|
599
|
+
// - web-minimal mode: Minimal response with layout, branding, and render hints excluded
|
|
600
|
+
|
|
601
|
+
// nativeFlowState.branding - branding information (logos, etc.) (`web` mode only)
|
|
602
|
+
// nativeFlowState.forms - array of form definitions with widgets
|
|
603
|
+
// nativeFlowState.layout - tree structure defining widget rendering order (`web` mode only)
|
|
604
|
+
// nativeFlowState.screen - current step identifier (e.g., 'identifier', 'password')
|
|
605
|
+
// nativeFlowState.finalizeUrl - set when authentication completes, pass to finalizeSession()
|
|
606
|
+
|
|
607
|
+
// Render the form based on nativeFlowState.screen, collect user input, then submit:
|
|
608
|
+
const nextNativeFlowState = await sdk.submitForm('identifier', {
|
|
609
|
+
identifier: 'user@example.com',
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
if (nextNativeFlowState.finalizeUrl) {
|
|
613
|
+
// Finalize the session at the IDP and exchange the authorization code for tokens and store them in the configured storage
|
|
614
|
+
await sdk.finalizeSession(nextNativeFlowState.finalizeUrl);
|
|
375
615
|
|
|
376
|
-
|
|
377
|
-
|
|
616
|
+
// Redirect to a protected page after successful authentication
|
|
617
|
+
if (await sdk.isAuthenticated) {
|
|
618
|
+
window.location.href = '/profile';
|
|
378
619
|
}
|
|
620
|
+
} else {
|
|
621
|
+
// Re-render for nextNativeFlowState.screen and repeat
|
|
379
622
|
}
|
|
380
623
|
```
|
|
381
624
|
|
|
382
|
-
|
|
383
|
-
import { initFlow } from '@strivacity/sdk-core';
|
|
384
|
-
import { MyLogger } from './logging/MyLogger';
|
|
625
|
+
#### Handle the callback
|
|
385
626
|
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
627
|
+
No separate callback page is needed. Once `nativeFlowState.finalizeUrl` is set, call `sdk.finalizeSession(nativeFlowState.finalizeUrl)` to exchange the authorization code for tokens and store the session (see the Login / Register example above).
|
|
628
|
+
|
|
629
|
+
#### Externally-initiated flows (entry)
|
|
630
|
+
|
|
631
|
+
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
|
+
|
|
633
|
+
You have two options:
|
|
634
|
+
|
|
635
|
+
**Option 1: Redirect to a separate login page**
|
|
636
|
+
|
|
637
|
+
Forward the parameters as query params to your login page:
|
|
638
|
+
|
|
639
|
+
```ts
|
|
640
|
+
// entry.ts
|
|
641
|
+
import { sdk } from './sdk';
|
|
642
|
+
|
|
643
|
+
const data = await sdk.entry();
|
|
644
|
+
|
|
645
|
+
// Redirect to login page with flow parameters
|
|
646
|
+
const params = new URLSearchParams({
|
|
647
|
+
session_id: data.session_id,
|
|
648
|
+
language: data.language,
|
|
649
|
+
});
|
|
650
|
+
window.location.href = `/login?${params}`;
|
|
651
|
+
```
|
|
652
|
+
|
|
653
|
+
Then on your login page, read the parameters and pass them to `startSession()`:
|
|
654
|
+
|
|
655
|
+
```ts
|
|
656
|
+
// login.ts
|
|
657
|
+
import { sdk } from './sdk';
|
|
658
|
+
|
|
659
|
+
// Read parameters from URL
|
|
660
|
+
const params = new URLSearchParams(window.location.search);
|
|
661
|
+
const state = await sdk.startSession({
|
|
662
|
+
sessionId: params.get('session_id'),
|
|
663
|
+
language: params.get('language'),
|
|
389
664
|
});
|
|
665
|
+
|
|
666
|
+
// Continue with the rendering loop (see minimal rendering example above)
|
|
390
667
|
```
|
|
391
668
|
|
|
392
|
-
|
|
669
|
+
**Option 2: Start session on the entry page**
|
|
393
670
|
|
|
394
|
-
|
|
671
|
+
Pass the parameters directly to `startSession()` on the same page:
|
|
395
672
|
|
|
396
|
-
|
|
673
|
+
```ts
|
|
674
|
+
// entry.ts
|
|
675
|
+
import { sdk } from './sdk';
|
|
397
676
|
|
|
398
|
-
|
|
677
|
+
const data = await sdk.entry();
|
|
678
|
+
const state = await sdk.startSession({
|
|
679
|
+
sessionId: data.session_id,
|
|
680
|
+
language: data.language,
|
|
681
|
+
});
|
|
399
682
|
|
|
400
|
-
|
|
401
|
-
|
|
683
|
+
// Continue with the rendering loop (see minimal rendering example above)
|
|
684
|
+
```
|
|
402
685
|
|
|
403
|
-
|
|
404
|
-
async request<T>(url: string, options?: RequestInit): Promise<HttpClientResponse<T>> {
|
|
405
|
-
const mergedOptions: RequestInit = {
|
|
406
|
-
...options,
|
|
407
|
-
headers: {
|
|
408
|
-
'x-sty-app-id': 'my-app',
|
|
409
|
-
...(options?.headers as Record<string, string>),
|
|
410
|
-
},
|
|
411
|
-
};
|
|
686
|
+
#### Logout
|
|
412
687
|
|
|
413
|
-
|
|
688
|
+
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`.
|
|
414
689
|
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
690
|
+
```ts
|
|
691
|
+
// logout.ts
|
|
692
|
+
import { sdk } from './sdk';
|
|
693
|
+
|
|
694
|
+
await sdk.logout({
|
|
695
|
+
postLogoutRedirectUri: window.location.origin,
|
|
696
|
+
});
|
|
697
|
+
```
|
|
698
|
+
|
|
699
|
+
> 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
|
+
|
|
701
|
+
#### Token management
|
|
702
|
+
|
|
703
|
+
Call these methods to manage the session and access token.
|
|
704
|
+
|
|
705
|
+
```ts
|
|
706
|
+
import { sdk } from './sdk';
|
|
707
|
+
|
|
708
|
+
// Refresh the access token using the refresh token
|
|
709
|
+
// (automatically waits for SDK initialization and session loading)
|
|
710
|
+
await sdk.refresh();
|
|
711
|
+
|
|
712
|
+
// Revoke all tokens at the authorization server and clear the local session
|
|
713
|
+
// (automatically waits for SDK initialization and session loading)
|
|
714
|
+
await sdk.revoke();
|
|
715
|
+
|
|
716
|
+
// Get the current access and refresh tokens (synchronous property - ensure SDK is initialized first)
|
|
717
|
+
await sdk.init();
|
|
718
|
+
const idToken = sdk.idTokenClaims // read-only property - returns the current ID token claims or null if not authenticated
|
|
719
|
+
const accessToken = sdk.accessToken; // read-only property - returns the current access token or null if not authenticated
|
|
720
|
+
const refreshToken = sdk.refreshToken; // read-only property - returns the current refresh token or null if not authenticated
|
|
721
|
+
```
|
|
722
|
+
|
|
723
|
+
---
|
|
724
|
+
|
|
725
|
+
### Server-side session management
|
|
726
|
+
|
|
727
|
+
Set `serverSessionUri` on the SDK options when using a backend-for-frontend ([BFF](../../README.md#bff)) architecture, to route login requests through your own server-side endpoint instead of the SDK's default IDP endpoint - useful when the request must be built/signed server-side. Setting it also switches the client SDK into server-managed session mode: tokens are never read from or written to client-side storage.
|
|
728
|
+
|
|
729
|
+
```ts
|
|
730
|
+
// sdk.ts
|
|
731
|
+
import { initFlow } from '@strivacity/sdk-core';
|
|
426
732
|
|
|
427
733
|
const sdk = initFlow({
|
|
428
|
-
//
|
|
429
|
-
|
|
734
|
+
mode: 'redirect', // works the same way in popup, embedded, and native modes
|
|
735
|
+
issuer: 'https://<YOUR_TENANT_DOMAIN>',
|
|
736
|
+
clientId: 'YOUR_CLIENT_ID',
|
|
737
|
+
redirectUri: 'https://your-app.example.com/callback',
|
|
738
|
+
scopes: ['openid', 'profile', 'email'],
|
|
739
|
+
serverSessionUri: '/api/auth/login', // Requests are routed through your server; tokens are NOT written to client storage
|
|
430
740
|
});
|
|
741
|
+
|
|
742
|
+
await sdk.login();
|
|
431
743
|
```
|
|
432
744
|
|
|
433
|
-
|
|
745
|
+
Sensitive OAuth2 parameters (`client_id`, `redirect_uri`, `scope`, ...) are never sent to `serverSessionUri` - your endpoint is responsible for adding those itself; only the non-sensitive extras (`prompt`, `display`, `acrValues`, `loginHint`, `uiLocales`, `audiences`) are appended as query params.
|
|
746
|
+
|
|
747
|
+
In this mode, the SDK relies on your backend to:
|
|
748
|
+
- Build/sign the authorization request and proxy it to the IDP
|
|
749
|
+
- Store tokens in server-side storage (e.g., Redis, encrypted cookies)
|
|
750
|
+
- Complete the OAuth2 code exchange and redirect the browser back to your app
|
|
434
751
|
|
|
435
|
-
|
|
752
|
+
#### Accessing the session on the client-side
|
|
436
753
|
|
|
437
|
-
|
|
754
|
+
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:
|
|
438
755
|
|
|
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.
|
|
757
|
+
- **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
|
+
|
|
759
|
+
```ts
|
|
760
|
+
// api/auth/session.ts (server route)
|
|
761
|
+
import { sdk } from './server';
|
|
762
|
+
|
|
763
|
+
export async function GET(req: Request) {
|
|
764
|
+
const session = await sdk.getSession(req);
|
|
765
|
+
return Response.json(session ? { claims: session.claims } : null);
|
|
766
|
+
}
|
|
439
767
|
```
|
|
440
|
-
|
|
768
|
+
|
|
769
|
+
```ts
|
|
770
|
+
// main.ts (client)
|
|
771
|
+
import { sdk } from './client';
|
|
772
|
+
|
|
773
|
+
const response = await fetch('/api/auth/session');
|
|
774
|
+
const session = await response.json();
|
|
775
|
+
|
|
776
|
+
sdk.session = session;
|
|
441
777
|
```
|
|
442
778
|
|
|
443
|
-
|
|
779
|
+
---
|
|
780
|
+
|
|
781
|
+
## Server SDK
|
|
782
|
+
|
|
783
|
+
This package provides a backend-for-frontend ([BFF](../../README.md#bff)) server implementation that handles OAuth2 state and PKCE management automatically. Use it when you need to route authentication traffic through your own server for request signing, custom headers, or additional validation.
|
|
784
|
+
|
|
785
|
+
<a id="server-usage"></a>
|
|
786
|
+
### Usage
|
|
444
787
|
|
|
445
|
-
|
|
788
|
+
Here's a minimal example of how to set up the server SDK with Express. The SDK handles all the OAuth2 flow and session management for you, so your routes can be very simple.
|
|
789
|
+
|
|
790
|
+
```ts
|
|
791
|
+
// server.ts
|
|
792
|
+
import { createBaseServerSDK } from '@strivacity/sdk-core/server';
|
|
793
|
+
import { toWebRequest } from './utils';
|
|
794
|
+
|
|
795
|
+
// Initialize the server SDK
|
|
796
|
+
const sdk = createBaseServerSDK(
|
|
797
|
+
|
|
798
|
+
// Server adapter
|
|
799
|
+
{
|
|
800
|
+
toRequest: (req) => toWebRequest(req), // converts framework-specific request to standard Web Request
|
|
801
|
+
redirect: (res, url) => res.redirect(url), // optional - override default redirect behavior
|
|
802
|
+
},
|
|
803
|
+
|
|
804
|
+
// Server configuration
|
|
805
|
+
{
|
|
806
|
+
mode: 'redirect', // authentication mode
|
|
807
|
+
issuer: 'https://<YOUR_TENANT_DOMAIN>', // OIDC provider URL
|
|
808
|
+
clientId: 'YOUR_CLIENT_ID', // OAuth2 client ID
|
|
809
|
+
redirectUri: 'https://your-app.example.com/callback', // callback URL after authentication
|
|
810
|
+
scopes: ['openid', 'profile', 'email'], // requested user permissions/data
|
|
811
|
+
secret: process.env.SECRET, // http-only cookie encryption key (random 32+ characters); Required if using cookie storage (default)
|
|
812
|
+
}
|
|
813
|
+
);
|
|
814
|
+
```
|
|
446
815
|
|
|
447
|
-
```
|
|
448
|
-
|
|
816
|
+
```ts
|
|
817
|
+
// routes.ts
|
|
818
|
+
import { sdk } from './server';
|
|
819
|
+
import { mount } from './utils';
|
|
820
|
+
|
|
821
|
+
/**
|
|
822
|
+
* Starts the login flow and redirects to the IDP.
|
|
823
|
+
* @modes all
|
|
824
|
+
* @returns 302 redirect to IDP
|
|
825
|
+
*/
|
|
826
|
+
mount('get', '/api/auth/login', (req) => sdk.handleLogin(req));
|
|
827
|
+
|
|
828
|
+
/**
|
|
829
|
+
* Starts the registration flow and redirects to the IDP.
|
|
830
|
+
* @modes all
|
|
831
|
+
* @returns 302 redirect to IDP
|
|
832
|
+
*/
|
|
833
|
+
mount('get', '/api/auth/register', (req) => sdk.handleRegister(req));
|
|
834
|
+
|
|
835
|
+
/**
|
|
836
|
+
* Handles external flow entry (e.g., password reset link).
|
|
837
|
+
* @modes embedded, native
|
|
838
|
+
* @returns JSON with session data
|
|
839
|
+
*/
|
|
840
|
+
mount('get', '/api/auth/entry', (req) => sdk.handleEntry(req));
|
|
841
|
+
|
|
842
|
+
/**
|
|
843
|
+
* Completes authentication (handles the callback from the IDP).
|
|
844
|
+
* @modes all
|
|
845
|
+
* @returns 302 redirect or popup close script
|
|
846
|
+
*/
|
|
847
|
+
mount('get', '/api/auth/callback', (req) => sdk.handleCallback(req));
|
|
848
|
+
|
|
849
|
+
/**
|
|
850
|
+
* Refreshes the access token.
|
|
851
|
+
* @modes all
|
|
852
|
+
* @returns 204 No Content or 302 redirect
|
|
853
|
+
*/
|
|
854
|
+
mount('get', '/api/auth/refresh', (req) => sdk.handleRefresh(req));
|
|
855
|
+
|
|
856
|
+
/**
|
|
857
|
+
* Revokes tokens and clears the session.
|
|
858
|
+
* @modes all
|
|
859
|
+
* @returns 204 No Content
|
|
860
|
+
*/
|
|
861
|
+
mount('get', '/api/auth/revoke', (req) => sdk.handleRevoke(req));
|
|
862
|
+
|
|
863
|
+
/**
|
|
864
|
+
* Ends the session and redirects to the IDP logout page.
|
|
865
|
+
* @modes all
|
|
866
|
+
* @returns 302 redirect to IDP logout
|
|
867
|
+
*/
|
|
868
|
+
mount('get', '/api/auth/logout', (req) => sdk.handleLogout(req));
|
|
869
|
+
|
|
870
|
+
/**
|
|
871
|
+
* Processes back-channel logout requests from the IDP.
|
|
872
|
+
* @modes all
|
|
873
|
+
* @returns 204 No Content
|
|
874
|
+
*/
|
|
875
|
+
mount('post', '/api/auth/backchannel-logout', (req) => sdk.handleBackChannelLogout(req));
|
|
449
876
|
```
|
|
450
877
|
|
|
451
|
-
|
|
878
|
+
```ts
|
|
879
|
+
// utils.ts
|
|
880
|
+
import type { Request as ExpressRequest, Response as ExpressResponse } from 'express';
|
|
881
|
+
import { Router } from 'express';
|
|
882
|
+
import { Readable } from 'stream';
|
|
452
883
|
|
|
453
|
-
|
|
884
|
+
const handlers = Router();
|
|
454
885
|
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
| `clientId` | `string` | ✅ | The client identifier for the application. |
|
|
459
|
-
| `redirectUri` | `string` | ✅ | The URI to redirect to after authentication. |
|
|
460
|
-
| `mode` | `'redirect' \| 'popup' \| 'native' \| 'embedded' \| 'custom'` | ⬜ | The authentication flow mode. Defaults to `'redirect'`. |
|
|
461
|
-
| `scopes` | `string[]` | ⬜ | The scopes to request. Defaults to `['openid']`. |
|
|
462
|
-
| `responseType` | `'code' \| 'id_token'` | ⬜ | The response type. |
|
|
463
|
-
| `responseMode` | `'query' \| 'fragment'` | ⬜ | The response mode. |
|
|
464
|
-
| `storageTokenName` | `string` | ⬜ | Custom name for the session storage key. Defaults to `'sty.session'`. |
|
|
465
|
-
| `storage` | `SDKStorageType` | ⬜ | Custom storage implementation. Defaults to `LocalStorage`. |
|
|
466
|
-
| `httpClient` | `SDKHttpClientType` | ⬜ | Custom HTTP client implementation. |
|
|
467
|
-
| `logging` | `SDKLoggingType` | ⬜ | Logging implementation class. |
|
|
468
|
-
| `customFlow` | `FlowType` | ⬜ | Custom flow class. Required when `mode` is `'custom'`. Must extend `BaseFlow`. |
|
|
886
|
+
// Apply Web Response to Express response
|
|
887
|
+
export async function applyResponse(response: Response, res: ExpressResponse): Promise<void> {
|
|
888
|
+
res.status(response.status);
|
|
469
889
|
|
|
470
|
-
|
|
890
|
+
for (const [key, value] of response.headers.entries()) {
|
|
891
|
+
if (key.toLowerCase() === 'set-cookie') {
|
|
892
|
+
continue;
|
|
893
|
+
}
|
|
471
894
|
|
|
472
|
-
|
|
895
|
+
res.setHeader(key, value);
|
|
896
|
+
}
|
|
473
897
|
|
|
474
|
-
|
|
898
|
+
const setCookies = response.headers.getSetCookie();
|
|
475
899
|
|
|
476
|
-
|
|
900
|
+
if (setCookies.length) {
|
|
901
|
+
res.setHeader('set-cookie', setCookies);
|
|
902
|
+
}
|
|
477
903
|
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
- **`refresh(): Promise<void>`**: Refreshes the session tokens.
|
|
483
|
-
- **`revoke(): Promise<void>`**: Revokes the current session tokens.
|
|
484
|
-
- **`logout(options?: LogoutOptions): Promise<void>`**: Logs the user out.
|
|
485
|
-
- **`subscribeToEvent(eventType: string, callback: Function): { dispose(): void }`**: Subscribes to an authentication lifecycle event. Returns an object with a `dispose()` method to unsubscribe.
|
|
904
|
+
if (!response.body) {
|
|
905
|
+
res.end();
|
|
906
|
+
return;
|
|
907
|
+
}
|
|
486
908
|
|
|
487
|
-
|
|
909
|
+
await new Promise<void>((resolve, reject) => {
|
|
910
|
+
Readable.fromWeb(response.body as Parameters<typeof Readable.fromWeb>[0])
|
|
911
|
+
.pipe(res)
|
|
912
|
+
.on('finish', resolve)
|
|
913
|
+
.on('error', reject);
|
|
914
|
+
});
|
|
915
|
+
}
|
|
488
916
|
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
917
|
+
// Mount a handler for a specific HTTP method and path
|
|
918
|
+
export function mount(method: 'get' | 'post', path: string, handler: (req: ExpressRequest) => Promise<Response>): void {
|
|
919
|
+
handlers[method](path, async (req, res, next) => {
|
|
920
|
+
try {
|
|
921
|
+
await applyResponse(await handler(req), res);
|
|
922
|
+
} catch (error) {
|
|
923
|
+
next(error);
|
|
924
|
+
}
|
|
925
|
+
});
|
|
926
|
+
}
|
|
496
927
|
|
|
497
|
-
|
|
928
|
+
// Convert Express request to Web Request
|
|
929
|
+
export function toWebRequest(req: ExpressRequest): Request {
|
|
930
|
+
const url = new URL(req.originalUrl, `${req.protocol}://${req.get('host')}`);
|
|
931
|
+
const headers = new Headers();
|
|
498
932
|
|
|
499
|
-
|
|
933
|
+
for (const [key, value] of Object.entries(req.headers)) {
|
|
934
|
+
if (value === undefined) continue;
|
|
935
|
+
for (const v of Array.isArray(value) ? value : [value]) {
|
|
936
|
+
headers.append(key, v);
|
|
937
|
+
}
|
|
938
|
+
}
|
|
500
939
|
|
|
501
|
-
|
|
940
|
+
const hasBody = req.method !== 'GET' && req.method !== 'HEAD';
|
|
502
941
|
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
942
|
+
return new Request(url, {
|
|
943
|
+
method: req.method,
|
|
944
|
+
headers,
|
|
945
|
+
...(hasBody ? { body: Readable.toWeb(req), duplex: 'half' } : {}),
|
|
946
|
+
} as RequestInit);
|
|
947
|
+
}
|
|
948
|
+
```
|
|
506
949
|
|
|
507
|
-
|
|
950
|
+
For complete utility implementations, see the [backend example app](../../apps/backend) in this repository.
|
|
508
951
|
|
|
509
|
-
|
|
952
|
+
<a id="server-storages"></a>
|
|
510
953
|
|
|
511
|
-
|
|
954
|
+
By default, the server SDK stores:
|
|
955
|
+
- **Tokens** (access_token, refresh_token, id_token): encrypted in http-only cookies
|
|
956
|
+
- **Login state**: global in-memory Map
|
|
512
957
|
|
|
513
|
-
|
|
958
|
+
#### Built-in session storages
|
|
514
959
|
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
| `eventType` | `string` | ✅ | The event type to listen for. |
|
|
518
|
-
| `callback` | `(eventData: any) => void` | ✅ | The handler called when the event fires. |
|
|
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. This supports back-channel logout out of the box.
|
|
519
962
|
|
|
520
|
-
|
|
963
|
+
```ts
|
|
964
|
+
// server.ts
|
|
965
|
+
import { createBaseServerSDK } from '@strivacity/sdk-core/server';
|
|
966
|
+
import { createSessionIdCookieStorage, createServerMemoryStorage } from '@strivacity/sdk-core/storages';
|
|
967
|
+
import { toWebRequest } from './utils';
|
|
968
|
+
|
|
969
|
+
const adapter = { toRequest: (req) => toWebRequest(req) };
|
|
970
|
+
|
|
971
|
+
const sdk = createBaseServerSDK(adapter, {
|
|
972
|
+
storage: createSessionIdCookieStorage(
|
|
973
|
+
adapter,
|
|
974
|
+
createServerMemoryStorage(),
|
|
975
|
+
{
|
|
976
|
+
// maxAge: 30 * 24 * 60 * 60 // Without maxAge this is a browser-session cookie that gets cleared when the browser closes
|
|
977
|
+
},
|
|
978
|
+
),
|
|
979
|
+
});
|
|
980
|
+
```
|
|
521
981
|
|
|
522
|
-
|
|
982
|
+
#### Custom storage
|
|
523
983
|
|
|
524
|
-
|
|
984
|
+
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):
|
|
525
986
|
|
|
526
|
-
|
|
987
|
+
```ts
|
|
988
|
+
// storage.ts
|
|
989
|
+
import { createStorage } from 'unstorage';
|
|
990
|
+
import redisDriver from 'unstorage/drivers/redis';
|
|
991
|
+
import type { ServerStorage, SDKStorage } from '@strivacity/sdk-core/types';
|
|
992
|
+
|
|
993
|
+
const unstorageInstance = createStorage({
|
|
994
|
+
driver: redisDriver({ url: process.env.REDIS_URL }),
|
|
995
|
+
});
|
|
527
996
|
|
|
528
|
-
|
|
997
|
+
// Custom session storage for tokens
|
|
998
|
+
export const sessionStorage: ServerStorage = {
|
|
999
|
+
async get(key) {
|
|
1000
|
+
return unstorageInstance.getItem<string>(key);
|
|
1001
|
+
},
|
|
1002
|
+
async set(key, value) {
|
|
1003
|
+
await unstorageInstance.setItem(key, value);
|
|
1004
|
+
},
|
|
1005
|
+
async delete(key) {
|
|
1006
|
+
await unstorageInstance.removeItem(key);
|
|
1007
|
+
},
|
|
1008
|
+
// Required for OIDC back-channel logout support.
|
|
1009
|
+
// Scans all stored sessions and removes those matching the logout token's sid or sub claim.
|
|
1010
|
+
async deleteByLogoutToken(logoutToken) {
|
|
1011
|
+
const keys = await unstorageInstance.getKeys();
|
|
1012
|
+
await Promise.all(
|
|
1013
|
+
keys.map(async (key) => {
|
|
1014
|
+
const raw = await unstorageInstance.getItem<string>(key);
|
|
1015
|
+
if (!raw) return;
|
|
1016
|
+
const session = JSON.parse(raw);
|
|
1017
|
+
if (
|
|
1018
|
+
(logoutToken.sid && session.sid === logoutToken.sid) ||
|
|
1019
|
+
(logoutToken.sub && session.sub === logoutToken.sub)
|
|
1020
|
+
) {
|
|
1021
|
+
await unstorageInstance.removeItem(key);
|
|
1022
|
+
}
|
|
1023
|
+
}),
|
|
1024
|
+
);
|
|
1025
|
+
},
|
|
1026
|
+
};
|
|
1027
|
+
|
|
1028
|
+
// Custom state storage for OAuth2 state parameter
|
|
1029
|
+
export const stateStorage: SDKStorage = {
|
|
1030
|
+
async get(key) {
|
|
1031
|
+
return unstorageInstance.getItem<string>(key);
|
|
1032
|
+
},
|
|
1033
|
+
async set(key, value) {
|
|
1034
|
+
await unstorageInstance.setItem(key, value);
|
|
1035
|
+
},
|
|
1036
|
+
async delete(key) {
|
|
1037
|
+
await unstorageInstance.removeItem(key);
|
|
1038
|
+
},
|
|
1039
|
+
};
|
|
1040
|
+
```
|
|
529
1041
|
|
|
530
|
-
|
|
1042
|
+
```ts
|
|
1043
|
+
// server.ts
|
|
1044
|
+
import { createBaseServerSDK } from '@strivacity/sdk-core/server';
|
|
1045
|
+
import { toWebRequest } from './utils';
|
|
1046
|
+
import { sessionStorage, stateStorage } from './storage';
|
|
1047
|
+
|
|
1048
|
+
const sdk = createBaseServerSDK(
|
|
1049
|
+
{
|
|
1050
|
+
toRequest: (req) => toWebRequest(req),
|
|
1051
|
+
},
|
|
1052
|
+
{
|
|
1053
|
+
// other configuration options
|
|
1054
|
+
storage: sessionStorage, // Custom Redis-backed session storage
|
|
1055
|
+
stateStorage: stateStorage, // Custom Redis-backed state storage
|
|
1056
|
+
}
|
|
1057
|
+
);
|
|
1058
|
+
```
|
|
531
1059
|
|
|
532
|
-
|
|
1060
|
+
<a id="server-backchannel-logout"></a>
|
|
1061
|
+
### Back-channel logout
|
|
1062
|
+
|
|
1063
|
+
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)`.
|
|
533
1064
|
|
|
534
|
-
|
|
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`, and `501` if the configured storage doesn't implement `deleteByLogoutToken`.
|
|
535
1066
|
|
|
536
|
-
|
|
1067
|
+
> **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.
|
|
537
1068
|
|
|
538
|
-
|
|
1069
|
+
Configure the **Back-channel logout URI** in your Strivacity application settings to:
|
|
1070
|
+
|
|
1071
|
+
```
|
|
1072
|
+
https://your-app.example.com/api/auth/backchannel-logout
|
|
1073
|
+
```
|
|
1074
|
+
|
|
1075
|
+
<a id="server-configuration-options"></a>
|
|
1076
|
+
### Configuration Options
|
|
1077
|
+
|
|
1078
|
+
The server SDK accepts the same configuration as the client SDK, plus server-specific options:
|
|
1079
|
+
|
|
1080
|
+
```ts
|
|
1081
|
+
{
|
|
1082
|
+
mode: 'redirect' | 'popup' | 'embedded' | 'native',
|
|
1083
|
+
issuer: string,
|
|
1084
|
+
clientId: string,
|
|
1085
|
+
redirectUri: string,
|
|
1086
|
+
scopes: string[],
|
|
1087
|
+
storage: ServerStorage, // Custom session storage
|
|
1088
|
+
stateStorage: SDKStorage, // Custom OAuth2 state storage
|
|
1089
|
+
serverSessionUri: string, // Optional - route login requests through your own server endpoint
|
|
1090
|
+
// ... other client SDK options
|
|
1091
|
+
|
|
1092
|
+
// server-specific options
|
|
1093
|
+
secret: '<your-encryption-key>', // encrypts http-only cookies
|
|
1094
|
+
authPrefix: '/auth', // Prefix for auth routes
|
|
1095
|
+
cookieMaxAge: 43200, // Max age for session cookies in seconds (default: 30 days)
|
|
1096
|
+
postLoginRedirectUri: '/', // Default redirect after login
|
|
1097
|
+
postLogoutRedirectUri: '/', // Default redirect after logout
|
|
1098
|
+
}
|
|
1099
|
+
```
|
|
1100
|
+
|
|
1101
|
+
---
|
|
1102
|
+
|
|
1103
|
+
## Shared features
|
|
1104
|
+
|
|
1105
|
+
These features are available in both client and server SDK implementations.
|
|
1106
|
+
|
|
1107
|
+
### Storages
|
|
1108
|
+
|
|
1109
|
+
The SDK provides several built-in storage drivers to help you persist session state and tokens. Choose the driver that fits your environment, target lifetime, and runtime constraints (Client vs. Server).
|
|
1110
|
+
|
|
1111
|
+
| Storage | Export | Persists Across | Environment | Default |
|
|
1112
|
+
| :--- | :--- | :--- | :--- | :--- |
|
|
1113
|
+
| **`localStorage`** | `createLocalStorage()` | Browser restarts | Client-side only | `storage` (client) |
|
|
1114
|
+
| **`sessionStorage`** | `createSessionStorage()` | Tab lifetime | Client-side only | `stateStorage` (client) |
|
|
1115
|
+
| **`IndexedDB`** | `createIndexedDBStorage()` | Browser restarts (larger quota) | Client-side only | - |
|
|
1116
|
+
| **`Cache API`** | `createCacheAPIStorage()` | Browser restarts (works in Service Workers too) | Client-side only | - |
|
|
1117
|
+
| **`Memory`** | `createMemoryStorage()` | Page / Process lifetime | Client-side & Server-side | - |
|
|
1118
|
+
| **`Worker`** | `createWorkerStorage(worker)` | Depends on the backing storage inside the Worker | Client-side & Server-side | - |
|
|
1119
|
+
| **`globalStorage`** | `createServerStateStorage()` | Process/Server runtime lifetime | Server-side only | `stateStorage` (server) |
|
|
1120
|
+
| **`cookieStorage`** | `createEncryptedCookieStorage()` | Cookie expiration / Browser restarts | Server-side only | `storage` (server) |
|
|
1121
|
+
| **`sessionIdStorage`** | `createSessionIdCookieStorage()` | Session cookie / Browser restarts | Server-side only | `storage` (server) |
|
|
1122
|
+
|
|
1123
|
+
#### Session Storages
|
|
1124
|
+
|
|
1125
|
+
Tokens and session data can be configured depending on your security and persistence needs. Swap out the default by passing any of the built-in factory functions - or any custom implementation of `SDKStorage` (`get`, `set`, `delete`) - as the `storage` option in `initFlow` (client) or when creating the server SDK.
|
|
1126
|
+
|
|
1127
|
+
#### State storages
|
|
1128
|
+
|
|
1129
|
+
The SDK stores login state parameters separately from session tokens. Override `stateStorage` independently from `storage`:
|
|
1130
|
+
|
|
1131
|
+
```ts
|
|
1132
|
+
import { initFlow } from '@strivacity/sdk-core';
|
|
1133
|
+
import { createSessionStorage } from '@strivacity/sdk-core/storages';
|
|
1134
|
+
|
|
1135
|
+
const sdk = initFlow({
|
|
1136
|
+
mode: 'redirect',
|
|
1137
|
+
issuer: 'https://<YOUR_TENANT_DOMAIN>',
|
|
1138
|
+
clientId: 'YOUR_CLIENT_ID',
|
|
1139
|
+
redirectUri: 'https://your-app.example.com/callback',
|
|
1140
|
+
scopes: ['openid', 'profile', 'email'],
|
|
1141
|
+
storage: createLocalStorage(), // session tokens in localStorage
|
|
1142
|
+
stateStorage: createSessionStorage(), // OAuth2 state in sessionStorage (cleared when tab closes)
|
|
1143
|
+
});
|
|
1144
|
+
```
|
|
1145
|
+
|
|
1146
|
+
**Why separate state storage?**
|
|
1147
|
+
|
|
1148
|
+
Login state parameters are temporary - they are only needed between starting the flow and completing the callback. Using `sessionStorage` (client-side) or `globalStorage` (server-side) for state improves security by ensuring these transient values do not persist across browser sessions, while tokens can safely remain in `localStorage` for persistent authentication.
|
|
1149
|
+
|
|
1150
|
+
Any storage implementing `SDKStorage` can be used for state storage.
|
|
1151
|
+
|
|
1152
|
+
#### Advanced Storage Configurations
|
|
1153
|
+
|
|
1154
|
+
Beyond standard drivers, the SDK supports isolated runtime environments and custom storage contracts.
|
|
1155
|
+
|
|
1156
|
+
##### Session-id Cookie Storage
|
|
1157
|
+
|
|
1158
|
+
Server-side only. `createSessionIdCookieStorage(adapter, storage, options?)` puts only a small, random session-id cookie on the client and delegates the actual session payload to the `storage` you provide - keeping large sessions out of cookies entirely, and enabling [back-channel logout](#server-backchannel-logout) as long as `storage` implements `deleteByLogoutToken`:
|
|
1159
|
+
|
|
1160
|
+
```ts
|
|
1161
|
+
// server.ts
|
|
1162
|
+
import { createBaseServerSDK } from '@strivacity/sdk-core/server';
|
|
1163
|
+
import { createSessionIdCookieStorage, createServerMemoryStorage } from '@strivacity/sdk-core/storages';
|
|
1164
|
+
import { toWebRequest } from './utils';
|
|
1165
|
+
|
|
1166
|
+
const adapter = { toRequest: (req) => toWebRequest(req) };
|
|
1167
|
+
|
|
1168
|
+
const sdk = createBaseServerSDK(adapter, {
|
|
1169
|
+
// ...
|
|
1170
|
+
storage: createSessionIdCookieStorage(adapter, createServerMemoryStorage()),
|
|
1171
|
+
});
|
|
1172
|
+
```
|
|
1173
|
+
|
|
1174
|
+
> See [Built-in session storages](#server-storages) for more details, including using an external store (e.g. Redis) instead of `createServerMemoryStorage()`.
|
|
1175
|
+
|
|
1176
|
+
##### Worker Storage
|
|
1177
|
+
|
|
1178
|
+
For enhanced security isolation, you can offload all storage operations to a Web Worker via `postMessage`, effectively keeping sensitive tokens off the main thread. Simply instantiate the Worker and bridge it using `handleWorkerStorageRequests(storage)` inside the worker context:
|
|
1179
|
+
|
|
1180
|
+
```ts
|
|
1181
|
+
// storage.worker.ts
|
|
1182
|
+
import { handleWorkerStorageRequests, createIndexedDBStorage } from '@strivacity/sdk-core/storages';
|
|
1183
|
+
|
|
1184
|
+
handleWorkerStorageRequests(createIndexedDBStorage());
|
|
1185
|
+
```
|
|
1186
|
+
|
|
1187
|
+
```ts
|
|
1188
|
+
// sdk.ts
|
|
1189
|
+
import { initFlow } from '@strivacity/sdk-core';
|
|
1190
|
+
import { createWorkerStorage } from '@strivacity/sdk-core/storages';
|
|
1191
|
+
|
|
1192
|
+
const worker = new Worker(new URL('./storage.worker.ts', import.meta.url), { type: 'module' });
|
|
1193
|
+
|
|
1194
|
+
const sdk = initFlow({
|
|
1195
|
+
// ...
|
|
1196
|
+
storage: createWorkerStorage(worker),
|
|
1197
|
+
});
|
|
1198
|
+
```
|
|
1199
|
+
|
|
1200
|
+
---
|
|
1201
|
+
|
|
1202
|
+
### Session state
|
|
1203
|
+
|
|
1204
|
+
All flow instances expose the same set of getters:
|
|
1205
|
+
|
|
1206
|
+
```ts
|
|
1207
|
+
// Async - waits for init, optionally auto-refreshes an expired token
|
|
1208
|
+
const isAuthenticated: boolean = await sdk.isAuthenticated;
|
|
1209
|
+
|
|
1210
|
+
// Sync - true only if a non-expired session is already in memory
|
|
1211
|
+
const isAuthenticated: boolean = sdk.isAuthenticatedSync;
|
|
1212
|
+
|
|
1213
|
+
// Token values (available synchronously after init)
|
|
1214
|
+
const accessToken: string | null = sdk.accessToken;
|
|
1215
|
+
const refreshToken: string | null = sdk.refreshToken;
|
|
1216
|
+
const idTokenClaims: IdTokenClaims | null = sdk.idTokenClaims;
|
|
1217
|
+
|
|
1218
|
+
// Token expiration info (available synchronously after init)
|
|
1219
|
+
const expired: boolean = sdk.accessTokenExpired;
|
|
1220
|
+
const expiresAt: number | null = sdk.accessTokenExpirationDate; // Unix seconds
|
|
1221
|
+
|
|
1222
|
+
// Session metadata (available in embedded and native modes)
|
|
1223
|
+
const sessionId: string | null = sdk.sessionId; // current session identifier
|
|
1224
|
+
const shortAppId: string | null = sdk.shortAppId; // short application identifier
|
|
1225
|
+
const language: string = sdk.language; // current UI language code
|
|
1226
|
+
|
|
1227
|
+
// Verify authentication status (auto-refreshes an expired token if a refresh token is available)
|
|
1228
|
+
const ok: boolean = await sdk.checkAuthentication();
|
|
1229
|
+
// Verify authentication status without attempting a refresh
|
|
1230
|
+
const ok: boolean = await sdk.checkAuthentication({ autoRefresh: false });
|
|
1231
|
+
```
|
|
1232
|
+
|
|
1233
|
+
---
|
|
1234
|
+
|
|
1235
|
+
### SDK events
|
|
1236
|
+
|
|
1237
|
+
Subscribe to authentication lifecycle events via `subscribeToEvent` (one specific event) or `subscribeToAllEvents` (all events). Both return a `{ dispose() }` handle - call `dispose()` to unsubscribe.
|
|
1238
|
+
|
|
1239
|
+
| Event | Payload | When it fires |
|
|
1240
|
+
| -------------------- | --------------------------------------- | ------------------------------------------------------------------------- |
|
|
1241
|
+
| `init` | - | SDK has finished initializing |
|
|
1242
|
+
| `sessionLoaded` | `{ idToken, accessToken, refreshToken, claims }` | An existing session was read from storage on startup |
|
|
1243
|
+
| `sessionUpdated` | `{ idToken, accessToken, refreshToken, claims }` | The session was updated (e.g., after a token refresh) |
|
|
1244
|
+
| `sessionCleared` | - | The session was cleared (e.g., after logout or token revocation) |
|
|
1245
|
+
| `loginInitiated` | - | A login or registration redirect / popup has started |
|
|
1246
|
+
| `flowInitiated` | `{ sessionId, shortAppId, language }` | A native or embedded flow has started |
|
|
1247
|
+
| `loggedIn` | `{ idToken, accessToken, refreshToken, claims }` | Tokens were received and stored after a successful login |
|
|
1248
|
+
| `logoutInitiated` | `{ idToken, claims }` | Logout was initiated, before the redirect to the IDP end-session endpoint |
|
|
1249
|
+
| `tokenRefreshed` | `{ idToken, accessToken, refreshToken, claims }` | Access token was silently refreshed |
|
|
1250
|
+
| `tokenRefreshFailed` | `{ refreshToken }` | A token refresh attempt failed (refresh token may be expired) |
|
|
1251
|
+
| `accessTokenExpired` | `{ accessToken, refreshToken }` | The stored access token has passed its expiration time |
|
|
1252
|
+
| `tokenRevoked` | `{ token, tokenTypeHint }` | A token was successfully revoked at the authorization server |
|
|
1253
|
+
| `tokenRevokeFailed` | `{ token, tokenTypeHint }` | A token revocation attempt failed |
|
|
1254
|
+
|
|
1255
|
+
```ts
|
|
1256
|
+
const sub = sdk.subscribeToEvent('tokenRefreshed', ({ accessToken }) => {
|
|
1257
|
+
console.log('Token refreshed:', accessToken);
|
|
1258
|
+
});
|
|
1259
|
+
|
|
1260
|
+
// Unsubscribe
|
|
1261
|
+
sub.dispose();
|
|
1262
|
+
|
|
1263
|
+
// Subscribe to all events with a single callback
|
|
1264
|
+
const sub = sdk.subscribeToAllEvents((...args) => {
|
|
1265
|
+
console.log('SDK event:', args);
|
|
1266
|
+
});
|
|
1267
|
+
|
|
1268
|
+
sub.dispose();
|
|
1269
|
+
```
|
|
1270
|
+
|
|
1271
|
+
---
|
|
1272
|
+
|
|
1273
|
+
### Logging
|
|
1274
|
+
|
|
1275
|
+
#### Built-in logger
|
|
1276
|
+
|
|
1277
|
+
Call `createDefaultLogging()` to get a console logger with per-request correlation ID support:
|
|
1278
|
+
|
|
1279
|
+
```ts
|
|
1280
|
+
import { initFlow } from '@strivacity/sdk-core';
|
|
1281
|
+
import { createDefaultLogging } from '@strivacity/sdk-core/utils';
|
|
1282
|
+
|
|
1283
|
+
const sdk = initFlow({
|
|
1284
|
+
// ...
|
|
1285
|
+
logging: createDefaultLogging(),
|
|
1286
|
+
});
|
|
1287
|
+
```
|
|
1288
|
+
|
|
1289
|
+
#### Custom logger
|
|
1290
|
+
|
|
1291
|
+
Use a custom logger when you need to integrate SDK logs with your application's logging infrastructure (e.g., Sentry, Datadog, Winston) or when you want to filter, format, or route logs differently than the console.
|
|
1292
|
+
|
|
1293
|
+
Create a factory function that returns an object implementing the `SDKLogging` interface:
|
|
1294
|
+
|
|
1295
|
+
```ts
|
|
1296
|
+
import type { SDKLogging } from '@strivacity/sdk-core/types';
|
|
1297
|
+
|
|
1298
|
+
export function createMyLogger(): SDKLogging {
|
|
1299
|
+
const logger: SDKLogging = {
|
|
1300
|
+
/** Set by the SDK per request; use to correlate related log lines */
|
|
1301
|
+
xEventId: undefined,
|
|
1302
|
+
|
|
1303
|
+
debug(message: string): void {
|
|
1304
|
+
const msg = logger.xEventId ? `[${logger.xEventId}] ${message}` : message;
|
|
1305
|
+
console.debug(msg);
|
|
1306
|
+
},
|
|
1307
|
+
|
|
1308
|
+
info(message: string): void {
|
|
1309
|
+
const msg = logger.xEventId ? `[${logger.xEventId}] ${message}` : message;
|
|
1310
|
+
console.info(msg);
|
|
1311
|
+
},
|
|
1312
|
+
|
|
1313
|
+
warn(message: string): void {
|
|
1314
|
+
const msg = logger.xEventId ? `[${logger.xEventId}] ${message}` : message;
|
|
1315
|
+
console.warn(msg);
|
|
1316
|
+
},
|
|
1317
|
+
|
|
1318
|
+
error(message: string, error?: unknown): void {
|
|
1319
|
+
const msg = logger.xEventId ? `[${logger.xEventId}] ${message}` : message;
|
|
1320
|
+
console.error(msg, error);
|
|
1321
|
+
},
|
|
1322
|
+
};
|
|
1323
|
+
|
|
1324
|
+
return logger;
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
const sdk = initFlow({
|
|
1328
|
+
// ...
|
|
1329
|
+
logging: createMyLogger(),
|
|
1330
|
+
});
|
|
1331
|
+
```
|
|
1332
|
+
|
|
1333
|
+
---
|
|
1334
|
+
|
|
1335
|
+
### HTTP client
|
|
1336
|
+
|
|
1337
|
+
The SDK uses `fetch` for all requests. Replace it by implementing the `SDKHttpClient` interface and passing an instance via `httpClient` (`request` and `sendTokenRequest` are both required, so it's easiest to wrap the built-in `createHttpClient()` and override just what you need). Useful for attaching custom headers to every request, routing traffic through a proxy, or using a platform-specific transport (e.g. Capacitor's `CapacitorHttp`).
|
|
1338
|
+
|
|
1339
|
+
```ts
|
|
1340
|
+
import type { SDKHttpClient, HttpClientResponse } from '@strivacity/sdk-core/types';
|
|
1341
|
+
import { initFlow } from '@strivacity/sdk-core';
|
|
1342
|
+
import { createHttpClient } from '@strivacity/sdk-core/utils';
|
|
1343
|
+
|
|
1344
|
+
function createCustomHttpClient(): SDKHttpClient {
|
|
1345
|
+
const base = createHttpClient();
|
|
1346
|
+
|
|
1347
|
+
return {
|
|
1348
|
+
...base,
|
|
1349
|
+
async request<T>(url: string | URL, options?: RequestInit): Promise<HttpClientResponse<T>> {
|
|
1350
|
+
return base.request<T>(url, {
|
|
1351
|
+
...options,
|
|
1352
|
+
headers: {
|
|
1353
|
+
'x-sty-app-id': 'my-app',
|
|
1354
|
+
...(options?.headers as Record<string, string>),
|
|
1355
|
+
},
|
|
1356
|
+
});
|
|
1357
|
+
},
|
|
1358
|
+
};
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
const sdk = initFlow({
|
|
1362
|
+
// ...
|
|
1363
|
+
httpClient: createCustomHttpClient(),
|
|
1364
|
+
});
|
|
1365
|
+
```
|
|
1366
|
+
|
|
1367
|
+
> **CORS note:** custom request headers must be explicitly listed in the Strivacity cluster's `Access-Control-Allow-Headers` configuration - otherwise the browser blocks the preflight `OPTIONS` request.
|
|
1368
|
+
|
|
1369
|
+
---
|
|
1370
|
+
|
|
1371
|
+
## Advanced
|
|
1372
|
+
|
|
1373
|
+
Advanced SDK customization patterns for specialized use cases.
|
|
1374
|
+
|
|
1375
|
+
### Custom flow
|
|
1376
|
+
|
|
1377
|
+
For cases where none of the built-in modes fit-for example, when all authentication traffic must go through your own backend server in a specific way - you can build a completely custom flow on top of `createBaseFlow`.
|
|
1378
|
+
|
|
1379
|
+
`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
|
+
|
|
1381
|
+
Wire it up through `initFlow` by passing your factory function as the `factory` option - when present, `initFlow` calls it with the resolved options instead of dispatching to the built-in `redirect`/`popup`/`embedded`/`native` flows.
|
|
1382
|
+
|
|
1383
|
+
```ts
|
|
1384
|
+
import type { SDKInitConfig, SDKOptions } from '@strivacity/sdk-core/types';
|
|
1385
|
+
import { getDefaultFlowState, getSDKOptions } from '@strivacity/sdk-core/utils';
|
|
1386
|
+
import { createBaseFlow } from '@strivacity/sdk-core/flows/base';
|
|
1387
|
+
|
|
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>,
|
|
1394
|
+
// Add your own methods here
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
export function createCustomFlow(initConfig: SDKInitConfig): CustomFlow {
|
|
1398
|
+
const state = getDefaultFlowState();
|
|
1399
|
+
const options = getSDKOptions<SDKOptions>(state, initConfig);
|
|
1400
|
+
const base = createBaseFlow(state, options);
|
|
1401
|
+
|
|
1402
|
+
async function login(params: Record<string, unknown> = {}): Promise<void> {
|
|
1403
|
+
await base.init();
|
|
1404
|
+
|
|
1405
|
+
const response = await fetch('/api/auth/login', {
|
|
1406
|
+
method: 'POST',
|
|
1407
|
+
credentials: 'include',
|
|
1408
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1409
|
+
body: JSON.stringify(params),
|
|
1410
|
+
});
|
|
1411
|
+
|
|
1412
|
+
if (!response.ok) {
|
|
1413
|
+
throw new Error('Login not started');
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
async function refresh(): Promise<void> {
|
|
1418
|
+
const response = await fetch('/api/auth/refresh', {
|
|
1419
|
+
method: 'POST',
|
|
1420
|
+
credentials: 'include',
|
|
1421
|
+
});
|
|
1422
|
+
|
|
1423
|
+
if (!response.ok) {
|
|
1424
|
+
await base.cleanupSession();
|
|
1425
|
+
base.dispatchEvent('tokenRefreshFailed', [{}]);
|
|
1426
|
+
return;
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
const session = await response.json();
|
|
1430
|
+
await base.updateSession(session);
|
|
1431
|
+
base.dispatchEvent('tokenRefreshed', [{ accessToken: session.access_token, claims: session.claims }]);
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
async function revoke(): Promise<void> {
|
|
1435
|
+
const response = await fetch('/api/auth/revoke', {
|
|
1436
|
+
method: 'POST',
|
|
1437
|
+
credentials: 'include',
|
|
1438
|
+
});
|
|
1439
|
+
|
|
1440
|
+
if (!response.ok) {
|
|
1441
|
+
await base.cleanupSession();
|
|
1442
|
+
base.dispatchEvent('tokenRevokeFailed', [{}]);
|
|
1443
|
+
return;
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
const session = await response.json();
|
|
1447
|
+
await base.updateSession(session);
|
|
1448
|
+
base.dispatchEvent('tokenRefreshed', [{ accessToken: session.access_token, claims: session.claims }]);
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
async function logout(): Promise<void> {
|
|
1452
|
+
await fetch('/api/auth/logout', { method: 'POST', credentials: 'include' });
|
|
1453
|
+
await base.cleanupSession();
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
return {
|
|
1457
|
+
init: base.init,
|
|
1458
|
+
login,
|
|
1459
|
+
refresh,
|
|
1460
|
+
revoke,
|
|
1461
|
+
logout,
|
|
1462
|
+
// Add your own methods here
|
|
1463
|
+
};
|
|
1464
|
+
}
|
|
1465
|
+
```
|
|
1466
|
+
|
|
1467
|
+
#### Usage:
|
|
1468
|
+
|
|
1469
|
+
You can call the factory directly:
|
|
1470
|
+
|
|
1471
|
+
```ts
|
|
1472
|
+
import { createCustomFlow } from './custom-flow';
|
|
1473
|
+
|
|
1474
|
+
const sdk = createCustomFlow({
|
|
1475
|
+
mode: 'native',
|
|
1476
|
+
issuer: 'https://<YOUR_TENANT_DOMAIN>',
|
|
1477
|
+
clientId: 'YOUR_CLIENT_ID',
|
|
1478
|
+
redirectUri: 'https://your-app.example.com/callback',
|
|
1479
|
+
scopes: ['openid', 'profile'],
|
|
1480
|
+
});
|
|
1481
|
+
|
|
1482
|
+
await sdk.login();
|
|
1483
|
+
```
|
|
1484
|
+
|
|
1485
|
+
Or wire it through `initFlow` via the `factory` option:
|
|
1486
|
+
|
|
1487
|
+
```ts
|
|
1488
|
+
import { initFlow } from '@strivacity/sdk-core';
|
|
1489
|
+
import { createCustomFlow } from './custom-flow';
|
|
1490
|
+
|
|
1491
|
+
const sdk = initFlow<CustomFlow>({
|
|
1492
|
+
mode: 'native',
|
|
1493
|
+
issuer: 'https://<YOUR_TENANT_DOMAIN>',
|
|
1494
|
+
clientId: 'YOUR_CLIENT_ID',
|
|
1495
|
+
redirectUri: 'https://your-app.example.com/callback',
|
|
1496
|
+
scopes: ['openid', 'profile'],
|
|
1497
|
+
factory: createCustomFlow,
|
|
1498
|
+
});
|
|
1499
|
+
|
|
1500
|
+
await sdk.login();
|
|
1501
|
+
```
|
|
1502
|
+
|
|
1503
|
+
---
|
|
1504
|
+
|
|
1505
|
+
### Error handling
|
|
1506
|
+
|
|
1507
|
+
The SDK exports specialized error classes to help you handle different failure scenarios. All SDK errors extend the base `Error` class and include additional context.
|
|
1508
|
+
|
|
1509
|
+
#### Unsupported flow error
|
|
1510
|
+
|
|
1511
|
+
`initFlow()` throws `UnsupportedFlowError` when `mode` is missing or isn't one of `'redirect' | 'popup' | 'embedded' | 'native'`. TypeScript consumers get this caught at compile time, but it can still happen at runtime for plain JS consumers or when `mode` comes from an untyped source (e.g. an environment variable or CMS config):
|
|
1512
|
+
|
|
1513
|
+
```ts
|
|
1514
|
+
import { initFlow } from '@strivacity/sdk-core';
|
|
1515
|
+
import { UnsupportedFlowError } from '@strivacity/sdk-core/utils';
|
|
1516
|
+
|
|
1517
|
+
try {
|
|
1518
|
+
const sdk = initFlow({
|
|
1519
|
+
mode: mode as never, // e.g. loaded from an environment variable - could be an invalid value
|
|
1520
|
+
issuer: 'https://<YOUR_TENANT_DOMAIN>',
|
|
1521
|
+
clientId: 'YOUR_CLIENT_ID',
|
|
1522
|
+
redirectUri: 'https://your-app.example.com/callback',
|
|
1523
|
+
});
|
|
1524
|
+
} catch (error) {
|
|
1525
|
+
if (error instanceof UnsupportedFlowError) {
|
|
1526
|
+
console.error(`Invalid mode: "${mode}". Must be one of: redirect, popup, embedded, native`);
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
```
|
|
1530
|
+
|
|
1531
|
+
#### User interaction errors
|
|
1532
|
+
|
|
1533
|
+
These errors occur when the user cancels or blocks the authentication flow:
|
|
1534
|
+
|
|
1535
|
+
```ts
|
|
1536
|
+
import { PopupBlockedError, PopupClosedError } from '@strivacity/sdk-core/utils';
|
|
1537
|
+
|
|
1538
|
+
try {
|
|
1539
|
+
await sdk.login();
|
|
1540
|
+
} catch (error) {
|
|
1541
|
+
if (error instanceof PopupBlockedError) {
|
|
1542
|
+
// Browser blocked the popup - show instructions to allow popups
|
|
1543
|
+
console.error('Please allow popups for this site');
|
|
1544
|
+
} else if (error instanceof PopupClosedError) {
|
|
1545
|
+
// User closed the popup before completing login
|
|
1546
|
+
console.error('Login cancelled by user');
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
```
|
|
1550
|
+
|
|
1551
|
+
#### SDK state errors
|
|
1552
|
+
|
|
1553
|
+
The SDK may throw `SessionExpiredError` when attempting operations with an expired session:
|
|
1554
|
+
|
|
1555
|
+
```ts
|
|
1556
|
+
import { SessionExpiredError } from '@strivacity/sdk-core/utils';
|
|
1557
|
+
|
|
1558
|
+
// Example: Session expired during a native flow
|
|
1559
|
+
try {
|
|
1560
|
+
const state = await sdk.startSession();
|
|
1561
|
+
// ... user fills in the form ...
|
|
1562
|
+
const nextState = await sdk.submitForm(state.screen, formData);
|
|
1563
|
+
} catch (error) {
|
|
1564
|
+
if (error instanceof SessionExpiredError) {
|
|
1565
|
+
// Session expired during the flow - try to reload the page
|
|
1566
|
+
}
|
|
1567
|
+
}
|
|
1568
|
+
```
|
|
1569
|
+
|
|
1570
|
+
#### Network and server errors
|
|
1571
|
+
|
|
1572
|
+
These errors have a `recoverable` flag indicating whether retrying might succeed:
|
|
1573
|
+
|
|
1574
|
+
```ts
|
|
1575
|
+
import { NetworkError, ServerError, OidcError, ProtocolError } from '@strivacity/sdk-core/utils';
|
|
1576
|
+
|
|
1577
|
+
try {
|
|
1578
|
+
await sdk.login();
|
|
1579
|
+
} catch (error) {
|
|
1580
|
+
if (error instanceof NetworkError) {
|
|
1581
|
+
// Network request failed (offline, timeout, etc.)
|
|
1582
|
+
// error.recoverable === true - retry might work
|
|
1583
|
+
console.error('Network error - retrying...', error.message);
|
|
1584
|
+
} else if (error instanceof ServerError) {
|
|
1585
|
+
// Server returned 5xx error
|
|
1586
|
+
// error.recoverable === true - temporary server issue
|
|
1587
|
+
// error.status contains HTTP status code
|
|
1588
|
+
console.error(`Server error ${error.status}:`, error.message);
|
|
1589
|
+
} else if (error instanceof OidcError) {
|
|
1590
|
+
// OIDC protocol error from authorization server
|
|
1591
|
+
// error.recoverable === false - don't retry
|
|
1592
|
+
// error.error contains OIDC error code
|
|
1593
|
+
// error.errorDescription contains human-readable description
|
|
1594
|
+
console.error(`OIDC error ${error.error}:`, error.errorDescription);
|
|
1595
|
+
} else if (error instanceof ProtocolError) {
|
|
1596
|
+
// Protocol violation or invalid response
|
|
1597
|
+
// error.recoverable === false
|
|
1598
|
+
console.error('Protocol error:', error.message);
|
|
1599
|
+
}
|
|
1600
|
+
}
|
|
1601
|
+
```
|
|
1602
|
+
|
|
1603
|
+
#### Error categories
|
|
1604
|
+
|
|
1605
|
+
All network/server errors include a `category` property for easier error handling:
|
|
1606
|
+
|
|
1607
|
+
```ts
|
|
1608
|
+
try {
|
|
1609
|
+
await sdk.refresh();
|
|
1610
|
+
} catch (error) {
|
|
1611
|
+
if (error.category === 'Network') {
|
|
1612
|
+
// Retry with exponential backoff
|
|
1613
|
+
} else if (error.category === 'Server') {
|
|
1614
|
+
// Show maintenance message
|
|
1615
|
+
} else if (error.category === 'Oidc') {
|
|
1616
|
+
// Invalid configuration or expired session
|
|
1617
|
+
} else if (error.category === 'Protocol') {
|
|
1618
|
+
// Invalid response from server
|
|
1619
|
+
}
|
|
1620
|
+
}
|
|
1621
|
+
```
|
|
1622
|
+
|
|
1623
|
+
**Error categories:**
|
|
1624
|
+
- `Network` - Network connectivity issues (recoverable)
|
|
1625
|
+
- `Server` - Server errors 5xx (recoverable)
|
|
1626
|
+
- `Oidc` - OIDC protocol errors from authorization server (not recoverable)
|
|
1627
|
+
- `Protocol` - Protocol violations or invalid responses (not recoverable)
|
|
1628
|
+
- `ConfigurationError` - Invalid SDK configuration (not recoverable)
|
|
1629
|
+
- `Internal` - Unexpected internal SDK errors (not recoverable)
|
|
1630
|
+
|
|
1631
|
+
---
|
|
1632
|
+
|
|
1633
|
+
### Utility functions
|
|
1634
|
+
|
|
1635
|
+
The SDK exports several utility functions for advanced use cases. These are primarily used internally but can be useful for custom integrations.
|
|
1636
|
+
|
|
1637
|
+
#### Base64URL encoding
|
|
1638
|
+
|
|
1639
|
+
Base64URL is a URL-safe variant of Base64 encoding used in OIDC and JWT:
|
|
1640
|
+
|
|
1641
|
+
```ts
|
|
1642
|
+
import { encodeBase64URL, decodeBase64URL } from '@strivacity/sdk-core/utils';
|
|
1643
|
+
|
|
1644
|
+
// Encode ArrayBuffer to Base64URL string
|
|
1645
|
+
const buffer = new TextEncoder().encode('Hello, World!');
|
|
1646
|
+
const encoded = encodeBase64URL(buffer.buffer);
|
|
1647
|
+
console.log(encoded); // 'SGVsbG8sIFdvcmxkIQ'
|
|
1648
|
+
|
|
1649
|
+
// Decode Base64URL string to ArrayBuffer
|
|
1650
|
+
const decoded = decodeBase64URL(encoded);
|
|
1651
|
+
const text = new TextDecoder().decode(decoded);
|
|
1652
|
+
console.log(text); // 'Hello, World!'
|
|
1653
|
+
```
|
|
1654
|
+
|
|
1655
|
+
#### String encryption/decryption
|
|
1656
|
+
|
|
1657
|
+
AES-GCM encryption with HKDF key derivation for encrypting sensitive data:
|
|
1658
|
+
|
|
1659
|
+
```ts
|
|
1660
|
+
import { encryptString, decryptString } from '@strivacity/sdk-core/utils';
|
|
1661
|
+
|
|
1662
|
+
const secret = 'your-encryption-secret';
|
|
1663
|
+
const context = 'user-session'; // binds key to specific purpose
|
|
1664
|
+
|
|
1665
|
+
// Encrypt a string
|
|
1666
|
+
const plaintext = 'sensitive data';
|
|
1667
|
+
const encrypted = await encryptString(plaintext, secret, context);
|
|
1668
|
+
console.log(encrypted); // Base64URL-encoded: salt(16) + iv(12) + ciphertext
|
|
1669
|
+
|
|
1670
|
+
// Decrypt a string
|
|
1671
|
+
const decrypted = await decryptString(encrypted, secret, context);
|
|
1672
|
+
console.log(decrypted); // 'sensitive data' or null if decryption fails
|
|
1673
|
+
```
|
|
1674
|
+
|
|
1675
|
+
The encryption format is: `Base64URL(salt || iv || ciphertext)` where:
|
|
1676
|
+
- `salt` (16 bytes) - random salt for HKDF key derivation
|
|
1677
|
+
- `iv` (12 bytes) - random initialization vector for AES-GCM
|
|
1678
|
+
- `ciphertext` - encrypted data
|
|
1679
|
+
|
|
1680
|
+
> **Note:** This is the same encryption used internally by the Server SDK for http-only cookies.
|
|
1681
|
+
|
|
1682
|
+
#### JWT decoding
|
|
1683
|
+
|
|
1684
|
+
Decode JWT tokens without verification (useful for debugging or extracting claims):
|
|
1685
|
+
|
|
1686
|
+
```ts
|
|
1687
|
+
import { decodeJwt } from '@strivacity/sdk-core/utils';
|
|
1688
|
+
|
|
1689
|
+
const idToken = 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...';
|
|
1690
|
+
const claims = decodeJwt<{ sub: string; email: string }>(idToken);
|
|
1691
|
+
|
|
1692
|
+
console.log(claims.sub); // user ID
|
|
1693
|
+
console.log(claims.email); // user email
|
|
1694
|
+
```
|
|
1695
|
+
|
|
1696
|
+
> **Warning:** `decodeJwt` does NOT verify the signature. Use it only for debugging or when the token has already been verified by the SDK.
|
|
1697
|
+
|
|
1698
|
+
#### JWT verification
|
|
1699
|
+
|
|
1700
|
+
Verify and decode JWT tokens using RS256 signature verification with JWKS:
|
|
1701
|
+
|
|
1702
|
+
```ts
|
|
1703
|
+
import type { SDKOptions } from '@strivacity/sdk-core/types';
|
|
1704
|
+
import { verifyJwt } from '@strivacity/sdk-core/utils';
|
|
1705
|
+
|
|
1706
|
+
// Verify an access token or ID token from an external source
|
|
1707
|
+
const token = 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...';
|
|
1708
|
+
const jwksUri = 'https://<YOUR_TENANT_DOMAIN>/.well-known/jwks.json';
|
|
1709
|
+
|
|
1710
|
+
// You need SDK options for HTTP client and caching
|
|
1711
|
+
const options: SDKOptions = {
|
|
1712
|
+
mode: 'redirect',
|
|
1713
|
+
issuer: 'https://<YOUR_TENANT_DOMAIN>',
|
|
1714
|
+
clientId: 'YOUR_CLIENT_ID',
|
|
1715
|
+
redirectUri: 'https://your-app.example.com/callback',
|
|
1716
|
+
scopes: ['openid', 'profile', 'email'],
|
|
1717
|
+
};
|
|
1718
|
+
|
|
1719
|
+
try {
|
|
1720
|
+
const claims = await verifyJwt<{ sub: string; email: string }>({
|
|
1721
|
+
token,
|
|
1722
|
+
jwksUri,
|
|
1723
|
+
options,
|
|
1724
|
+
});
|
|
1725
|
+
|
|
1726
|
+
console.log('Token verified successfully');
|
|
1727
|
+
console.log(claims.sub); // user ID
|
|
1728
|
+
console.log(claims.email); // user email
|
|
1729
|
+
} catch (error) {
|
|
1730
|
+
console.error('Token verification failed:', error.message);
|
|
1731
|
+
// Throws ProtocolError if signature is invalid or token is malformed
|
|
1732
|
+
}
|
|
1733
|
+
```
|
|
1734
|
+
|
|
1735
|
+
**When to use `verifyJwt`:**
|
|
1736
|
+
- Verifying tokens received from external sources (e.g., webhooks, API callbacks)
|
|
1737
|
+
- Server-side token verification when building custom server flows
|
|
1738
|
+
|
|
1739
|
+
> **Note:** The SDK automatically verifies ID tokens during login and refresh flows. You only need `verifyJwt` when handling tokens from external sources.
|
|
1740
|
+
|
|
1741
|
+
#### PKCE code verifier generation
|
|
1742
|
+
|
|
1743
|
+
Generate a cryptographically secure PKCE code verifier:
|
|
1744
|
+
|
|
1745
|
+
```ts
|
|
1746
|
+
import { generateCodeVerifier, generateRandomHex } from '@strivacity/sdk-core/utils';
|
|
1747
|
+
|
|
1748
|
+
// Generate PKCE code verifier (43-128 characters, base64url-encoded)
|
|
1749
|
+
const codeVerifier = generateCodeVerifier();
|
|
1750
|
+
console.log(codeVerifier); // e.g., 'a3K8Bx...' (128 chars)
|
|
1751
|
+
|
|
1752
|
+
// Generate random hex string (useful for state parameters)
|
|
1753
|
+
const state = generateRandomHex(16); // 16 bytes = 32 hex chars
|
|
1754
|
+
console.log(state); // e.g., '5f3a8b2c4d1e9f7a6b8c3d2e1f4a5b6c'
|
|
1755
|
+
```
|
|
1756
|
+
|
|
1757
|
+
#### Script injection
|
|
1758
|
+
|
|
1759
|
+
Dynamically load external scripts (used internally for loading web components):
|
|
1760
|
+
|
|
1761
|
+
```ts
|
|
1762
|
+
import { injectScript } from '@strivacity/sdk-core/utils';
|
|
1763
|
+
|
|
1764
|
+
// Inject a script tag with the given ID and source URL
|
|
1765
|
+
injectScript('my-script', 'https://example.com/script.js');
|
|
1766
|
+
|
|
1767
|
+
// If a script with the same ID already exists, it won't be injected again
|
|
1768
|
+
```
|
|
1769
|
+
|
|
1770
|
+
---
|
|
1771
|
+
|
|
1772
|
+
## Configuration reference
|
|
1773
|
+
|
|
1774
|
+
| Option | Type | Required | Default | Description |
|
|
1775
|
+
| ------------------------ | ---------------------------------------------------- | -------- | ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
1776
|
+
| `mode` | `'redirect' \| 'popup' \| 'embedded' \| 'native'` | Yes | - | Authentication flow mode |
|
|
1777
|
+
| `issuer` | `string` | Yes | - | OIDC issuer URL of your Strivacity tenant |
|
|
1778
|
+
| `clientId` | `string` | Yes | - | OAuth2 public client ID |
|
|
1779
|
+
| `redirectUri` | `string` | Yes | - | OAuth2 redirect URI (must match your application configuration) |
|
|
1780
|
+
| `scopes` | `string[]` | No | `['openid']` | Requested OIDC scopes |
|
|
1781
|
+
| `responseType` | `'code'` | No | `'code'` | OAuth2 response type |
|
|
1782
|
+
| `responseMode` | `'query' \| 'fragment'` | No | `'query'` | OAuth2 response mode |
|
|
1783
|
+
| `storage` | `SDKStorage` | No | `localStorage` | [Session storage](#storages) |
|
|
1784
|
+
| `stateStorage` | `SDKStorage` | No | same as `storage` | OAuth2 PKCE / state storage |
|
|
1785
|
+
| `storageTokenName` | `string` | No | `'sty.session'` | Key under which the session is stored in the storage |
|
|
1786
|
+
| `autoRefresh` | `boolean` | No | `true` | Automatically refreshes the access token before it expires. Set to `false` to manage refresh manually |
|
|
1787
|
+
| `lazyLoad` | `boolean` | No | `false` | When `true`, defers initialization until the first method call and returns the instance synchronously |
|
|
1788
|
+
| `serverSessionUri` | `string` | No | - | Routes login requests through your server and enables server-managed sessions; see [Server-side session management](#server-side-session-management) |
|
|
1789
|
+
| `loginUri` | `string` | No | `'/login'` | URI of the app's login page; used in `embedded` and `native` modes to redirect the user when a new login is required |
|
|
1790
|
+
| `logging` | `SDKLogging` | No | - | Logging adapter; see [Logging](#logging) |
|
|
1791
|
+
| `httpClient` | `SDKHttpClient` | No | fetch | Custom HTTP client adapter; see [HTTP client](#http-client) |
|
|
1792
|
+
| `discoveryDocumentCacheTTL` | `number` | No | `3600` | OIDC metadata cache duration in seconds (1 hour by default) |
|
|
1793
|
+
| `jwksCacheTTL` | `number` | No | `600` | JWKS (JSON Web Key Set) cache duration in seconds (10 minutes by default) |
|
|
1794
|
+
| `urlHandler` | `(url, params?) => Promise<unknown>` | No | - | Custom handler for URL redirects (e.g. to integrate with a router instead of `window.location`) |
|
|
1795
|
+
| `callbackHandler` | `(url, responseMode?) => Promise<unknown>` | No | - | Custom handler for processing the authorization server callback URL |
|
|
1796
|
+
| `factory` | `(options: SDKInitConfig) => unknown` | No | - | Custom flow factory; when set, `initFlow` calls it with the options instead of the built-in flows - see [Custom flow](#custom-flow) |
|
|
1797
|
+
|
|
1798
|
+
---
|
|
1799
|
+
|
|
1800
|
+
### Configuration examples
|
|
1801
|
+
|
|
1802
|
+
#### urlHandler
|
|
1803
|
+
|
|
1804
|
+
Override the default redirect behavior (which uses `window.location`) to integrate with client-side routers or custom navigation logic.
|
|
1805
|
+
|
|
1806
|
+
```ts
|
|
1807
|
+
// Capacitor InAppBrowser example
|
|
1808
|
+
import { Capacitor } from '@capacitor/core';
|
|
1809
|
+
import { InAppBrowser } from '@capgo/inappbrowser';
|
|
1810
|
+
|
|
1811
|
+
const sdk = initFlow({
|
|
1812
|
+
// ... other config
|
|
1813
|
+
urlHandler: async (url) => {
|
|
1814
|
+
if (Capacitor.getPlatform() === 'web') {
|
|
1815
|
+
// On web: use standard window.location redirect
|
|
1816
|
+
window.location.href = url;
|
|
1817
|
+
} else {
|
|
1818
|
+
// On native (iOS/Android): open in InAppBrowser
|
|
1819
|
+
await InAppBrowser.openInWebView({
|
|
1820
|
+
url,
|
|
1821
|
+
options: { /* Configure InAppBrowser options*/ },
|
|
1822
|
+
});
|
|
1823
|
+
}
|
|
1824
|
+
},
|
|
1825
|
+
});
|
|
1826
|
+
```
|
|
1827
|
+
|
|
1828
|
+
#### callbackHandler
|
|
1829
|
+
|
|
1830
|
+
Override the default callback URL parsing to handle custom redirect flows or extract the callback URL from a non-standard location.
|
|
1831
|
+
|
|
1832
|
+
```ts
|
|
1833
|
+
// Capacitor InAppBrowser callback handling
|
|
1834
|
+
import { Capacitor } from '@capacitor/core';
|
|
1835
|
+
import { InAppBrowser } from '@capgo/inappbrowser';
|
|
1836
|
+
import type { PluginListenerHandle } from '@capacitor/core';
|
|
1837
|
+
|
|
1838
|
+
const sdk = initFlow({
|
|
1839
|
+
// ... other config
|
|
1840
|
+
callbackHandler: async (url, responseMode) => {
|
|
1841
|
+
if (Capacitor.getPlatform() === 'web') {
|
|
1842
|
+
// On web: use default callback handling
|
|
1843
|
+
const urlObj = new URL(window.location.href);
|
|
1844
|
+
const params = responseMode === 'fragment'
|
|
1845
|
+
? new URLSearchParams(urlObj.hash.slice(1))
|
|
1846
|
+
: urlObj.searchParams;
|
|
1847
|
+
|
|
1848
|
+
return {
|
|
1849
|
+
code: params.get('code'),
|
|
1850
|
+
state: params.get('state'),
|
|
1851
|
+
error: params.get('error'),
|
|
1852
|
+
error_description: params.get('error_description'),
|
|
1853
|
+
};
|
|
1854
|
+
}
|
|
1855
|
+
|
|
1856
|
+
// On native: listen for navigation events in InAppBrowser
|
|
1857
|
+
return new Promise(async (resolve, reject) => {
|
|
1858
|
+
let navigationListener: PluginListenerHandle | null = null;
|
|
1859
|
+
let finishListener: PluginListenerHandle | null = null;
|
|
1860
|
+
let userCancelled = true;
|
|
1861
|
+
|
|
1862
|
+
const cleanupListeners = async () => {
|
|
1863
|
+
if (navigationListener) {
|
|
1864
|
+
await navigationListener.remove();
|
|
1865
|
+
navigationListener = null;
|
|
1866
|
+
}
|
|
1867
|
+
if (finishListener) {
|
|
1868
|
+
await finishListener.remove();
|
|
1869
|
+
finishListener = null;
|
|
1870
|
+
}
|
|
1871
|
+
};
|
|
1872
|
+
|
|
1873
|
+
try {
|
|
1874
|
+
// Listen for page navigation in InAppBrowser
|
|
1875
|
+
navigationListener = await InAppBrowser.addListener(
|
|
1876
|
+
'browserPageNavigationCompleted',
|
|
1877
|
+
async (event) => {
|
|
1878
|
+
const navigatedUrl = event.url;
|
|
1879
|
+
|
|
1880
|
+
// Check if the navigated URL matches our callback URL
|
|
1881
|
+
if (navigatedUrl && navigatedUrl.startsWith(url)) {
|
|
1882
|
+
try {
|
|
1883
|
+
const urlInstance = new URL(navigatedUrl);
|
|
1884
|
+
const dataString = responseMode === 'query'
|
|
1885
|
+
? urlInstance.search
|
|
1886
|
+
: urlInstance.hash;
|
|
1887
|
+
const params = Object.fromEntries(
|
|
1888
|
+
new URLSearchParams(dataString.slice(1))
|
|
1889
|
+
);
|
|
1890
|
+
|
|
1891
|
+
userCancelled = false;
|
|
1892
|
+
await InAppBrowser.close();
|
|
1893
|
+
resolve(params);
|
|
1894
|
+
} catch (error) {
|
|
1895
|
+
await InAppBrowser.close();
|
|
1896
|
+
reject(error);
|
|
1897
|
+
}
|
|
1898
|
+
}
|
|
1899
|
+
}
|
|
1900
|
+
);
|
|
1901
|
+
|
|
1902
|
+
// Listen for browser close event
|
|
1903
|
+
finishListener = await InAppBrowser.addListener('browserClosed', async () => {
|
|
1904
|
+
await cleanupListeners();
|
|
1905
|
+
|
|
1906
|
+
if (userCancelled) {
|
|
1907
|
+
reject(new Error('InAppBrowser flow cancelled by user.'));
|
|
1908
|
+
}
|
|
1909
|
+
});
|
|
1910
|
+
} catch (error) {
|
|
1911
|
+
await cleanupListeners();
|
|
1912
|
+
reject(error);
|
|
1913
|
+
}
|
|
1914
|
+
});
|
|
1915
|
+
},
|
|
1916
|
+
});
|
|
1917
|
+
```
|
|
1918
|
+
|
|
1919
|
+
---
|
|
1920
|
+
|
|
1921
|
+
### Caching
|
|
1922
|
+
|
|
1923
|
+
The SDK automatically caches OIDC metadata and JWKS (JSON Web Key Set) to reduce network requests and improve performance.
|
|
1924
|
+
|
|
1925
|
+
#### Metadata caching
|
|
1926
|
+
|
|
1927
|
+
OIDC metadata (authorization endpoint, token endpoint, etc.) is fetched from the `issuer` URL during SDK initialization and cached for **1 hour**.
|
|
1928
|
+
|
|
1929
|
+
The cache is shared across all SDK instances with the same `issuer` URL and stored in memory for the lifetime of the application.
|
|
1930
|
+
|
|
1931
|
+
#### JWKS caching
|
|
1932
|
+
|
|
1933
|
+
The JSON Web Key Set (used for ID token signature verification) is fetched from the `jwks_uri` endpoint and cached for **10 minutes**.
|
|
1934
|
+
|
|
1935
|
+
```ts
|
|
1936
|
+
// First token verification: fetches JWKS from the IDP
|
|
1937
|
+
await sdk.handleCallback(); // Verifies ID token signature
|
|
1938
|
+
|
|
1939
|
+
// Within 10 minutes: reuses cached JWKS (no network request)
|
|
1940
|
+
await sdk.refresh(); // Verifies new ID token signature using cached keys
|
|
1941
|
+
|
|
1942
|
+
// After 10 minutes: refetches JWKS from the IDP
|
|
1943
|
+
await sdk.refresh(); // Fetches fresh keys if cache expired
|
|
1944
|
+
```
|
|
1945
|
+
|
|
1946
|
+
The JWKS cache is also shared across all SDK instances with the same `jwks_uri` and stored in memory.
|
|
1947
|
+
|
|
1948
|
+
#### Configuring cache durations
|
|
1949
|
+
|
|
1950
|
+
You can customize the cache durations by passing `discoveryDocumentCacheTTL` and `jwksCacheTTL` options (in seconds) during SDK initialization:
|
|
1951
|
+
|
|
1952
|
+
```ts
|
|
1953
|
+
const sdk = initFlow({
|
|
1954
|
+
mode: 'redirect',
|
|
1955
|
+
issuer: 'https://<YOUR_TENANT_DOMAIN>',
|
|
1956
|
+
clientId: 'YOUR_CLIENT_ID',
|
|
1957
|
+
redirectUri: 'https://your-app.example.com/callback',
|
|
1958
|
+
scopes: ['openid', 'profile', 'email'],
|
|
1959
|
+
discoveryDocumentCacheTTL: 7200, // Cache openID configuration for 2 hours instead of 1 hour
|
|
1960
|
+
jwksCacheTTL: 1800, // Cache JWKS for 30 minutes instead of 10 minutes
|
|
1961
|
+
});
|
|
1962
|
+
```
|
|
1963
|
+
|
|
1964
|
+
---
|
|
1965
|
+
|
|
1966
|
+
## Migration guide
|
|
1967
|
+
|
|
1968
|
+
### Migrating to v4.0
|
|
1969
|
+
|
|
1970
|
+
v4 replaces the SDK's class-based flow architecture with function-based architecture, and adds first-class support for server-managed (BFF) sessions. `initFlow` itself and all built-in `redirect`/`popup`/`embedded`/`native` flow methods (`login`, `register`, `handleCallback`, `refresh`, `revoke`, `logout`, `subscribeToEvent`, ...) are unchanged - only apps that used `mode: 'custom'` or drove `native` mode through the old `NativeFlowHandler` need to update their code.
|
|
1971
|
+
|
|
1972
|
+
#### Class-based flows replaced by functions
|
|
1973
|
+
|
|
1974
|
+
In v3, flows were classes (`RedirectFlow`, `PopupFlow`, `NativeFlow`, `EmbeddedFlow`), and the only way to customize behavior - for example, to proxy authentication through your own backend - was `mode: 'custom'` with a `customFlow` class that extended one of them and override its methods:
|
|
1975
|
+
|
|
1976
|
+
```ts
|
|
1977
|
+
// v3
|
|
1978
|
+
import { NativeFlow } from '@strivacity/sdk-core/flows/NativeFlow';
|
|
1979
|
+
|
|
1980
|
+
export class CustomNativeFlow extends NativeFlow {
|
|
1981
|
+
override async refresh(): Promise<void> {
|
|
1982
|
+
// ...
|
|
1983
|
+
}
|
|
1984
|
+
}
|
|
1985
|
+
|
|
1986
|
+
const sdk = initFlow({ mode: 'custom', customFlow: CustomNativeFlow /* ... */ });
|
|
1987
|
+
```
|
|
1988
|
+
|
|
1989
|
+
v4 removes `mode: 'custom'` and the `customFlow` option entirely, along with the flow classes and deep imports used to extend them. In their place, `createBaseFlow` 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:
|
|
1990
|
+
|
|
1991
|
+
```ts
|
|
1992
|
+
// v4
|
|
1993
|
+
import { createBaseFlow } from '@strivacity/sdk-core/flows/base';
|
|
1994
|
+
import { getDefaultFlowState, getSDKOptions } from '@strivacity/sdk-core/utils';
|
|
1995
|
+
|
|
1996
|
+
export function createCustomFlow(initConfig: SDKInitConfig) {
|
|
1997
|
+
const state = getDefaultFlowState();
|
|
1998
|
+
const options = getSDKOptions(state, initConfig);
|
|
1999
|
+
const base = createBaseFlow(state, options);
|
|
2000
|
+
|
|
2001
|
+
async function refresh(): Promise<void> {
|
|
2002
|
+
// ...
|
|
2003
|
+
}
|
|
2004
|
+
|
|
2005
|
+
return { ...base, refresh };
|
|
2006
|
+
}
|
|
2007
|
+
```
|
|
2008
|
+
|
|
2009
|
+
See [Custom flow](#custom-flow) above for the full pattern.
|
|
2010
|
+
|
|
2011
|
+
#### Server-managed sessions ([BFF](../../README.md#bff)) are now built in
|
|
2012
|
+
|
|
2013
|
+
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.
|
|
2014
|
+
|
|
2015
|
+
v4 replaces v3's approach with a first-class **Server SDK** and a single client-side option. Set `serverSessionUri` on your existing SDK options and the built-in `redirect`/`popup`/`embedded`/`native` flows automatically route login through your server (instead of the identity provider) with tokens kept server-side instead of in client storage:
|
|
2016
|
+
|
|
2017
|
+
```ts
|
|
2018
|
+
// v4
|
|
2019
|
+
const sdk = initFlow({
|
|
2020
|
+
mode: 'redirect',
|
|
2021
|
+
issuer: 'https://<YOUR_TENANT_DOMAIN>',
|
|
2022
|
+
clientId: 'YOUR_CLIENT_ID',
|
|
2023
|
+
redirectUri: 'https://your-app.example.com/callback',
|
|
2024
|
+
scopes: ['openid', 'profile', 'email'],
|
|
2025
|
+
serverSessionUri: '/api/auth/login', // requests are routed through your server; tokens are never written to client storage
|
|
2026
|
+
});
|
|
2027
|
+
```
|
|
2028
|
+
|
|
2029
|
+
Pair it with `createBaseServerSDK` from `@strivacity/sdk-core/server` on the backend - PKCE, state, and session storage are all handled by the Server SDK. See [Server-side session management](#server-side-session-management) and [Server SDK](#server-sdk) above, and the [backend example app](../../apps/backend) for a working reference.
|
|
2030
|
+
|
|
2031
|
+
#### Native mode: no more `NativeFlowHandler`
|
|
2032
|
+
|
|
2033
|
+
In v3, `native` mode's `login()`/`register()` returned a separate `NativeFlowHandler` instance, and the flow was driven through that handler:
|
|
2034
|
+
|
|
2035
|
+
```ts
|
|
2036
|
+
// v3
|
|
2037
|
+
const handler = await sdk.login();
|
|
2038
|
+
const state = await handler.startSession(sessionId);
|
|
2039
|
+
const nextState = await handler.submitForm('formId', { identifier: 'user@example.com' });
|
|
2040
|
+
await handler.finalizeSession(nextState.finalizeUrl);
|
|
2041
|
+
```
|
|
2042
|
+
|
|
2043
|
+
v4 moves `startSession()`, `submitForm()`, and `finalizeSession()` directly onto the flow instance itself - there's no separate handler object to manage:
|
|
2044
|
+
|
|
2045
|
+
```ts
|
|
2046
|
+
// v4
|
|
2047
|
+
const state = await sdk.startSession({ sessionId });
|
|
2048
|
+
const nextState = await sdk.submitForm('formId', { identifier: 'user@example.com' });
|
|
2049
|
+
await sdk.finalizeSession(nextState.finalizeUrl);
|
|
2050
|
+
```
|
|
2051
|
+
|
|
2052
|
+
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
|
+
|
|
2054
|
+
### Migrating to v3.0
|
|
2055
|
+
|
|
2056
|
+
##### Entry API Major Changes
|
|
2057
|
+
`sdk.entry()` API now returns a structured object instead of a plain string. Check the example above in the usage section for more details.
|
|
2058
|
+
|
|
2059
|
+
---
|
|
2060
|
+
|
|
2061
|
+
## Vulnerability Reporting
|
|
2062
|
+
|
|
2063
|
+
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.
|
|
2064
|
+
|
|
2065
|
+
## License
|
|
2066
|
+
|
|
2067
|
+
This package is available under the MIT License. See the [LICENSE](https://github.com/Strivacity/sdk-js/blob/main/LICENSE) file for more info.
|
|
2068
|
+
|
|
2069
|
+
## Contributing
|
|
2070
|
+
|
|
2071
|
+
Please see our [contributing guide](https://github.com/Strivacity/sdk-js/blob/main/CONTRIBUTING.md).
|