@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 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 this line entirely for client-managed sessions
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(sdkOptions.authUrlPrefix!, serverSdk.handlers);
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 | `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` |
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 popup resolves automatically - no callback route is needed. Token exchange happens inside the popup and the result is posted back to the opener window.
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
- 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.
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 (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
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
- entry(): Promise<EntryData>; // Handle external entry (embedded/native only)
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; // 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
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(sdkOptions.authUrlPrefix!, serverSdk.handlers);
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 from `provideStrivacityServerSession()`'s own `provideAppInitializer` (already wired up in [Quick start](#quick-start)), to read the current session without going through the client SDK:
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
- - **`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.
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 { AngularServerStorage, SDKStorage } from '@strivacity/sdk-angular/server';
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
- // Custom session storage for tokens
1931
- export const sessionStorage: AngularServerStorage = {
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.getItem<string>(key);
1963
+ return unstorageInstance.getItemRaw<string>(`session:${key}`);
1934
1964
  },
1935
1965
  async set(key, value) {
1936
- await unstorageInstance.setItem(key, value);
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 matching the logout token's sid or sub claim.
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.getItem<string>(key);
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 { sessionStorage, stateStorage } from './storage';
2006
+ import { sessionStore, stateStorage } from './storage';
1977
2007
 
1978
2008
  export const serverSdk = createServerSDK({
1979
2009
  ...sdkOptions,
1980
- storage: sessionStorage, // Custom Redis-backed session 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`, and `501` if the configured storage doesn't implement `deleteByLogoutToken`.
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` (e.g. `createServerMemoryStorage()` for local testing), or a fully custom server storage as shown in the [Custom storage](#server-storages) example above.
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 | - | Default redirect after login when no `?returnTo=` is given |
2046
- | `postLogoutRedirectUri` | `string` | No | - | Default redirect after logout |
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 authService.sdk.isAuthenticated) {
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. `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.
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>(state, initConfig);
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
  ```