This sample application modifies the react-sample-app for Microsoft Authentication Library (MSAL) for JavaScript. It demonstrates the following scenarios:
- It calls a ASP.NET Core Web API built with Microsoft.Identity.Web which helps integrating ASP.NET Core middleware with Microsoft Identity Platform (formerly Azure AD v2.0 endpoint).
- With a configuration change in the frontend .env and backend AppSettings.json, this app can authenticate with Azure AD (Single Tenant or B2B) or Azure AD B2C.
- When used with Azure AD B2B, it calls a Web API which further calls a Graph API to invite a B2B user, or to check the user's invitation status.
- There are 3 frontend samples:
- react-sample-app is a React.js SPA using MSAL v1 which supports OAuth2 Implicit flow.
- next-sample-app is a Next.js frontend also using MSAL v1 with implicit flow. Note that MSAL references
window
object so this sample loads MSAL on the client side rather than server side. - react-pkce-sample-app is a React.js sample using MSAL v2 which support PKCE. At the time of this writing, I can only make it work with @azure/msal-browser 2.0.0-beta.2 due to this issue in 2.0.0-beta.4. Make sure you register your SPA in Azure AD to enable PKCE as documented here.
Before you can run this app, your must register an application in Azure AD B2C and/or Azure AD. You can register a single application for both frontend UI and backend web service. Refer to the documentation on how to register an application for Azure AD or for Azure AD B2C in general. Customize the app as following for B2B and B2C respectively.
- Configure the application type - register the app as a single tenant application with Accounts in this organizational directory only
- Configure the token - go to Token configuration, Add optional claim, click ID, and add
email
andupn
. Further edit theupn
claim to turn onupn
for guests as shown below:
- Configure exposed API scopes - go to Expose an API, Add a scope for our
WeatherForecast
API. Since we own both the frontend and the backend, we can add our application as an Authorized client application without having to ask the user to consent for the frontend to access the API.
-
Configure required API permissions - go to API permissions, and add the following delegated permissions from Microsoft Graph API. This is such that the backend web api can access Graph API on-behalf-of the logged in user. Optionally grant admin consent for the permissions.
- User.Invite.All
- User.Read
- User.Read.All
-
Optionally assign users to this app so that this app shows up on their
myapps
page - go to Enterprise applications of Azure AD, find the application corresponding to the registered application, Assign users and groups. -
Optionally allow only assigned users to access this application - go to properties of the enterprise applicatio, turn on/off User assignment required.
- Configure the application type - register the app with Accounts in any organizational directory or any identity provider. For authenticating users with Azure AD B2C.
- Configure the token - go to your signup/signin user flow, Application claims, and check Email Addresses.
- Configure exposed API scopes - go to Expose an API, Add a scope for our
WeatherForecast
API. - Configure required API permissions - go to API permissions, Add a permission, my APIs, and add the scopes defined above.
- Go to the Authentication menu of the app, Add a platform to add a Web application. Add Redirect URIs, and enable Implicit grant for Access tokens and ID tokens. MSAL.js 2.0 is still in preview as of May 2020. This sample isn't using PKCE yet.
- Go to Manifest, and change
"accessTokenAcceptedVersion": null,
to"accessTokenAcceptedVersion": 2,
. This is so that Azure AD will use the v2 endpoints.
-
Start the backend web service
- configure AppSettings.json and replace the parameters with your Azure AD values
dotnet run
, and the app will start athttps://localhost:5001
by default
-
Start the frontend app
- to run the react.js app
- if it's the first time you run the app,
npm install
npm start
, and the app will start athttp://localhost:3000
by default
- if it's the first time you run the app,
- to run the next.js app
- if it's the first time you run the app,
npm install
npm run dev
, and the app will start athttp://localhost:3000
by default
- if it's the first time you run the app,
- to run the react.js app
-
Follow the UI to:
- sign in
- call Weather API (available for both B2B and B2C)
- invite a user to B2B (only available when configured for B2B)
- check a B2B user's invitation status (only available when configured for B2B)
The Web API calls the Graph API on-behalf-of the user, rather than using the application's identity. This means:
- To be able to invite a B2B guest user, the signed-in user making the call must have
User.Invite.All
permission in Azure AD. There are several knobs your Azure AD admin can tune for this. Go to your Azure AD portal, User settings, Manage external collaboration:
- To be able to check a B2B guest user's invitation status, the signed-user must have
User.Read.All
permission in Azure AD. If guests permissions are limited as shown below, then guests won't be able to read other guests' invitation status unless they are assgined theDirectory readers
role.
- It's ok to invite a guest user multiple times, you won't see any error.
- If you invite a user whose email ends in the target Azure AD tenant domain name, you will see an error saying that the invitee is in the invitor tenant. For example, the target Azure AD tenant is
contoso.onmicrosofot.com
, and you try to invite[email protected]
. - If you invite a user whose email ends in a validated domain name in the target Azure AD, you will see an error saying that this user cannot be invited because the domain of the user's email address is a verified domain of this directory.
- B2B users with personal accounts are not redirected to the app after they log out. B2B users with work or school accounts or B2C don't have this problem. This is a known issue discussed in many places, incuding this ADAL issue. It's still an issue in MSAL.
- After the user logs out, it appears that the user is redirected to either the
postLogoutRedirectUrl
configured in UserAgentApplication or the start up page if it's not configured. What's configured in AAD app registration asLogout URL
seems to be ignored. This also happens to B2B.Logout URL
is honored in B2C whenID_TOKEN_HINT
is set. - You can use
upn (user principal name)
orobject id
to check a user's invitation status. Note thatupn
for a guest user is not their email. Typically, if a guest with email address[email protected]
is invited tohost.com
tenant, then the guest'supn
isalice_guest.com#EXT#@host.com
. This can become even more complex after[email protected]
is invited tohost.com
, and now creates another tenanthost2.com
. Now herupn
will bealice_guest.com#EXT#_host.com#EXT#@host2.com
. But she can't log in tohost2.com
using eitherupn
s until[email protected]
is invited tohost2.com
again. - While a B2B guest user always has
email
in the ID token, a user belongs to the tenant often hasemail
set to null. This is becauseemail
is used to invite a guest user, but for native tenant users, its value need to be retrieved from Office 365. - Things I learned converting from react.js to next.js:
- Place the UI pages in the
pages
folder, the REST API calls in theapi
folder, and other components in thecomponents
folder. - If a component uses browser side objects such as
window
, it can't be rendered on server side. With HOC (High Order Component) wrapping around theApp
, I found it also challenging to dynamically import MSAL with no server side rendering. But simply validating thewindow
object works fine. - Environment variables are handled differently in react.js vs next.js. By default, in next.js, they are only available on the server side. You can either prefix them with
NEXT_PUBLIC_
or pass them through innext.config.js
to make them available to the browser. I used the latter so I can have the same environment variable names for react.js and next.js. - In next.js, dynamic environment variable name doesn't work. For example, the following doesn't work on the browser side in config.js:
const authScheme = process.env.AUTH_SCHEME; console.log(process.env["API_SCOPES_" + authScheme])
- Place the UI pages in the