Login and use an ASP.NET Core API with Azure AD Auth and user access tokens

In this blog post, Azure AD will be setup and used to authenticate and authorize an ASP.NET core Razor Page application which uses an API from a separate ASP.NET Core MVC project. User access tokens are used to access to API, so that an email can be used in the API. The API is not dependent on the UI project as the access token comes straight from Azure AD token server.

Code: https://github.com/damienbod/AzureAD-Auth-MyUI-with-MyAPI

Posts in this Series

History

2020-10-09 Updated Microsoft.Identity.Web to 1.1.0
2020-09-11 Updated Microsoft.Identity.Web to 0.4.0-preview
2020-08-09 Updated Microsoft.Identity.Web to 0.2.2-preview

Setup the APP registrations in Azure

Two Azure AD APP registrations can be created to configure this setup. One registration will be used for the Web API and a second registration is used for the UI application. In this post, the Azure portal is used to this up. The email claim will be added to the access token which is then used in the ASP.NET Core Web API.

Setup the Web API APP registration

In the Azure Active directory, click the App registrations and create a new registration using the New registration button.

Leave all the defaults and Register. We want to only use this inside our tenant.

Click the Expose an API, and add a new scope using Add a scope. We want to use the API for user access tokens.

The Application ID URI needs to be created before the required scope can be added. Save and continue.

Now add the access_as_user scope. set the Admins and users, add the required texts, and Add scope. This scope can be used as “api://–clientId–/access_as_user”

Now we need to add a permission so that the email claim can be added to the access token. Click the Add Permission.

From the Microsoft Graph, Delegated permissions, add the email permission. You can add whatever you require in the access token.

In the Token Configuration add the optional email claim to the access token.

Setup the UI APP registration

Now that the Web API is setup, the user interface client APP registration can be created. An ASP.NET Core Razor Page application will be used and this will the access the API. This type of application requires the WEB setup.

Create a new registration for the UI. set the redirect URL to match your application. Click Register

In the Authentication blade, define a Logout URL which matches your application and add support for ID Tokens.

In the API permissions add the API registration which was created above. This can be done in the API permissions, Add a permission, My APIs and add.

The ASP.NET Core application requires a secret to access the API. In Certificates & secrets, create a new secret, and save this somewhere for later usage.

Web API implementation

The Web API can now be implemented using ASP.NET Core. An API project was created using the Visual Studio templates.

Now add the Microsoft.Identity.Web Nuget package to the project. This will be used for the Azure AD auth.

Add the AzureAd configuration to the app.settings.json which must match the APP registration created above.

{
  "AzureAd": {
    "Instance": "https://login.microsoftonline.com/",
    "Domain": "damienbodhotmail.onmicrosoft.com",
    "TenantId": "7ff95b15-dc21-4ba6-bc92-824856578fc1",
    "ClientId": "98328d53-55ec-4f14-8407-0ca5ff2f2d20"
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*"
}

In the Startup class, add the AddProtectedWebApi from the Microsoft.Identity.Web package to the ConfigureServices method. Then switch off the default ASP.NET Core claim mappings, and add an authorization policy to only allowed authorized requests and the access token must contain an email claim.

public void ConfigureServices(IServiceCollection services)
{
	JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();

	services.AddMicrosoftIdentityWebApiAuthentication(Configuration);

	services.AddControllers(options =>
	{
		var policy = new AuthorizationPolicyBuilder()
			.RequireAuthenticatedUser()
			.RequireClaim("email")
			.Build();
		options.Filters.Add(new AuthorizeFilter(policy));
	});
}

Add the UseAuthentication and the UseAuthorization middleware in the correct order.

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
	if (env.IsDevelopment())
	{
		app.UseDeveloperExceptionPage();
	}

	app.UseHttpsRedirection();

	app.UseRouting();

	app.UseAuthentication();
	app.UseAuthorization();

	app.UseEndpoints(endpoints =>
	{
		endpoints.MapControllers();
	});
}

Razor Page UI implementation

The User interfaxe application is implemented using a ASP.NET Core razor page application. This again was created using the Visual Studio templates.
Add the Microsoft.Identity.Web nuget package and also the Microsoft.Identity.Web.UI package to the UI project.

Add the AzureAd configuration to the app.settings.json and also the API url and the scope to use this. These settings must match what was configured in the portal for the UI registration.

{
  "AzureAd": {
    "Instance": "https://login.microsoftonline.com/",
    "Domain": "damienbodhotmail.onmicrosoft.com",
    "TenantId": "7ff95b15-dc21-4ba6-bc92-824856578fc1",
    "ClientId": "64ecb044-417b-4892-83d4-5c03e8c977b9",
    "CallbackPath": "/signin-oidc",
    "SignedOutCallbackPath ": "/signout-callback-oidc"
  },
  "CallApi": {
    "ScopeForAccessToken": "api://98328d53-55ec-4f14-8407-0ca5ff2f2d20/access_as_user",
    "ApiBaseAddress": "https://localhost:44390"

  },
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*"
}

Add the secret to the project using the secrets manager. This is also added in the AzureAd Json object.

{
  "AzureAd": {
    "ClientSecret": "your secret.."
  }
}

Add an AddSignIn and an EnableTokenAcquisitionToCallDownstreamApi method to login the UI in Azure, and to configure the API for use. Add the authorization as required for the UI app. The AddMicrosoftIdentityUI is required for the UI views.

public void ConfigureServices(IServiceCollection services)
{
	services.AddTransient<ApiService>();
	services.AddHttpClient();

	services.AddOptions();

	string[] initialScopes = Configuration.GetValue<string>("CallApi:ScopeForAccessToken")?.Split(' ');

	services.AddMicrosoftIdentityWebAppAuthentication(Configuration)
		.EnableTokenAcquisitionToCallDownstreamApi(initialScopes)
		.AddInMemoryTokenCaches();

	services.AddRazorPages().AddMvcOptions(options =>
	{
		var policy = new AuthorizationPolicyBuilder()
			.RequireAuthenticatedUser()
			.Build();
		options.Filters.Add(new AuthorizeFilter(policy));
	}).AddMicrosoftIdentityUI();
}

Add the UseAuthentication and the UseAuthorization middleware in the correct order.

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
	if (env.IsDevelopment())
	{
		app.UseDeveloperExceptionPage();
	}
	else
	{
		app.UseExceptionHandler("/Error");
		app.UseHsts();
	}

	app.UseHttpsRedirection();
	app.UseStaticFiles();

	app.UseRouting();

	app.UseAuthentication();
	app.UseAuthorization();

	app.UseEndpoints(endpoints =>
	{
		endpoints.MapRazorPages();
		endpoints.MapControllers();
	});
}

Implement the API service as required. The ITokenAcquisition interface is used to get the access token, so that the API can be used. The required scope for the API is read from the configuration.

using Microsoft.Extensions.Configuration;
using Microsoft.Identity.Web;
using Newtonsoft.Json.Linq;
using System;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;

namespace MyServerRenderedPortal
{
    public class ApiService
    {
        private readonly IHttpClientFactory _clientFactory;
        private readonly ITokenAcquisition _tokenAcquisition;
        private readonly IConfiguration _configuration;

        public ApiService(IHttpClientFactory clientFactory, 
            ITokenAcquisition tokenAcquisition, 
            IConfiguration configuration)
        {
            _clientFactory = clientFactory;
            _tokenAcquisition = tokenAcquisition;
            _configuration = configuration;
        }

        public async Task<JArray> GetApiDataAsync()
        {
            try
            {
                var client = _clientFactory.CreateClient();

                var scope = _configuration["CallApi:ScopeForAccessToken"];
                var accessToken = await _tokenAcquisition.GetAccessTokenForUserAsync(new[] { scope });

                client.BaseAddress = new Uri(_configuration["CallApi:ApiBaseAddress"]);
                client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
                client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
       
                var response = await client.GetAsync("weatherforecast");
                if (response.IsSuccessStatusCode)
                {
                    var responseContent = await response.Content.ReadAsStringAsync();
                    var data = JArray.Parse(responseContent);

                    return data;
                }

                throw new ApplicationException($"Status code: {response.StatusCode}, Error: {response.ReasonPhrase}");
            }
            catch (Exception e)
            {
                throw new ApplicationException($"Exception {e}");
            }
        }
    }
}

Use the API service as required in the razor pages.

using Microsoft.AspNetCore.Mvc.RazorPages;
using Newtonsoft.Json.Linq;
using System.Threading.Tasks;

namespace MyServerRenderedPortal.Pages
{
    public class CallApiModel : PageModel
    {
        private readonly ApiService _apiService;

        public JArray DataFromApi { get; set; }
        public CallApiModel(ApiService apiService)
        {
            _apiService = apiService;
        }

        public async Task OnGetAsync()
        {
            DataFromApi = await _apiService.GetApiDataAsync();
        }
    }
}

The login and the logout need to be added for the application which uses the Microsoft.Identity.Web.UI package.

The _LoginPartial.cshtml can be implemented, and this uses the UI package which added the MicrosoftIdentity area and the view implementation.


<ul class="navbar-nav">
@if (User.Identity.IsAuthenticated)
{
        <li class="nav-item">
            <span class="navbar-text text-dark">Hello @User.Identity.Name!</span>
        </li>
        <li class="nav-item">
            <a class="nav-link text-dark" asp-area="MicrosoftIdentity" asp-controller="Account" asp-action="SignOut">Sign out</a>
        </li>
}
else
{
        <li class="nav-item">
            <a class="nav-link text-dark" asp-area="MicrosoftIdentity" asp-controller="Account" asp-action="SignIn">Sign in</a>
        </li>
}
</ul>

Now you can start both applications, and if everything is configured correctly, the UI project can login, and use the API in a secure way.

Links:

https://github.com/AzureAD/microsoft-identity-web

https://github.com/Azure-Samples/active-directory-aspnetcore-webapp-openidconnect-v2

https://jwt.io/

10 comments

  1. […] Login and use an ASP.NET Core API with Azure AD Auth and user access tokens – Damien Bowden […]

  2. […] Login and use an ASP.NET Core API with Azure AD Auth and user access tokens (Damien Bowden) […]

  3. […] Login and use an ASP.NET Core API with Azure AD Auth and user access tokens […]

  4. […] Login and use an ASP.NET Core API with Azure AD Auth and user access tokens […]

  5. Adrian Nasui · · Reply

    Hi Damien, thanks for the description. One question, the Microsoft.Identity.Web package is still in preview, what are alternatives to using that package in a asp net core 3.1 Api service to perform access_token checks on the Bearer token received from the SPA?
    Thanks,
    Adrian

  6. suresh · · Reply

    I got the below error
    Microsoft.Identity.Web.MicrosoftIdentityWebChallengeUserException: IDW10502: An MsalUiRequiredException was thrown due to a challenge for the user. See https://aka.ms/ms-id-web/ca_incremental-consent.
    —> MSAL.NetCore.4.17.1.0.MsalUiRequiredException:
    ErrorCode: user_null
    Microsoft.Identity.Client.MsalUiRequiredException: No account or login hint was passed to the AcquireTokenSilent call.
    at Microsoft.Identity.Client.AcquireTokenSilentParameterBuilder.Validate()
    at Microsoft.Identity.Client.AbstractAcquireTokenParameterBuilder`1.ValidateAndCalculateApiId()
    at Microsoft.Identity.Client.AbstractClientAppBaseAcquireTokenParameterBuilder`1.ExecuteAsync(CancellationToken cancellationToken)
    at Microsoft.Identity.Client.AbstractAcquireTokenParameterBuilder`1.ExecuteAsync()
    at Microsoft.Identity.Web.TokenAcquisition.GetAccessTokenForWebAppWithAccountFromCacheAsync(IConfidentialClientApplication application, IAccount account, IEnumerable`1 scopes, String authority, String userFlow)
    at Microsoft.Identity.Web.TokenAcquisition.GetAccessTokenForWebAppWithAccountFromCacheAsync(IConfidentialClientApplication application, ClaimsPrincipal claimsPrincipal, IEnumerable`1 scopes, String authority, String userFlow)
    at Microsoft.Identity.Web.TokenAcquisition.GetAccessTokenForUserAsync(IEnumerable`1 scopes, String tenant, String userFlow, ClaimsPrincipal user)
    StatusCode: 0

  7. It is possible that in the image ‘appregistrationsuiapi_09.png’ the incorrect localhost url is displayed?

    I think it should be: ‘https://localhost:44377/’

    1. HI Wout, yes the redirect_url must match the APP. I must have changed this after.

      Greetings Damien

  8. […] The Azure App Registration and the downstream API App Registration is configured as shown in this post. […]

Leave a Reply to The Morning Brew - Chris Alcock » The Morning Brew #3004 Cancel reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.

%d bloggers like this: