page_type | name | services | platforms | urlFragment | description | languages | products | ||||
---|---|---|---|---|---|---|---|---|---|---|---|
sample |
active-directory-aspnetcore-webapp-openidconnect-v2 |
active-directory |
dotnet |
active-directory-aspnetcore-webapp-openidconnect-v2 |
Sign-in users interactively server-side (Confidential client) and silently acquire token for MS Graph for a Single-page app (SPA) |
|
|
Table Of Contents
This is a ASP.NET Core single page application/web application hybrid sample that calls the Microsoft Graph API using Razor and MSAL.js.
Use the hybrid-SPA code flow to obtain an Access token for your Web API in he backend and use it in the client SPA [without re-authenticating the user]
- The client ASP.NET Core Web App uses the Microsoft.Identity.Web to sign-in and obtain a JWT Access Token from Microsoft Entra ID as well as an additional Spa Authorization Code to be passed to a client-side single page application.
- The Spa Authorization Code is passed to the client-side application using the session configuration for the application.
- The Spa Authorization Code is exchanged for another Access Token in the client-side application.
- The Access Token is used by the client-side application as a bearer token to call the Microsoft Graph API.
- Either Visual Studio or Visual Studio Code and .NET Core SDK
- An Microsoft Entra ID tenant. For more information, see: How to get a Microsoft Entra tenant
- A user account in your Microsoft Entra ID tenant. This sample will not work with a personal Microsoft account. If you're signed in to the Microsoft Entra admin center with a personal Microsoft account and have not created a user account in your directory before, you will need to create one before proceeding.
From your shell or command line:
git clone https://github.com/Azure-Samples/active-directory-aspnetcore-webapp-openidconnect-v2.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.
cd 2-WebApp-graph-user\2-5-HybridFlow
There are two projects in this sample. Each needs to be separately registered in your Microsoft Entra tenant. To register these projects:
You can follow the manual steps to set the up the application or use the automated steps below.
- use PowerShell scripts that:
- automatically creates the Microsoft Entra applications and related objects (passwords, permissions, dependencies) for you.
- modify the projects' configuration files.
WARNING: If you have never used Azure AD Powershell before, we recommend you go through the App Creation Scripts guide once to ensure that your environment is prepared correctly for this step.
-
On Windows, run PowerShell as Administrator and navigate to the root of the cloned directory
-
In PowerShell run:
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope Process -Force
-
Run the script to create your Microsoft Entra application and configure the code of the sample application accordingly.
-
For interactive process - in PowerShell run:
cd .\AppCreationScripts\ .\Configure.ps1 -TenantId "[Optional] - your tenant id" -Environment "[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.
Note: You should skip the following steps if you've used the automation provided above.
Follow the steps below for manually register and configure your apps
- Sign in to the Microsoft Entra admin center.
- If your account is present in more than one Microsoft Entra 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 Microsoft Entra tenant.
- Navigate to the Microsoft Entra admin center and select the Microsoft Entra ID service.
- Select the App Registrations blade on the left, then select New registration.
- In the Register an application page that appears, enter your application's registration information:
- In the Name section, enter a meaningful application name that will be displayed to users of the app, for example
HybridFlow-aspnetcore
.
- In the Name section, enter a meaningful application name that will be displayed to users of the app, for example
- Under Supported account types, select Accounts in this organizational directory only
- Click Register to create the application.
- In the app's registration screen, find and note the Application (client) ID. You'll need to use this value in your app's configuration files.
- In the app's registration screen, select Authentication in the menu.
- If you don't have a platform added, select Add a platform and select the Web option.
- In the Redirect URIs section, enter the following redirect URIs.
https://localhost:44321/signin-oidc/
- Click on the Add a platform button in the Platform configurations section of the page
- Select the Single-page application button and enter
https://localhost:44321/
as the Redirect URI and click the Configure button
- Select the Single-page application button and enter
- Select Save to save your changes.
- In the app's registration screen, select the Certificates & secrets blade in the left to open the page where we can generate secrets and upload certificates.
- In the Client secrets section, select New client secret:
- Type a key description (for instance
app secret
), - Select one of the available key durations (6 months, 12 months or Custom) as per your security posture.
- The generated key value will be displayed when you select the Add button. Copy and save the generated value for use in later steps.
- You'll need this key later in your code's configuration files. This key value will not be displayed again, and is not retrievable by any other means, so make sure to note it from the Microsoft Entra admin center before navigating to any other screen or blade.
- Type a key description (for instance
- 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.
- Select the Add a permission button and then,
- Ensure that the Microsoft APIs tab is selected.
- In the Commonly used Microsoft APIs section, select Microsoft Graph
- In the Delegated permissions section, select the User.Read in the list followed by Contacts.Read. Use the search box if necessary.
- Select the Add permissions button at the bottom.
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".
- Open the
appsettings.json
file. - Find the key
Domain
and replace the existing value with your Microsoft Entra tenant name. - Find the key
TenantId
and replace the existing value with your Microsoft Entra tenant ID. - Find the key
ClientId
and replace the existing value with the application ID (clientId) ofHybridFlow-aspnetcore
app copied from the Microsoft Entra admin center. - Find the key
ClientSecret
and replace the existing value with the client secret ofHybridFlow-aspnetcore
app copied from the Microsoft Entra admin center.
To complete this step, you will use the New-SelfSignedCertificate
Powershell command. You can find more information about the New-SelfSignedCertificate command here.
-
Open PowerShell and run
New-SelfSignedCertificate
with the following parameters to create a self-signed certificate in the user certificate store on your computer:$cert=New-SelfSignedCertificate -Subject "CN=HybridFlowCert" -CertStoreLocation "Cert:\CurrentUser\My" -KeyExportPolicy Exportable -KeySpec Signature
-
Export this certificate using the "Manage User Certificate" MMC snap-in accessible from the Windows Control Panel. You can also add other options to generate the certificate in a different store such as the Computer or service store (See How to: View Certificates with the MMC Snap-in).
Alternatively you can use an existing certificate if you have one (just be sure to record its name for the next steps)
In the application registration blade for your application, in the Certificates & secrets page, in the Certificates section:
- Click on Upload certificate and, in click the browse button on the right to select the certificate you just exported (or your existing certificate)
- Click Add
To change the visual studio project to enable certificates you need to:
- Open the
appsettings.json
file - Find the app key
Certificate
in theAzureAd
section and insert theCertificateDescription
properties of your certificate. You can see some examples below and read more about how to configure certificate descriptions here.
You can retrieve a certificate from your local store by adding the configuration below to the ClientCertificates
array in the appsettings.json
file replacing <CERTIFICATE_STORE_PATH> with the store path to your certificate and <CERTIFICATE_DISTINGUISHED_NAME> with the distinguished name of your certificate. If you used the configuration scripts to generate the application this will be done for you using a sample self-signed certificate. You can read more about certificate stores here.
{
// ...
"AzureAd": {
// ...
"ClientCertificates": [{
"SourceType": "StoreWithDistinguishedName",
"CertificateStorePath": "<CERTIFICATE_STORE_PATH>",
"CertificateDistinguishedName": "<CERTIFICATE_DISTINGUISHED_NAME>"
}]
}
}
It's possible to get a certificate file, such as a pfx file, directly from a file path on your machine and load it into the application by using the configuration as shown below. Add the configuration below to the ClientCertificates
array of the appsettings.json
file. Replace <PATH_TO_YOUR_CERTIFICATE_FILE>
with the path to your certificate file and <CERTIFICATE_PASSWORD>
with that certificates password. If you created the application with the Configure.ps1
script found in the AppCreationScripts-withCert
a pfx file called HybridFlowCert.pfx will be generated with the certificate that is associated with your app and can be used as a credential. If you like, you can use configure the Certificate
property to reference this file and use it as a credential.
{
// ...
"AzureAd": {
// ...
"ClientCertificates": [{
"SourceType": "Path",
"CertificateDiskPath": "<PATH_TO_YOUR_CERTIFICATE_FILE>",
"CertificatePassword": "<CERTIFICATE_PASSWORD>"
}]
}
}
It's also possible to get certificates from an Azure Key Vault. Add the configuration below to the ClientCertificates
array of the appsettings.json
file. Replace <YOUR_KEY_VAULT_URL>
with the URL of the Key Vault holding your certificate and <YOUR_KEY_VAULT_CERTIFICATE_NAME>
with the name of that certificate as shown in your Key Vault. If you created the application with the Configure.ps1
script found in the AppCreationScripts-withCert
a pfx file called HybridFlowCert.pfx will be generated that is associated with the certificate that can be used as a credential for your app. If you like, you can load that certificate into a Key Vault and then access that Key Vault to use as a credential for your application.
{
// ...
"AzureAd": {
// ...
"ClientCertificates": [{
"SourceType": "KeyVault",
"KeyVaultUrl": "<YOUR_KEY_VAULT_URL>",
"KeyVaultCertificateName": "<YOUR_KEY_VAULT_CERTIFICATE_NAME>"
}]
}
}
- If you had set
ClientSecret
previously, change its value to an empty string,""
.
To execute this sample in the command line run:
cd 2-WebApp-graph-user\2-5-HybridFlow
dotnet run
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
adal
msal
dotnet
].
If you find a bug in the sample, please raise the issue on GitHub Issues.
To provide a recommendation, visit the following User Voice page.
Open your web browser and navigate to https://localhost:44321
. You should see the application home page with a link for Home, Privacy and Sign in. Click the Sign in link and you'll be redirected to the Microsoft login page. Sign in using an user account in your tenant. After you sign in the client side MSAL.js
client will receive an auth code from the server that will be exchanged for an authorization token and cached immediately in the browser.
- Click on
Profile
tab to see the signed in user's information and contacts. This information is retrieved using theMSAL.js
library in the browser.
Did the sample not work for you as expected? Did you encounter issues trying this sample? Then please reach out to us using the GitHub Issues page.
Consider taking a moment to share your experience with us.
The entire application is built on ASP.NET Core using Razor pages. The web app is then secured using the Microsoft Identity Web library.
When configuring the application in the appsettings.json
file we'll need to set the WithSpaAuthCode
property of the AzureAd
object to true
. This makes it possible for Azure to respond with not only an access token for users but also an access code for that user which can be sent to the retrieved by the SPA for an access token. This will be discussed in more detail later.
{
"AzureAd": {
// ...
"WithSpaAuthCode": true
},
// ...
}
Within the Program.cs
file you will see the WebApplicationBuilder builder which injects all dependencies into your application.
The first thing that needs to be done is to configure a Session
for your application which will contain the authorization code your SPA will exchange for an access token.
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSession(options =>
{
options.Cookie.IsEssential = true;
});
Next, the initial scopes are extracted from the appsettings.json
file from within the DownstreamApi
object. These scopes will be used in the access token stored within a cache within your server and also be contained within the token which will be retrieved by the SPA after it exchanges its access code. The server-side token cache will be cleared out for users after they sign-out.
var initialScopes = builder.Configuration.GetSection("DownstreamApi:Scopes")
.Value
.Split(' ');
builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(builder.Configuration)
.EnableTokenAcquisitionToCallDownstreamApi(initialScopes)
.AddDistributedTokenCaches();
The single page application in this sample is run using the MSAL.js library.
The razor page generating the client will read the settings within the appsettings.json
file and configure the application for you.
const msalInstance = new msal.PublicClientApplication({
auth: {
@{
var clientId = Configuration["AzureAd:ClientId"];
var instance = Configuration["AzureAd:Instance"];
var domain = Configuration["AzureAd:Domain"];
var redirectUri = Configuration["SpaRedirectUri"];
@Html.Raw($"clientId: '{clientId}',");
@Html.Raw($"redirectUri: '{redirectUri}',");
@Html.Raw($"authority: '{instance}{domain}',");
@Html.Raw($"postLogoutRedirectUri: '{redirectUri}',");
}
},
cache: {
cacheLocation: 'sessionStorage',
storeAuthStateInCookie: false,
}
});
Because this app is configured to make a simple web application using the AddMicrosoftIdentityWebApp
this makes it possible to associate sessions with each login instance. The authorization code intended for redemption by the client side application is automatically passed into the client side through the SpaAuthCode
session property as the server redeems an authorization code for itself.
This authorization code is extracted by the _Layout.cshtml
razor pag and exchanged for an authentication token using the MSAL.js client
which is then cached in the application and the SpaAuthCode
value is removed from the session.
After the either a token is redeemed for the user from Azure or a token is found withing the cache of the PublicClientApplication an event is triggered named AUTHENTICATED
which alerts other parts of the application a token is available to make requests with.
(function () {
const scopes =
@{
var apiScopes = Configuration["DownstreamApi:Scopes"].Split(' ');
@Html.Raw("[");
foreach(var scope in apiScopes) {
@Html.Raw($"'{scope}',")
}
@Html.Raw("];");
Context.Session.TryGetValue(Constants.SpaAuthCode, out var spaCode);
if (spaCode is not null && !string.IsNullOrEmpty(Encoding.Default.GetString(spaCode)))
{
@Html.Raw($"const code = '{Encoding.Default.GetString(spaCode)}';");
Context.Session.Remove(Constants.SpaAuthCode);
}
else
{
@Html.Raw($"const code = '';");
}
}
if (!!code) {
msalInstance
.handleRedirectPromise()
.then(result => {
if (result) {
console.log('MSAL: Returning from login');
document.dispatchEvent(new Event('AUTHENTICATED'));
return result;
}
const timeLabel = "Time for acquireTokenByCode";
console.time(timeLabel);
console.log('MSAL: acquireTokenByCode hybrid parameters present');
return msalInstance.acquireTokenByCode({
code,
scopes,
})
.then(result => {
console.timeEnd(timeLabel);
console.log('MSAL: acquireTokenByCode completed successfully', result);
document.dispatchEvent(new Event('AUTHENTICATED'));
})
.catch(error => {
console.timeEnd(timeLabel);
console.error('MSAL: acquireTokenByCode failed', error);
if (error instanceof msal.InteractionRequiredAuthError) {
console.log('MSAL: acquireTokenByCode failed, redirecting');
@{
if (User.Identity is not null)
{
@Html.Raw($"const username = '{User.Identity.Name}';");
}
else
{
@Html.Raw($"const username = '';");
}
}
const account = msalInstance.getAllAccounts()
.find(account => account.username === username);
return msalInstance.acquireTokenRedirect({
account,
scopes
});
}
});
})
}
else {
document.dispatchEvent(new Event('AUTHENTICATED'));
}
})();
In order to make a successful call to Microsoft Graph you first need to get an access token from the PublicClientApplication
cache and then use that access token within the Authorization
header along with the Bearer
key word.
The function getTokenFromCache
defined within _Layout.cshtml
does just that.
function getTokenFromCache(scopes) {
@if (User.Identity is not null)
{
@Html.Raw($"const username = '{User.Identity.Name}';");
}
else
{
@Html.Raw($"const username = '';");
}
const account = msalInstance.getAllAccounts()
.find(account => account.username === username);
return msalInstance.acquireTokenSilent({
account,
scopes
})
.catch(error => {
if (error instanceof msal.InteractionRequiredAuthError) {
return msalInstance.acquireTokenRedirect({
account,
scopes
});
}
});
};
Actual requests made to the Graph API are handled by the callMSGraph
function in the _Layout.cshtml
file.
function callMSGraph(path, token) {
@{
var graphEndpoint = Configuration["DownstreamApi:BaseUrl"];
if (!string.IsNullOrEmpty(graphEndpoint))
{
@Html.Raw($"const graphEndpoint = '{graphEndpoint}';");
}
else
{
@Html.Raw($"const graphEndpoint = '';");
}
}
const headers = new Headers();
const bearer = `Bearer ${token}`;
headers.append("Authorization", bearer);
const options = {
method: "GET",
headers
};
console.log('request made to Graph API at: ' + new Date().toString());
return fetch(`${graphEndpoint}${path}`, options)
.catch(error => console.log(error))
}
Learn how to:
- Change your app to sign-in users from any organization or any Microsoft accounts
- Enable users from National clouds to sign-in to your application
- Enable your Web App to call a Web API on behalf of the signed-in user
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.
Feel free to to take part in our sruvey to provide us with useful information to improve our samples in the future.
-
Microsoft identity platform (Microsoft Entra ID for developers)
-
Register an application with the Microsoft identity platform
-
Microsoft identity platform (Microsoft Entra ID for developers)
-
Register an application with the Microsoft identity platform
For more information, visit the following links:
*To lean more about the application registration, visit: *Quickstart: Register an application with the Microsoft identity platform *Quickstart: Configure a client application to access web APIs *Quickstart: Configure an application to expose web APIs
*To learn more about the code, visit: *Conceptual documentation for MSAL.NET and in particular: *Acquiring tokens with authorization codes on web apps *Customizing Token cache serialization
*To learn more about security in aspnetcore, *Introduction to Identity on ASP.NET Core *AuthenticationBuilder *Microsoft Entra ID with ASP.NET Core