@strivacity/sdk-angular 3.0.3 → 4.0.0-beta.0

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