Skip to content

Commit

Permalink
[Identity] InteractiveBrowserCredential to use MSAL Browser 2.0 (#13263)
Browse files Browse the repository at this point in the history
* wip

* msal changes

* correlationId from uuid, for now at least

* using msal-browser solely instead of msal-common

* updates after several tests

* avoiding generating a correlationId ourselves, MSAL already does this

* lint fix

* no more getAllAccounts and stopping execution after initial redirect

* no more chededirectResponse

* removed logout

* error in the multiple account case

* using both MSALs - code changes, docs come later

* changelog entry

* README change

* removed the locale info from the changelog

* feedback from Jonathan and other details that seemed nice

* readme update

* readme update

* removed en-us from one link

* Scott Schaab feedback

* avoiding relative paths

* build fixes

* pnpm-lock update

* implicit redirect fix
  • Loading branch information
sadasant authored Feb 13, 2021
1 parent ebf23ca commit 29825f0
Show file tree
Hide file tree
Showing 13 changed files with 961 additions and 226 deletions.
382 changes: 262 additions & 120 deletions common/config/rush/pnpm-lock.yaml

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions eng/ignore-links.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ https://github.com/Azure/azure-sdk-for-js/tree/master/sdk/storage/storage-blob-c
https://github.com/Azure/azure-sdk-for-js/tree/master/sdk/storage/storage-blob-changefeed/samples/typescript
https://github.com/Azure/azure-sdk-for-js/tree/master/sdk/storage/storage-blob-changefeed/test/
https://github.com/Azure/azure-digital-twins/blob/private-preview/Documentation/how-to-manage-routes.md
https://github.com/Azure/azure-sdk-for-js/blob/master/sdk/identity/identity/interactive-browser-credential.md
3 changes: 2 additions & 1 deletion sdk/identity/identity/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
# Release History

## 1.2.4 (Unreleased)
## 1.2.4-beta.1 (Unreleased)

- Breaking Change: Updated `InteractiveBrowserCredential` to use the Auth Code Flow with PKCE rather than Implicit Grant Flow by default in the browser, to better support browsers with enhanced security restrictions. A new file was added to provide more information about this credential [here](https://github.com/Azure/azure-sdk-for-js/blob/master/sdk/identity/identity/interactive-browser-credential.md).

## 1.2.3 (2021-02-09)

Expand Down
24 changes: 12 additions & 12 deletions sdk/identity/identity/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -178,33 +178,33 @@ const client = new KeyClient(vaultUrl, credentialChain);

| credential | usage |
| --------------------------- | --------------------------------------------------------------------------------------------------------------- |
| `DefaultAzureCredential` | provides a simplified authentication experience to quickly start developing applications run in the Azure cloud |
| `ChainedTokenCredential` | allows users to define custom authentication flows composing multiple credentials |
| `EnvironmentCredential` | authenticates a service principal or user via credential information specified in environment variables |
| `ManagedIdentityCredential` | authenticates the managed identity of an Azure resource |
| `DefaultAzureCredential` | Provides a simplified authentication experience to quickly start developing applications run in the Azure cloud. |
| `ChainedTokenCredential` | Allows users to define custom authentication flows composing multiple credentials. |
| `EnvironmentCredential` | Authenticates a service principal or user via credential information specified in environment variables. |
| `ManagedIdentityCredential` | Authenticates the managed identity of an Azure resource. |

### Authenticating Service Principals

| credential | usage |
| ----------------------------- | ----------------------------------------------------- |
| `ClientSecretCredential` | authenticates a service principal using a secret |
| `ClientCertificateCredential` | authenticates a service principal using a certificate |
| `ClientSecretCredential` | Authenticates a service principal using a secret. |
| `ClientCertificateCredential` | Authenticates a service principal using a certificate. |

### Authenticating Users

| credential | usage |
| ------------------------------ | ------------------------------------------------------------------ |
| `InteractiveBrowserCredential` | interactively authenticates a user with the default system browser |
| `DeviceCodeCredential` | interactively authenticates a user on devices with limited UI |
| `UserPasswordCredential` | authenticates a user with a username and password |
| `AuthorizationCodeCredential` | authenticate a user with a previously obtained authorization code |
| `InteractiveBrowserCredential` | Interactively authenticates a user with the default system browser. Read more about how this happens [here](https://github.com/Azure/azure-sdk-for-js/blob/master/sdk/identity/identity/interactive-browser-credential.md). |
| `DeviceCodeCredential` | Interactively authenticates a user on devices with limited UI. |
| `UserPasswordCredential` | Authenticates a user with a username and password. |
| `AuthorizationCodeCredential` | Authenticate a user with a previously obtained authorization code. |

### Authenticating via Development Tools

| credential | usage |
| ---------------------------- | ----------------------------------------------------------------- |
| `AzureCliCredential` | authenticate in a development environment with the Azure CLI |
| `VisualStudioCodeCredential` | authenticate in a development environment with Visual Studio Code |
| `AzureCliCredential` | Authenticate in a development environment with the Azure CLI. |
| `VisualStudioCodeCredential` | Authenticate in a development environment with Visual Studio Code. |

## Troubleshooting

Expand Down
16 changes: 16 additions & 0 deletions sdk/identity/identity/interactive-browser-credential.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Interactive Browser Credential

The `InteractiveBrowserCredential` uses [Auth Code Flow][AuthCodeFlow], which uses [Proof Key for Code Exchange (PKCE)](https://tools.ietf.org/html/rfc7636), uses [MSAL v2.x](https://github.com/AzureAD/microsoft-authentication-library-for-js), and is enabled by default. It also allows switching back to the [Implicit Grant Flow][ImplicitGrantFlow] by passing the `flow` property with the value `implicit-grant` through to the constructor of the `InteractiveBrowserCredential`.

Follow the instructions for [creating your single-page application](https://docs.microsoft.com/azure/active-directory/develop/scenario-spa-app-registration#redirect-uri-msaljs-20-with-auth-code-flow) to correctly mark your redirect URI as enabled for CORS.

If you attempt to use the authorization code flow and see this error:

```
access to XMLHttpRequest at 'https://login.microsoftonline.com/common/v2.0/oauth2/token' from origin 'yourApp.com' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
```

Then you need to visit your app registration and update the redirect URI for your app to type `spa` (for "single page application").

[AuthCodeFlow]: https://docs.microsoft.com/azure/active-directory/develop/v2-oauth2-auth-code-flow
[ImplicitGrantFlow]: https://docs.microsoft.com/azure/active-directory/develop/v2-oauth2-implicit-grant-flow
3 changes: 2 additions & 1 deletion sdk/identity/identity/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@azure/identity",
"sdk-type": "client",
"version": "1.2.4",
"version": "1.2.4-beta.1",
"description": "Provides credential implementations for Azure SDK libraries that can authenticate with Azure Active Directory",
"main": "dist/index.js",
"module": "dist-esm/src/index.js",
Expand Down Expand Up @@ -84,6 +84,7 @@
"@azure/core-tracing": "1.0.0-preview.9",
"@azure/logger": "^1.0.0",
"@azure/msal-node": "1.0.0-beta.6",
"@azure/msal-browser": "2.9.0",
"@opentelemetry/api": "^0.10.2",
"@types/express": "^4.16.0",
"axios": "^0.21.1",
Expand Down
6 changes: 6 additions & 0 deletions sdk/identity/identity/review/identity.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,9 @@ export function getDefaultAzureCredential(): TokenCredential;

export { GetTokenOptions }

// @public
export type InteractiveBrowserAuthenticationFlow = "implicit-grant" | "auth-code";

// @public
export class InteractiveBrowserCredential implements TokenCredential {
constructor(options?: InteractiveBrowserCredentialOptions);
Expand All @@ -153,7 +156,10 @@ export class InteractiveBrowserCredential implements TokenCredential {

// @public
export interface InteractiveBrowserCredentialOptions extends TokenCredentialOptions {
authenticationRecord?: AuthenticationRecord;
clientId?: string;
correlationId?: string;
flow?: InteractiveBrowserAuthenticationFlow;
loginStyle?: BrowserLoginStyle;
postLogoutRedirectUri?: string | (() => string);
redirectUri?: string | (() => string);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

import * as msal from "msal";
import { AccessToken, TokenCredential, GetTokenOptions } from "@azure/core-http";
import { IdentityClient } from "../client/identityClient";
import {
Expand All @@ -12,6 +11,9 @@ import { createSpan } from "../util/tracing";
import { CanonicalCode } from "@opentelemetry/api";
import { DefaultTenantId, DeveloperSignOnClientId } from "../constants";
import { credentialLogger, formatSuccess, formatError } from "../util/logging";
import { MSALAuthCode } from "./msalBrowser/msalAuthCode";
import { MSALImplicit } from "./msalBrowser/msalImplicit";
import { IMSALBrowserFlow, MSALOptions } from "./msalBrowser/msalCommon";

const logger = credentialLogger("InteractiveBrowserCredential");

Expand All @@ -21,114 +23,75 @@ const logger = credentialLogger("InteractiveBrowserCredential");
* window.
*/
export class InteractiveBrowserCredential implements TokenCredential {
private tenantId: string;
private clientId: string;
private loginStyle: BrowserLoginStyle;
private msalConfig: msal.Configuration;
private msalObject: msal.UserAgentApplication;
private msal: IMSALBrowserFlow;

/**
* Creates an instance of the InteractiveBrowserCredential with the
* details needed to authenticate against Azure Active Directory with
* a user identity.
*
* @param tenantId - The Azure Active Directory tenant (directory) ID.
* @param clientId - The client (application) ID of an App Registration in the tenant.
* @param options - Options for configuring the client which makes the authentication request.
*/
constructor(options?: InteractiveBrowserCredentialOptions) {
this.tenantId = (options && options.tenantId) || DefaultTenantId;

// TODO: temporary - this is the Azure CLI clientID - we'll replace it when
// Developer Sign On application is available
// https://github.com/Azure/azure-sdk-for-net/blob/master/sdk/identity/Azure.Identity/src/Constants.cs#L9
this.clientId = (options && options.clientId) || DeveloperSignOnClientId;

options = {
...IdentityClient.getDefaultOptions(),
...options,
tenantId: (options && options.tenantId) || DefaultTenantId,
// TODO: temporary - this is the Azure CLI clientID - we'll replace it when
// Developer Sign On application is available
// https://github.com/Azure/azure-sdk-for-net/blob/master/sdk/identity/Azure.Identity/src/Constants.cs#L9
clientId: (options && options.clientId) || DeveloperSignOnClientId
tenantId: this.tenantId,
clientId: this.clientId
};

this.loginStyle = options.loginStyle || "popup";
if (["redirect", "popup"].indexOf(this.loginStyle) === -1) {
const error = new Error(`Invalid loginStyle: ${options.loginStyle}`);
const loginStyles = ["redirect", "popup"];
if (loginStyles.indexOf(this.loginStyle) === -1) {
const error = new Error(
`Invalid loginStyle: ${
options.loginStyle
}. Should be any of the following: ${loginStyles.join(", ")}.`
);
logger.info(formatError("", error));
throw error;
}

const knownAuthorities =
options.tenantId === "adfs" ? (options.authorityHost ? [options.authorityHost] : []) : [];

this.msalConfig = {
auth: {
clientId: options.clientId!, // we just initialized it above
authority: `${options.authorityHost}/${options.tenantId}`,
knownAuthorities,
...(options.redirectUri && { redirectUri: options.redirectUri }),
...(options.postLogoutRedirectUri && { redirectUri: options.postLogoutRedirectUri })
},
cache: {
cacheLocation: "localStorage",
storeAuthStateInCookie: true
}
const {
clientId,
tenantId,
authorityHost,
correlationId,
redirectUri,
postLogoutRedirectUri,
authenticationRecord
} = options;

const msalOptions: MSALOptions = {
clientId,
tenantId,
authorityHost,
correlationId,
authenticationRecord,
loginStyle: this.loginStyle,
knownAuthorities: tenantId === "adfs" ? (authorityHost ? [authorityHost] : []) : [],
redirectUri: typeof redirectUri === "function" ? redirectUri() : redirectUri,
postLogoutRedirectUri:
typeof postLogoutRedirectUri === "function"
? postLogoutRedirectUri()
: postLogoutRedirectUri
};

this.msalObject = new msal.UserAgentApplication(this.msalConfig);
}

private login(): Promise<msal.AuthResponse> {
switch (this.loginStyle) {
case "redirect": {
const loginPromise = new Promise<msal.AuthResponse>((resolve, reject) => {
this.msalObject.handleRedirectCallback(resolve, reject);
});
this.msalObject.loginRedirect();
return loginPromise;
}
case "popup":
return this.msalObject.loginPopup();
}
}

private async acquireToken(
authParams: msal.AuthenticationParameters
): Promise<msal.AuthResponse | undefined> {
let authResponse: msal.AuthResponse | undefined;
try {
logger.info("Attempting to acquire token silently");
authResponse = await this.msalObject.acquireTokenSilent(authParams);
} catch (err) {
if (err instanceof msal.AuthError) {
switch (err.errorCode) {
case "consent_required":
case "interaction_required":
case "login_required":
logger.info(`Authentication returned errorCode ${err.errorCode}`);
break;
default:
logger.info(formatError(authParams.scopes, `Failed to acquire token: ${err.message}`));
throw err;
}
}
}

let authPromise: Promise<msal.AuthResponse> | undefined;
if (authResponse === undefined) {
logger.info(
`Silent authentication failed, falling back to interactive method ${this.loginStyle}`
);
switch (this.loginStyle) {
case "redirect":
authPromise = new Promise((resolve, reject) => {
this.msalObject.handleRedirectCallback(resolve, reject);
});
this.msalObject.acquireTokenRedirect(authParams);
break;
case "popup":
authPromise = this.msalObject.acquireTokenPopup(authParams);
break;
}

authResponse = authPromise && (await authPromise);
if (options.flow === "implicit-grant") {
this.msal = new MSALImplicit(msalOptions);
} else {
this.msal = new MSALAuthCode(msalOptions);
}

return authResponse;
}

/**
Expand All @@ -147,13 +110,25 @@ export class InteractiveBrowserCredential implements TokenCredential {
): Promise<AccessToken | null> {
const { span } = createSpan("InteractiveBrowserCredential-getToken", options);
try {
if (!this.msalObject.getAccount()) {
await this.login();
const authResponse = await this.msal.acquireToken({
scopes,
...options
});

if (!authResponse) {
logger.getToken.info("No response");
return null;
}

const authResponse = await this.acquireToken({
scopes: Array.isArray(scopes) ? scopes : scopes.split(",")
});
if (!authResponse.expiresOn) {
logger.getToken.info(`Response had no "expiresOn" property.`);
return null;
}

if (!authResponse.accessToken) {
logger.getToken.info(`Response had no "accessToken" property.`);
return null;
}

if (authResponse) {
const expiresOnTimestamp = authResponse.expiresOn.getTime();
Expand Down
Loading

0 comments on commit 29825f0

Please sign in to comment.