Skip to content

Commit

Permalink
[identity] Add support for useDefaultBrokerAccount (#28979)
Browse files Browse the repository at this point in the history
### Packages impacted by this PR

@azure/identity

### Issues associated with this PR

Resolves #27390

### Describe the problem that is addressed by this PR

Adds `useDefaultBrokerAccount` to `brokerOptions` and plumbs it through
to the InteractiveBrowserCredential. When the user enables this feature,
brokered IBC will first attempt to use the default OS account. If that fails, 
it falls back to the existing behavior.

### Are there test cases added in this PR? _(If not, why?)_

We desperately need to improve our test coverage, but we do not have IBC
tests today.

Co-authored-by: Scott Addie <10702007+scottaddie@users.noreply.github.com>
  • Loading branch information
maorleger and scottaddie committed Mar 20, 2024
1 parent 22d7556 commit c9ac436
Show file tree
Hide file tree
Showing 6 changed files with 122 additions and 24 deletions.
39 changes: 36 additions & 3 deletions sdk/identity/identity-broker/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,8 @@ useIdentityPlugin(nativeBrokerPlugin);
const credential = new InteractiveBrowserCredential({
brokerOptions: {
enabled: true,
},
});
},
});
```

After calling `useIdentityPlugin`, the native broker plugin is registered to the `@azure/identity` package and will be available on the `InteractiveBrowserCredential` that supports WAM broker authentication. This credential has `brokerOptions` in the constructor options.
Expand Down Expand Up @@ -101,7 +101,40 @@ main().catch((error) => {
process.exit(1);
});
```
For an example of using an Electron app for retrieving a window handle, see [this sample](https://github.com/Azure/azure-sdk-for-js/blob/main/sdk/identity/identity-broker/samples/v1/typescript/src/index.ts).

For a complete example of using an Electron app for retrieving a window handle, see [this sample](https://github.com/Azure/azure-sdk-for-js/blob/main/sdk/identity/identity-broker/samples/v1/typescript/src/index.ts).

### Use the default account for sign-in

When the `useDefaultBrokerAccount` option is set to `true`, the credential will attempt to silently use the default broker account. If using the default account fails, the credential will fall back to interactive authentication.

```typescript
import { nativeBrokerPlugin } from "@azure/identity-broker";
import { useIdentityPlugin, InteractiveBrowserCredential } from "@azure/identity";

useIdentityPlugin(nativeBrokerPlugin);

async function main() {
const credential = new InteractiveBrowserCredential({
brokerOptions: {
enabled: true,
useDefaultBrokerAccount: true,
parentWindowHandle: <insert_current_window_handle>
},
});

// We'll use the Microsoft Graph scope as an example
const scope = "https://graph.microsoft.com/.default";

// Print out part of the access token
console.log((await credential.getToken(scope)).token.substr(0, 10), "...");
}

main().catch((error) => {
console.error("An error occurred:", error);
process.exit(1);
});
```

## Troubleshooting

Expand Down
2 changes: 2 additions & 0 deletions sdk/identity/identity/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

### Features Added

- `InteractiveBrowserCredential`: Added support for using the default broker account. [#28979](https://github.com/Azure/azure-sdk-for-js/pull/28979)

### Breaking Changes

### Bugs Fixed
Expand Down
1 change: 1 addition & 0 deletions sdk/identity/identity/review/identity.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ export interface BrokerEnabledOptions {
enabled: true;
legacyEnableMsaPassthrough?: boolean;
parentWindowHandle: Uint8Array;
useDefaultBrokerAccount?: boolean;
}

// @public
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
processMultiTenantRequest,
resolveAdditionallyAllowedTenantIds,
} from "../util/tenantIdUtils";

import { AuthenticationRecord } from "../msal/types";
import { MsalFlow } from "../msal/flows";
import { MsalOpenBrowser } from "../msal/nodeFlows/msalOpenBrowser";
Expand Down Expand Up @@ -73,6 +74,7 @@ export class InteractiveBrowserCredential implements TokenCredential {
enabled: true,
parentWindowHandle: ibcNodeOptions.brokerOptions.parentWindowHandle,
legacyEnableMsaPassthrough: ibcNodeOptions.brokerOptions?.legacyEnableMsaPassthrough,
useDefaultBrokerAccount: ibcNodeOptions.brokerOptions?.useDefaultBrokerAccount,
},
});
}
Expand Down
13 changes: 9 additions & 4 deletions sdk/identity/identity/src/msal/nodeFlows/brokerOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export type BrokerOptions = BrokerEnabledOptions | BrokerDisabledOptions;
*/
export interface BrokerDisabledOptions {
/**
* If set to true, broker will be enabled for WAM support on Windows
* If set to true, broker will be enabled for WAM support on Windows.
*/
enabled: false;

Expand All @@ -19,7 +19,7 @@ export interface BrokerDisabledOptions {
*/
legacyEnableMsaPassthrough?: undefined;
/**
* Window handle for parent window, required for WAM authentication
* Window handle for parent window, required for WAM authentication.
*/
parentWindowHandle: undefined;
}
Expand All @@ -29,15 +29,20 @@ export interface BrokerDisabledOptions {
*/
export interface BrokerEnabledOptions {
/**
* If set to true, broker will be enabled for WAM support on Windows
* If set to true, broker will be enabled for WAM support on Windows.
*/
enabled: true;
/**
* If set to true, MSA account will be passed through, required for WAM authentication.
*/
legacyEnableMsaPassthrough?: boolean;
/**
* Window handle for parent window, required for WAM authentication
* Window handle for parent window, required for WAM authentication.
*/
parentWindowHandle: Uint8Array;

/**
* If set to true, the credential will attempt to use the default broker account for authentication before falling back to interactive authentication.
*/
useDefaultBrokerAccount?: boolean;
}
89 changes: 72 additions & 17 deletions sdk/identity/identity/src/msal/nodeFlows/msalOpenBrowser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,18 +42,21 @@ export class MsalOpenBrowser extends MsalNode {
private loginHint?: string;
private errorTemplate?: string;
private successTemplate?: string;
private useDefaultBrokerAccount?: boolean;

constructor(options: MsalOpenBrowserOptions) {
super(options);
this.loginHint = options.loginHint;
this.errorTemplate = options.browserCustomizationOptions?.errorMessage;
this.successTemplate = options.browserCustomizationOptions?.successMessage;
this.logger = credentialLogger("Node.js MSAL Open Browser");
this.useDefaultBrokerAccount =
options.brokerOptions?.enabled && options.brokerOptions?.useDefaultBrokerAccount;
}

protected async doGetToken(
scopes: string[],
options?: CredentialFlowGetTokenOptions,
options: CredentialFlowGetTokenOptions = {},
): Promise<AccessToken> {
try {
const interactiveRequest: msalNode.InteractiveRequest = {
Expand All @@ -68,36 +71,88 @@ export class MsalOpenBrowser extends MsalNode {
errorTemplate: this.errorTemplate,
successTemplate: this.successTemplate,
};

if (hasNativeBroker() && this.enableBroker) {
this.logger.verbose("Authentication will resume through the broker");
if (this.parentWindowHandle) {
interactiveRequest.windowHandle = Buffer.from(this.parentWindowHandle);
} else {
// error should have been thrown from within the constructor of InteractiveBrowserCredential
this.logger.warning(
"Parent window handle is not specified for the broker. This may cause unexpected behavior. Please provide the parentWindowHandle.",
);
}

if (this.enableMsaPassthrough) {
(interactiveRequest.tokenQueryParameters ??= {})["msal_request_type"] =
"consumer_passthrough";
}
return this.doGetBrokeredToken(scopes, interactiveRequest, {
enableCae: options.enableCae,
useDefaultBrokerAccount: this.useDefaultBrokerAccount,
});
}

// If the broker is not enabled, we will fall back to interactive authentication

if (hasNativeBroker() && !this.enableBroker) {
this.logger.verbose(
"Authentication will resume normally without the broker, since it's not enabled",
);
}

const result = await this.getApp("public", options?.enableCae).acquireTokenInteractive(
interactiveRequest,
);
return this.handleResult(scopes, result || undefined);
} catch (err: any) {
throw handleMsalError(scopes, err, options);
}
}

/**
* A helper function that supports brokered authentication through the MSAL's public application.
*
* When options.useDefaultBrokerAccount is true, the method will attempt to authenticate using the default broker account.
* If the default broker account is not available, the method will fall back to interactive authentication.
*/
private async doGetBrokeredToken(
scopes: string[],
interactiveRequest: msalNode.InteractiveRequest,
options: {
enableCae?: boolean;
useDefaultBrokerAccount?: boolean;
},
): Promise<AccessToken> {
this.logger.verbose("Authentication will resume through the broker");
if (this.parentWindowHandle) {
interactiveRequest.windowHandle = Buffer.from(this.parentWindowHandle);
} else {
// error should have been thrown from within the constructor of InteractiveBrowserCredential
this.logger.warning(
"Parent window handle is not specified for the broker. This may cause unexpected behavior. Please provide the parentWindowHandle.",
);
}

if (this.enableMsaPassthrough) {
(interactiveRequest.tokenQueryParameters ??= {})["msal_request_type"] =
"consumer_passthrough";
}

if (options.useDefaultBrokerAccount) {
interactiveRequest.prompt = "none";
this.logger.verbose("Attempting broker authentication using the default broker account");
} else {
interactiveRequest.prompt = undefined;
this.logger.verbose("Attempting broker authentication without the default broker account");
}

try {
const result = await this.getApp("public", options?.enableCae).acquireTokenInteractive(
interactiveRequest,
);
if (result.fromNativeBroker) {
this.logger.verbose(`This result is returned from native broker`);
}
return this.handleResult(scopes, result || undefined);
} catch (err: any) {
throw handleMsalError(scopes, err, options);
} catch (e: any) {
this.logger.verbose(`Failed to authenticate through the broker: ${e.message}`);
// If we tried to use the default broker account and failed, fall back to interactive authentication
if (options.useDefaultBrokerAccount) {
return this.doGetBrokeredToken(scopes, interactiveRequest, {
enableCae: options.enableCae,
useDefaultBrokerAccount: false,
});
} else {
// If we're not using the default broker account, throw the error
throw handleMsalError(scopes, e);
}
}
}
}

0 comments on commit c9ac436

Please sign in to comment.