@microsoft/rayfin-auth-provider-fabric 1.36.0-alpha.1601 → 1.36.0-alpha.1663
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 +16 -0
- package/assets/docs/index.md +80 -9
- package/dist/PostMessageAuthTransport.d.ts +9 -0
- package/dist/PostMessageAuthTransport.js +17 -7
- package/dist/embeddedFabricLogin.js +25 -4
- package/dist/ensureSignedInWithFabric.d.ts +5 -2
- package/dist/ensureSignedInWithFabric.js +26 -5
- package/dist/externalEmbedClassification.d.ts +49 -0
- package/dist/externalEmbedClassification.js +107 -0
- package/dist/externalEmbedProtocol.d.ts +9 -0
- package/dist/externalEmbedProtocol.js +27 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/initEmbeddedAuth.d.ts +9 -5
- package/dist/initEmbeddedAuth.js +26 -9
- package/dist/signInWithEntraToken.d.ts +28 -0
- package/dist/signInWithEntraToken.js +165 -0
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -8,6 +8,22 @@ npm create @microsoft/rayfin@latest
|
|
|
8
8
|
|
|
9
9
|
For more details, [visit our docs](https://aka.ms/rayfin/docs).
|
|
10
10
|
|
|
11
|
+
## Direct Entra token sign-in
|
|
12
|
+
|
|
13
|
+
Already have a delegated Entra access token?
|
|
14
|
+
Use `signInWithEntraToken` to establish a Rayfin session on an existing `Auth` instance without a popup or iframe.
|
|
15
|
+
The helper supports browsers and Node.js.
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
import { signInWithEntraToken } from '@microsoft/rayfin-auth-provider-fabric';
|
|
19
|
+
|
|
20
|
+
const session = await signInWithEntraToken(client.auth, { entraToken });
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Configure the client with the target AppBackend's workload API base URL.
|
|
24
|
+
The helper derives the direct token-exchange endpoint and makes the resulting Rayfin session available to attached SDK clients.
|
|
25
|
+
See [direct sign-in setup and usage](./assets/docs/index.md#direct-entra-token-sign-in) for token requirements, Node.js usage, and session handling.
|
|
26
|
+
|
|
11
27
|
## Security
|
|
12
28
|
|
|
13
29
|
Microsoft takes the security of our software products and services seriously, which
|
package/assets/docs/index.md
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
# Fabric auth provider
|
|
2
2
|
|
|
3
|
-
Fabric brokered authentication helpers for Rayfin
|
|
3
|
+
Fabric brokered authentication helpers for Rayfin applications.
|
|
4
4
|
|
|
5
|
-
Use `@microsoft/rayfin-auth-provider-fabric`
|
|
5
|
+
Use `@microsoft/rayfin-auth-provider-fabric` to sign in through a Fabric-hosted browser experience or exchange an existing delegated Entra token directly for a Rayfin session.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Popup and iframe helpers require a browser.
|
|
8
|
+
`signInWithEntraToken()` supports browsers and Node.js and installs the exchanged session on your existing `Auth` instance.
|
|
8
9
|
|
|
9
10
|
## Installation
|
|
10
11
|
|
|
@@ -12,9 +13,71 @@ The package is designed for browser applications that already use `@microsoft/ra
|
|
|
12
13
|
npm install @microsoft/rayfin-auth-provider-fabric @microsoft/rayfin-auth @microsoft/rayfin-lib
|
|
13
14
|
```
|
|
14
15
|
|
|
15
|
-
##
|
|
16
|
+
## Direct Entra token sign-in
|
|
16
17
|
|
|
17
|
-
|
|
18
|
+
Use this flow when your app already obtains a delegated Entra token through MSAL, Azure CLI, or another identity library.
|
|
19
|
+
Token acquisition remains your responsibility.
|
|
20
|
+
The token must target the Fabric/Power BI resource accepted by your deployment, contain delegated `Item.Execute.All`, and belong to the target item's owning tenant.
|
|
21
|
+
The user must also have Execute permission on that item; app-only tokens are not supported.
|
|
22
|
+
The app must have external Entra exchange enabled.
|
|
23
|
+
|
|
24
|
+
```typescript
|
|
25
|
+
import { Auth } from '@microsoft/rayfin-auth';
|
|
26
|
+
import { ApiClient } from '@microsoft/rayfin-lib';
|
|
27
|
+
import { signInWithEntraToken } from '@microsoft/rayfin-auth-provider-fabric';
|
|
28
|
+
|
|
29
|
+
const apiClient = new ApiClient({
|
|
30
|
+
baseUrl: rayfinEndpoint,
|
|
31
|
+
publishableKey,
|
|
32
|
+
});
|
|
33
|
+
const auth = new Auth(apiClient, { storage: false });
|
|
34
|
+
auth.attachToClient(apiClient);
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
const session = await signInWithEntraToken(auth, { entraToken });
|
|
38
|
+
console.log('Authenticated', session.isAuthenticated);
|
|
39
|
+
// Use SDK clients attached to apiClient here.
|
|
40
|
+
} finally {
|
|
41
|
+
auth.destroy();
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
This example works in Node.js without browser globals.
|
|
46
|
+
`rayfinEndpoint` is the trusted HTTPS AppBackend workload API base URL, including its existing path, not the app's static-hosting URL or a full token endpoint.
|
|
47
|
+
Use an absolute HTTPS URL, including for localhost, with no credentials, query string, or fragment; relative proxy bases are not supported.
|
|
48
|
+
`entraToken` is the raw token string, without the `Bearer` scheme prefix.
|
|
49
|
+
The SDK resolves `/api/auth/v1/brokered/token` against the configured base.
|
|
50
|
+
No `FabricAuthOptions`, popup, iframe, PKCE, or return origin is needed.
|
|
51
|
+
|
|
52
|
+
```typescript
|
|
53
|
+
function signInWithEntraToken(
|
|
54
|
+
auth: Auth,
|
|
55
|
+
options: EntraTokenSignInOptions
|
|
56
|
+
): Promise<OpaqueSession>;
|
|
57
|
+
|
|
58
|
+
interface EntraTokenSignInOptions {
|
|
59
|
+
entraToken: string;
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
For a `RayfinClient`, pass `client.auth`; it is already attached to the client's HTTP transport.
|
|
64
|
+
The returned `OpaqueSession` exposes session metadata, not tokens.
|
|
65
|
+
Subsequent authenticated requests and refresh use the Rayfin session, not the Entra token.
|
|
66
|
+
The supplied `Auth` instance controls persistence; `storage: false` keeps the session in memory.
|
|
67
|
+
Use a separate instance per user on a server and call `auth.destroy()` when it is no longer needed.
|
|
68
|
+
`RayfinServerClient` does not expose `auth`; this helper expects a standalone `Auth` or `RayfinClient.auth`, not an access-token callback.
|
|
69
|
+
|
|
70
|
+
Each explicit call attempts an exchange rather than silently returning a previous session.
|
|
71
|
+
Concurrent direct calls on the same `Auth` instance run in invocation order and coordinate with its refresh operations.
|
|
72
|
+
Success persists and replaces the session before emitting the login event; failure preserves an existing unexpired in-memory session without treating it as a successful new sign-in.
|
|
73
|
+
The helper does not revoke the previous server session.
|
|
74
|
+
Failures reject with `AuthError`; do not treat a failed sign-in as authentication of the requested identity.
|
|
75
|
+
Browser callers need the service's CORS policy to permit the calling origin and authorization header.
|
|
76
|
+
The helper does not acquire Entra tokens, follow exchange redirects, or automatically retry a rejected exchange.
|
|
77
|
+
|
|
78
|
+
## Browser quick start
|
|
79
|
+
|
|
80
|
+
For the browser broker experience, the recommended entry point is `ensureSignedInWithFabric()`.
|
|
18
81
|
|
|
19
82
|
It performs a three-step waterfall:
|
|
20
83
|
|
|
@@ -73,6 +136,8 @@ This supports production and development portal URLs such as `https://app.fabric
|
|
|
73
136
|
|
|
74
137
|
## Supported flows
|
|
75
138
|
|
|
139
|
+
Use `signInWithEntraToken()` when your app already has a delegated Entra token and wants to sign in without a browser broker.
|
|
140
|
+
|
|
76
141
|
Use `ensureSignedInWithFabric()` when you want silent-session and refresh-token fallback behavior before opening the broker UI.
|
|
77
142
|
|
|
78
143
|
Use `initiateFabricLogin()` when you only want the broker step and do not need the session and refresh pre-checks.
|
|
@@ -114,7 +179,7 @@ The bridge returns `true` when it handled a Fabric handoff and `false` when the
|
|
|
114
179
|
|
|
115
180
|
## Behavior notes
|
|
116
181
|
|
|
117
|
-
- The broker URL is built with PKCE using the `S256` challenge method.
|
|
182
|
+
- The browser broker URL is built with PKCE using the `S256` challenge method; direct token sign-in does not use PKCE.
|
|
118
183
|
- Existing query parameters on `fabricPortalUrl` are preserved.
|
|
119
184
|
- `callbackUrl` defaults to `${returnOrigin}/auth/callback` when omitted.
|
|
120
185
|
- The broker handoff waits up to five minutes before timing out.
|
|
@@ -126,6 +191,10 @@ The package throws `AuthError` values from `@microsoft/rayfin-lib` for validatio
|
|
|
126
191
|
|
|
127
192
|
Common cases include missing required options, blocked popups, explicit broker errors, and handoff timeout.
|
|
128
193
|
|
|
194
|
+
Direct token sign-in reports `INVALID_REQUEST` for invalid local inputs, `EXCHANGE_NOT_ENABLED` for a disabled exchange, `AUTH_FAILED` for token rejection, `INSUFFICIENT_PERMISSIONS` for missing Execute access, and `NOT_AVAILABLE` for an unavailable endpoint.
|
|
195
|
+
Transport or other HTTP failures use `TOKEN_EXCHANGE_FAILED`; malformed successful responses use `INVALID_TOKEN_RESPONSE`.
|
|
196
|
+
Errors do not include the supplied token or raw server response.
|
|
197
|
+
|
|
129
198
|
```typescript
|
|
130
199
|
import { AuthError } from '@microsoft/rayfin-lib';
|
|
131
200
|
|
|
@@ -138,8 +207,10 @@ try {
|
|
|
138
207
|
}
|
|
139
208
|
```
|
|
140
209
|
|
|
141
|
-
##
|
|
210
|
+
## Runtime requirements
|
|
142
211
|
|
|
143
|
-
|
|
212
|
+
The public package entry point and `signInWithEntraToken()` can be imported and used in Node.js without browser globals.
|
|
213
|
+
Use memory-only or appropriate custom `Auth` storage outside a browser.
|
|
144
214
|
|
|
145
|
-
|
|
215
|
+
The popup, iframe, and legacy callback helpers remain browser-only.
|
|
216
|
+
They depend on browser APIs such as `window.open()`, `postMessage`, `BroadcastChannel`, and `window.location`.
|
|
@@ -35,5 +35,14 @@ export declare function requestHandoff(params: {
|
|
|
35
35
|
codeChallengeMethod: string;
|
|
36
36
|
state: string;
|
|
37
37
|
timeoutMs?: number;
|
|
38
|
+
/** externalEmbed only: the app's brokered-authorize endpoint URL. */
|
|
39
|
+
brokeredAuthorizeUrl?: string;
|
|
40
|
+
/** externalEmbed only: the app's Fabric artifact identifier. */
|
|
41
|
+
artifactId?: string;
|
|
42
|
+
/**
|
|
43
|
+
* externalEmbed only: the pinned parent origin. When set, the request is
|
|
44
|
+
* delivered to (and the response accepted only from) this origin.
|
|
45
|
+
*/
|
|
46
|
+
targetOrigin?: string;
|
|
38
47
|
}): Promise<PostMessageHandoffResult>;
|
|
39
48
|
//# sourceMappingURL=PostMessageAuthTransport.d.ts.map
|
|
@@ -21,21 +21,31 @@ import { AuthError } from '@microsoft/rayfin-lib';
|
|
|
21
21
|
* supported public API.
|
|
22
22
|
*/
|
|
23
23
|
export function requestHandoff(params) {
|
|
24
|
-
const { callbackUrl, codeChallenge, codeChallengeMethod, state, timeoutMs } = params;
|
|
24
|
+
const { callbackUrl, codeChallenge, codeChallengeMethod, state, timeoutMs, brokeredAuthorizeUrl, artifactId, targetOrigin, } = params;
|
|
25
25
|
if (!window.parent || window.parent === window) {
|
|
26
26
|
return Promise.reject(new AuthError('No parent window — embedded auth requires an iframe host.', 'NO_PARENT_WINDOW'));
|
|
27
27
|
}
|
|
28
|
+
// Normal Fabric payload is left byte-for-byte unchanged; the externalEmbed
|
|
29
|
+
// fields are added only when supplied.
|
|
30
|
+
const payload = {
|
|
31
|
+
callbackUrl,
|
|
32
|
+
codeChallenge,
|
|
33
|
+
codeChallengeMethod,
|
|
34
|
+
state,
|
|
35
|
+
};
|
|
36
|
+
if (brokeredAuthorizeUrl !== undefined) {
|
|
37
|
+
payload.brokeredAuthorizeUrl = brokeredAuthorizeUrl;
|
|
38
|
+
}
|
|
39
|
+
if (artifactId !== undefined) {
|
|
40
|
+
payload.artifactId = artifactId;
|
|
41
|
+
}
|
|
28
42
|
return sendBridgeRequest({
|
|
29
43
|
target: window.parent,
|
|
30
44
|
channel: 'fabric-auth',
|
|
31
45
|
kind: 'auth.requestHandoff',
|
|
32
|
-
payload
|
|
33
|
-
callbackUrl,
|
|
34
|
-
codeChallenge,
|
|
35
|
-
codeChallengeMethod,
|
|
36
|
-
state,
|
|
37
|
-
},
|
|
46
|
+
payload,
|
|
38
47
|
timeoutMs,
|
|
48
|
+
...(targetOrigin !== undefined ? { targetOrigin } : {}),
|
|
39
49
|
}).catch((err) => {
|
|
40
50
|
// Re-wrap BridgeError as AuthError for backward compatibility
|
|
41
51
|
if (err instanceof BridgeError) {
|
|
@@ -2,6 +2,7 @@ import { generateCodeVerifier, generateCodeChallenge, generateState, } from '@mi
|
|
|
2
2
|
import { createSessionFromTokenResponse } from '@microsoft/rayfin-auth/_internal';
|
|
3
3
|
import { AuthError, assertBrowser } from '@microsoft/rayfin-lib';
|
|
4
4
|
import { requestHandoff } from './PostMessageAuthTransport.js';
|
|
5
|
+
import { getPinnedParentOrigin, isExternalEmbedScenario, } from './externalEmbedClassification.js';
|
|
5
6
|
import { markEmbeddedHandoffCompleted } from './fabricAuthHelpers.js';
|
|
6
7
|
import { hasFabricUserHint } from './fabricUserHint.js';
|
|
7
8
|
/**
|
|
@@ -33,10 +34,15 @@ export async function embeddedFabricLogin(auth, options) {
|
|
|
33
34
|
if (!options.returnOrigin) {
|
|
34
35
|
throw new AuthError('returnOrigin is required for embedded Fabric authentication.', 'MISSING_RETURN_ORIGIN');
|
|
35
36
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
37
|
+
// In the acked externalEmbed scenario there is no server-side serve-cookie
|
|
38
|
+
// identity gate, so a `?_fu=` hint carries no comparison and is ignored: the
|
|
39
|
+
// SDK always signs out before the handoff. On the normal Fabric path the
|
|
40
|
+
// hint-gated sign-out below is unchanged.
|
|
41
|
+
const externalEmbed = isExternalEmbedScenario();
|
|
42
|
+
if (externalEmbed || !hasFabricUserHint()) {
|
|
43
|
+
// Discard any prior session before the handoff. Errors are swallowed — the
|
|
44
|
+
// stale token may already be invalid server-side, and the local clear that
|
|
45
|
+
// signOut performs in its own catch is what matters.
|
|
40
46
|
try {
|
|
41
47
|
await auth.signOut();
|
|
42
48
|
}
|
|
@@ -54,11 +60,26 @@ export async function embeddedFabricLogin(auth, options) {
|
|
|
54
60
|
// not the portal itself. The extension runs at a different origin than
|
|
55
61
|
// fabricPortalUrl, and the child iframe cannot read window.parent.origin.
|
|
56
62
|
// Security relies on UUID v4 requestId correlation + PKCE.
|
|
63
|
+
// In the externalEmbed scenario the parent host has no per-app config, so it
|
|
64
|
+
// is told which endpoint to broker against (`brokeredAuthorizeUrl`, derived
|
|
65
|
+
// from the SDK's own Fabric capacity URL) and which artifact the token is for
|
|
66
|
+
// (`artifactId`). The request is also pinned to the parent origin
|
|
67
|
+
// acknowledged during classification. On the normal Fabric path all three are
|
|
68
|
+
// omitted and the payload/target are unchanged.
|
|
57
69
|
const result = await requestHandoff({
|
|
58
70
|
callbackUrl: options.returnOrigin,
|
|
59
71
|
codeChallenge,
|
|
60
72
|
codeChallengeMethod: 'S256',
|
|
61
73
|
state,
|
|
74
|
+
...(externalEmbed
|
|
75
|
+
? {
|
|
76
|
+
brokeredAuthorizeUrl: auth
|
|
77
|
+
.getAuthApi()
|
|
78
|
+
.getBrokeredAuthorizeExternalUrl(),
|
|
79
|
+
artifactId: options.projectId,
|
|
80
|
+
targetOrigin: getPinnedParentOrigin(),
|
|
81
|
+
}
|
|
82
|
+
: {}),
|
|
62
83
|
});
|
|
63
84
|
// Validate state to prevent replay attacks.
|
|
64
85
|
if (result.state !== state) {
|
|
@@ -12,8 +12,11 @@ import type { FabricAuthOptions } from './types.js';
|
|
|
12
12
|
* 2. **Refresh token** — if a refresh token is available, attempt `auth.refreshSession()`.
|
|
13
13
|
* Return the refreshed session on success; continue on failure.
|
|
14
14
|
* Subject to the same skip.
|
|
15
|
-
* 3. **Embedded mode** — if running inside a Fabric iframe (`fabricEmbedded=true`)
|
|
16
|
-
*
|
|
15
|
+
* 3. **Embedded mode** — if running inside a Fabric iframe (`fabricEmbedded=true`)
|
|
16
|
+
* or a third-party portal that acknowledges the externalEmbed classification
|
|
17
|
+
* handshake, use `embeddedFabricLogin()` to acquire a session via `postMessage`
|
|
18
|
+
* handoff. The externalEmbed scenario is detected from the handshake alone and
|
|
19
|
+
* does not require the Fabric `fabricEmbedded` flag.
|
|
17
20
|
* 4. **Open Fabric broker** — no existing auth path available. Open the Fabric Portal
|
|
18
21
|
* in a new tab via `initiateFabricLogin()` and wait for the Fabric extension to post
|
|
19
22
|
* the handoff code via `postMessage`. The function exchanges the code internally
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { AuthError, assertBrowser } from '@microsoft/rayfin-lib';
|
|
2
2
|
import { embeddedFabricLogin } from './embeddedFabricLogin.js';
|
|
3
|
+
import { classifyExternalEmbed, isExternalEmbedScenario, } from './externalEmbedClassification.js';
|
|
3
4
|
import { hasEmbeddedHandoffCompleted, isEmbeddedMode, tryResumeSession, } from './fabricAuthHelpers.js';
|
|
4
5
|
import { hasFabricUserHint } from './fabricUserHint.js';
|
|
5
6
|
import { initiateFabricLogin } from './initiateFabricLogin.js';
|
|
@@ -15,8 +16,11 @@ import { initiateFabricLogin } from './initiateFabricLogin.js';
|
|
|
15
16
|
* 2. **Refresh token** — if a refresh token is available, attempt `auth.refreshSession()`.
|
|
16
17
|
* Return the refreshed session on success; continue on failure.
|
|
17
18
|
* Subject to the same skip.
|
|
18
|
-
* 3. **Embedded mode** — if running inside a Fabric iframe (`fabricEmbedded=true`)
|
|
19
|
-
*
|
|
19
|
+
* 3. **Embedded mode** — if running inside a Fabric iframe (`fabricEmbedded=true`)
|
|
20
|
+
* or a third-party portal that acknowledges the externalEmbed classification
|
|
21
|
+
* handshake, use `embeddedFabricLogin()` to acquire a session via `postMessage`
|
|
22
|
+
* handoff. The externalEmbed scenario is detected from the handshake alone and
|
|
23
|
+
* does not require the Fabric `fabricEmbedded` flag.
|
|
20
24
|
* 4. **Open Fabric broker** — no existing auth path available. Open the Fabric Portal
|
|
21
25
|
* in a new tab via `initiateFabricLogin()` and wait for the Fabric extension to post
|
|
22
26
|
* the handoff code via `postMessage`. The function exchanges the code internally
|
|
@@ -66,6 +70,19 @@ import { initiateFabricLogin } from './initiateFabricLogin.js';
|
|
|
66
70
|
export async function ensureSignedInWithFabric(auth, options) {
|
|
67
71
|
assertBrowser('ensureSignedInWithFabric');
|
|
68
72
|
const inEmbeddedMode = isEmbeddedMode(options);
|
|
73
|
+
// Run the one-shot externalEmbed classification handshake before any resume
|
|
74
|
+
// decision — and independently of Fabric's `fabricEmbedded` flag. The
|
|
75
|
+
// handshake self-gates on the presence of a parent frame (no parent → instant
|
|
76
|
+
// no-op), so a standalone tab pays nothing. A third-party parent that brokers
|
|
77
|
+
// external Entra acknowledges here; a normal Fabric host does not (the short
|
|
78
|
+
// timeout then classifies as normal). A successful acknowledgement — not the
|
|
79
|
+
// Fabric-UX `fabricEmbedded` hint — is what defines the externalEmbed
|
|
80
|
+
// scenario, so a portal embedding the app need stamp no Fabric-specific flag.
|
|
81
|
+
await classifyExternalEmbed();
|
|
82
|
+
const externalEmbed = isExternalEmbedScenario();
|
|
83
|
+
// The postMessage handoff path applies whenever the app is embedded — either
|
|
84
|
+
// Fabric embedded mode (`fabricEmbedded`) or a classified externalEmbed parent.
|
|
85
|
+
const embedded = inEmbeddedMode || externalEmbed;
|
|
69
86
|
// Steps 1-2: existing session or refresh.
|
|
70
87
|
//
|
|
71
88
|
// A host that stamps `?_fu=` has already had its cookie identity-checked by the workload gate
|
|
@@ -75,9 +92,13 @@ export async function ensureSignedInWithFabric(auth, options) {
|
|
|
75
92
|
// A host that stamps no hint is indistinguishable from the pre-feature world: nothing compared the
|
|
76
93
|
// identities, so the persisted session may belong to a previously signed-in Fabric user. There we
|
|
77
94
|
// keep the original behaviour and skip resume until a handoff has run this page load.
|
|
78
|
-
|
|
95
|
+
//
|
|
96
|
+
// The externalEmbed scenario always skips resume: there is no serve-cookie identity gate and the
|
|
97
|
+
// parent brokers a fresh delegated handoff, so any persisted session must be discarded.
|
|
98
|
+
const skipResume = externalEmbed ||
|
|
99
|
+
(inEmbeddedMode && !hasFabricUserHint() && !hasEmbeddedHandoffCompleted());
|
|
79
100
|
if (skipResume) {
|
|
80
|
-
console.debug('[FabricAuth]
|
|
101
|
+
console.debug('[FabricAuth] Skipping session resume (externalEmbed scenario or embedded host stamped no user hint) to avoid reusing a stale session');
|
|
81
102
|
}
|
|
82
103
|
else {
|
|
83
104
|
const resumed = await tryResumeSession(auth);
|
|
@@ -86,7 +107,7 @@ export async function ensureSignedInWithFabric(auth, options) {
|
|
|
86
107
|
}
|
|
87
108
|
}
|
|
88
109
|
// Step 3: Embedded mode — postMessage auth via parent iframe host
|
|
89
|
-
if (
|
|
110
|
+
if (embedded) {
|
|
90
111
|
console.debug('[FabricAuth] Embedded mode detected, using postMessage auth');
|
|
91
112
|
try {
|
|
92
113
|
await embeddedFabricLogin(auth, options);
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* externalEmbed scenario classification.
|
|
3
|
+
*
|
|
4
|
+
* In embedded mode a third-party parent page may broker external-Entra
|
|
5
|
+
* authentication for this app. The app cannot tell such a host apart from the
|
|
6
|
+
* normal Fabric extension host by inspection, so on load it announces readiness
|
|
7
|
+
* once and treats a scenario acknowledgement as the classifier.
|
|
8
|
+
*
|
|
9
|
+
* The result is held in module state — like the Fabric user hint — so that
|
|
10
|
+
* every entry point (`ensureSignedInWithFabric`, `initEmbeddedAuth`) triggers
|
|
11
|
+
* the same one-shot classification and `embeddedFabricLogin` can read it to
|
|
12
|
+
* decide sign-out and payload shape without re-running the handshake.
|
|
13
|
+
*
|
|
14
|
+
* @internal Not exported from the package barrel.
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* Short classification timeout (ms), deliberately distinct from the handoff
|
|
18
|
+
* request timeout. A normal Fabric host never acknowledges, so this bound is
|
|
19
|
+
* added to a normal embedded login in full; it is kept small so that path
|
|
20
|
+
* incurs no material delay. An externalEmbed parent acknowledges synchronously,
|
|
21
|
+
* well within the bound.
|
|
22
|
+
*/
|
|
23
|
+
export declare const EXTERNAL_EMBED_CLASSIFY_TIMEOUT_MS = 300;
|
|
24
|
+
/**
|
|
25
|
+
* Runs the one-shot externalEmbed handshake if it has not run this page load.
|
|
26
|
+
*
|
|
27
|
+
* Posts a single readiness message to the parent and waits up to
|
|
28
|
+
* {@link EXTERNAL_EMBED_CLASSIFY_TIMEOUT_MS} for an acknowledgement declaring
|
|
29
|
+
* the externalEmbed scenario. Idempotent: subsequent calls are no-ops.
|
|
30
|
+
*/
|
|
31
|
+
export declare function classifyExternalEmbed(timeoutMs?: number): Promise<void>;
|
|
32
|
+
/**
|
|
33
|
+
* Whether the current load was classified as the externalEmbed scenario.
|
|
34
|
+
* Returns `false` until {@link classifyExternalEmbed} has resolved.
|
|
35
|
+
*/
|
|
36
|
+
export declare function isExternalEmbedScenario(): boolean;
|
|
37
|
+
/**
|
|
38
|
+
* The parent origin pinned during the handshake, or `undefined` when not in the
|
|
39
|
+
* externalEmbed scenario. Used as the `postMessage` target for the handoff so
|
|
40
|
+
* the request goes only to the acknowledged parent.
|
|
41
|
+
*/
|
|
42
|
+
export declare function getPinnedParentOrigin(): string | undefined;
|
|
43
|
+
/**
|
|
44
|
+
* Resets classification state.
|
|
45
|
+
*
|
|
46
|
+
* @internal Test helper.
|
|
47
|
+
*/
|
|
48
|
+
export declare function resetExternalEmbedClassificationForTests(): void;
|
|
49
|
+
//# sourceMappingURL=externalEmbedClassification.d.ts.map
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { assertBrowser } from '@microsoft/rayfin-lib';
|
|
2
|
+
import { EXTERNAL_EMBED_ACK_KIND, EXTERNAL_EMBED_AUTH_CHANNEL, EXTERNAL_EMBED_READY_KIND, EXTERNAL_EMBED_SCENARIO, } from './externalEmbedProtocol.js';
|
|
3
|
+
/**
|
|
4
|
+
* externalEmbed scenario classification.
|
|
5
|
+
*
|
|
6
|
+
* In embedded mode a third-party parent page may broker external-Entra
|
|
7
|
+
* authentication for this app. The app cannot tell such a host apart from the
|
|
8
|
+
* normal Fabric extension host by inspection, so on load it announces readiness
|
|
9
|
+
* once and treats a scenario acknowledgement as the classifier.
|
|
10
|
+
*
|
|
11
|
+
* The result is held in module state — like the Fabric user hint — so that
|
|
12
|
+
* every entry point (`ensureSignedInWithFabric`, `initEmbeddedAuth`) triggers
|
|
13
|
+
* the same one-shot classification and `embeddedFabricLogin` can read it to
|
|
14
|
+
* decide sign-out and payload shape without re-running the handshake.
|
|
15
|
+
*
|
|
16
|
+
* @internal Not exported from the package barrel.
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* Short classification timeout (ms), deliberately distinct from the handoff
|
|
20
|
+
* request timeout. A normal Fabric host never acknowledges, so this bound is
|
|
21
|
+
* added to a normal embedded login in full; it is kept small so that path
|
|
22
|
+
* incurs no material delay. An externalEmbed parent acknowledges synchronously,
|
|
23
|
+
* well within the bound.
|
|
24
|
+
*/
|
|
25
|
+
export const EXTERNAL_EMBED_CLASSIFY_TIMEOUT_MS = 300;
|
|
26
|
+
// `undefined` = not classified yet; `null` = normal Fabric; object = externalEmbed (pinned parent).
|
|
27
|
+
let classification;
|
|
28
|
+
/**
|
|
29
|
+
* Runs the one-shot externalEmbed handshake if it has not run this page load.
|
|
30
|
+
*
|
|
31
|
+
* Posts a single readiness message to the parent and waits up to
|
|
32
|
+
* {@link EXTERNAL_EMBED_CLASSIFY_TIMEOUT_MS} for an acknowledgement declaring
|
|
33
|
+
* the externalEmbed scenario. Idempotent: subsequent calls are no-ops.
|
|
34
|
+
*/
|
|
35
|
+
export async function classifyExternalEmbed(timeoutMs = EXTERNAL_EMBED_CLASSIFY_TIMEOUT_MS) {
|
|
36
|
+
assertBrowser('classifyExternalEmbed');
|
|
37
|
+
if (classification !== undefined) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const parent = window.parent;
|
|
41
|
+
if (!parent || parent === window) {
|
|
42
|
+
classification = null;
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
classification = await new Promise((resolve) => {
|
|
46
|
+
let settled = false;
|
|
47
|
+
const requestId = crypto.randomUUID();
|
|
48
|
+
function finish(result) {
|
|
49
|
+
if (settled)
|
|
50
|
+
return;
|
|
51
|
+
settled = true;
|
|
52
|
+
window.removeEventListener('message', onMessage);
|
|
53
|
+
clearTimeout(timer);
|
|
54
|
+
resolve(result);
|
|
55
|
+
}
|
|
56
|
+
function onMessage(event) {
|
|
57
|
+
const data = event.data;
|
|
58
|
+
if (!data || typeof data !== 'object')
|
|
59
|
+
return;
|
|
60
|
+
if (event.source !== parent)
|
|
61
|
+
return;
|
|
62
|
+
if (data.channel !== EXTERNAL_EMBED_AUTH_CHANNEL)
|
|
63
|
+
return;
|
|
64
|
+
if (data.kind !== EXTERNAL_EMBED_ACK_KIND)
|
|
65
|
+
return;
|
|
66
|
+
if (data.scenario !== EXTERNAL_EMBED_SCENARIO)
|
|
67
|
+
return;
|
|
68
|
+
// Pin the parent's origin from the acknowledgement for the later handoff.
|
|
69
|
+
finish({ source: parent, origin: event.origin });
|
|
70
|
+
}
|
|
71
|
+
window.addEventListener('message', onMessage);
|
|
72
|
+
const timer = setTimeout(() => finish(null), timeoutMs);
|
|
73
|
+
// The iframe cannot know the parent's origin a priori and the readiness
|
|
74
|
+
// signal carries no secret, so it is posted with a wildcard target; the
|
|
75
|
+
// acknowledgement pins the origin for everything that follows.
|
|
76
|
+
parent.postMessage({
|
|
77
|
+
channel: EXTERNAL_EMBED_AUTH_CHANNEL,
|
|
78
|
+
version: 1,
|
|
79
|
+
kind: EXTERNAL_EMBED_READY_KIND,
|
|
80
|
+
requestId,
|
|
81
|
+
}, '*');
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Whether the current load was classified as the externalEmbed scenario.
|
|
86
|
+
* Returns `false` until {@link classifyExternalEmbed} has resolved.
|
|
87
|
+
*/
|
|
88
|
+
export function isExternalEmbedScenario() {
|
|
89
|
+
return Boolean(classification);
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* The parent origin pinned during the handshake, or `undefined` when not in the
|
|
93
|
+
* externalEmbed scenario. Used as the `postMessage` target for the handoff so
|
|
94
|
+
* the request goes only to the acknowledged parent.
|
|
95
|
+
*/
|
|
96
|
+
export function getPinnedParentOrigin() {
|
|
97
|
+
return classification ? classification.origin : undefined;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Resets classification state.
|
|
101
|
+
*
|
|
102
|
+
* @internal Test helper.
|
|
103
|
+
*/
|
|
104
|
+
export function resetExternalEmbedClassificationForTests() {
|
|
105
|
+
classification = undefined;
|
|
106
|
+
}
|
|
107
|
+
//# sourceMappingURL=externalEmbedClassification.js.map
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Bridge channel carrying Fabric brokered-auth messages. */
|
|
2
|
+
export declare const EXTERNAL_EMBED_AUTH_CHANNEL = "fabric-auth";
|
|
3
|
+
/** `kind` of the iframe's one-shot readiness announcement. */
|
|
4
|
+
export declare const EXTERNAL_EMBED_READY_KIND = "externalEmbed.ready";
|
|
5
|
+
/** `kind` of the parent's scenario acknowledgement. */
|
|
6
|
+
export declare const EXTERNAL_EMBED_ACK_KIND = "externalEmbed.ack";
|
|
7
|
+
/** Scenario tag declared by the acknowledgement. */
|
|
8
|
+
export declare const EXTERNAL_EMBED_SCENARIO = "externalEmbed";
|
|
9
|
+
//# sourceMappingURL=externalEmbedProtocol.d.ts.map
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* externalEmbed wire protocol — iframe (embedded app) side.
|
|
3
|
+
*
|
|
4
|
+
* These `channel`/`kind`/scenario literals are the wire contract shared with
|
|
5
|
+
* the parent embed host (`@microsoft/rayfin-embed-host`). They are owned in the
|
|
6
|
+
* auth layer — the same layer that already owns the `fabric-auth` channel and
|
|
7
|
+
* the `auth.requestHandoff` kind — rather than in the transport package, which
|
|
8
|
+
* stays plugin-agnostic (it defines only the generic envelope; callers supply
|
|
9
|
+
* `channel` and `kind`). The peer package declares the same literals; a
|
|
10
|
+
* contract test in each package pins them to these canonical values so the two
|
|
11
|
+
* ends cannot drift.
|
|
12
|
+
*
|
|
13
|
+
* Canonical values (see docs/rfc/external-entra-embed-host-sdk.md):
|
|
14
|
+
* channel = "fabric-auth", ready = "externalEmbed.ready",
|
|
15
|
+
* ack = "externalEmbed.ack", scenario = "externalEmbed".
|
|
16
|
+
*
|
|
17
|
+
* @internal Not exported from the package barrel.
|
|
18
|
+
*/
|
|
19
|
+
/** Bridge channel carrying Fabric brokered-auth messages. */
|
|
20
|
+
export const EXTERNAL_EMBED_AUTH_CHANNEL = 'fabric-auth';
|
|
21
|
+
/** `kind` of the iframe's one-shot readiness announcement. */
|
|
22
|
+
export const EXTERNAL_EMBED_READY_KIND = 'externalEmbed.ready';
|
|
23
|
+
/** `kind` of the parent's scenario acknowledgement. */
|
|
24
|
+
export const EXTERNAL_EMBED_ACK_KIND = 'externalEmbed.ack';
|
|
25
|
+
/** Scenario tag declared by the acknowledgement. */
|
|
26
|
+
export const EXTERNAL_EMBED_SCENARIO = 'externalEmbed';
|
|
27
|
+
//# sourceMappingURL=externalEmbedProtocol.js.map
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
export type { FabricAuthOptions } from './types.js';
|
|
2
|
+
export type { EntraTokenSignInOptions } from './signInWithEntraToken.js';
|
|
3
|
+
export { signInWithEntraToken } from './signInWithEntraToken.js';
|
|
2
4
|
export { bridgeFabricCallback } from './bridgeFabricCallback.js';
|
|
3
5
|
export { embeddedFabricLogin } from './embeddedFabricLogin.js';
|
|
4
6
|
export { ensureSignedInWithFabric } from './ensureSignedInWithFabric.js';
|
package/dist/index.js
CHANGED
|
@@ -9,13 +9,17 @@ import type { FabricAuthOptions } from './types.js';
|
|
|
9
9
|
* new tab.
|
|
10
10
|
*
|
|
11
11
|
* **Behaviour:**
|
|
12
|
-
* -
|
|
13
|
-
*
|
|
12
|
+
* - Runs the one-shot externalEmbed classification handshake first. If a
|
|
13
|
+
* third-party parent acknowledges it, the externalEmbed flow proceeds even
|
|
14
|
+
* when the Fabric `fabricEmbedded` flag is absent.
|
|
15
|
+
* - If neither the handshake is acknowledged **nor** `fabricEmbedded` is set
|
|
16
|
+
* (option or `?fabricEmbedded=true`), returns `null` (no-op). A standalone
|
|
17
|
+
* page with no parent frame reaches this immediately.
|
|
14
18
|
* - When the host stamps a `?_fu=` hint, runs the standard resume waterfall
|
|
15
19
|
* (existing session → refresh token → handoff).
|
|
16
|
-
* - When it does not
|
|
17
|
-
* straight to the handoff, preserving the
|
|
18
|
-
* a previously signed-in
|
|
20
|
+
* - When it does not (or in the externalEmbed scenario), skips resume on the
|
|
21
|
+
* first call per page load and goes straight to the handoff, preserving the
|
|
22
|
+
* protection against reusing a previously signed-in user's session.
|
|
19
23
|
*
|
|
20
24
|
* Apps that also support the popup flow should continue to call
|
|
21
25
|
* {@link ensureSignedInWithFabric} from a user-gesture handler for the
|
package/dist/initEmbeddedAuth.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { embeddedFabricLogin } from './embeddedFabricLogin.js';
|
|
2
|
+
import { classifyExternalEmbed, isExternalEmbedScenario, } from './externalEmbedClassification.js';
|
|
2
3
|
import { hasEmbeddedHandoffCompleted, isEmbeddedMode, tryResumeSession, } from './fabricAuthHelpers.js';
|
|
3
4
|
import { hasFabricUserHint } from './fabricUserHint.js';
|
|
4
5
|
/**
|
|
@@ -10,13 +11,17 @@ import { hasFabricUserHint } from './fabricUserHint.js';
|
|
|
10
11
|
* new tab.
|
|
11
12
|
*
|
|
12
13
|
* **Behaviour:**
|
|
13
|
-
* -
|
|
14
|
-
*
|
|
14
|
+
* - Runs the one-shot externalEmbed classification handshake first. If a
|
|
15
|
+
* third-party parent acknowledges it, the externalEmbed flow proceeds even
|
|
16
|
+
* when the Fabric `fabricEmbedded` flag is absent.
|
|
17
|
+
* - If neither the handshake is acknowledged **nor** `fabricEmbedded` is set
|
|
18
|
+
* (option or `?fabricEmbedded=true`), returns `null` (no-op). A standalone
|
|
19
|
+
* page with no parent frame reaches this immediately.
|
|
15
20
|
* - When the host stamps a `?_fu=` hint, runs the standard resume waterfall
|
|
16
21
|
* (existing session → refresh token → handoff).
|
|
17
|
-
* - When it does not
|
|
18
|
-
* straight to the handoff, preserving the
|
|
19
|
-
* a previously signed-in
|
|
22
|
+
* - When it does not (or in the externalEmbed scenario), skips resume on the
|
|
23
|
+
* first call per page load and goes straight to the handoff, preserving the
|
|
24
|
+
* protection against reusing a previously signed-in user's session.
|
|
20
25
|
*
|
|
21
26
|
* Apps that also support the popup flow should continue to call
|
|
22
27
|
* {@link ensureSignedInWithFabric} from a user-gesture handler for the
|
|
@@ -27,20 +32,32 @@ import { hasFabricUserHint } from './fabricUserHint.js';
|
|
|
27
32
|
* @returns The authenticated session, or `null` if not in embedded mode.
|
|
28
33
|
*/
|
|
29
34
|
export async function initEmbeddedAuth(auth, options) {
|
|
30
|
-
|
|
35
|
+
const inEmbeddedMode = isEmbeddedMode(options);
|
|
36
|
+
// One-shot externalEmbed classification handshake before any resume or
|
|
37
|
+
// early-return decision. A successful acknowledgement means a third-party
|
|
38
|
+
// parent is brokering external Entra — detected from the handshake alone,
|
|
39
|
+
// independently of Fabric's `fabricEmbedded` flag. The handshake self-gates on
|
|
40
|
+
// a parent frame, so a standalone page is an instant no-op and still returns
|
|
41
|
+
// `null` below without opening anything.
|
|
42
|
+
await classifyExternalEmbed();
|
|
43
|
+
const externalEmbed = isExternalEmbedScenario();
|
|
44
|
+
// Not embedded by either signal → nothing to do on page load.
|
|
45
|
+
if (!externalEmbed && !inEmbeddedMode) {
|
|
31
46
|
return null;
|
|
32
47
|
}
|
|
33
48
|
// Steps 1-2: existing session or refresh. Skipped on a legacy host (no `?_fu=` hint) until a
|
|
34
49
|
// handoff has run this page load, because nothing server-side compared the session's identity
|
|
35
|
-
// against the current Fabric user.
|
|
36
|
-
|
|
50
|
+
// against the current Fabric user. The externalEmbed scenario always skips resume — the parent
|
|
51
|
+
// brokers a fresh delegated handoff. See ensureSignedInWithFabric for the full reasoning.
|
|
52
|
+
if (!externalEmbed &&
|
|
53
|
+
(hasFabricUserHint() || hasEmbeddedHandoffCompleted())) {
|
|
37
54
|
const resumed = await tryResumeSession(auth);
|
|
38
55
|
if (resumed) {
|
|
39
56
|
return resumed;
|
|
40
57
|
}
|
|
41
58
|
}
|
|
42
59
|
else {
|
|
43
|
-
console.debug('[FabricAuth:initEmbedded]
|
|
60
|
+
console.debug('[FabricAuth:initEmbedded] Skipping session resume (externalEmbed scenario or no user hint) to avoid a stale cross-user session');
|
|
44
61
|
}
|
|
45
62
|
// Step 3: postMessage handoff (no popup, no window.open)
|
|
46
63
|
console.debug('[FabricAuth:initEmbedded] Starting postMessage handoff flow');
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { Auth, OpaqueSession } from '@microsoft/rayfin-auth';
|
|
2
|
+
/** Options for signing in to a Fabric AppBackend with an existing Entra token. */
|
|
3
|
+
export interface EntraTokenSignInOptions {
|
|
4
|
+
/** Already-acquired delegated Entra access token, without a Bearer prefix. */
|
|
5
|
+
entraToken: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Signs in to the AppBackend configured on `auth` using a delegated Entra token.
|
|
9
|
+
* Works in browsers and Node.js; no popup or iframe is required.
|
|
10
|
+
*
|
|
11
|
+
* The configured backend must be an absolute HTTPS workload URL, including its
|
|
12
|
+
* capacity/workspace/artifact path, not a static-hosting URL. The token is used
|
|
13
|
+
* only for this request; subsequent refresh uses the Rayfin refresh token.
|
|
14
|
+
* Success replaces the session on this Auth instance. Failures reject without
|
|
15
|
+
* returning a prior session. Concurrent calls on the same instance run in call
|
|
16
|
+
* order, after any active refresh; refresh waits until these calls settle.
|
|
17
|
+
*
|
|
18
|
+
* @param auth - The Auth instance used by your SDK clients.
|
|
19
|
+
* @param options - The already-acquired Entra credential.
|
|
20
|
+
* @returns The installed session metadata, without access or refresh tokens.
|
|
21
|
+
* @throws `AuthError` with a stable code and a credential-free message.
|
|
22
|
+
* @example
|
|
23
|
+
* ```typescript
|
|
24
|
+
* const session = await signInWithEntraToken(client.auth, { entraToken });
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
export declare function signInWithEntraToken(auth: Auth, options: EntraTokenSignInOptions): Promise<OpaqueSession>;
|
|
28
|
+
//# sourceMappingURL=signInWithEntraToken.d.ts.map
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { createSessionFromTokenResponse, getAuthProviderClient, runProviderSignIn, } from '@microsoft/rayfin-auth/_internal';
|
|
2
|
+
import { AuthError } from '@microsoft/rayfin-lib';
|
|
3
|
+
const TOKEN_PATH = '/api/auth/v1/brokered/token';
|
|
4
|
+
function invalidResponse() {
|
|
5
|
+
return new AuthError('The token exchange returned an invalid token response.', 'INVALID_TOKEN_RESPONSE');
|
|
6
|
+
}
|
|
7
|
+
async function readTokenResponse(response) {
|
|
8
|
+
try {
|
|
9
|
+
return await response.json();
|
|
10
|
+
}
|
|
11
|
+
catch (error) {
|
|
12
|
+
if (error instanceof SyntaxError) {
|
|
13
|
+
return undefined;
|
|
14
|
+
}
|
|
15
|
+
throw error;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function validateTokenResponse(value) {
|
|
19
|
+
if (!value || typeof value !== 'object') {
|
|
20
|
+
throw invalidResponse();
|
|
21
|
+
}
|
|
22
|
+
if (!('accessToken' in value) ||
|
|
23
|
+
typeof value.accessToken !== 'string' ||
|
|
24
|
+
!value.accessToken.trim() ||
|
|
25
|
+
/\s/.test(value.accessToken) ||
|
|
26
|
+
!('tokenType' in value) ||
|
|
27
|
+
typeof value.tokenType !== 'string' ||
|
|
28
|
+
value.tokenType.toLowerCase() !== 'bearer' ||
|
|
29
|
+
!('expiresIn' in value) ||
|
|
30
|
+
typeof value.expiresIn !== 'number' ||
|
|
31
|
+
!Number.isFinite(value.expiresIn) ||
|
|
32
|
+
value.expiresIn <= 0) {
|
|
33
|
+
throw invalidResponse();
|
|
34
|
+
}
|
|
35
|
+
const now = Date.now();
|
|
36
|
+
const expiry = new Date(now + value.expiresIn * 1000).getTime();
|
|
37
|
+
if (!Number.isFinite(expiry) || expiry <= now) {
|
|
38
|
+
throw invalidResponse();
|
|
39
|
+
}
|
|
40
|
+
const refreshToken = 'refreshToken' in value ? value.refreshToken : undefined;
|
|
41
|
+
const scope = 'scope' in value ? value.scope : undefined;
|
|
42
|
+
if ((refreshToken != null && typeof refreshToken !== 'string') ||
|
|
43
|
+
(scope != null && typeof scope !== 'string')) {
|
|
44
|
+
throw invalidResponse();
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
accessToken: value.accessToken,
|
|
48
|
+
tokenType: value.tokenType,
|
|
49
|
+
expiresIn: value.expiresIn,
|
|
50
|
+
refreshToken,
|
|
51
|
+
scope,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
function exchangeError(status) {
|
|
55
|
+
switch (status) {
|
|
56
|
+
case 400:
|
|
57
|
+
return new AuthError('External Entra exchange is not enabled.', 'EXCHANGE_NOT_ENABLED');
|
|
58
|
+
case 401:
|
|
59
|
+
return new AuthError('Entra authentication failed.', 'AUTH_FAILED');
|
|
60
|
+
case 403:
|
|
61
|
+
return new AuthError('Item Execute permission is required.', 'INSUFFICIENT_PERMISSIONS');
|
|
62
|
+
case 404:
|
|
63
|
+
return new AuthError('External Entra exchange is not available.', 'NOT_AVAILABLE');
|
|
64
|
+
default:
|
|
65
|
+
return new AuthError('Entra token exchange failed.', 'TOKEN_EXCHANGE_FAILED');
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
function sanitizeSignInFailure(error) {
|
|
69
|
+
if (error instanceof AuthError) {
|
|
70
|
+
switch (error.code) {
|
|
71
|
+
case 'INVALID_TOKEN_RESPONSE':
|
|
72
|
+
throw invalidResponse();
|
|
73
|
+
case 'EXCHANGE_NOT_ENABLED':
|
|
74
|
+
throw exchangeError(400);
|
|
75
|
+
case 'AUTH_FAILED':
|
|
76
|
+
throw exchangeError(401);
|
|
77
|
+
case 'INSUFFICIENT_PERMISSIONS':
|
|
78
|
+
throw exchangeError(403);
|
|
79
|
+
case 'NOT_AVAILABLE':
|
|
80
|
+
throw exchangeError(404);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
// Initialization and custom storage can also fail, outside the HTTP path.
|
|
84
|
+
throw exchangeError();
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Signs in to the AppBackend configured on `auth` using a delegated Entra token.
|
|
88
|
+
* Works in browsers and Node.js; no popup or iframe is required.
|
|
89
|
+
*
|
|
90
|
+
* The configured backend must be an absolute HTTPS workload URL, including its
|
|
91
|
+
* capacity/workspace/artifact path, not a static-hosting URL. The token is used
|
|
92
|
+
* only for this request; subsequent refresh uses the Rayfin refresh token.
|
|
93
|
+
* Success replaces the session on this Auth instance. Failures reject without
|
|
94
|
+
* returning a prior session. Concurrent calls on the same instance run in call
|
|
95
|
+
* order, after any active refresh; refresh waits until these calls settle.
|
|
96
|
+
*
|
|
97
|
+
* @param auth - The Auth instance used by your SDK clients.
|
|
98
|
+
* @param options - The already-acquired Entra credential.
|
|
99
|
+
* @returns The installed session metadata, without access or refresh tokens.
|
|
100
|
+
* @throws `AuthError` with a stable code and a credential-free message.
|
|
101
|
+
* @example
|
|
102
|
+
* ```typescript
|
|
103
|
+
* const session = await signInWithEntraToken(client.auth, { entraToken });
|
|
104
|
+
* ```
|
|
105
|
+
*/
|
|
106
|
+
export async function signInWithEntraToken(auth, options) {
|
|
107
|
+
const token = options?.entraToken;
|
|
108
|
+
if (typeof token !== 'string' ||
|
|
109
|
+
!token ||
|
|
110
|
+
/\s/.test(token) ||
|
|
111
|
+
/^bearer$/i.test(token)) {
|
|
112
|
+
throw new AuthError('A raw Entra access token is required.', 'INVALID_REQUEST');
|
|
113
|
+
}
|
|
114
|
+
const client = getAuthProviderClient(auth);
|
|
115
|
+
const endpoint = client.resolveUrl(TOKEN_PATH);
|
|
116
|
+
try {
|
|
117
|
+
const url = new URL(endpoint);
|
|
118
|
+
if (url.protocol !== 'https:' ||
|
|
119
|
+
url.username ||
|
|
120
|
+
url.password ||
|
|
121
|
+
url.search ||
|
|
122
|
+
url.hash ||
|
|
123
|
+
/[\s\\]/.test(endpoint) ||
|
|
124
|
+
!url.pathname.endsWith(TOKEN_PATH)) {
|
|
125
|
+
throw new Error();
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
throw new AuthError('An absolute HTTPS backend URL is required.', 'INVALID_REQUEST');
|
|
130
|
+
}
|
|
131
|
+
return runProviderSignIn(auth, async () => {
|
|
132
|
+
let result;
|
|
133
|
+
try {
|
|
134
|
+
result = await client.requestIsolated(TOKEN_PATH, { Authorization: `Bearer ${token}` }, async (response) => {
|
|
135
|
+
const rejected = !response.ok || response.redirected;
|
|
136
|
+
if (rejected) {
|
|
137
|
+
// Release unread streams without inspecting credential-bearing
|
|
138
|
+
// error bodies. Cancellation failures follow the sanitized
|
|
139
|
+
// transport-failure path, while the request timeout is still active.
|
|
140
|
+
await response.body?.cancel();
|
|
141
|
+
}
|
|
142
|
+
return {
|
|
143
|
+
status: response.status,
|
|
144
|
+
ok: response.ok,
|
|
145
|
+
redirected: response.redirected,
|
|
146
|
+
value: rejected ? undefined : await readTokenResponse(response),
|
|
147
|
+
};
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
throw exchangeError();
|
|
152
|
+
}
|
|
153
|
+
if (result.redirected) {
|
|
154
|
+
throw exchangeError();
|
|
155
|
+
}
|
|
156
|
+
if (!result.ok) {
|
|
157
|
+
throw exchangeError(result.status);
|
|
158
|
+
}
|
|
159
|
+
const response = validateTokenResponse(result.value);
|
|
160
|
+
return createSessionFromTokenResponse(auth, response, {
|
|
161
|
+
persistBeforeCommit: true,
|
|
162
|
+
});
|
|
163
|
+
}).catch(sanitizeSignInFailure);
|
|
164
|
+
}
|
|
165
|
+
//# sourceMappingURL=signInWithEntraToken.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@microsoft/rayfin-auth-provider-fabric",
|
|
3
|
-
"version": "1.36.0-alpha.
|
|
3
|
+
"version": "1.36.0-alpha.1663",
|
|
4
4
|
"description": "Fabric brokered authentication provider for Rayfin SDK",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -13,9 +13,9 @@
|
|
|
13
13
|
],
|
|
14
14
|
"type": "module",
|
|
15
15
|
"dependencies": {
|
|
16
|
-
"@microsoft/rayfin-auth": "1.36.0-alpha.
|
|
17
|
-
"@microsoft/fabric-embedded-host": "1.36.0-alpha.
|
|
18
|
-
"@microsoft/rayfin-lib": "1.36.0-alpha.
|
|
16
|
+
"@microsoft/rayfin-auth": "1.36.0-alpha.1663",
|
|
17
|
+
"@microsoft/fabric-embedded-host": "1.36.0-alpha.1663",
|
|
18
|
+
"@microsoft/rayfin-lib": "1.36.0-alpha.1663"
|
|
19
19
|
},
|
|
20
20
|
"devDependencies": {
|
|
21
21
|
"typescript": "^5.8.3",
|