@strivacity/sdk-angular 3.0.2 → 4.0.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/README.md +1991 -609
- package/dist/README.md +1991 -609
- package/dist/fesm2022/strivacity-sdk-angular-src-server.mjs +221 -0
- package/dist/fesm2022/strivacity-sdk-angular-src-server.mjs.map +1 -0
- package/dist/fesm2022/strivacity-sdk-angular-src-types.mjs +6 -0
- package/dist/fesm2022/strivacity-sdk-angular-src-types.mjs.map +1 -0
- package/dist/fesm2022/strivacity-sdk-angular.mjs +284 -498
- package/dist/fesm2022/strivacity-sdk-angular.mjs.map +1 -1
- package/dist/types/strivacity-sdk-angular-src-server.d.ts +82 -0
- package/dist/types/strivacity-sdk-angular-src-types.d.ts +41 -0
- package/dist/types/strivacity-sdk-angular.d.ts +147 -0
- package/eslint.config.mjs +31 -0
- package/ng-package.json +3 -3
- package/package.json +29 -11
- package/project.json +33 -0
- package/src/index.ts +8 -0
- package/src/lib/services/auth.service.ts +131 -0
- package/src/lib/services/index.ts +2 -0
- package/src/lib/services/native-login.service.ts +172 -0
- package/src/lib/storages.ts +12 -0
- package/src/lib/utils.ts +39 -0
- package/src/server/errors.ts +1 -0
- package/src/server/index.ts +6 -0
- package/src/server/ng-package.json +6 -0
- package/src/server/sdk.ts +113 -0
- package/src/server/session.ts +30 -0
- package/src/server/storages.ts +25 -0
- package/src/server/types.ts +32 -0
- package/src/server/utils.ts +74 -0
- package/src/types/index.ts +47 -0
- package/src/types/ng-package.json +6 -0
- package/testing/setup.ts +10 -0
- package/testing/tests/auth.service.spec.ts +236 -0
- package/testing/tests/index.spec.ts +193 -0
- package/testing/tests/native-login.service.spec.ts +311 -0
- package/testing/tests/server/errors.spec.ts +14 -0
- package/testing/tests/server/sdk.spec.ts +197 -0
- package/testing/tests/server/session.spec.ts +52 -0
- package/testing/tests/server/storages.spec.ts +58 -0
- package/testing/tests/server/utils.spec.ts +112 -0
- package/testing/tests/storages.spec.ts +31 -0
- package/testing/tests/utils.spec.ts +24 -0
- package/testing/utils/testbed.ts +26 -0
- package/tsconfig.lib.json +13 -0
- package/tsconfig.lib.prod.json +9 -0
- package/tsconfig.spec.json +8 -0
- package/vite.config.mts +11 -0
- package/dist/index.d.ts +0 -5
- package/dist/lib/components/login-renderer.component.d.ts +0 -38
- package/dist/lib/components/widget-renderer.component.d.ts +0 -16
- package/dist/lib/services/auth.service.d.ts +0 -93
- package/dist/lib/services/widget.service.d.ts +0 -25
- package/dist/lib/strivacity-auth.module.d.ts +0 -10
- package/dist/lib/utils/helpers.d.ts +0 -16
- package/dist/lib/utils/types.d.ts +0 -41
- package/dist/public-api.d.ts +0 -16
package/README.md
CHANGED
|
@@ -1,835 +1,2217 @@
|
|
|
1
1
|
# @strivacity/sdk-angular
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
43
|
+
---
|
|
6
44
|
|
|
7
|
-
##
|
|
45
|
+
## Prerequisites
|
|
8
46
|
|
|
9
|
-
|
|
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
|
-
|
|
51
|
+
---
|
|
12
52
|
|
|
13
|
-
|
|
14
|
-
- [Ionic Example app](https://github.com/Strivacity/sdk-js/tree/main/apps/ionic-angular)
|
|
53
|
+
## Installation
|
|
15
54
|
|
|
16
|
-
|
|
55
|
+
```bash
|
|
56
|
+
npm install @strivacity/sdk-angular
|
|
57
|
+
```
|
|
17
58
|
|
|
18
|
-
|
|
59
|
+
---
|
|
19
60
|
|
|
20
|
-
##
|
|
61
|
+
## Choosing a mode
|
|
21
62
|
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
111
|
+
### 2. Set up the Server SDK
|
|
27
112
|
|
|
28
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
36
|
-
import
|
|
37
|
-
import {
|
|
38
|
-
import {
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}
|
|
53
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
85
|
-
export class AppComponent {
|
|
86
|
-
constructor(private strivacityAuthService: StrivacityAuthService) {}
|
|
87
|
-
}
|
|
88
|
-
```
|
|
198
|
+
---
|
|
89
199
|
|
|
90
|
-
|
|
200
|
+
## Client SDK
|
|
91
201
|
|
|
92
|
-
|
|
202
|
+
### Authentication modes
|
|
93
203
|
|
|
94
|
-
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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.
|
|
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
|
-
|
|
110
|
-
|
|
111
|
-
|
|
224
|
+
selector: 'app-login-page',
|
|
225
|
+
template: `
|
|
226
|
+
<section>
|
|
227
|
+
<h1>Redirecting to login...</h1>
|
|
228
|
+
</section>
|
|
229
|
+
`,
|
|
112
230
|
})
|
|
113
|
-
export class
|
|
114
|
-
|
|
231
|
+
export class LoginPage implements OnInit {
|
|
232
|
+
private readonly authService = inject(StrivacityAuthService);
|
|
115
233
|
|
|
116
234
|
ngOnInit(): void {
|
|
117
|
-
this.
|
|
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
|
-
|
|
245
|
+
**Server-managed sessions**:
|
|
123
246
|
|
|
124
|
-
|
|
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
|
-
```
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
|
|
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.
|
|
143
|
-
import { Component, OnInit,
|
|
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
|
-
|
|
150
|
-
|
|
151
|
-
|
|
288
|
+
selector: 'app-callback-page',
|
|
289
|
+
template: `
|
|
290
|
+
<section>
|
|
291
|
+
<h1>Logging in...</h1>
|
|
292
|
+
</section>
|
|
293
|
+
`,
|
|
152
294
|
})
|
|
153
|
-
export class
|
|
154
|
-
private
|
|
155
|
-
|
|
156
|
-
|
|
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.
|
|
166
|
-
this.
|
|
167
|
-
|
|
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
|
-
|
|
180
|
-
this.
|
|
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
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
}),
|
|
239
|
-
);
|
|
240
|
-
}
|
|
310
|
+
if (params['error'] || params['error_description']) {
|
|
311
|
+
await this.router.navigate(['/error'], { queryParams: params });
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
241
314
|
|
|
242
|
-
|
|
243
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
261
|
-
import { Component, OnInit,
|
|
262
|
-
import {
|
|
263
|
-
import {
|
|
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
|
-
|
|
268
|
-
|
|
269
|
-
|
|
336
|
+
selector: 'app-callback-page',
|
|
337
|
+
template: `
|
|
338
|
+
<section>
|
|
339
|
+
<h1>Logging in...</h1>
|
|
340
|
+
</section>
|
|
341
|
+
`,
|
|
270
342
|
})
|
|
271
|
-
export class
|
|
272
|
-
readonly
|
|
273
|
-
|
|
274
|
-
|
|
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
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
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
|
-
|
|
362
|
+
##### Registration
|
|
294
363
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
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.
|
|
308
|
-
import { Component,
|
|
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-
|
|
314
|
-
|
|
315
|
-
|
|
374
|
+
selector: 'app-register-page',
|
|
375
|
+
template: `
|
|
376
|
+
<section>
|
|
377
|
+
<h1>Redirecting to registration...</h1>
|
|
378
|
+
</section>
|
|
379
|
+
`,
|
|
316
380
|
})
|
|
317
|
-
export class
|
|
318
|
-
private
|
|
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
|
-
|
|
336
|
-
this.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
409
|
-
import { Component, OnInit } from '@angular/core';
|
|
410
|
-
import {
|
|
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
|
-
|
|
416
|
-
|
|
417
|
-
templateUrl: './login.component.html',
|
|
418
|
-
imports: [StyLoginRenderer],
|
|
402
|
+
selector: 'app-register-page',
|
|
403
|
+
template: '',
|
|
419
404
|
})
|
|
420
|
-
export class
|
|
421
|
-
|
|
422
|
-
|
|
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 (
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
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
|
-
|
|
442
|
-
this.router.navigateByUrl('/profile');
|
|
443
|
-
}
|
|
420
|
+
##### Logout
|
|
444
421
|
|
|
445
|
-
|
|
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
|
-
|
|
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
|
-
|
|
458
|
-
|
|
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
|
-
|
|
462
|
-
|
|
463
|
-
|
|
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
|
-
|
|
467
|
-
this.
|
|
444
|
+
ngOnInit(): void {
|
|
445
|
+
if (isPlatformBrowser(this.platformId)) {
|
|
446
|
+
void this.authService.logout();
|
|
447
|
+
}
|
|
468
448
|
}
|
|
469
449
|
}
|
|
470
450
|
```
|
|
471
451
|
|
|
472
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
493
|
-
import { Component, OnInit,
|
|
494
|
-
import {
|
|
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
|
-
|
|
500
|
-
|
|
501
|
-
|
|
462
|
+
selector: 'app-logout-page',
|
|
463
|
+
template: `
|
|
464
|
+
<section>
|
|
465
|
+
<h1>Logging out...</h1>
|
|
466
|
+
</section>
|
|
467
|
+
`,
|
|
502
468
|
})
|
|
503
|
-
export class
|
|
504
|
-
private
|
|
505
|
-
|
|
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
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
this.
|
|
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
|
-
|
|
484
|
+
##### Token management
|
|
543
485
|
|
|
544
|
-
|
|
486
|
+
**Client-managed sessions**:
|
|
487
|
+
|
|
488
|
+
Call these methods to manage the session and access token client-side.
|
|
545
489
|
|
|
546
490
|
```ts
|
|
547
|
-
|
|
548
|
-
import { Component,
|
|
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
|
-
|
|
555
|
-
|
|
556
|
-
|
|
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
|
|
559
|
-
readonly
|
|
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
|
-
|
|
571
|
-
|
|
572
|
-
|
|
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
|
-
|
|
582
|
-
|
|
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
|
-
|
|
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
|
-
|
|
539
|
+
---
|
|
590
540
|
|
|
591
|
-
####
|
|
541
|
+
#### popup mode
|
|
592
542
|
|
|
593
|
-
|
|
543
|
+
> For details on how this mode works, see the [hosted journey documentation](https://docs.strivacity.com/reference/hosted-journey).
|
|
594
544
|
|
|
595
|
-
|
|
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
|
-
|
|
547
|
+
##### Login
|
|
598
548
|
|
|
599
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
612
|
-
import { Component,
|
|
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
|
-
|
|
616
|
-
selector: 'app-login',
|
|
617
|
-
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
|
564
|
+
selector: 'app-login-page',
|
|
618
565
|
template: `
|
|
619
|
-
<
|
|
620
|
-
|
|
621
|
-
|
|
566
|
+
<section>
|
|
567
|
+
<h1>Opening login popup...</h1>
|
|
568
|
+
</section>
|
|
622
569
|
`,
|
|
623
570
|
})
|
|
624
|
-
export class
|
|
625
|
-
|
|
626
|
-
|
|
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
|
-
|
|
639
|
-
this.
|
|
640
|
-
|
|
576
|
+
ngOnInit(): void {
|
|
577
|
+
if (!isPlatformBrowser(this.platformId)) {
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
641
580
|
|
|
642
|
-
|
|
643
|
-
location.reload();
|
|
581
|
+
void this.startLogin();
|
|
644
582
|
}
|
|
645
583
|
|
|
646
|
-
|
|
647
|
-
|
|
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
|
-
|
|
611
|
+
**Server-managed sessions**:
|
|
653
612
|
|
|
654
|
-
|
|
613
|
+
Popup mode always needs client-side JavaScript to open the window, so there's no server-only alternative here.
|
|
655
614
|
|
|
656
|
-
|
|
615
|
+
##### Handle the callback
|
|
657
616
|
|
|
658
|
-
|
|
617
|
+
**Client-managed sessions**:
|
|
659
618
|
|
|
660
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
625
|
+
##### Registration
|
|
680
626
|
|
|
681
|
-
|
|
627
|
+
**Client-managed sessions**:
|
|
682
628
|
|
|
683
|
-
|
|
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
|
-
|
|
687
|
-
|
|
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
|
-
|
|
690
|
-
|
|
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
|
-
|
|
694
|
-
|
|
695
|
-
|
|
652
|
+
ngOnInit(): void {
|
|
653
|
+
if (!isPlatformBrowser(this.platformId)) {
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
696
656
|
|
|
697
|
-
|
|
698
|
-
console.warn(this.xEventId ? `[${this.xEventId}] ${message}` : message);
|
|
657
|
+
void this.startRegister();
|
|
699
658
|
}
|
|
700
659
|
|
|
701
|
-
|
|
702
|
-
|
|
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
|
-
|
|
687
|
+
**Server-managed sessions**:
|
|
708
688
|
|
|
709
|
-
|
|
689
|
+
Popup mode always needs client-side JavaScript to open the window, so there's no server-only alternative here.
|
|
710
690
|
|
|
711
|
-
|
|
691
|
+
##### Logout
|
|
712
692
|
|
|
713
|
-
|
|
693
|
+
**Client-managed sessions**:
|
|
714
694
|
|
|
715
|
-
|
|
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
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
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
|
-
|
|
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
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
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
|
-
|
|
723
|
+
**Server-managed sessions**:
|
|
754
724
|
|
|
755
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
755
|
+
##### Token management
|
|
764
756
|
|
|
765
|
-
|
|
757
|
+
**Client-managed sessions**:
|
|
766
758
|
|
|
767
|
-
|
|
759
|
+
Call these methods to manage the session and access token client-side.
|
|
768
760
|
|
|
769
|
-
|
|
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
|
-
|
|
772
|
-
-
|
|
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
|
-
|
|
782
|
+
async onRefresh(): Promise<void> {
|
|
783
|
+
// Refresh the access token using the refresh token
|
|
784
|
+
await this.authService.refresh();
|
|
785
|
+
}
|
|
775
786
|
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
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
|
-
**
|
|
794
|
+
**Server-managed sessions**:
|
|
785
795
|
|
|
786
|
-
-
|
|
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
|
-
|
|
804
|
+
function onRevoke() {
|
|
805
|
+
// sdk.revokeSession() runs server-side, then redirects to postLogoutRedirectUri
|
|
806
|
+
globalThis.location.href = '/auth/revoke';
|
|
807
|
+
}
|
|
808
|
+
```
|
|
798
809
|
|
|
799
|
-
|
|
810
|
+
---
|
|
800
811
|
|
|
801
|
-
|
|
812
|
+
#### embedded mode
|
|
802
813
|
|
|
803
|
-
|
|
814
|
+
> For details on how this mode works, see the [embedded journey documentation](https://docs.strivacity.com/reference/embedded-journey).
|
|
804
815
|
|
|
805
|
-
-
|
|
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
|
-
|
|
818
|
+
##### Login / Register
|
|
811
819
|
|
|
812
|
-
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
859
|
+
ngOnInit(): void {
|
|
860
|
+
if (!isPlatformBrowser(this.platformId)) {
|
|
861
|
+
return;
|
|
862
|
+
}
|
|
826
863
|
|
|
827
|
-
|
|
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
|
-
|
|
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).
|