@microsoft/rayfin-auth-provider-fabric 1.24.0 → 1.27.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.
@@ -4,11 +4,14 @@ import type { FabricAuthOptions } from './types';
4
4
  * Embedded-mode Fabric login — acquires a session via postMessage to the
5
5
  * parent Fabric Extension Host without opening a popup.
6
6
  *
7
- * 1. Generates PKCE parameters in local variables (no `localStorage`).
8
- * 2. Sends `auth.requestHandoff` to the parent via {@link requestHandoff}.
9
- * 3. Receives the handoff code from the host's `AuthPlugin`.
10
- * 4. Exchanges the handoff code for tokens via `exchangeVerificationCode`.
11
- * 5. Creates a session via `createSessionFromTokenResponse`.
7
+ * 1. Clears any prior Rayfin session in this iframe (`auth.signOut()`)
8
+ * so a stale session from a previous Fabric user cannot be reused.
9
+ * 2. Generates PKCE parameters in local variables (no `localStorage`).
10
+ * 3. Sends `auth.requestHandoff` to the parent via {@link requestHandoff}.
11
+ * 4. Receives the handoff code from the host's `AuthPlugin`.
12
+ * 5. Exchanges the handoff code for tokens via `exchangeVerificationCode`.
13
+ * 6. Creates a session via `createSessionFromTokenResponse` and marks
14
+ * embedded handoff as completed for this page load.
12
15
  *
13
16
  * The session is stored in the iframe's own `localStorage`.
14
17
  *
@@ -1,15 +1,19 @@
1
1
  import { generateCodeVerifier, generateCodeChallenge, generateState, } from '@microsoft/rayfin-auth';
2
2
  import { AuthError } from '@microsoft/rayfin-lib';
3
3
  import { requestHandoff } from './PostMessageAuthTransport';
4
+ import { markEmbeddedHandoffCompleted } from './fabricAuthHelpers';
4
5
  /**
5
6
  * Embedded-mode Fabric login — acquires a session via postMessage to the
6
7
  * parent Fabric Extension Host without opening a popup.
7
8
  *
8
- * 1. Generates PKCE parameters in local variables (no `localStorage`).
9
- * 2. Sends `auth.requestHandoff` to the parent via {@link requestHandoff}.
10
- * 3. Receives the handoff code from the host's `AuthPlugin`.
11
- * 4. Exchanges the handoff code for tokens via `exchangeVerificationCode`.
12
- * 5. Creates a session via `createSessionFromTokenResponse`.
9
+ * 1. Clears any prior Rayfin session in this iframe (`auth.signOut()`)
10
+ * so a stale session from a previous Fabric user cannot be reused.
11
+ * 2. Generates PKCE parameters in local variables (no `localStorage`).
12
+ * 3. Sends `auth.requestHandoff` to the parent via {@link requestHandoff}.
13
+ * 4. Receives the handoff code from the host's `AuthPlugin`.
14
+ * 5. Exchanges the handoff code for tokens via `exchangeVerificationCode`.
15
+ * 6. Creates a session via `createSessionFromTokenResponse` and marks
16
+ * embedded handoff as completed for this page load.
13
17
  *
14
18
  * The session is stored in the iframe's own `localStorage`.
15
19
  *
@@ -21,6 +25,23 @@ export async function embeddedFabricLogin(auth, options) {
21
25
  if (!options.returnOrigin) {
22
26
  throw new AuthError('returnOrigin is required for embedded Fabric authentication.', 'MISSING_RETURN_ORIGIN');
23
27
  }
28
+ // Embedded mode: the parent Fabric host is the source of truth for "who
29
+ // is signed in". Discard any prior Rayfin session that may have been
30
+ // left behind in this iframe's localStorage by a previous Fabric user
31
+ // BEFORE we ask for a fresh handoff. Without this, a Fabric user
32
+ // switch (User A → User B) would leave the SPA authenticated as
33
+ // User A because tryResumeSession would happily return the stale
34
+ // User A session and we would never reach the handoff path.
35
+ //
36
+ // Errors from signOut are swallowed: the stale token may already be
37
+ // invalid server-side, and the local clear (which signOut performs in
38
+ // its own catch block) is what actually matters here.
39
+ try {
40
+ await auth.signOut();
41
+ }
42
+ catch (signOutError) {
43
+ console.debug('[FabricAuth:embedded] Pre-handoff signOut failed (non-fatal); local session has been cleared', signOutError);
44
+ }
24
45
  // PKCE in local variables only — not persisted to localStorage.
25
46
  const codeVerifier = generateCodeVerifier();
26
47
  const codeChallenge = await generateCodeChallenge(codeVerifier);
@@ -50,6 +71,7 @@ export async function embeddedFabricLogin(auth, options) {
50
71
  redirectUri,
51
72
  });
52
73
  auth.createSessionFromTokenResponse(tokenResponse);
74
+ markEmbeddedHandoffCompleted();
53
75
  console.debug('[FabricAuth:embedded] Session established');
54
76
  }
55
77
  //# sourceMappingURL=embeddedFabricLogin.js.map
@@ -6,11 +6,16 @@ import type { FabricAuthOptions } from './types';
6
6
  * Implements a multi-step waterfall — the first step that succeeds short-circuits the rest:
7
7
  *
8
8
  * 1. **Already authenticated** — if `auth.getSession().isAuthenticated` is true,
9
- * return the existing session immediately.
9
+ * return the existing session immediately. **Skipped on the first
10
+ * embedded call per page load** to avoid silently reusing a stale
11
+ * session belonging to a previously signed-in Fabric user.
10
12
  * 2. **Refresh token** — if a refresh token is available, attempt `auth.refreshSession()`.
11
13
  * Return the refreshed session on success; continue on failure.
14
+ * Also skipped on the first embedded call per page load.
12
15
  * 3. **Embedded mode** — if running inside a Fabric iframe (`fabricEmbedded=true`),
13
16
  * use `embeddedFabricLogin()` to acquire a session via `postMessage` handoff.
17
+ * `embeddedFabricLogin` performs a hard `auth.signOut()` before the
18
+ * handoff so the new session always reflects the current Fabric user.
14
19
  * 4. **Open Fabric broker** — no existing auth path available. Open the Fabric Portal
15
20
  * in a new tab via `initiateFabricLogin()` and wait for the Fabric extension to post
16
21
  * the handoff code via `postMessage`. The function exchanges the code internally
@@ -1,6 +1,6 @@
1
1
  import { AuthError } from '@microsoft/rayfin-lib';
2
2
  import { embeddedFabricLogin } from './embeddedFabricLogin';
3
- import { isEmbeddedMode, tryResumeSession } from './fabricAuthHelpers';
3
+ import { hasEmbeddedHandoffCompleted, isEmbeddedMode, tryResumeSession, } from './fabricAuthHelpers';
4
4
  import { initiateFabricLogin } from './initiateFabricLogin';
5
5
  /**
6
6
  * Ensures the user is signed in via Fabric brokered authentication.
@@ -8,11 +8,16 @@ import { initiateFabricLogin } from './initiateFabricLogin';
8
8
  * Implements a multi-step waterfall — the first step that succeeds short-circuits the rest:
9
9
  *
10
10
  * 1. **Already authenticated** — if `auth.getSession().isAuthenticated` is true,
11
- * return the existing session immediately.
11
+ * return the existing session immediately. **Skipped on the first
12
+ * embedded call per page load** to avoid silently reusing a stale
13
+ * session belonging to a previously signed-in Fabric user.
12
14
  * 2. **Refresh token** — if a refresh token is available, attempt `auth.refreshSession()`.
13
15
  * Return the refreshed session on success; continue on failure.
16
+ * Also skipped on the first embedded call per page load.
14
17
  * 3. **Embedded mode** — if running inside a Fabric iframe (`fabricEmbedded=true`),
15
18
  * use `embeddedFabricLogin()` to acquire a session via `postMessage` handoff.
19
+ * `embeddedFabricLogin` performs a hard `auth.signOut()` before the
20
+ * handoff so the new session always reflects the current Fabric user.
16
21
  * 4. **Open Fabric broker** — no existing auth path available. Open the Fabric Portal
17
22
  * in a new tab via `initiateFabricLogin()` and wait for the Fabric extension to post
18
23
  * the handoff code via `postMessage`. The function exchanges the code internally
@@ -28,13 +33,25 @@ import { initiateFabricLogin } from './initiateFabricLogin';
28
33
  * @throws {AuthError} If all steps fail or the broker tab is blocked.
29
34
  */
30
35
  export async function ensureSignedInWithFabric(auth, options) {
31
- // Steps 1-2: existing session or refresh
32
- const resumed = await tryResumeSession(auth);
33
- if (resumed) {
34
- return resumed;
36
+ // Steps 1-2: existing session or refresh.
37
+ // In embedded mode we MUST skip this on the first call per page load —
38
+ // the session in iframe localStorage may belong to a previously
39
+ // signed-in Fabric user. After the first successful handoff this load
40
+ // (`hasEmbeddedHandoffCompleted()` is true) the cached session is the
41
+ // current user's, so resuming is safe and avoids redundant handoffs.
42
+ const inEmbeddedMode = isEmbeddedMode(options);
43
+ const skipResume = inEmbeddedMode && !hasEmbeddedHandoffCompleted();
44
+ if (!skipResume) {
45
+ const resumed = await tryResumeSession(auth);
46
+ if (resumed) {
47
+ return resumed;
48
+ }
49
+ }
50
+ else {
51
+ console.debug('[FabricAuth] Embedded mode first load — skipping session resume to avoid stale cross-user session');
35
52
  }
36
53
  // Step 3: Embedded mode — postMessage auth via parent iframe host
37
- if (isEmbeddedMode(options)) {
54
+ if (inEmbeddedMode) {
38
55
  console.debug('[FabricAuth] Embedded mode detected, using postMessage auth');
39
56
  try {
40
57
  await embeddedFabricLogin(auth, options);
@@ -10,6 +10,24 @@ import type { FabricAuthOptions } from './types';
10
10
  * `EmbeddedModeOptions` interface.
11
11
  */
12
12
  export declare function isEmbeddedMode(options: FabricAuthOptions): boolean;
13
+ /**
14
+ * Returns whether an embedded postMessage handoff has already completed
15
+ * during the current page load.
16
+ */
17
+ export declare function hasEmbeddedHandoffCompleted(): boolean;
18
+ /**
19
+ * Marks that an embedded postMessage handoff has completed for this page
20
+ * load. Subsequent calls to {@link tryResumeSession} will be allowed
21
+ * (the established session is the current user's session).
22
+ */
23
+ export declare function markEmbeddedHandoffCompleted(): void;
24
+ /**
25
+ * Test-only helper that clears the embedded-handoff completion flag.
26
+ * Not exported from the package barrel.
27
+ *
28
+ * @internal
29
+ */
30
+ export declare function resetEmbeddedHandoffStateForTests(): void;
13
31
  /**
14
32
  * Attempts to resume an existing session without any user interaction.
15
33
  *
@@ -11,6 +11,48 @@ import { isEmbeddedMode as sharedIsEmbeddedMode } from '@microsoft/fabric-embedd
11
11
  export function isEmbeddedMode(options) {
12
12
  return sharedIsEmbeddedMode(options);
13
13
  }
14
+ /**
15
+ * Module-level flag tracking whether an embedded postMessage handoff has
16
+ * already completed during the current page load.
17
+ *
18
+ * In embedded mode the parent Fabric host is the source of truth for "who
19
+ * is signed in". A stale Rayfin session left over from a previous user in
20
+ * the iframe's `localStorage` must NOT be silently reused — otherwise
21
+ * switching Fabric users without reloading the iframe leaves the SPA
22
+ * authenticated as the previous user.
23
+ *
24
+ * On the first embedded auth call per page load we therefore skip
25
+ * {@link tryResumeSession} and run the full sign-out + handoff path
26
+ * inside {@link embeddedFabricLogin}. Once that completes we set this
27
+ * flag so subsequent calls (e.g. additional components asking for the
28
+ * session) can reuse the freshly established session via
29
+ * {@link tryResumeSession} without thrashing the postMessage channel.
30
+ */
31
+ let embeddedHandoffCompletedThisLoad = false;
32
+ /**
33
+ * Returns whether an embedded postMessage handoff has already completed
34
+ * during the current page load.
35
+ */
36
+ export function hasEmbeddedHandoffCompleted() {
37
+ return embeddedHandoffCompletedThisLoad;
38
+ }
39
+ /**
40
+ * Marks that an embedded postMessage handoff has completed for this page
41
+ * load. Subsequent calls to {@link tryResumeSession} will be allowed
42
+ * (the established session is the current user's session).
43
+ */
44
+ export function markEmbeddedHandoffCompleted() {
45
+ embeddedHandoffCompletedThisLoad = true;
46
+ }
47
+ /**
48
+ * Test-only helper that clears the embedded-handoff completion flag.
49
+ * Not exported from the package barrel.
50
+ *
51
+ * @internal
52
+ */
53
+ export function resetEmbeddedHandoffStateForTests() {
54
+ embeddedHandoffCompletedThisLoad = false;
55
+ }
14
56
  /**
15
57
  * Attempts to resume an existing session without any user interaction.
16
58
  *
@@ -11,11 +11,13 @@ import type { FabricAuthOptions } from './types';
11
11
  * **Behaviour:**
12
12
  * - If `fabricEmbedded=true` is **not** in the URL and `options.fabricEmbedded`
13
13
  * is not `true`, returns `null` immediately (no-op).
14
- * - If embedded mode is detected, runs a 3-step waterfall:
15
- * 1. Return existing session if already authenticated.
16
- * 2. Attempt refresh via refresh token.
17
- * 3. Request a handoff code from the parent host via `postMessage`
18
- * and exchange it for a session (PKCE, no popup).
14
+ * - On the first embedded call per page load, skips session resume to
15
+ * avoid silently reusing a stale session belonging to a previously
16
+ * signed-in Fabric user, and goes straight to the postMessage handoff.
17
+ * `embeddedFabricLogin` performs a hard `auth.signOut()` before the
18
+ * handoff so the new session always reflects the current Fabric user.
19
+ * - On subsequent calls within the same page load, runs the standard
20
+ * resume waterfall (existing session → refresh token → handoff).
19
21
  *
20
22
  * Apps that also support the popup flow should continue to call
21
23
  * {@link ensureSignedInWithFabric} from a user-gesture handler for the
@@ -1,5 +1,5 @@
1
1
  import { embeddedFabricLogin } from './embeddedFabricLogin';
2
- import { isEmbeddedMode, tryResumeSession } from './fabricAuthHelpers';
2
+ import { hasEmbeddedHandoffCompleted, isEmbeddedMode, tryResumeSession, } from './fabricAuthHelpers';
3
3
  /**
4
4
  * Initializes embedded Fabric authentication if running inside an iframe
5
5
  * with `?fabricEmbedded=true`.
@@ -11,11 +11,13 @@ import { isEmbeddedMode, tryResumeSession } from './fabricAuthHelpers';
11
11
  * **Behaviour:**
12
12
  * - If `fabricEmbedded=true` is **not** in the URL and `options.fabricEmbedded`
13
13
  * is not `true`, returns `null` immediately (no-op).
14
- * - If embedded mode is detected, runs a 3-step waterfall:
15
- * 1. Return existing session if already authenticated.
16
- * 2. Attempt refresh via refresh token.
17
- * 3. Request a handoff code from the parent host via `postMessage`
18
- * and exchange it for a session (PKCE, no popup).
14
+ * - On the first embedded call per page load, skips session resume to
15
+ * avoid silently reusing a stale session belonging to a previously
16
+ * signed-in Fabric user, and goes straight to the postMessage handoff.
17
+ * `embeddedFabricLogin` performs a hard `auth.signOut()` before the
18
+ * handoff so the new session always reflects the current Fabric user.
19
+ * - On subsequent calls within the same page load, runs the standard
20
+ * resume waterfall (existing session → refresh token → handoff).
19
21
  *
20
22
  * Apps that also support the popup flow should continue to call
21
23
  * {@link ensureSignedInWithFabric} from a user-gesture handler for the
@@ -29,10 +31,20 @@ export async function initEmbeddedAuth(auth, options) {
29
31
  if (!isEmbeddedMode(options)) {
30
32
  return null;
31
33
  }
32
- // Steps 1-2: existing session or refresh
33
- const resumed = await tryResumeSession(auth);
34
- if (resumed) {
35
- return resumed;
34
+ // Steps 1-2: existing session or refresh.
35
+ // Skip on the first embedded call per page load — a session left in
36
+ // iframe localStorage may belong to a previously signed-in Fabric
37
+ // user, and silently resuming it would defeat the handoff. Once
38
+ // `embeddedFabricLogin` has succeeded for this load, the cached
39
+ // session is the current user's and resuming is safe.
40
+ if (hasEmbeddedHandoffCompleted()) {
41
+ const resumed = await tryResumeSession(auth);
42
+ if (resumed) {
43
+ return resumed;
44
+ }
45
+ }
46
+ else {
47
+ console.debug('[FabricAuth:initEmbedded] First embedded call this page load — skipping session resume to avoid stale cross-user session');
36
48
  }
37
49
  // Step 3: postMessage handoff (no popup, no window.open)
38
50
  console.debug('[FabricAuth:initEmbedded] Starting postMessage handoff flow');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@microsoft/rayfin-auth-provider-fabric",
3
- "version": "1.24.0",
3
+ "version": "1.27.0",
4
4
  "description": "Fabric brokered authentication provider for Rayfin SDK",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -12,9 +12,9 @@
12
12
  ],
13
13
  "type": "module",
14
14
  "dependencies": {
15
- "@microsoft/rayfin-auth": "1.24.0",
16
- "@microsoft/fabric-embedded-host": "1.24.0",
17
- "@microsoft/rayfin-lib": "1.24.0"
15
+ "@microsoft/fabric-embedded-host": "1.27.0",
16
+ "@microsoft/rayfin-auth": "1.27.0",
17
+ "@microsoft/rayfin-lib": "1.27.0"
18
18
  },
19
19
  "devDependencies": {
20
20
  "typescript": "^5.8.3",