Your app’s TypeScript/JavaScript client-side React frontend needs to claim the scopes that will allow it to access your app’s secured backend.
In Part 8: Securing a Web Service in an Azure App Service, I covered how to create an App Registration for a Web Service running in an App Service. That App Registration defined one or more scopes that the Web Service’s code expects a client-side frontend to provide before the Web Service would execute any of the frontend’s requests.
In this post, I’m going to walk through creating an App Registration for a client-side (e.g., JavaScript/TypeScript) frontend that makes those scopes available to your frontend application, along with the code to claim and pass those scopes on to the Web Service that expects them. I use React for my client-side development, so I’m going to leverage its features in my sample code.
To create an App Registration for your frontend, surf to the Azure Portal, and open the App Registrations page. Then:
If you’re creating this registration for someone else to modify, in the menu on the left, select the Owners choice and add the user who will manage the registration as an Owner for the App Registration.
For your client-side frontend, you must add a URL to your App Registration where user authentication tokens can be sent (note: not authorization tokens—that will come later). These authentication tokens identify the currently logged on user to the app. To add this URL:
Now you need to select which of the scopes defined in your Web Service’s App Registration are the ones you want your client-side frontend to use (probably, all of them). For that:
When your list of roles refreshes, their Status column will display a warning sign and a message beginning Not granted for…. Someone (either an administrator during “configuration time” or a user at “run time”) can grant consent to those scopes being given to your frontend app.
If you grant consent now, then, at runtime, the user running the app won’t be asked to grant consent (and, if that user is an administrator, the user can grant that permission to everyone in the organization at that time). Granting consent may be something else that you’re not allowed to do and will need to get an administrator to surf to your registration to grant consent.
To grant consent now, still on the API Permissions page, at the top of the list of permissions, click the green checkmark beside the Grant admin consent for… choice and click Yes in the “are you sure” textbox that appears. All of the scopes you’ve assigned will be given consent (if you want, you can use the ellipsis—…—at the right end of each scope to selectively remove Admin consent).
You’re now ready to have your frontend app claim this App Registration with its scopes and then pass those scopes to your Web Service.
It’s worth taking a moment to use the provided tool to check that you’ve set up your Web Service’s registration correctly.
From the menu down the left side of your App Registration, select Integration assistant to open the assistant on the right. From the dropdown list, select Single page app (SPA).
After making your selection in the first dropdown list, click anywhere on the page to close the list and reveal the Is this application calling APIs? option. Set the toggle beneath that to Yes and then click the Evaluate my app registration button.
The assistant will display a list of required and optional recommendations to implement plus a set of recommendations that you shouldn’t implement. Provided you have green checkmarks on all the required and shouldn’t recommendations, your App Registration should be ready to use. (You can safely ignore the optional recommendations, at least for now).
Before you can start coding your client-side you’ll need to add the MSAL for JavaScript package to your app. If you’re using React and Node.js to build your app, as I am, then you want to install these two packages:
npm i @azure/msal-browser
npm i @azure/msal-react
Next, you need to instantiate a PublicClientApplication
object. You want to make sure that you do this only once for your page, so, in a React app, it makes sense to do that in your app’s startup component. Depending on what scaffolding framework you used to create your app that will either be:
When you instantiate the PublicClientApplication
object, you must pass it an object literal with three pieces of information:
To do all that, you’ll first need to import the components you’ll need at the top of your root file (main.tsx or Index.tsx):
import { MsalProvider } from '@azure/msal-react';
import { PublicClientApplication } from '@azure/msal-browser';
This code defines the object literal that holds that information and creates the PublicClientApplication
from that object literal (you can call this object literal anything you want—I used initMsal
):
const initMsal = {
auth: {
clientId: "<your client id from the App Registration Overview page>",
authority: "https://login.microsoftonline.com/<your tenant id from the App Registration Overview page>",
redirectUri: "https://<name of your frontend’s app service>.azurewebsites.net"
}
};
const pca:PublicClientApplication = new PublicClientApplication(msalConfig);
A typical example would look like this:
const initMsal = {
auth: {
clientId: "d11…-…-…-….",
authority: "https://login.microsoftonline.com/e98...-...-...-...",
redirectUri: "https://warehousemgmtfrontendreact.azurewebsites.net"
}
};
const pca:PublicClientApplication = new PublicClientApplication(msalConfig);
If you want to test your frontend without having to deploy it to your App Service, set the redirectUri to the URL used when testing your frontend locally (e.g., “http://localhost:5172”). The easiest way to find that URL in Visual Studio Code is to use the npm run dev command in your terminal window to start your app and then copy the URL that the command displays.
You’ll need to use that PublicClientApplication
in your app’s components to get the token you need to pass to your Web Service. React makes it easy to share the PublicClientApplication
object with your app’s child components: Just wrap your top-level child components in an MsalProvider component, passing your PublicClientApplication
object to the component’s instance property.
This code now makes the PublicClientApplication
object I created in my earlier code available to a component named App (and all of the App component’s children):
createRoot(document.getElementById('root')!).render(
<MsalProvider instance={pca}>
<App />
</MsalProvider>
)
Your next step is, in the component where you intend to call your Web Service (in my case, the App component), to retrieve your PublicClientApplication
object and the information/authorization tokens for the currently logged on user.
The React NuGet packages simplify this by providing the useMsal
hook, which returns a reference to the PublicClientApplication
object (in a variable called instance
) and an array of the currently logged-in users (that array is called accounts
).
You’ll need to import it and the AuthenticationResult
type at the top of your component (I’ve included some additional React components that I’ll also need later):
import { useMsal } from "@azure/msal-react";
import { AuthenticationResult } from "@azure/msal-browser";
import { useState } from "react";
import { v6 } from "uuid";
You can use the MSAL hook like this to give you access to your PublicClientApplication
(in instance
) and the list of logged-in users (the accounts
array):
const { instance, accounts } = useMsal();
As you’ll see, I’m going to retrieve the access token in one method and use it in another method (you might, in real life, use two different components rather than just two different methods). I need a place to store that token between when it’s retrieved and when it’s used, so I create a variable and method for updating that updating that variable using React’s useState hook:
const [accessToken, setAccessToken] = useState<string>("");
After the call to useMsal
, If the accounts array is empty, it indicates that the user has not yet logged in using your app. You can use the PublicClientApplication object’s loginPopup
method to both log the application with the user in and to get the user’s access token.
If the user logs in successfully via the popup, the accounts
array will be automatically loaded with the current user’s information (though not necessarily “immediately”—you can’t count on the array being filled until the component’s UI is displayed). If user has already logged in, the method will just load the accounts array with user’s information and the user won’t be asked to log in again (though a dialog may flash on the screen).
You can call the loginPopup
method in a separate component in your application but, for this case study, I’ve just set up my App component to display multiple UIs. Specifically, my component has three UIs that let the user:
If the app hasn’t logged the user in yet, the accounts
array won’t have any members. So I first have a method that, if the accounts
array is empty, displays a UI that provides the user with a button they can use to log in (and, as a result, loads the accounts
array with the user’s information and gets the access token):
if (accounts.length === 0)
{
return (
<>
<p>
This app is not signed in!
<br/>
<button onClick={() => GetAccounts()}>Log In</button>
</p>
</>
);
}
That GetAccounts
function first checks to see if the access token that lets me call the Web Service has already been retrieved (if so, I can skip the rest of this method). If not, I call the loginPopup
method, passing an object literal that has its scopes
property set to the scope that I want (in this case, the Product.Read.All
scope).
The loginPopup
method returns an AuthenticationResult
that holds the access token which I store to be used later:
const GetAccounts = async () => {
if (accessToken === "")
{
let respAPI:AuthenticationResult = await instance.loginPopup(
{
scopes: [
"api://abd…-…-…-…-…/Product.Read.All",
],
});
setAccessToken(respAPI.accessToken);
}
}
My next if-block checks to see if:
loginPopup
method succeeded, it will load the accounts
array automatically, so the array’s length indicates that the app has logged in and retrieved the current user’s information).In this if-block, I display a UI that provides the user with a button to retrieve the objects from the Web Service:
if (accounts.length > 0 && products.length === 0)
{
return (
<>
<p>
This app is logged in and you can now retrieve Product objects
<br/>
<button onClick={() => callTheWebService()}>Get products</button>
</p>
</>
)
}
If you’re using the JavaScript Fetch API to call your Web Service, the method that calls the Web Service looks very much like the code from an earlier post. The only real difference is that this code adds an Authorization header that holds the accessToken as a Bearer header:
const correlationid:string = v6();
//replace with your Web service's URL
const WarehouseMngmntDomain: string = "warehousemgmtservice.azurewebsites.net";
let callResult = await fetch("https://" + WarehouseMngmntDomain + "/products",
{
headers: {'X-Correlation-ID': correlationid,
'Authorization': 'Bearer ' + accessToken }
}
);
if (callResult.ok) { setProducts(await callResult.json()); }
};
You’ll need to verify that your Web Service’s App Service has its CORS settings configured to accept requests from your frontend—either when running in your frontend’s App Service or when running locally during testing.
My third and final if-block checks to see if the products
array is not empty (i.e., the Web Service has been successfully called) and displays a list of retrieved Product objects:
if (products.length > 0)
{
return (
<>
<br />
{products.map(
(prd:Product) => {
return(
<>
<p>
{/* replace with property names from your type */}
Product Id: {prd.productId} <br/>
Product Name: {prd.name} <br/>
Product Price: {prd.listPrice} <br/>
</p>
</>
)
} )
}
</>
);
}
}
You can run this frontend from your development environment (Visual Studio or Visual Studio Code) or deploy it to its App Service.
There is more that you can do here to secure access between your Web Service and its frontend. In a later post, for example, I’ll integrate Web API Management into this application and restrict access to my Web Service’s App Services to only requests coming from my frontend’s App Service (with some caveats). I’ll also move all the sensitive information in my appsettings.json file into the Azure Key Vault.
You could also consider adding an Application Gateway with a Web Application Firewall to your Web Service’s App Service and use that to accept only requests from your frontend’s App Service’s outbound TCP addresses (as I did with the Azure SQL database).
But, while the backend Web Service is secured, the frontend application is not. In my next post, I’ll cover how to assign permissions to your frontend that you can check in your frontend’s code.
Peter Vogel is both the author of the Coding Azure series and the instructor for Coding Azure in the Classroom. Peter’s company provides full-stack development from UX design through object modeling to database design. Peter holds multiple certifications in Azure administration, architecture, development and security and is a Microsoft Certified Trainer.