Call API Management from Blazor

azure api management service wallpaper

The title ”Call API Management from Blazor” is not explain fully what I’m going to explain in this post but it is only a title. So, consider the following scenario.

Scenario

On Azure API Management Service you have your APIs. For more protection, you want to add another level of security asking to the API Management to validate the user token for each request. The token is validated again your own Identity Server.

Once the API Management is configured to use Identity Server for the validation, you want to call the APIs from a Blazor WebAssembly application. So, the first issue you will face is how to read the user token with Blazor and then add the Authorization to the HttpClient request.

Now, I must confess I spent almost 2 weeks to find a solution to all of this. I hope this post could be useful for someone else.

Before starting to read this post, I recommend to read my other posts about the API Management Service:

Configure OpenID Connect

So, first step is to configure the OpenID connect on the API Management Service on Azure. For that, go to the resource and on the menu on the left, select OAuth 2.0 + OpenID Connect.

Add OpenID to the API Management Service - Call API Management from Blazor
Add OpenID to the API Management Service

Then, at the top click on Add to add a new configuration.Now, type the Display name and the Name you want to use, the Description and the Metadata endpoint URL. If you are using the Identity Server and the Skoruba Admin UI, click on Discovery Document to obtain the URL.

Skoruba Admin UI - Discovery Document - Call API Management from Blazor
Skoruba Admin UI – Discovery Document

Then, Client ID and Client Secret are required. Again, you can use Skoruba Admin UI to create them. The configuration is quite simple:

  • Require Client Secret
  • Allow Offline Access
  • Allow Access Token Via Browser
  • Allowed scopes:
    • openid
    • profile
    • roles
    • email
  • Allowed Grant types
    • client_credentials
    • implicit
    • authorization_code
    • password
    • hybrid

Then, set a Client Secret. If you use the code to configure the IdentityServer use the following:

new Client
{
    ClientId = "...",
    ClientName = "...",
    AllowedGrantTypes = GrantTypes.HybridAndClientCredentials,

    ClientSecrets =
    {
        new Secret("secret".Sha256())
    },

    RedirectUris = { "https://local:44352/signin-oidc" },
    PostLogoutRedirectUris = { "https://local:44352/signout-callback-oidc" },

    AllowedScopes =
    {
        IdentityServerConstants.StandardScopes.OpenId,
        IdentityServerConstants.StandardScopes.Profile
    },
    AllowOfflineAccess = true,
    AllowAccessTokensViaBrowser = true,
}

Now, that this configuration is done, we have to add the IdentityServer to the API.

Configure the Security of the APIs

So, I assume that you have already created the APIs in the API Management Service. Now, click on Settings of the API and scroll down in the section.

Security in the API Management Service - Call API Management from Blazor
Security in the API Management Service

Here, select OpenID connect and select from the dropdown list your Identity Server. Then, press the Save button.

Configure the Design of the APIs

Next step is to validate the request. For that, we want to be sure that the request contains a valid JWT token. The token is generate from the Identity Server for each session and user when the user logs in the application. When the application calls the API, the HttpClient has to include in the header this user token.

So, I have to configure the Inbound processing to validate the JWT token and only if it is valid, the API Manager proceeds with the request. In the other case, the API Manager returns an HTTP 401. Now, click on All operations and then open the API design.

API Management Service - API Design
API Management Service – API Design

Now, Add policy and I have the following screen.

Add inbound policy
Add inbound policy

Select Validate JWT. Now, the configuration is:

  • Validate by: Header
  • Header name: Authorization
  • Failed validation HTTP code: 401 – Unauthorized
  • Open ID Urls: https://youridsrv/.well-known/openid-configuration
Validate JWT
Validate JWT

Also, as a reminder, you have to configure the CORS. To add a bit more security to the output of your API, I like to set some parameters (see my other post about it). Then, the Policies of the API looks like the following configuration:

<policies>
    <inbound>
        <validate-jwt header-name="Authorization" failed-validation-httpcode="401" 
            require-scheme="Bearer" output-token-variable-name="jwt">
            <openid-config url="https://yourids/.well-known/openid-configuration" />
        </validate-jwt>
        <cors>
            <allowed-origins>
                <origin>*</origin>
            </allowed-origins>
            <allowed-methods preflight-result-max-age="300">
                <method>GET</method>
                <method>POST</method>
            </allowed-methods>
            <allowed-headers>
                <header>*</header>
            </allowed-headers>
            <expose-headers>
                <header>*</header>
            </expose-headers>
        </cors>
        <base />
    </inbound>
    <backend>
        <base />
    </backend>
    <outbound>
        <base />
        <set-header name="Strict-Transport-Security" exists-action="override">
            <value>max-age=31536000</value>
        </set-header>
        <set-header name="X-XSS-Protection" exists-action="override">
            <value>1; mode=block</value>
        </set-header>
        <set-header name="Content-Security-Policy" exists-action="override">
            <value>script-src 'self'</value>
        </set-header>
        <set-header name="X-Frame-Options" exists-action="override">
            <value>deny</value>
        </set-header>
        <set-header name="X-Content-Type-Options" exists-action="override">
            <value>nosniff</value>
        </set-header>
        <set-header name="Expect-Ct" exists-action="override">
            <value>max-age=604800,enforce</value>
        </set-header>
        <set-header name="Cache-Control" exists-action="override">
            <value>none</value>
        </set-header>
        <set-header name="X-Powered-By" exists-action="delete" />
        <set-header name="X-AspNet-Version" exists-action="delete" />
    </outbound>
    <on-error>
        <base />
    </on-error>
</policies>

Redirect all HTTP request to HTTPS

So, Microsoft says that currently API Management Service doesn’t support HSTS header.

You can configure each API to listen on HTTP, HTTPS or both but this does not support redirection, if you configure an API to only listen on https and sends http request you will get 404 from API Management.

However, you can add an input policy which can redirect all HTTP calls to HTTPS. This is the most recommended approach.

<inbound>
    <choose>
        <when condition="@(context.Request.OriginalUrl.Scheme.Equals("http"))">
            <return-response>
                <set-status code="302" reason="Requires SSL" />
                <set-header exists-action="override" name="Location">
                    <value>
                        @("https://" + context.Request.OriginalUrl.Host + context.Request.OriginalUrl.Path)
                    </value>
                </set-header>
            </return-response>
        </when>
    </choose>
</inbound>

Configure Blazor

Now, it is time to add some configuration to the Blazor project. The main problem in Blazor is how to have access to the user token. If you google, you find a lot of solutions and most of them are quite complicated. Fortunately, Microsoft gives us, recently, a quite simple way. In the API Service, I have to inject IAccessTokenProvider that allows me to read the token.

First, the namespaces we have to use. Thanks to NET6, I can create a GlobalUsing.cs to add all the packages I want to use across the project.

global using Microsoft.AspNetCore.Authorization;
global using Microsoft.AspNetCore.Components.Web;
global using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
global using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
global using Microsoft.Extensions.Configuration;

Appsettings.json

Now, I want to save the settings for the API URL and the HttpClient settings in the appsettings.json. So, my settings is like that

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "oidc": {
    "Authority": "https://your-identity-server-url/",
    "ClientId": "220005UI",
    "ResponseType": "code",
    "DefaultScopes": [
      "openid",
      "profile",
      "roles",
      "email",
      "offline_access",
      "220005_api"
    ],
    "PostLogoutRedirectUri": "authentication/logout-callback",
    "RedirectUri": "authentication/login-callback"
  },
  "Api": {
    "EndpointsUrl": "https://api.psc.com/5/v1/",
    "Scope": "my_api"
  },
  "ApplicationSettings": {
    "AuthorizedUrls": [
      "https://localhost:7241"
    ],
    "Scopes": [
      "my_api"
    ],
    "SubscriptionKey": "3251"
  }
}

The oidc configuration is for the connection with Identity Server as required for the AddOidcAuthentication. To recognise the user’s roles, I’m using my package PSC.Blazor.AuthExtensions.

To read the configuration for the application, this is model called ApplicationSettingsModel

public class ApplicationSettingsModel
{
	public Applicationsettings ApplicationSettings { get; set; }
	public string SubscriptionKey { get; set; }
}

public class Applicationsettings
{
	public string[] AuthorizedUrls { get; set; }
	public string[] Scopes { get; set; }
}

Program.cs

Now, I can change the Program.cs to read the configuration and set up the HttpClient correctly.

var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app");
builder.RootComponents.Add<HeadOutlet>("head::after");

#region Read configuration

string apiEndpoint = builder.Configuration["Api:EndpointsUrl"];
string apiScope = builder.Configuration["Api:Scope"];

ApplicationSettingsModel settings = new ApplicationSettingsModel();
builder.Configuration.Bind(settings);

#endregion
#region Dependecy injection
builder.Services.AddTransient(_ =>
{
	return builder.Configuration.GetSection("ApplicationSettings").Get<ApplicationSettingsModel>();
});

builder.Services.AddScoped<APIService>();

#endregion
#region Configure HTTP Client

builder.Services.AddHttpClient("myAPI", cl =>
	{
		cl.BaseAddress = new Uri(apiEndpoint);
	})
	.AddHttpMessageHandler(sp =>
	{
		var handler = sp.GetService<AuthorizationMessageHandler>()
		.ConfigureHandler(
			authorizedUrls: settings.ApplicationSettings.AuthorizedUrls,
			scopes: settings.ApplicationSettings.Scopes
			);
		return handler;
	});
builder.Services.AddScoped(sp => sp.GetService<IHttpClientFactory>().CreateClient("myAPI"));

#endregion
#region Configure Authentication and Authorization

builder.Services.AddOidcAuthentication(options =>
{
	builder.Configuration.Bind("oidc", options.ProviderOptions);
	options.UserOptions.RoleClaim = "role";
})
.AddAccountClaimsPrincipalFactory<MultipleRoleClaimsPrincipalFactory<RemoteUserAccount>>();

builder.Services.AddAuthorizationCore();

#endregion

await builder.Build().RunAsync();

The configuration of the HttpClient uses AddHttpMessageHandler that coming from the Microsoft Authorization namespace.

Create the API service with authentication

Finally, the API Service implementation. Here, in the constructor we need the IAccessTokenProvider to obtain the user’s token. Also, I inject the ApplicationSettingsModel to have the configuration I need.

public class APIService
{
	private readonly HttpClient _httpClient;
	private readonly IAccessTokenProvider _accessToken;
	private readonly JsonSerializerOptions _options;

	public APIService(HttpClient httpClient, IAccessTokenProvider accessToken,
		ApplicationSettingsModel settings)
	{
		_httpClient = httpClient;
		_httpClient.DefaultRequestHeaders.Add("Cache-Control", "no-cache");
		_httpClient.DefaultRequestHeaders.Add("Apim-Subscription-Key", settings.SubscriptionKey);

		_options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };

		_accessToken = accessToken;
	}

	public async Task<APIResponse> GetAttributeAsync(APIRequest apirequest)
	{
		try
		{
			var tokenResult = await _accessToken.RequestAccessToken();
			tokenResult.TryGetToken(out var token);

			HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, $"yourAPI");
			request.Headers.Authorization = 
               new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token.Value);

			var content = 
               new StringContent(JsonSerializer.Serialize(apirequest), Encoding.UTF8, "application/json");
			request.Content = content;

			HttpResponseMessage responseMessage;
			responseMessage = await _httpClient.SendAsync(request);
			responseMessage.EnsureSuccessStatusCode();

			if (responseMessage.IsSuccessStatusCode)
			{
				var responseContent = await responseMessage.Content.ReadAsStringAsync();
#if DEBUG
				Console.WriteLine("[GetAttributeAsync] API Response: " + responseContent);
#endif
				return JsonSerializer.Deserialize<APIResponse>(responseContent, _options);
			}
			else
				return new APIResponse() { Success = false };
		}
		catch (Exception ex)
		{
			return new APIResponse() { Success = false };
		}
	}
}

The important part is how I read the token. The code is here

var tokenResult = await _accessToken.RequestAccessToken();
tokenResult.TryGetToken(out var token);

Using the instance of IAccessTokenProvider, I can read the user’s token and pass it in the header of the HttpClient.

So, now we can test the solution. It works in my end! 😁

Wrap up

In conclusion, in this post I showed how to call an Azure API Management protected with Identity Server from Blazor. I sent a lot of time to sort out how to do it. I hope this code it is useful to someone else!

Happy coding!

Leave a Reply

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