@microsoft/rayfin-auth-provider-fabric 1.20.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/LICENSE +21 -0
- package/README.md +43 -0
- package/dist/bridgeFabricCallback.d.ts +19 -0
- package/dist/bridgeFabricCallback.js +73 -0
- package/dist/brokerUrl.d.ts +14 -0
- package/dist/brokerUrl.js +29 -0
- package/dist/ensureSignedInWithFabric.d.ts +27 -0
- package/dist/ensureSignedInWithFabric.js +65 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/initiateFabricLogin.d.ts +18 -0
- package/dist/initiateFabricLogin.js +128 -0
- package/dist/types.d.ts +38 -0
- package/dist/types.js +2 -0
- package/package.json +43 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
Copyright (c) Microsoft Corporation.
|
|
2
|
+
|
|
3
|
+
MIT License
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# @microsoft/rayfin-auth-provider-fabric
|
|
2
|
+
|
|
3
|
+
## Security
|
|
4
|
+
|
|
5
|
+
Microsoft takes the security of our software products and services seriously, which
|
|
6
|
+
includes all source code repositories in our GitHub organizations.
|
|
7
|
+
|
|
8
|
+
**Please do not report security vulnerabilities through public GitHub issues.**
|
|
9
|
+
|
|
10
|
+
For security reporting information, locations, contact information, and policies,
|
|
11
|
+
please review the latest guidance for Microsoft repositories at
|
|
12
|
+
[https://aka.ms/SECURITY.md](https://aka.ms/SECURITY.md).
|
|
13
|
+
|
|
14
|
+
## Trademarks
|
|
15
|
+
|
|
16
|
+
This project may contain trademarks or logos for projects, products, or services.
|
|
17
|
+
Authorized use of Microsoft trademarks or logos must follow the [Microsoft Trademark and Brand Guidelines](https://www.microsoft.com/legal/intellectualproperty/trademarks/usage/general).
|
|
18
|
+
Use of Microsoft trademarks or logos in modified versions of this project must not cause confusion or imply Microsoft sponsorship.
|
|
19
|
+
Any use of third-party trademarks or logos is subject to those third parties' policies.
|
|
20
|
+
|
|
21
|
+
## License
|
|
22
|
+
|
|
23
|
+
Copyright (c) Microsoft Corporation.
|
|
24
|
+
|
|
25
|
+
MIT License
|
|
26
|
+
|
|
27
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
28
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
29
|
+
in the Software without restriction, including without limitation the rights
|
|
30
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
31
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
32
|
+
furnished to do so, subject to the following conditions:
|
|
33
|
+
|
|
34
|
+
The above copyright notice and this permission notice shall be included in all
|
|
35
|
+
copies or substantial portions of the Software.
|
|
36
|
+
|
|
37
|
+
THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
38
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
39
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
40
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
41
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
42
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
43
|
+
SOFTWARE.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** @internal BroadcastChannel name for backward-compat cross-tab handoff messaging. */
|
|
2
|
+
export declare const FABRIC_AUTH_CHANNEL = "rayfin_fabric_auth";
|
|
3
|
+
/**
|
|
4
|
+
* Bridge for backward compatibility with legacy broker redirect flows.
|
|
5
|
+
*
|
|
6
|
+
* Legacy brokers redirect the popup to the SPA's callback URL with handoff
|
|
7
|
+
* params instead of using postMessage. This function detects that scenario,
|
|
8
|
+
* forwards the params to the opener tab, and closes the popup.
|
|
9
|
+
*
|
|
10
|
+
* Delivery: postMessage when `window.opener` exists, BroadcastChannel otherwise.
|
|
11
|
+
*
|
|
12
|
+
* Call early in your callback page. Returns `true` if bridge fired (skip
|
|
13
|
+
* other callback handling), `false` if no handoff params detected.
|
|
14
|
+
*
|
|
15
|
+
* @deprecated Remove once legacy redirect flow is fully retired.
|
|
16
|
+
* @returns Whether a bridge message was sent.
|
|
17
|
+
*/
|
|
18
|
+
export declare function bridgeFabricCallback(): boolean;
|
|
19
|
+
//# sourceMappingURL=bridgeFabricCallback.d.ts.map
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/** @internal BroadcastChannel name for backward-compat cross-tab handoff messaging. */
|
|
2
|
+
export const FABRIC_AUTH_CHANNEL = 'rayfin_fabric_auth';
|
|
3
|
+
/**
|
|
4
|
+
* Bridge for backward compatibility with legacy broker redirect flows.
|
|
5
|
+
*
|
|
6
|
+
* Legacy brokers redirect the popup to the SPA's callback URL with handoff
|
|
7
|
+
* params instead of using postMessage. This function detects that scenario,
|
|
8
|
+
* forwards the params to the opener tab, and closes the popup.
|
|
9
|
+
*
|
|
10
|
+
* Delivery: postMessage when `window.opener` exists, BroadcastChannel otherwise.
|
|
11
|
+
*
|
|
12
|
+
* Call early in your callback page. Returns `true` if bridge fired (skip
|
|
13
|
+
* other callback handling), `false` if no handoff params detected.
|
|
14
|
+
*
|
|
15
|
+
* @deprecated Remove once legacy redirect flow is fully retired.
|
|
16
|
+
* @returns Whether a bridge message was sent.
|
|
17
|
+
*/
|
|
18
|
+
export function bridgeFabricCallback() {
|
|
19
|
+
// Query params first, then hash fragment. Only match verification_code/code
|
|
20
|
+
// from query params when window.opener exists (popup context) to avoid
|
|
21
|
+
// colliding with magic-link callbacks that use the same param names.
|
|
22
|
+
const queryParams = new URLSearchParams(window.location.search);
|
|
23
|
+
const hashRaw = window.location.hash.startsWith('#')
|
|
24
|
+
? window.location.hash.slice(1)
|
|
25
|
+
: window.location.hash;
|
|
26
|
+
const hashParams = new URLSearchParams(hashRaw);
|
|
27
|
+
// With opener: any code param could be a brokered handoff.
|
|
28
|
+
// Without opener: only the explicit `handoff` param is unambiguous.
|
|
29
|
+
const queryHandoffCode = window.opener
|
|
30
|
+
? queryParams.get('verification_code') ||
|
|
31
|
+
queryParams.get('code') ||
|
|
32
|
+
queryParams.get('handoff')
|
|
33
|
+
: queryParams.get('handoff');
|
|
34
|
+
const handoffCode = queryHandoffCode ||
|
|
35
|
+
hashParams.get('handoff') ||
|
|
36
|
+
hashParams.get('verification_code') ||
|
|
37
|
+
hashParams.get('code') ||
|
|
38
|
+
null;
|
|
39
|
+
const state = queryParams.get('state') || hashParams.get('state') || null;
|
|
40
|
+
if (!handoffCode || !state) {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
const payload = { type: 'brokeredAuth.handoff', state, handoffCode };
|
|
44
|
+
if (window.opener) {
|
|
45
|
+
// postMessage to same-origin opener.
|
|
46
|
+
console.debug('[FabricAuth] Bridge: forwarding handoff via postMessage to opener');
|
|
47
|
+
window.opener.postMessage(payload, window.location.origin);
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
// Opener severed — fall back to BroadcastChannel.
|
|
51
|
+
console.debug('[FabricAuth] Bridge: no opener — forwarding handoff via BroadcastChannel');
|
|
52
|
+
try {
|
|
53
|
+
const channel = new BroadcastChannel(FABRIC_AUTH_CHANNEL);
|
|
54
|
+
channel.postMessage(payload);
|
|
55
|
+
channel.close();
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
console.warn('[FabricAuth] Bridge: BroadcastChannel unavailable — cannot forward handoff');
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
// Scrub handoff params from URL
|
|
63
|
+
if (window.history?.replaceState) {
|
|
64
|
+
window.history.replaceState(null, '', window.location.pathname +
|
|
65
|
+
window.location.search
|
|
66
|
+
.replace(/[?&](?:verification_code|code|handoff|state)=[^&]*/g, '')
|
|
67
|
+
.replace(/^\?$/, ''));
|
|
68
|
+
}
|
|
69
|
+
// Attempt to close popup (no-op for user-opened tabs).
|
|
70
|
+
window.close();
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
//# sourceMappingURL=bridgeFabricCallback.js.map
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { FabricAuthOptions } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* Constructs the broker launch URL with PKCE and state parameters.
|
|
4
|
+
*
|
|
5
|
+
* Preserves any existing path and query parameters on `fabricPortalUrl`,
|
|
6
|
+
* appends the artifact deep-link path, and adds PKCE query parameters.
|
|
7
|
+
*
|
|
8
|
+
* @param options Auth options (workspace/project IDs, portal URL).
|
|
9
|
+
* @param codeChallenge PKCE S256 code challenge.
|
|
10
|
+
* @param state CSRF/correlation state nonce.
|
|
11
|
+
* @returns Fully constructed broker URL.
|
|
12
|
+
*/
|
|
13
|
+
export declare function buildBrokerUrl(options: FabricAuthOptions, codeChallenge: string, state: string): string;
|
|
14
|
+
//# sourceMappingURL=brokerUrl.d.ts.map
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Constructs the broker launch URL with PKCE and state parameters.
|
|
3
|
+
*
|
|
4
|
+
* Preserves any existing path and query parameters on `fabricPortalUrl`,
|
|
5
|
+
* appends the artifact deep-link path, and adds PKCE query parameters.
|
|
6
|
+
*
|
|
7
|
+
* @param options Auth options (workspace/project IDs, portal URL).
|
|
8
|
+
* @param codeChallenge PKCE S256 code challenge.
|
|
9
|
+
* @param state CSRF/correlation state nonce.
|
|
10
|
+
* @returns Fully constructed broker URL.
|
|
11
|
+
*/
|
|
12
|
+
export function buildBrokerUrl(options, codeChallenge, state) {
|
|
13
|
+
const portalUrl = new URL(options.fabricPortalUrl);
|
|
14
|
+
// Append artifact deep-link path
|
|
15
|
+
const basePath = portalUrl.pathname.replace(/\/$/, '');
|
|
16
|
+
const artifactPath = `/groups/${encodeURIComponent(options.workspaceId)}/appbackends/${encodeURIComponent(options.projectId)}`;
|
|
17
|
+
portalUrl.pathname = `${basePath}${artifactPath}`;
|
|
18
|
+
// `returnOrigin` must be a bare origin (scheme + host + port) — used as
|
|
19
|
+
// the postMessage targetOrigin and the redirect target for legacy brokers.
|
|
20
|
+
portalUrl.searchParams.set('returnOrigin', options.returnOrigin);
|
|
21
|
+
// Fallback redirect URL for legacy brokers that don't use postMessage.
|
|
22
|
+
const callbackUrl = options.callbackUrl || `${options.returnOrigin}/auth/callback`;
|
|
23
|
+
portalUrl.searchParams.set('callbackUrl', callbackUrl);
|
|
24
|
+
portalUrl.searchParams.set('code_challenge', codeChallenge);
|
|
25
|
+
portalUrl.searchParams.set('code_challenge_method', 'S256');
|
|
26
|
+
portalUrl.searchParams.set('state', state);
|
|
27
|
+
return portalUrl.toString();
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=brokerUrl.js.map
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { Auth, OpaqueSession } from '@microsoft/rayfin-auth';
|
|
2
|
+
import type { FabricAuthOptions } from './types';
|
|
3
|
+
/**
|
|
4
|
+
* Ensures the user is signed in via Fabric brokered authentication.
|
|
5
|
+
*
|
|
6
|
+
* Implements a 3-step waterfall — the first step that succeeds short-circuits the rest:
|
|
7
|
+
*
|
|
8
|
+
* 1. **Already authenticated** — if `auth.getSession().isAuthenticated` is true,
|
|
9
|
+
* return the existing session immediately.
|
|
10
|
+
* 2. **Refresh token** — if a refresh token is available, attempt `auth.refreshSession()`.
|
|
11
|
+
* Return the refreshed session on success; continue on failure.
|
|
12
|
+
* 3. **Open Fabric broker** — no existing auth path available. Open the Fabric Portal
|
|
13
|
+
* in a new tab via `initiateFabricLogin()` and wait for the Fabric extension to post
|
|
14
|
+
* the handoff code via `postMessage`. The function exchanges the code internally
|
|
15
|
+
* and creates the session. Once the promise resolves, return the new session.
|
|
16
|
+
*
|
|
17
|
+
* **Step 3 calls `window.open()`** — to avoid popup/tab blockers, call this function
|
|
18
|
+
* from inside a synchronous user-gesture handler (e.g., a button click).
|
|
19
|
+
* Steps 1–2 do not open windows and are safe to call on page load.
|
|
20
|
+
*
|
|
21
|
+
* @param auth The Auth instance.
|
|
22
|
+
* @param options Fabric authentication options (workspaceId, projectId, returnOrigin).
|
|
23
|
+
* @returns A promise that resolves with the authenticated session.
|
|
24
|
+
* @throws {AuthError} If all steps fail or the broker tab is blocked.
|
|
25
|
+
*/
|
|
26
|
+
export declare function ensureSignedInWithFabric(auth: Auth, options: FabricAuthOptions): Promise<OpaqueSession>;
|
|
27
|
+
//# sourceMappingURL=ensureSignedInWithFabric.d.ts.map
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { AuthError } from '@microsoft/rayfin-lib';
|
|
2
|
+
import { initiateFabricLogin } from './initiateFabricLogin';
|
|
3
|
+
/**
|
|
4
|
+
* Ensures the user is signed in via Fabric brokered authentication.
|
|
5
|
+
*
|
|
6
|
+
* Implements a 3-step waterfall — the first step that succeeds short-circuits the rest:
|
|
7
|
+
*
|
|
8
|
+
* 1. **Already authenticated** — if `auth.getSession().isAuthenticated` is true,
|
|
9
|
+
* return the existing session immediately.
|
|
10
|
+
* 2. **Refresh token** — if a refresh token is available, attempt `auth.refreshSession()`.
|
|
11
|
+
* Return the refreshed session on success; continue on failure.
|
|
12
|
+
* 3. **Open Fabric broker** — no existing auth path available. Open the Fabric Portal
|
|
13
|
+
* in a new tab via `initiateFabricLogin()` and wait for the Fabric extension to post
|
|
14
|
+
* the handoff code via `postMessage`. The function exchanges the code internally
|
|
15
|
+
* and creates the session. Once the promise resolves, return the new session.
|
|
16
|
+
*
|
|
17
|
+
* **Step 3 calls `window.open()`** — to avoid popup/tab blockers, call this function
|
|
18
|
+
* from inside a synchronous user-gesture handler (e.g., a button click).
|
|
19
|
+
* Steps 1–2 do not open windows and are safe to call on page load.
|
|
20
|
+
*
|
|
21
|
+
* @param auth The Auth instance.
|
|
22
|
+
* @param options Fabric authentication options (workspaceId, projectId, returnOrigin).
|
|
23
|
+
* @returns A promise that resolves with the authenticated session.
|
|
24
|
+
* @throws {AuthError} If all steps fail or the broker tab is blocked.
|
|
25
|
+
*/
|
|
26
|
+
export async function ensureSignedInWithFabric(auth, options) {
|
|
27
|
+
// Step 1: Already authenticated
|
|
28
|
+
console.debug('[FabricAuth] Step 1: checking for existing session');
|
|
29
|
+
const currentSession = auth.getSession();
|
|
30
|
+
if (currentSession.isAuthenticated) {
|
|
31
|
+
console.debug('[FabricAuth] Step 1: existing session found, skipping remaining steps');
|
|
32
|
+
return currentSession;
|
|
33
|
+
}
|
|
34
|
+
// Step 2: Try refresh token
|
|
35
|
+
console.debug('[FabricAuth] Step 2: checking for refresh token');
|
|
36
|
+
if (auth.hasRefreshToken()) {
|
|
37
|
+
try {
|
|
38
|
+
await auth.refreshSession();
|
|
39
|
+
const refreshedSession = auth.getSession();
|
|
40
|
+
if (refreshedSession.isAuthenticated) {
|
|
41
|
+
console.debug('[FabricAuth] Step 2: session refreshed successfully');
|
|
42
|
+
return refreshedSession;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
console.warn('[FabricAuth] Step 2: session refresh failed, continuing to step 3');
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
console.debug('[FabricAuth] Step 2: no refresh token available, skipping to step 3');
|
|
51
|
+
}
|
|
52
|
+
// Step 3: Open Fabric broker in a new tab
|
|
53
|
+
// WARNING: This calls window.open() — must be in a user-gesture context
|
|
54
|
+
console.debug('[FabricAuth] Step 3: initiating Fabric broker login');
|
|
55
|
+
await initiateFabricLogin(auth, options);
|
|
56
|
+
// Session was created inside initiateFabricLogin via createSessionFromTokenResponse
|
|
57
|
+
const newSession = auth.getSession();
|
|
58
|
+
if (newSession.isAuthenticated) {
|
|
59
|
+
console.debug('[FabricAuth] Step 3: Fabric broker login succeeded, session established');
|
|
60
|
+
return newSession;
|
|
61
|
+
}
|
|
62
|
+
console.warn('[FabricAuth] Step 3: Fabric broker login completed but no session was established');
|
|
63
|
+
throw new AuthError('Fabric authentication completed but no session was established.', 'SESSION_NOT_ESTABLISHED');
|
|
64
|
+
}
|
|
65
|
+
//# sourceMappingURL=ensureSignedInWithFabric.js.map
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export type { FabricAuthOptions } from './types';
|
|
2
|
+
export { bridgeFabricCallback } from './bridgeFabricCallback';
|
|
3
|
+
export { ensureSignedInWithFabric } from './ensureSignedInWithFabric';
|
|
4
|
+
export { initiateFabricLogin } from './initiateFabricLogin';
|
|
5
|
+
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Auth } from '@microsoft/rayfin-auth';
|
|
2
|
+
import type { FabricAuthOptions } from './types';
|
|
3
|
+
/**
|
|
4
|
+
* Initiates brokered authentication by opening the broker portal in a popup.
|
|
5
|
+
*
|
|
6
|
+
* **Must be called inside a user click handler** to avoid popup blockers.
|
|
7
|
+
*
|
|
8
|
+
* Generates PKCE params, opens the broker URL, and listens for a
|
|
9
|
+
* `brokeredAuth.handoff` postMessage. On handoff, exchanges the code for
|
|
10
|
+
* tokens and creates a session. The `code_verifier` is kept in closure only.
|
|
11
|
+
*
|
|
12
|
+
* @param auth Auth instance for token exchange.
|
|
13
|
+
* @param options Broker auth options (`returnOrigin` required).
|
|
14
|
+
* @returns Resolves when the session is established.
|
|
15
|
+
* @throws {AuthError} On missing options, blocked popup, or auth failure.
|
|
16
|
+
*/
|
|
17
|
+
export declare function initiateFabricLogin(auth: Auth, options: FabricAuthOptions): Promise<void>;
|
|
18
|
+
//# sourceMappingURL=initiateFabricLogin.d.ts.map
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { generateCodeVerifier, generateCodeChallenge, generateState, } from '@microsoft/rayfin-auth';
|
|
2
|
+
import { AuthError } from '@microsoft/rayfin-lib';
|
|
3
|
+
import { FABRIC_AUTH_CHANNEL } from './bridgeFabricCallback';
|
|
4
|
+
import { buildBrokerUrl } from './brokerUrl';
|
|
5
|
+
/**
|
|
6
|
+
* Initiates brokered authentication by opening the broker portal in a popup.
|
|
7
|
+
*
|
|
8
|
+
* **Must be called inside a user click handler** to avoid popup blockers.
|
|
9
|
+
*
|
|
10
|
+
* Generates PKCE params, opens the broker URL, and listens for a
|
|
11
|
+
* `brokeredAuth.handoff` postMessage. On handoff, exchanges the code for
|
|
12
|
+
* tokens and creates a session. The `code_verifier` is kept in closure only.
|
|
13
|
+
*
|
|
14
|
+
* @param auth Auth instance for token exchange.
|
|
15
|
+
* @param options Broker auth options (`returnOrigin` required).
|
|
16
|
+
* @returns Resolves when the session is established.
|
|
17
|
+
* @throws {AuthError} On missing options, blocked popup, or auth failure.
|
|
18
|
+
*/
|
|
19
|
+
export async function initiateFabricLogin(auth, options) {
|
|
20
|
+
// Validate required params
|
|
21
|
+
if (!options.workspaceId) {
|
|
22
|
+
throw new AuthError('workspaceId is required.', 'MISSING_WORKSPACE_ID');
|
|
23
|
+
}
|
|
24
|
+
if (!options.projectId) {
|
|
25
|
+
throw new AuthError('projectId is required.', 'MISSING_PROJECT_ID');
|
|
26
|
+
}
|
|
27
|
+
if (!options.returnOrigin) {
|
|
28
|
+
throw new AuthError('returnOrigin is required for Fabric authentication.', 'MISSING_RETURN_ORIGIN');
|
|
29
|
+
}
|
|
30
|
+
if (!options.fabricPortalUrl) {
|
|
31
|
+
throw new AuthError('fabricPortalUrl is required for Fabric authentication.', 'MISSING_FABRIC_PORTAL_URL');
|
|
32
|
+
}
|
|
33
|
+
// PKCE + state
|
|
34
|
+
const codeVerifier = generateCodeVerifier();
|
|
35
|
+
const codeChallenge = await generateCodeChallenge(codeVerifier);
|
|
36
|
+
const state = generateState();
|
|
37
|
+
const expectedBrokerOrigin = new URL(options.fabricPortalUrl).origin;
|
|
38
|
+
const brokerUrl = buildBrokerUrl(options, codeChallenge, state);
|
|
39
|
+
return new Promise((resolve, reject) => {
|
|
40
|
+
let fabricWindow = null;
|
|
41
|
+
let channel = null;
|
|
42
|
+
// 5-minute timeout — matches the backend's handoff code TTL.
|
|
43
|
+
const TIMEOUT_MS = 5 * 60 * 1000;
|
|
44
|
+
function cleanup() {
|
|
45
|
+
window.removeEventListener('message', handleMessage);
|
|
46
|
+
channel?.close();
|
|
47
|
+
channel = null;
|
|
48
|
+
clearTimeout(timeoutId);
|
|
49
|
+
if (fabricWindow && !fabricWindow.closed) {
|
|
50
|
+
fabricWindow.close();
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/** Handles incoming brokeredAuth postMessage events. */
|
|
54
|
+
function handleMessage(event) {
|
|
55
|
+
// Origin check is advisory — state + PKCE provide the real security.
|
|
56
|
+
if (event.origin !== expectedBrokerOrigin) {
|
|
57
|
+
console.warn(`[FabricAuth] postMessage origin mismatch (expected="${expectedBrokerOrigin}", actual="${event.origin}") — continuing (secured by state + PKCE)`);
|
|
58
|
+
}
|
|
59
|
+
if (!event.data || typeof event.data !== 'object') {
|
|
60
|
+
console.debug('[FabricAuth] postMessage ignored: non-object payload');
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
const { type, state: msgState } = event.data;
|
|
64
|
+
console.debug(`[FabricAuth] postMessage received: type=${String(type)}, stateMatch=${msgState === state}`);
|
|
65
|
+
if (msgState !== state) {
|
|
66
|
+
console.debug('[FabricAuth] postMessage ignored: state mismatch');
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
if (type === 'brokeredAuth.handoff') {
|
|
70
|
+
console.debug('[FabricAuth] Handling brokeredAuth.handoff — exchanging handoff code for tokens');
|
|
71
|
+
const { handoffCode } = event.data;
|
|
72
|
+
cleanup();
|
|
73
|
+
// redirectUri must match the value the broker used when creating the handoff code.
|
|
74
|
+
const redirectUri = options.returnOrigin;
|
|
75
|
+
auth
|
|
76
|
+
.getAuthApi()
|
|
77
|
+
.exchangeVerificationCode({
|
|
78
|
+
verificationCode: handoffCode,
|
|
79
|
+
codeVerifier,
|
|
80
|
+
codeType: 'fabric_handoff',
|
|
81
|
+
redirectUri,
|
|
82
|
+
})
|
|
83
|
+
.then((tokenResponse) => {
|
|
84
|
+
console.debug('[FabricAuth] Token exchange succeeded, creating session');
|
|
85
|
+
auth.createSessionFromTokenResponse(tokenResponse);
|
|
86
|
+
resolve();
|
|
87
|
+
})
|
|
88
|
+
.catch((err) => {
|
|
89
|
+
console.warn('[FabricAuth] Token exchange failed', err);
|
|
90
|
+
reject(err);
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
else if (type === 'brokeredAuth.error') {
|
|
94
|
+
console.warn(`[FabricAuth] Handling brokeredAuth.error — code=${String(event.data.error ?? 'unknown')}`);
|
|
95
|
+
cleanup();
|
|
96
|
+
reject(new AuthError(event.data.errorDescription || 'Fabric authentication failed.', event.data.error || 'FABRIC_AUTH_FAILED'));
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
const timeoutId = setTimeout(() => {
|
|
100
|
+
console.warn('[FabricAuth] Broker handoff timed out after 5 minutes');
|
|
101
|
+
cleanup();
|
|
102
|
+
reject(new AuthError('Fabric authentication timed out after 5 minutes. Please try again.', 'FABRIC_AUTH_TIMEOUT'));
|
|
103
|
+
}, TIMEOUT_MS);
|
|
104
|
+
// Listen for handoff via postMessage and BroadcastChannel (legacy bridge).
|
|
105
|
+
console.debug('[FabricAuth] Registering postMessage listener for broker handoff');
|
|
106
|
+
window.addEventListener('message', handleMessage);
|
|
107
|
+
try {
|
|
108
|
+
channel = new BroadcastChannel(FABRIC_AUTH_CHANNEL);
|
|
109
|
+
channel.onmessage = handleMessage;
|
|
110
|
+
console.debug('[FabricAuth] BroadcastChannel listener registered for backward-compat bridge');
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
console.debug('[FabricAuth] BroadcastChannel not available — relying on postMessage only');
|
|
114
|
+
}
|
|
115
|
+
// Open broker portal in a centered popup
|
|
116
|
+
const popupWidth = 600;
|
|
117
|
+
const popupHeight = 700;
|
|
118
|
+
const left = Math.round(window.screenX + (window.outerWidth - popupWidth) / 2);
|
|
119
|
+
const top = Math.round(window.screenY + (window.outerHeight - popupHeight) / 2);
|
|
120
|
+
fabricWindow = window.open(brokerUrl, 'fabricAuth', `popup=yes,width=${popupWidth},height=${popupHeight},left=${left},top=${top}`);
|
|
121
|
+
if (!fabricWindow) {
|
|
122
|
+
cleanup();
|
|
123
|
+
reject(new AuthError('Failed to open Fabric sign-in page. ' +
|
|
124
|
+
'Please allow popups for this site and try again.', 'TAB_BLOCKED'));
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
//# sourceMappingURL=initiateFabricLogin.js.map
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Options for initiating Fabric brokered authentication.
|
|
3
|
+
*/
|
|
4
|
+
export interface FabricAuthOptions {
|
|
5
|
+
/** The Fabric workspace ID. */
|
|
6
|
+
workspaceId: string;
|
|
7
|
+
/** The Fabric project/app ID (artifact ID). */
|
|
8
|
+
projectId: string;
|
|
9
|
+
/**
|
|
10
|
+
* The Fabric Portal URL, including any existing path and query parameters.
|
|
11
|
+
* The SDK appends `/groups/{workspaceId}/appbackends/{projectId}` to the
|
|
12
|
+
* existing path and appends PKCE query parameters, preserving everything
|
|
13
|
+
* already present in the URL.
|
|
14
|
+
*
|
|
15
|
+
* Examples:
|
|
16
|
+
* - Production: `https://app.fabric.microsoft.com`
|
|
17
|
+
* - With path: `https://app.fabric.microsoft.com/some/portal/path`
|
|
18
|
+
* - Dev/test: `https://powerbi-df.analysis-df.windows.net?debug.useLocalManifests=1&experience=power-bi`
|
|
19
|
+
*/
|
|
20
|
+
fabricPortalUrl: string;
|
|
21
|
+
/**
|
|
22
|
+
* The origin of the SPA that initiated the Fabric auth flow.
|
|
23
|
+
* Used as the `targetOrigin` for `postMessage` delivery of the handoff code.
|
|
24
|
+
* Must be a bare origin string (e.g., `https://myapp.com`), not a full URL with path.
|
|
25
|
+
*/
|
|
26
|
+
returnOrigin: string;
|
|
27
|
+
/**
|
|
28
|
+
* Full redirect callback URL for old Fabric Portal deployments that haven't
|
|
29
|
+
* adopted postMessage yet. When provided, this URL is sent as the `callbackUrl`
|
|
30
|
+
* query parameter so the old Portal can redirect back with the handoff code.
|
|
31
|
+
*
|
|
32
|
+
* Defaults to `${returnOrigin}/auth/callback` when omitted.
|
|
33
|
+
*
|
|
34
|
+
* @deprecated Backward-compat only — will be removed once postMessage rollout is complete.
|
|
35
|
+
*/
|
|
36
|
+
callbackUrl?: string;
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=types.d.ts.map
|
package/dist/types.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@microsoft/rayfin-auth-provider-fabric",
|
|
3
|
+
"version": "1.20.0",
|
|
4
|
+
"description": "Fabric brokered authentication provider for Rayfin SDK",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"files": [
|
|
8
|
+
"dist/**/*.js",
|
|
9
|
+
"dist/**/*.d.ts",
|
|
10
|
+
"!dist/**/__tests__/**",
|
|
11
|
+
"LICENSE"
|
|
12
|
+
],
|
|
13
|
+
"type": "module",
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"@microsoft/rayfin-lib": "1.20.0",
|
|
16
|
+
"@microsoft/rayfin-auth": "1.20.0"
|
|
17
|
+
},
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"typescript": "^5.8.3",
|
|
20
|
+
"vitest": "^3.2.3",
|
|
21
|
+
"@vitest/coverage-v8": "~3.2.4",
|
|
22
|
+
"rimraf": "~6.0.1"
|
|
23
|
+
},
|
|
24
|
+
"publishConfig": {
|
|
25
|
+
"registry": "https://npm.pkg.github.com",
|
|
26
|
+
"access": "restricted"
|
|
27
|
+
},
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "https://github.com/microsoft/project-rayfin.git",
|
|
31
|
+
"directory": "packages/typescript-sdk/auth-provider-fabric"
|
|
32
|
+
},
|
|
33
|
+
"keywords": [],
|
|
34
|
+
"author": "",
|
|
35
|
+
"license": "MIT",
|
|
36
|
+
"scripts": {
|
|
37
|
+
"build": "tsc",
|
|
38
|
+
"build:watch": "tsc --watch",
|
|
39
|
+
"clean": "rimraf dist && rimraf .tsbuildinfo",
|
|
40
|
+
"test": "vitest run",
|
|
41
|
+
"test:watch": "vitest"
|
|
42
|
+
}
|
|
43
|
+
}
|