@strivacity/sdk-angular 4.0.0-beta.1 → 4.0.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +108 -62
- package/dist/fesm2022/strivacity-sdk-angular.mjs +36 -131
- package/package.json +2 -2
- package/src/lib/services/auth.service.ts +2 -2
- package/src/lib/services/native-login.service.ts +20 -140
- package/src/server/sdk.ts +1 -1
- package/src/server/session.ts +17 -3
- package/src/server/types.ts +3 -3
- package/src/types/index.ts +5 -33
- package/testing/tests/auth.service.spec.ts +1 -1
- package/testing/tests/index.spec.ts +7 -0
- package/testing/tests/native-login.service.spec.ts +10 -14
- package/testing/tests/server/sdk.spec.ts +1 -1
- package/testing/tests/server/session.spec.ts +51 -6
- package/dist/README.md +0 -2217
- package/dist/fesm2022/strivacity-sdk-angular-src-server.mjs +0 -221
- package/dist/fesm2022/strivacity-sdk-angular-src-server.mjs.map +0 -1
- package/dist/fesm2022/strivacity-sdk-angular-src-types.mjs +0 -6
- package/dist/fesm2022/strivacity-sdk-angular-src-types.mjs.map +0 -1
- package/dist/fesm2022/strivacity-sdk-angular.mjs.map +0 -1
- package/dist/types/strivacity-sdk-angular-src-server.d.ts +0 -82
- package/dist/types/strivacity-sdk-angular-src-types.d.ts +0 -41
- package/dist/types/strivacity-sdk-angular.d.ts +0 -147
package/README.md
CHANGED
|
@@ -103,7 +103,7 @@ export const sdkOptions: SDKInitConfig = {
|
|
|
103
103
|
redirectUri: 'https://your-app.example.com/callback', // callback URL after authentication
|
|
104
104
|
scopes: ['openid', 'profile', 'email'], // requested user permissions/data
|
|
105
105
|
|
|
106
|
-
// Omit
|
|
106
|
+
// Omit these lines entirely for client-managed sessions
|
|
107
107
|
serverSessionUri: '/auth/login',
|
|
108
108
|
};
|
|
109
109
|
```
|
|
@@ -132,13 +132,12 @@ export const serverSdk = createServerSDK({
|
|
|
132
132
|
// src/server/server.ts
|
|
133
133
|
import express from 'express';
|
|
134
134
|
import { AngularNodeAppEngine, writeResponseToNodeResponse } from '@angular/ssr/node';
|
|
135
|
-
import { sdkOptions } from '../options';
|
|
136
135
|
import { serverSdk } from './strivacity';
|
|
137
136
|
|
|
138
137
|
const app = express();
|
|
139
138
|
const angularApp = new AngularNodeAppEngine();
|
|
140
139
|
|
|
141
|
-
app.use(
|
|
140
|
+
app.use(serverSdk.options.authUrlPrefix, serverSdk.handlers);
|
|
142
141
|
|
|
143
142
|
app.use((req, res, next) => {
|
|
144
143
|
angularApp
|
|
@@ -148,7 +147,7 @@ app.use((req, res, next) => {
|
|
|
148
147
|
});
|
|
149
148
|
```
|
|
150
149
|
|
|
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`:
|
|
150
|
+
Also hydrate the session into Angular's `TransferState` before the app renders, so the client can pick it up without an extra round-trip. A near-expiry session is refreshed here first (the browser SDK never refreshes with server-managed sessions), and the rotated cookie is written to the rendered response through Angular's `RESPONSE_INIT` (set `autoRefresh: false` in the Server SDK options to turn this off) - add this to your server-only `ApplicationConfig`:
|
|
152
151
|
|
|
153
152
|
```ts
|
|
154
153
|
// src/server/app.config.server.ts
|
|
@@ -171,10 +170,10 @@ export const config = mergeApplicationConfig(appConfig, serverConfig);
|
|
|
171
170
|
| `GET` | `/auth/register` | Starts the registration flow and redirects to the IDP | `302` redirect to IDP |
|
|
172
171
|
| `GET` | `/auth/callback` | Completes authentication (handles the IDP callback) | `302` redirect or popup close script |
|
|
173
172
|
| `GET` | `/auth/refresh` | Refreshes the access token | `204 No Content` or `302` redirect |
|
|
174
|
-
| `GET` | `/auth/revoke` | Revokes tokens and clears the session | `
|
|
175
|
-
| `GET` | `/auth/logout` | Ends the session and redirects to the IDP logout page
|
|
176
|
-
| `GET` | `/auth/entry` | Handles external flow entry (e.g., password reset link) - embedded/native modes only |
|
|
177
|
-
| `POST` | `/auth/backchannel-logout` | Processes back-channel logout requests from the IDP | `
|
|
173
|
+
| `GET` | `/auth/revoke` | Revokes tokens and clears the session | `302` redirect to `postLogoutRedirectUri` |
|
|
174
|
+
| `GET` | `/auth/logout` | Ends the session and redirects to the IDP logout page; accepts a same-origin `?returnTo=` (default: `postLogoutRedirectUri`) | `302` to IDP logout (straight to `returnTo` when the session has no ID token) |
|
|
175
|
+
| `GET` | `/auth/entry` | Handles external flow entry (e.g., password reset link) - embedded/native modes only | `302` redirect to `loginUri` |
|
|
176
|
+
| `POST` | `/auth/backchannel-logout` | Processes back-channel logout requests from the IDP | `200 OK` (`400` invalid token, `501` storage without `deleteByLogoutToken`, `500` delete failed) |
|
|
178
177
|
|
|
179
178
|
> The `/auth` prefix and route names come from `authUrlPrefix` - see [Server configuration reference](#server-configuration-reference).
|
|
180
179
|
|
|
@@ -451,7 +450,7 @@ export class LogoutPage implements OnInit {
|
|
|
451
450
|
|
|
452
451
|
**Server-managed sessions**:
|
|
453
452
|
|
|
454
|
-
With `serverSessionUri` configured, redirect to `/auth/logout` instead - the Server SDK clears the session and redirects to the IDP end-session endpoint:
|
|
453
|
+
With `serverSessionUri` configured, the `logout()` call above doesn't clear the server session cookie, so redirect to `/auth/logout` instead - the Server SDK clears the session and redirects to the IDP end-session endpoint, which then sends the user back to `postLogoutRedirectUri`. To land somewhere else, add a same-origin `?returnTo=` (e.g. `/auth/logout?returnTo=/goodbye`); a missing, invalid, or cross-origin value falls back to `postLogoutRedirectUri`. Whichever URL is used must be registered as an allowed post-logout redirect URI:
|
|
455
454
|
|
|
456
455
|
```ts
|
|
457
456
|
// src/app/pages/logout/logout.page.ts
|
|
@@ -522,7 +521,7 @@ export class TokenPanelComponent {
|
|
|
522
521
|
|
|
523
522
|
**Server-managed sessions**:
|
|
524
523
|
|
|
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:
|
|
524
|
+
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. `authService.sdk.refresh({ returnTo: '/profile' })` performs the same refresh navigation for you:
|
|
526
525
|
|
|
527
526
|
```ts
|
|
528
527
|
function onRefresh() {
|
|
@@ -616,11 +615,35 @@ Popup mode always needs client-side JavaScript to open the window, so there's no
|
|
|
616
615
|
|
|
617
616
|
**Client-managed sessions**:
|
|
618
617
|
|
|
619
|
-
The
|
|
618
|
+
The pop-up still navigates to your `redirectUri`, so you still need a callback route there - without it nothing posts the result back to the opener. Call `handleCallback()` there; in popup mode it never resolves (the opener closes the pop-up once it receives the result), so don't rely on code after it running:
|
|
619
|
+
|
|
620
|
+
```ts
|
|
621
|
+
// src/app/pages/callback/callback.page.ts
|
|
622
|
+
import { Component, type OnInit, inject } from '@angular/core';
|
|
623
|
+
import { StrivacityAuthService } from '@strivacity/sdk-angular';
|
|
624
|
+
|
|
625
|
+
@Component({
|
|
626
|
+
selector: 'app-callback-page',
|
|
627
|
+
template: `
|
|
628
|
+
<section>
|
|
629
|
+
<h1>Logging in...</h1>
|
|
630
|
+
</section>
|
|
631
|
+
`,
|
|
632
|
+
})
|
|
633
|
+
export class CallbackPage implements OnInit {
|
|
634
|
+
private readonly authService = inject(StrivacityAuthService);
|
|
635
|
+
|
|
636
|
+
ngOnInit(): void {
|
|
637
|
+
void this.authService.handleCallback();
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
```
|
|
641
|
+
|
|
642
|
+
`login()` resolves in the opener once the result has been received and, for client-managed sessions, the tokens have been exchanged.
|
|
620
643
|
|
|
621
644
|
**Server-managed sessions**:
|
|
622
645
|
|
|
623
|
-
|
|
646
|
+
The callback page must not call `handleCallback()`: forward the query string to `/auth/callback` instead, as in the server-managed redirect callback. The Server SDK exchanges the code, sets the session cookie and responds with a page that posts the result to the opener and closes the pop-up.
|
|
624
647
|
|
|
625
648
|
##### Registration
|
|
626
649
|
|
|
@@ -722,7 +745,7 @@ export class LogoutPage implements OnInit {
|
|
|
722
745
|
|
|
723
746
|
**Server-managed sessions**:
|
|
724
747
|
|
|
725
|
-
With `serverSessionUri` configured, redirect to `/auth/logout` instead - the Server SDK clears the session and redirects to the IDP end-session endpoint:
|
|
748
|
+
With `serverSessionUri` configured, the `logout()` call above doesn't clear the server session cookie, so redirect to `/auth/logout` instead - the Server SDK clears the session and redirects to the IDP end-session endpoint, which then sends the user back to `postLogoutRedirectUri`. To land somewhere else, add a same-origin `?returnTo=` (e.g. `/auth/logout?returnTo=/goodbye`); a missing, invalid, or cross-origin value falls back to `postLogoutRedirectUri`. Whichever URL is used must be registered as an allowed post-logout redirect URI:
|
|
726
749
|
|
|
727
750
|
```ts
|
|
728
751
|
// src/app/pages/logout/logout.page.ts
|
|
@@ -793,7 +816,7 @@ export class TokenPanelComponent {
|
|
|
793
816
|
|
|
794
817
|
**Server-managed sessions**:
|
|
795
818
|
|
|
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:
|
|
819
|
+
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. `authService.sdk.refresh({ returnTo: '/profile' })` performs the same refresh navigation for you:
|
|
797
820
|
|
|
798
821
|
```ts
|
|
799
822
|
function onRefresh() {
|
|
@@ -1264,7 +1287,7 @@ export class LogoutPage implements OnInit {
|
|
|
1264
1287
|
|
|
1265
1288
|
**Server-managed sessions**:
|
|
1266
1289
|
|
|
1267
|
-
With `serverSessionUri` configured, redirect to `/auth/logout` instead - the Server SDK clears the session and redirects to the IDP end-session endpoint:
|
|
1290
|
+
With `serverSessionUri` configured, the `logout()` call above doesn't clear the server session cookie, so redirect to `/auth/logout` instead - the Server SDK clears the session and redirects to the IDP end-session endpoint, which then sends the user back to `postLogoutRedirectUri`. To land somewhere else, add a same-origin `?returnTo=` (e.g. `/auth/logout?returnTo=/goodbye`); a missing, invalid, or cross-origin value falls back to `postLogoutRedirectUri`. Whichever URL is used must be registered as an allowed post-logout redirect URI:
|
|
1268
1291
|
|
|
1269
1292
|
```ts
|
|
1270
1293
|
// src/app/pages/logout/logout.page.ts
|
|
@@ -1335,7 +1358,7 @@ export class TokenPanelComponent {
|
|
|
1335
1358
|
|
|
1336
1359
|
**Server-managed sessions**:
|
|
1337
1360
|
|
|
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:
|
|
1361
|
+
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. `authService.sdk.refresh({ returnTo: '/profile' })` performs the same refresh navigation for you:
|
|
1339
1362
|
|
|
1340
1363
|
```ts
|
|
1341
1364
|
function onRefresh() {
|
|
@@ -1425,7 +1448,6 @@ export class LoginPage implements OnInit {
|
|
|
1425
1448
|
params: {
|
|
1426
1449
|
prompt: 'login', // use 'create' to open the registration flow instead
|
|
1427
1450
|
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
1451
|
sessionId: this.route.snapshot.queryParamMap.get('session_id'), // pass a session ID to resume an existing flow
|
|
1430
1452
|
},
|
|
1431
1453
|
onLogin: async () => {
|
|
@@ -1637,7 +1659,7 @@ export class LogoutPage implements OnInit {
|
|
|
1637
1659
|
|
|
1638
1660
|
**Server-managed sessions**:
|
|
1639
1661
|
|
|
1640
|
-
With `serverSessionUri` configured, redirect to `/auth/logout` instead - the Server SDK clears the session and redirects to the IDP end-session endpoint:
|
|
1662
|
+
With `serverSessionUri` configured, the `logout()` call above doesn't clear the server session cookie, so redirect to `/auth/logout` instead - the Server SDK clears the session and redirects to the IDP end-session endpoint, which then sends the user back to `postLogoutRedirectUri`. To land somewhere else, add a same-origin `?returnTo=` (e.g. `/auth/logout?returnTo=/goodbye`); a missing, invalid, or cross-origin value falls back to `postLogoutRedirectUri`. Whichever URL is used must be registered as an allowed post-logout redirect URI:
|
|
1641
1663
|
|
|
1642
1664
|
```ts
|
|
1643
1665
|
// src/app/pages/logout/logout.page.ts
|
|
@@ -1708,7 +1730,7 @@ export class TokenPanelComponent {
|
|
|
1708
1730
|
|
|
1709
1731
|
**Server-managed sessions**:
|
|
1710
1732
|
|
|
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:
|
|
1733
|
+
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. `authService.sdk.refresh({ returnTo: '/profile' })` performs the same refresh navigation for you:
|
|
1712
1734
|
|
|
1713
1735
|
```ts
|
|
1714
1736
|
function onRefresh() {
|
|
@@ -1746,6 +1768,7 @@ export class SomeComponent {
|
|
|
1746
1768
|
{
|
|
1747
1769
|
// SDK instance (access any SDK method)
|
|
1748
1770
|
readonly sdk: RedirectFlow | PopupFlow | EmbeddedFlow | NativeFlow;
|
|
1771
|
+
readonly options: AngularSDKInitConfig; // The options passed to `provideStrivacity()`
|
|
1749
1772
|
|
|
1750
1773
|
// Reactive state (signals - call them, e.g. `authService.loading()`; templates re-render when they change)
|
|
1751
1774
|
readonly loading: Signal<boolean>; // True during initialization
|
|
@@ -1757,14 +1780,19 @@ export class SomeComponent {
|
|
|
1757
1780
|
readonly accessTokenExpired: Signal<boolean>; // True once the access token has expired
|
|
1758
1781
|
readonly accessTokenExpirationDate: Signal<number | null>; // Access token expiration timestamp
|
|
1759
1782
|
|
|
1760
|
-
// Methods (
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1783
|
+
// Methods (proxied to `sdk`; parameter/return types follow the flow type)
|
|
1784
|
+
init(): Promise<void>; // Load metadata and restore any persisted session
|
|
1785
|
+
checkAuthentication(params?: { autoRefresh: boolean }): Promise<boolean>; // Check the session, optionally refreshing tokens (never when serverSessionUri is set)
|
|
1786
|
+
login(params?: RedirectParams | PopupParams): Promise<void>; // Start login flow (redirect: `returnTo` sets where handleCallback() sends the user) (embedded/native: deprecated, returns a login flow instance - use `sdk.startSession()`)
|
|
1787
|
+
register(params?: RedirectParams | PopupParams): Promise<void>; // Start registration flow (same caveat as `login`)
|
|
1788
|
+
handleCallback(url?: string | URL): Promise<{ returnTo?: string }>; // Handle OAuth callback; resolves to the `returnTo` given to login()
|
|
1764
1789
|
logout(params?: LogoutParams): Promise<void>; // End session
|
|
1765
|
-
refresh(): Promise<void>; // Refresh access token
|
|
1790
|
+
refresh(params?: { returnTo?: string }): Promise<void>; // Refresh access token (server-managed: navigates to the server's refresh route, `${authUrlPrefix}/refresh`, instead; `returnTo` sets where it returns, default: the current URL)
|
|
1766
1791
|
revoke(): Promise<void>; // Revoke tokens
|
|
1767
|
-
|
|
1792
|
+
tokenExchange(params?: Record<string, string>): Promise<void>; // Perform a token exchange
|
|
1793
|
+
entry(url?: string | URL): Promise<EntryResponse>; // Handle external entry (embedded/native; resolves `{ session_id, short_app_id, language }`). Redirect/popup: `entry(url?, params?: RedirectOptions): Promise<void>`
|
|
1794
|
+
subscribeToEvent(eventName, callbackFn): { dispose: () => void }; // Subscribe to a single SDK event
|
|
1795
|
+
subscribeToAllEvents(callbackFn): { dispose: () => void }; // Subscribe to all SDK events
|
|
1768
1796
|
}
|
|
1769
1797
|
```
|
|
1770
1798
|
|
|
@@ -1797,10 +1825,10 @@ export class SomeComponent implements OnInit {
|
|
|
1797
1825
|
{
|
|
1798
1826
|
params?: NativeParams; // Initial flow parameters
|
|
1799
1827
|
onLogin?: (session: SessionData) => void | Promise<void>; // Called on successful login
|
|
1800
|
-
onClose?: () => void
|
|
1801
|
-
onError?: (error:
|
|
1802
|
-
onFallback?: (error: FallbackError) => void
|
|
1803
|
-
onGlobalMessage?: (message: NativeFlowMessage) => void
|
|
1828
|
+
onClose?: () => void | Promise<void>; // Called when user closes the flow
|
|
1829
|
+
onError?: (error: Error) => void | Promise<void>; // Called on error
|
|
1830
|
+
onFallback?: (error: FallbackError) => void | Promise<void>; // Called when fallback needed
|
|
1831
|
+
onGlobalMessage?: (message: NativeFlowMessage) => void | Promise<void>; // Called for global messages
|
|
1804
1832
|
}
|
|
1805
1833
|
```
|
|
1806
1834
|
|
|
@@ -1849,17 +1877,16 @@ Mount the Express router once - see [Quick start](#quick-start) for the full `se
|
|
|
1849
1877
|
```ts
|
|
1850
1878
|
// src/server/server.ts
|
|
1851
1879
|
import express from 'express';
|
|
1852
|
-
import { sdkOptions } from '../options';
|
|
1853
1880
|
import { serverSdk } from './strivacity';
|
|
1854
1881
|
|
|
1855
1882
|
const app = express();
|
|
1856
1883
|
|
|
1857
|
-
app.use(
|
|
1884
|
+
app.use(serverSdk.options.authUrlPrefix, serverSdk.handlers);
|
|
1858
1885
|
```
|
|
1859
1886
|
|
|
1860
1887
|
### Accessing the session server-side
|
|
1861
1888
|
|
|
1862
|
-
Call `getSession(req)` from any Express route handler, or
|
|
1889
|
+
Call `getSession(req)` from any Express route handler, or with Angular's `REQUEST` token in your own server-side code (`provideStrivacityServerSession()`, wired up in [Quick start](#quick-start), already does this), to read the current session without going through the client SDK. With `autoRefresh` (the default) it refreshes an expired (or within `refreshSkew`) session first and queues the rotated cookie on `req`, so write it to the response with `flushSetCookies(req, response)` - otherwise the rotated refresh token is lost. See [Server SDK API reference](#server-sdk-api-reference):
|
|
1863
1890
|
|
|
1864
1891
|
```ts
|
|
1865
1892
|
// src/server/session.ts
|
|
@@ -1871,7 +1898,7 @@ app.get('/api/me', async (req, res) => {
|
|
|
1871
1898
|
});
|
|
1872
1899
|
```
|
|
1873
1900
|
|
|
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:
|
|
1901
|
+
Angular's own `REQUEST` token (populated by `@angular/ssr` during SSR) works the same way - `provideStrivacityServerSession()` (see [Quick start](#quick-start)) already uses it with `getSession` to hydrate `TransferState` before the app renders, passing the session through `toClientSession()` (exported from `@strivacity/sdk-angular` and `@strivacity/sdk-angular/server`) so the refresh and ID tokens stay out of the rendered HTML:
|
|
1875
1902
|
|
|
1876
1903
|
```ts
|
|
1877
1904
|
import { REQUEST, inject } from '@angular/core';
|
|
@@ -1887,12 +1914,12 @@ By default the Server SDK stores tokens encrypted in http-only cookies and login
|
|
|
1887
1914
|
|
|
1888
1915
|
#### Built-in session storages
|
|
1889
1916
|
|
|
1890
|
-
-
|
|
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.
|
|
1917
|
+
- **Encrypted cookie storage** - the default, used when no `storage` is provided: keeps the whole session encrypted with `secret` in an http-only cookie.
|
|
1918
|
+
- **`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. It supports back-channel logout when `storage` implements `deleteByLogoutToken`.
|
|
1892
1919
|
|
|
1893
1920
|
```ts
|
|
1894
1921
|
// src/server/storage.ts
|
|
1895
|
-
import { createSessionIdCookieStorage, createServerMemoryStorage } from '@strivacity/sdk-angular';
|
|
1922
|
+
import { createSessionIdCookieStorage, createServerMemoryStorage } from '@strivacity/sdk-angular/server';
|
|
1896
1923
|
|
|
1897
1924
|
export const sessionStorage = createSessionIdCookieStorage(
|
|
1898
1925
|
createServerMemoryStorage(),
|
|
@@ -1917,37 +1944,40 @@ export const serverSdk = createServerSDK({
|
|
|
1917
1944
|
|
|
1918
1945
|
#### Custom storage
|
|
1919
1946
|
|
|
1920
|
-
For example you can use Redis via [unstorage](https://npmjs.com/package/unstorage)
|
|
1947
|
+
For example you can use Redis via [unstorage](https://npmjs.com/package/unstorage).
|
|
1948
|
+
|
|
1949
|
+
The SDK always reads and writes the session under the same key (`storageTokenName`), and passes the current request as the last argument. A storage that ignores the request would therefore share a single session across all users, so key the data per browser session instead. The easiest way is to wrap your key-value store with `createSessionIdCookieStorage`, which puts a random session id in a cookie and calls your store with that id as the key:
|
|
1921
1950
|
|
|
1922
1951
|
```ts
|
|
1923
1952
|
// src/server/storage.ts
|
|
1924
1953
|
import { createStorage } from 'unstorage';
|
|
1925
1954
|
import redisDriver from 'unstorage/drivers/redis';
|
|
1926
|
-
import type {
|
|
1955
|
+
import type { ServerStorage, SDKStorage } from '@strivacity/sdk-angular/server';
|
|
1927
1956
|
|
|
1928
1957
|
const unstorageInstance = createStorage({ driver: redisDriver({ url: process.env.REDIS_URL }) });
|
|
1929
1958
|
|
|
1930
|
-
//
|
|
1931
|
-
|
|
1959
|
+
// Session payload store - `key` is the per-browser session id provided by `createSessionIdCookieStorage`
|
|
1960
|
+
// (raw get/set keep the serialized session string as-is)
|
|
1961
|
+
export const sessionStore: ServerStorage = {
|
|
1932
1962
|
async get(key) {
|
|
1933
|
-
return unstorageInstance.
|
|
1963
|
+
return unstorageInstance.getItemRaw<string>(`session:${key}`);
|
|
1934
1964
|
},
|
|
1935
1965
|
async set(key, value) {
|
|
1936
|
-
await unstorageInstance.
|
|
1966
|
+
await unstorageInstance.setItemRaw(`session:${key}`, value);
|
|
1937
1967
|
},
|
|
1938
1968
|
async delete(key) {
|
|
1939
|
-
await unstorageInstance.removeItem(key);
|
|
1969
|
+
await unstorageInstance.removeItem(`session:${key}`);
|
|
1940
1970
|
},
|
|
1941
1971
|
// Required for back-channel logout support - see below.
|
|
1942
|
-
// Scans all stored sessions and removes those
|
|
1972
|
+
// Scans all stored sessions and removes those whose ID token claims match the logout token's sid or sub claim.
|
|
1943
1973
|
async deleteByLogoutToken(logoutToken) {
|
|
1944
|
-
const keys = await unstorageInstance.getKeys();
|
|
1974
|
+
const keys = await unstorageInstance.getKeys('session');
|
|
1945
1975
|
await Promise.all(
|
|
1946
1976
|
keys.map(async (key) => {
|
|
1947
|
-
const raw = await unstorageInstance.
|
|
1977
|
+
const raw = await unstorageInstance.getItemRaw<string>(key);
|
|
1948
1978
|
if (!raw) return;
|
|
1949
1979
|
const session = JSON.parse(raw);
|
|
1950
|
-
if ((logoutToken.sid && session.sid === logoutToken.sid) || (logoutToken.sub && session.sub === logoutToken.sub)) {
|
|
1980
|
+
if ((logoutToken.sid && session.claims?.sid === logoutToken.sid) || (logoutToken.sub && session.claims?.sub === logoutToken.sub)) {
|
|
1951
1981
|
await unstorageInstance.removeItem(key);
|
|
1952
1982
|
}
|
|
1953
1983
|
}),
|
|
@@ -1971,13 +2001,13 @@ export const stateStorage: SDKStorage = {
|
|
|
1971
2001
|
|
|
1972
2002
|
```ts
|
|
1973
2003
|
// src/server/strivacity.ts
|
|
1974
|
-
import { createServerSDK } from '@strivacity/sdk-angular/server';
|
|
2004
|
+
import { createServerSDK, createSessionIdCookieStorage } from '@strivacity/sdk-angular/server';
|
|
1975
2005
|
import { sdkOptions } from '../options';
|
|
1976
|
-
import {
|
|
2006
|
+
import { sessionStore, stateStorage } from './storage';
|
|
1977
2007
|
|
|
1978
2008
|
export const serverSdk = createServerSDK({
|
|
1979
2009
|
...sdkOptions,
|
|
1980
|
-
storage:
|
|
2010
|
+
storage: createSessionIdCookieStorage(sessionStore), // Custom Redis-backed session storage, keyed by a session-id cookie
|
|
1981
2011
|
stateStorage, // Custom Redis-backed state storage
|
|
1982
2012
|
});
|
|
1983
2013
|
```
|
|
@@ -1988,9 +2018,9 @@ export const serverSdk = createServerSDK({
|
|
|
1988
2018
|
|
|
1989
2019
|
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
2020
|
|
|
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`,
|
|
2021
|
+
The handler verifies the token's signature against the IDP's JWKS, validates the `iss`, `aud`, `iat` (freshness), and `jti` (replay protection) claims, requires the `http://schemas.openid.net/event/backchannel-logout` event and a `sid` or `sub` claim, then calls `storage.deleteByLogoutToken({ sid?, sub? })` to remove the matching session(s). It responds `200` on success, `400` for an invalid or malformed `logout_token`, `501` if the configured storage doesn't implement `deleteByLogoutToken`, and `500` if deleting the session fails.
|
|
1992
2022
|
|
|
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
|
|
2023
|
+
> **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 the Redis-backed store from the [Custom storage](#server-storages) example above.
|
|
1994
2024
|
|
|
1995
2025
|
Configure the **Back-channel logout URI** in your Strivacity application settings to:
|
|
1996
2026
|
|
|
@@ -2007,7 +2037,7 @@ For a complete explanation of the handshake and validation performed, see the co
|
|
|
2007
2037
|
options: ServerSDKOptions; // resolved server SDK configuration
|
|
2008
2038
|
|
|
2009
2039
|
// Session management
|
|
2010
|
-
getSession(req?): Promise<SessionData | null>; // read the current session
|
|
2040
|
+
getSession(req?): Promise<SessionData | null>; // read the current session; with `autoRefresh` (default) it refreshes an expired (or within `refreshSkew`) session first, at most once per request (calls with the same `req` share the result); null if the refresh fails. The rotated cookie is queued on `req` - write it with `flushSetCookies(req, response)`
|
|
2011
2041
|
updateSession(session, req?): Promise<void>; // persist new session data
|
|
2012
2042
|
refreshSession(req?): Promise<SessionData>; // refresh tokens using the refresh token
|
|
2013
2043
|
revokeSession(req?): Promise<void>; // revoke tokens and clear the session
|
|
@@ -2042,33 +2072,44 @@ The Server SDK accepts the same configuration as the client SDK (see [Configurat
|
|
|
2042
2072
|
| `stateStorage` | `SDKStorage` | No | In-memory `Map` | Custom OAuth2 state storage |
|
|
2043
2073
|
| `authUrlPrefix` | `string` | No | `'/auth'` | URL prefix matched by `sdk.handlers` |
|
|
2044
2074
|
| `loginUri` | `string` | No | `'/login'` | Route your own [route guard](#route-guards) redirects to when there's no session |
|
|
2045
|
-
| `postLoginRedirectUri` | `string` | No |
|
|
2046
|
-
| `postLogoutRedirectUri` | `string` | No |
|
|
2075
|
+
| `postLoginRedirectUri` | `string` | No | origin of `redirectUri` | Default redirect after login when no `?returnTo=` is given |
|
|
2076
|
+
| `postLogoutRedirectUri` | `string` | No | origin of `redirectUri` | Default redirect after logout |
|
|
2047
2077
|
| `cookieMaxAge` | `number` | No | `2592000` (30 days) | Max age of the session cookie in seconds |
|
|
2048
2078
|
|
|
2049
2079
|
---
|
|
2050
2080
|
|
|
2051
2081
|
## Route guards
|
|
2052
2082
|
|
|
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
|
|
2083
|
+
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.
|
|
2084
|
+
|
|
2085
|
+
With server-managed sessions the browser never refreshes tokens itself, so a session can expire between client-side navigations. The guard below sends an expired session through the Server SDK's refresh route (`sdk.refresh({ returnTo })`, a full-page navigation that comes back to the target URL) and cancels the navigation. During SSR this isn't needed: `provideStrivacityServerSession()` has already refreshed the session.
|
|
2054
2086
|
|
|
2055
2087
|
```ts
|
|
2056
2088
|
// src/app/guards/auth.guard.ts
|
|
2057
2089
|
import type { CanActivateFn } from '@angular/router';
|
|
2058
|
-
import { inject } from '@angular/core';
|
|
2090
|
+
import { PLATFORM_ID, inject } from '@angular/core';
|
|
2091
|
+
import { isPlatformBrowser } from '@angular/common';
|
|
2059
2092
|
import { RedirectCommand, Router } from '@angular/router';
|
|
2060
2093
|
import { StrivacityAuthService } from '@strivacity/sdk-angular';
|
|
2061
2094
|
|
|
2062
|
-
export const authGuard: CanActivateFn = async () => {
|
|
2095
|
+
export const authGuard: CanActivateFn = async (_route, state) => {
|
|
2063
2096
|
const authService = inject(StrivacityAuthService);
|
|
2064
2097
|
const router = inject(Router);
|
|
2098
|
+
const isBrowser = isPlatformBrowser(inject(PLATFORM_ID)); // inject() only works before the first await
|
|
2099
|
+
const { sdk } = authService;
|
|
2065
2100
|
|
|
2066
2101
|
await authService.init();
|
|
2067
2102
|
|
|
2068
|
-
if (await
|
|
2103
|
+
if (await sdk.isAuthenticated) {
|
|
2069
2104
|
return true;
|
|
2070
2105
|
}
|
|
2071
2106
|
|
|
2107
|
+
// Server-managed sessions: refresh an expired session through the Server SDK's refresh route, then come back to state.url
|
|
2108
|
+
if (isBrowser && sdk.options.serverSessionUri && sdk.session) {
|
|
2109
|
+
await sdk.refresh({ returnTo: state.url });
|
|
2110
|
+
return false;
|
|
2111
|
+
}
|
|
2112
|
+
|
|
2072
2113
|
return new RedirectCommand(router.parseUrl('/login'));
|
|
2073
2114
|
};
|
|
2074
2115
|
```
|
|
@@ -2108,7 +2149,12 @@ The client SDK accepts the same configuration as the core SDK - see the [core SD
|
|
|
2108
2149
|
|
|
2109
2150
|
### Migrating to v4.0
|
|
2110
2151
|
|
|
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. `
|
|
2152
|
+
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. `provideStrivacity()` / `StrivacityAuthModule.forRoot()` and the built-in `redirect`/`popup`/`embedded`/`native` modes are still the entry points, but the Angular API itself changed:
|
|
2153
|
+
|
|
2154
|
+
- `StrivacityAuthService` is no longer `providedIn: 'root'` - it is a plain `@Injectable()` registered by `provideStrivacity()` / `StrivacityAuthModule.forRoot()`.
|
|
2155
|
+
- Session state is exposed as signals (`authService.isAuthenticated()`, `authService.idTokenClaims()`, ...) instead of the `session$` Observable, and methods return Promises instead of Observables (`await authService.login()` instead of `authService.login().subscribe()`) - see [StrivacityAuthService](#strivacityauthservice).
|
|
2156
|
+
- In `native` mode, `StyLoginRenderer` (`<sty-login-renderer>`) and `StrivacityWidgetService` are replaced by [`StrivacityNativeLoginService`](#strivacitynativeloginservice).
|
|
2157
|
+
- Apps that used `mode: 'custom'` or drove `native` mode through the old `NativeFlowHandler` need the changes described below.
|
|
2112
2158
|
|
|
2113
2159
|
#### Class-based flows replaced by functions
|
|
2114
2160
|
|
|
@@ -2135,7 +2181,7 @@ import type { SDKInitConfig, SDKOptions } from '@strivacity/sdk-core/types';
|
|
|
2135
2181
|
|
|
2136
2182
|
export function createCustomFlow(initConfig: SDKInitConfig) {
|
|
2137
2183
|
const state = getDefaultFlowState();
|
|
2138
|
-
const options = getSDKOptions<SDKOptions>(
|
|
2184
|
+
const options = getSDKOptions<SDKOptions>(initConfig);
|
|
2139
2185
|
const base = createBaseFlow(state, options);
|
|
2140
2186
|
|
|
2141
2187
|
async function refresh(): Promise<void> {
|
|
@@ -2189,7 +2235,7 @@ export class LoginPage {
|
|
|
2189
2235
|
readonly nativeLoginService = inject(StrivacityNativeLoginService);
|
|
2190
2236
|
|
|
2191
2237
|
async ngOnInit() {
|
|
2192
|
-
await this.nativeLoginService.start({ sessionId });
|
|
2238
|
+
await this.nativeLoginService.start({ params: { sessionId } });
|
|
2193
2239
|
}
|
|
2194
2240
|
}
|
|
2195
2241
|
```
|