Skip to content

Latest commit

 

History

History
477 lines (367 loc) · 25.8 KB

File metadata and controls

477 lines (367 loc) · 25.8 KB
page_type services client service level languages products platform endpoint urlFragment name description
sample
ms-identity
React SPA
Azure REST Api
200
javascript
react
azure-active-directory
msal-js
msal-react
azure-storage
azure-resource-manager
Javascript
AAD v2.0
ms-identity-javascript-react-tutorial
React single-page application using MSAL React to sign-in users and call Azure REST API and Azure Storage
This sample demonstrates a React single-page application (SPA) that signs-in users with Azure AD and calls the [Azure Resource Manager API](https://docs.microsoft.com/rest/api/resources) and [Azure Storage API](https://docs.microsoft.com/rest/api/storageservices/) using the [Microsoft Authentication Library for React](https://github.com/AzureAD/microsoft-authentication-library-for-js/tree/dev/lib/msal-react) (MSAL React).

React single-page application using MSAL React to sign-in users and call Azure REST API and Azure Storage

Overview

This sample demonstrates a React single-page application (SPA) that signs-in users with Azure AD and calls the Azure Resource Manager API and Azure Storage API using the Microsoft Authentication Library for React (MSAL React).

Here you'll learn how to sign-in, acquire a token and call a protected web API, as well as Dynamic Scopes and Incremental Consent, working with multiple resources and securing your routes and more.

ℹ️ See the community call: Deep dive on using MSAL.js to integrate React single-page applications with Azure Active Directory

Scenario

  1. The client React SPA uses MSAL React to sign-in a user and obtain a JWT access token for Azure Resource Manager API and Azure Storage API from Azure AD.
  2. The access token is used to authorize the user with Azure SDK for JavaScript to call Azure Resource Manager API and Azure Storage API

Overview

Contents

File/folder Description
App.jsx Main application logic resides here.
authConfig.js Contains authentication configuration parameters.
pages/Home.jsx Contains a table with ID token claims and description
pages/Tenant.jsx Calls Azure Resource Manager with Azure SDK.
pages/BlobStorage.jsx Calls Azure Storage API with Azure SDK.
azureManagement.js Initialize an instance of SubscriptionClient and BlobServiceClient.

Prerequisites

  • Node.js must be installed to run this sample.
  • Visual Studio Code is recommended for running and editing this sample.
  • VS Code Azure Tools extension is recommended for interacting with Azure through VS Code Interface.
  • A modern web browser. This sample uses ES6 conventions and will not run on Internet Explorer.
  • An Azure AD tenant. For more information, see: How to get an Azure AD tenant
  • A user account in your Azure AD tenant. This sample will not work with a personal Microsoft account. If you're signed in to the Azure portal with a personal Microsoft account and have not created a user account in your directory before, you will need to create one before proceeding.
  • An Azure Storage account To access Azure Storage you will need an active storage account.

Setup the sample

Step 1: Clone or download this repository

From your shell or command line:

git clone https://github.com/Azure-Samples/ms-identity-javascript-react-tutorial.git

or download and extract the repository .zip file.

⚠️ To avoid path length limitations on Windows, we recommend cloning into a directory near the root of your drive.

Step 2: Install project dependencies

    cd 2-Authorization-I\2-call-arm\SPA 
    npm install

Step 3: Register the sample application(s) in your tenant

There is one project in this sample. To register it, you can:

  • follow the steps below for manually register your apps

  • or use PowerShell scripts that:

    • automatically creates the Azure AD applications and related objects (passwords, permissions, dependencies) for you.
    • modify the projects' configuration files.
    Expand this section if you want to use this automation:

    ⚠️ If you have never used Microsoft Graph PowerShell before, we recommend you go through the App Creation Scripts Guide once to ensure that your environment is prepared correctly for this step.

    1. On Windows, run PowerShell as Administrator and navigate to the root of the cloned directory

    2. In PowerShell run:

      Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope Process -Force
    3. Run the script to create your Azure AD application and configure the code of the sample application accordingly.

    4. For interactive process -in PowerShell, run:

      cd .\AppCreationScripts\
      .\Configure.ps1 -TenantId "[Optional] - your tenant id" -AzureEnvironmentName "[Optional] - Azure environment, defaults to 'Global'"

    Other ways of running the scripts are described in App Creation Scripts guide. The scripts also provide a guide to automated application registration, configuration and removal which can help in your CI/CD scenarios.

Choose the Azure AD tenant where you want to create your applications

To manually register the apps, as a first step you'll need to:

  1. Sign in to the Azure portal.
  2. If your account is present in more than one Azure AD tenant, select your profile at the top right corner in the menu on top of the page, and then switch directory to change your portal session to the desired Azure AD tenant.

Register the spa app (ms-identity-react-c2s2)

  1. Navigate to the Azure portal and select the Azure Active Directory service.
  2. Select the App Registrations blade on the left, then select New registration.
  3. In the Register an application page that appears, enter your application's registration information:
    1. In the Name section, enter a meaningful application name that will be displayed to users of the app, for example ms-identity-react-c2s2.
    2. Under Supported account types, select Accounts in this organizational directory only
    3. Select Register to create the application.
  4. In the Overview blade, find and note the Application (client) ID. You use this value in your app's configuration file(s) later in your code.
  5. In the app's registration screen, select the Authentication blade to the left.
  6. If you don't have a platform added, select Add a platform and select the Single-page application option.
    1. In the Redirect URI section enter the following redirect URIs:
      1. http://localhost:3000/
      2. http://localhost:3000/redirect.html
    2. Click Save to save your changes.
  7. Since this app signs-in users, we will now proceed to select delegated permissions, which is is required by apps signing-in users.
    1. In the app's registration screen, select the API permissions blade in the left to open the page where we add access to the APIs that your application needs:
    2. Select the Add a permission button and then:
    3. Ensure that the Microsoft APIs tab is selected.
    4. In the list of APIs, select the API Windows Azure Service Management API.
      • Since this app signs-in users, we will now proceed to select delegated permissions, which is is requested by apps when signing-in users.
        1. In the Delegated permissions section, select user_impersonation in the list. Use the search box if necessary.
    5. Select the Add permissions button at the bottom.
    6. Select the Add a permission button and then:
    7. Ensure that the Microsoft APIs tab is selected.
    8. In the list of APIs, select the API Azure Storage.
      • Since this app signs-in users, we will now proceed to select delegated permissions, which is is requested by apps when signing-in users.
        1. In the Delegated permissions section, select user_impersonation in the list. Use the search box if necessary.
    9. Select the Add permissions button at the bottom.
Configure Optional Claims
  1. Still on the same app registration, select the Token configuration blade to the left.
  2. Select Add optional claim:
    1. Select optional claim type, then choose ID.
    2. Select the optional claim acct.

    Provides user's account status in tenant. If the user is a member of the tenant, the value is 0. If they're a guest, the value is 1.

    1. Select Add to save your changes
Assign Azure role-based access control (Azure RBAC)
  1. Ensure that an Azure Storage Account was created. If not, please create one.
  2. Assign the role Storage Blob Data Contributor to your user or group to have read and write access to your blob storage. Please see Assign Azure roles using the Azure portal.
Configure Cross-Origin Resource Sharing (CORS) support for your Azure Storage
  1. Navigate to your Azure Storage Account.
  2. Select the Resource sharing (CORS) blade on the left. Make sure that Blob service is selected.
  3. In Allowed origins add the domain name http://localhost:3000 and make sure there is no trailing slash.
  4. In Allowed methods select all options.
  5. In Allowed headers add *.
  6. In Exposed headers add *.
  7. Select Save.
Configure the spa app (ms-identity-react-c2s2) to use your app registration

Open the project in your IDE (like Visual Studio or Visual Studio Code) to configure the code.

In the steps below, "ClientID" is the same as "Application ID" or "AppId".

  1. Open the SPA\src\authConfig.js file.
  2. Find the key Enter_the_Application_Id_Here and replace the existing value with the application ID (clientId) of ms-identity-react-c2s2 app copied from the Azure portal.
  3. Find the key Enter_the_Tenant_Info_Here and replace the existing value with your Azure AD tenant/directory ID.
  4. find the key Enter_Storage_Account_Name and replace the existing value with your storage account name.

Step 4: Running the sample

From your shell or command line, execute the following commands:

    cd 2-Authorization-I\2-call-arm\SPA
    npm start

Explore the sample

  1. Open your browser and navigate to http://localhost:3000.
  2. Select the Sign In button on the top right corner.
  3. Select the Tenants button on the navigation bar. This will make a call to the Azure Resource Manager API. Screenshot
  4. Select the Storage button on the navigation bar. After that use the form upload a file to Azure Storage. Screenshot
  5. Navigate back to Azure Portal and check the uploaded file under your storage account under the ms-identity-react-c2s2 container. Screenshot

ℹ️ Did the sample not work for you as expected? Then please reach out to us using the GitHub Issues page.

We'd love your feedback!

Were we successful in addressing your learning objective? Consider taking a moment to share your experience with us.

Troubleshooting

Expand for troubleshooting info

Use Stack Overflow to get support from the community. Ask your questions on Stack Overflow first and browse existing issues to see if someone has asked your question before. Make sure that your questions or comments are tagged with [azure-active-directory react ms-identity adal msal].

If you find a bug in the sample, raise the issue on GitHub Issues.

About the code

Protected resources and scopes

In order to access a protected resource on behalf of a signed-in user, the app needs to present a valid Access Token to that resource owner (in this case, Azure Resource Manager API and Azure Storage API). Access Token requests in MSAL are meant to be per-resource-per-scope(s). This means that an Access Token requested for resource A with scope scp1:

  • cannot be used for accessing resource A with scope scp2, and,
  • cannot be used for accessing resource B of any scope.

The intended recipient of an Access Token is represented by the aud claim (in this case, it should be the Microsoft Azure Resource Manager API and Azure Storage API's App ID); in case the value for the aud claim does not mach the resource APP ID URI, the token will be considered invalid by the API. Likewise, the permissions that an Access Token grants are provided in the scp claim. See Access Token claims for more information.

Working with multiple resources

When you have to access multiple resources, initiate a separate token request for each:

    // "User.Read" stands as shorthand for "graph.microsoft.com/User.Read"
    const graphToken = await msalInstance.acquireTokenSilent({
         scopes: [ "" ]
    });
    const customApiToken = await msalInstance.acquireTokenSilent({
         scopes: [ "api://<myCustomApiClientId>/My.Scope" ]
    });

Bear in mind that you can request multiple scopes for the same resource (e.g. User.Read, User.Write and Calendar.Read for MS Graph API).

    const graphToken = await msalInstance.acquireTokenSilent({
         scopes: [ "User.Read", "User.Write", "Calendar.Read"] // all MS Graph API scopes
    });

In case you erroneously pass multiple resources in your token request, Azure AD will throw an exception, and your request will fail.

     // your request will fail for both resources
     const myToken = await msalInstance.acquireTokenSilent({
          scopes: [ "https://management.azure.com/user_impersonation", "api://<myCustomApiClientId>/My.Scope" ]
     });

Acquire a Token

MSAL.js exposes 3 APIs for acquiring a token: acquireTokenPopup(), acquireTokenRedirect() and acquireTokenSilent(). MSAL React uses these APIs underneath, while offering developers higher level hooks and templates to simplify the token acquisition process:

export const Tenant = () => {
    const { instance } = useMsal();
    const [tenantInfo, setTenantInfo] = useState(null);
    const account = instance.getActiveAccount();
    const request = {
        scopes: ["https://management.azure.com/user_impersonation"],
        account: account,
    };

    const { login, result, error } = useMsalAuthentication(InteractionType.Popup, request);

    useEffect(() => {
        if (!!tenantInfo) {
            return;
        }

        if (!!error) {
            // in case popup is blocked, use redirect instead
            if (error.errorCode === 'popup_window_error' || error.errorCode === 'empty_window_error') {
                login(InteractionType.Redirect, request);
            }

            console.log(error);
            return;
        }

        const fetchData = async (accessToken) => {
            const client = await getSubscriptionClient(accessToken);
            const resArray = [];
            for await (let item of client.tenants.list()) {
                resArray.push(item);
            }
            setTenantInfo(resArray);
        };
        if (result) {
            fetchData(result.accessToken).catch((error) => {
                console.log(error);
            });
        }
    }, [tenantInfo, result, error, login]);

    if (error) {
        return <div>Error: {error.message}</div>;
    }
    return <>{tenantInfo ? <TenantData response={result} tenantInfo={tenantInfo} /> : null}</>;
};

ℹ️ Please see the documentation on acquiring an access token to learn more about various methods available in MSAL.js to acquire tokens. For MSAL React in particular, see the useIsAuthenticated hook to learn more about useMsalAuthentication hook to acquire tokens.

Calling the Microsoft Azure REST API and Azure Storage

Azure SDK for JavaScript contains libraries for the breadth of Azure services. Management libraries are packages that you would use to provision and manage Azure resources. Client libraries are packages that you would use to consume these resources and interact with them. While the SDK has a default authentication provider that can be used in basic scenarios, it can also be extended to use with a pre-fetched access token. To do so, we will initialize the SubscriptionClient object and the BlobServiceClient object, which both contain a StaticTokenCredential object of a class that implements the TokenCredential abstraction. It takes a pre-fetched access token in its constructor as an AccessToken and returns that from its implementation of getToken().

/**
 * Returns a subscription client object with the provided token acquisition options
 */
export const getSubscriptionClient = async (accessToken) => {
      const credential = new StaticTokenCredential({
          token: accessToken,
          expiresOnTimestamp: accessToken.exp
      });

    const client = new SubscriptionClient(credential);
    return client;
};

/**
 * Returns a blob service client object with the provided token acquisition options
 */
export const getBlobServiceClient = async (accessToken) => {

    const credential = new StaticTokenCredential({
        token: accessToken,
        expiresOnTimestamp: accessToken.exp,
    });

    const client = new BlobServiceClient(`https://${storageInformation.accountName}.blob.core.windows.net`, credential);
    return client;
};

StaticTokenCredential needs to implement the TokenCredential interface, which can be done as shown below:

class StaticTokenCredential {
  constructor(accessToken) {
    this.accessToken = accessToken; 
  }

  async getToken() {
    return this.accessToken;
  }

}

See azureManagement.js. The Subscription client can be used in your components as shown below:

 useEffect(() => {
        if (!!tenantInfo) {
            return;
        }

        if (!!error) {
            // in case popup is blocked, use redirect instead
            if (error.errorCode === 'popup_window_error' || error.errorCode === 'empty_window_error') {
                login(InteractionType.Redirect, request);
            }

            console.log(error);
            return;
        }

        const fetchData = async (accessToken) => {
            const client = await getSubscriptionClient();
            const resArray = [];
            for await (let item of client.tenants.list()) {
                resArray.push(item);
            }
            setTenantInfo(resArray);
        };
        if (result) {
            fetchData(result.accessToken).catch((error) => {
                console.log(error);
            });
        }
    }, [tenantInfo, result, error, login]);

The Azure BlobServiceClient can be used in your component as shown:

const handleSubmit = async (e) => {
        e.preventDefault();
        if (uploadedFile) {
            try {
                const client = await getBlobServiceClient(result.accessToken);
                const containerClient = client.getContainerClient(storageInformation.containerName);
                const hasContainer = await containerExist(client, storageInformation.containerName);
                if (hasContainer) {
                    const blockBlobClient = containerClient.getBlockBlobClient(uploadedFile.name);
                    blockBlobClient.uploadData(uploadedFile);
                } else {
                    const createContainerResponse = await containerClient.create();
                    const blockBlobClient = containerClient.getBlockBlobClient(uploadedFile.name);
                    blockBlobClient.uploadData(uploadedFile);
                    console.log('Container was created successfully', createContainerResponse.requestId);
                }
            } catch (error) {
                console.log(error);
            }
        }
    };

Next Steps

Learn how to:

Contributing

If you'd like to contribute to this sample, see CONTRIBUTING.MD.

This project has adopted the Microsoft Open Source Code of Conduct. For more information, see the Code of Conduct FAQ or contact [email protected] with any additional questions or comments.

Learn More