Today, we are going to learn how to create a secure connection in Blazor using HttpClient with authentication to gain access to the protected resources on the Web API’s side. Everything is based on IdentityServer. Until now, we secure Blazor WebAssembly With IdentityServer4 and enabled login and logout actions. After successful login, IdentityServer sends us the id_token
and the access_token
. But we are not using that access_token
yet.
So, in this article, we are going to change that. But, using the access token with Blazor WebAssembly is not going to be our only topic. We are going to learn how to configure our HTTP client to send unauthorized requests as well.
The full source code of this project is on GitHub.
More article about Blazor WebAssembly
- Getting Started With C# And Blazor
- Setting Up A Blazor WebAssembly Application
- Working With Blazor’s Component Model
- Secure Blazor WebAssembly With IdentityServer4
Table of contents
- More article about Blazor WebAssembly
- Web API with IdentityServer4 Configuration Overview
- Fetching Data from Blazor WebAssembly
- Accessing Protected Resources by Using Access Token with Blazor WebAssembly Http Client
- Different Approach to Using Access Token with Blazor WebAssembly
- Sending Unauthorized HTTP Requests from Blazor WebAssembly
Web API with IdentityServer4 Configuration Overview
If you take a look at our source code repo, you will find a prepared Web API project. So logically, the initial setup between these two is already in place. That said, let’s just do a quick overview.
Let’s open the Startup
class in the Web API project and inspect the ConfigureServices
class. There, you will find the authentication configuration:
services.AddAuthentication("Bearer")
.AddJwtBearer("Bearer", opt =>
{
opt.RequireHttpsMetadata = false;
opt.Authority = "https://localhost:5005";
opt.Audience = "companyApi";
});
As you can see, we populate the Authority
property with the URI address of our IDP, and also set up the Audience
to the companyApi
. For this, we are using the Microsoft.AspNetCore.Authentication.JwtBearer
library. Also in the Configure
method we call the app.UseAuthentication
method to add the authentication middleware to the request pipeline.
Next, if we inspect the Identity Server configuration inside the InMemoryConfig
class:
public static IEnumerable<ApiScope> GetApiScopes() =>
new List<ApiScope> { new ApiScope("companyApi", "CompanyEmployee API") };
public static IEnumerable<ApiResource> GetApiResources() =>
new List<ApiResource>
{
new ApiResource("companyApi", "CompanyEmployee API")
{
Scopes = { "companyApi" }
}
};
We can see the configuration for the Web API scopes and resources using the Web API’s Audience
value.
Finally, if we inspect the CompaniesController
in the Web API, we are going to find the [Authorize]
attribute on top of the Get
action. We use this attribute to protect our resources from unauthorized calls. Yes, it is commented out, and for now, we are going to leave it as-is.
So, the communication between the Identity Server and Web API is prepared and ready. Of course, you can read the mentioned OIDC series to learn more about this process.
Fetching Data from Blazor WebAssembly
Before we start using the access token with Blazor WebAssembly, we have to modify the FetchData page to show the data from the protected resource.
First, let’s add a new FetchData.razor.cs
file in the Pages
folder and modify it:
public partial class FetchData
{
[Inject]
public HttpClient Http { get; set; }
private CompanyDto[] _companies;
protected override async Task OnInitializedAsync()
{
_companies = await Http.GetFromJsonAsync<CompanyDto[]>("companies");
}
}
public class CompanyDto
{
public Guid Id { get; set; }
public string Name { get; set; }
public string FullAddress { get; set; }
}
So, nothing special here. We just use the HttpClient property to fetch the data from the Web API’s GetCompanies
endpoint. Also, you can see a helper CompanyDto
class that we use for the data deserialization. We create it in the same file for the sake of simplicity, but of course, you can extract it in another folder or shared project.
Now, we have to modify the FetchData.razor
file:
@page "/fetchdata"
<h1>Companies</h1>
<p>This component demonstrates fetching data from the server.</p>
@if (_companies == null)
{
<p><em>Loading...</em></p>
}
else
{
<table class="table">
<thead>
<tr>
<th>Id</th>
<th>Name</th>
<th>Address (F)</th>
</tr>
</thead>
<tbody>
@foreach (var company in _companies)
{
<tr>
<td>@company.Id</td>
<td>@company.Name</td>
<td>@company.FullAddress</td>
</tr>
}
</tbody>
</table>
}
As you can see, if we don’t have the _companies
array populated, we show the Loading...
message. Otherwise, we just create a table and display all the companies from the array.
Finally, we have to modify the HttpClient
configuration in the Program.cs
class:
builder.Services.AddScoped(sp => new HttpClient {
BaseAddress = new Uri("https://localhost:5001/api/") });
Accessing Protected Resources by Using Access Token with Blazor WebAssembly Http Client
Before we move on, we have to protect our API resource. To do that, let’s just uncomment the [Authorize]
attribute:
[Authorize]
[HttpGet]
public IActionResult GetCompanies()
Next, we have to modify the Identity Server Client configuration by adding the required API scope in a list of scopes for the blazorWASM
client:
new Client
{
ClientId = "blazorWASM",
AllowedGrantTypes = GrantTypes.Code,
RequirePkce = true,
RequireClientSecret = false,
AllowedCorsOrigins = { "https://localhost:5020" },
AllowedScopes =
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
"companyApi"
},
RedirectUris = { "https://localhost:5020/authentication/login-callback" },
PostLogoutRedirectUris = { "https://localhost:5020/authentication/logout-callback" }
}
Since we have this configuration in the database, we have to remove the existing CompanyEmployeeOAuth
database from the SQL Server. As soon as we start our IDP app, the database will be recreated.
Now, let’s move on to the client app, and install the Microsoft.Extensions.Http
library:
Install-Package Microsoft.Extensions.Http -Version 3.1.8
After the installation, we are going to modify the Program.cs
class:
public class Program
{
public static async Task Main(string[] args)
{
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("app");
builder.Services.AddHttpClient("companiesAPI", cl =>
{
cl.BaseAddress = new Uri("https://localhost:5001/api/");
})
.AddHttpMessageHandler(sp =>
{
var handler = sp.GetService<AuthorizationMessageHandler>()
.ConfigureHandler(
authorizedUrls: new[] { "https://localhost:5001" },
scopes: new[] { "companyApi" }
);
return handler;
});
builder.Services.AddScoped(
sp => sp.GetService<IHttpClientFactory>().CreateClient("companiesAPI"));
builder.Services.AddOidcAuthentication(options =>
{
builder.Configuration.Bind("oidc", options.ProviderOptions);
});
await builder.Build().RunAsync();
}
}
We use the AddHttpClient
method, that resides in the Microsoft.Extensions.Http
library, to add the IHttpClientFactory
to the service collection and configure a named HttpClient
. As you can see, we provide the base address of our API. Then, we call the AddHttpMessageHandler
method to attach access tokens to the outgoing HTTP requests. With this handler, we configure the API’s base address and the scope, which must be the same as the one in the IDP configuration.
After we configure our handler, we register the default HttpClient as a scoped instance in the IoC container with the companiesAPI
name.
Next, we have to provide the scope to the Oidc configuration in the appsettings.json
file:
{
"oidc": {
"Authority": "https://localhost:5005/",
"ClientId": "blazorWASM",
"ResponseType": "code",
"DefaultScopes": [
"openid",
"profile",
"companyApi"
],
"PostLogoutRedirectUri": "authentication/logout-callback",
"RedirectUri": "authentication/login-callback"
}
}
Finally, let’s protect our FetchData
component from unauthorized users:
@page "/fetchdata"
@attribute [Authorize]
For this, we require a using directive, which we can add in the _Imports.razor
file:
@using Microsoft.AspNetCore.Authorization
Now, we can start all the applications. If we try to navigate to the FetchData
page, we are going to be redirected to the Login screen. There, once we enter valid credentials, the application will navigate us back to the FetchData
page and we are going to see our data. So, we have successfully used the access token with the Blazor WebAssembly HttpClient.
To prove this, we can do two things.
As you can see the validation was successful.
Also, we can place a breakpoint in our GetCompanies action and inspect the token:
We can see all the properties from our token sent to the Web Api with the HTTP request.
Different Approach to Using Access Token with Blazor WebAssembly
Right now, we have our access token included inside the HTTP request, but all of our logic is in the Program.cs
class. We don’t want to say this is bad, but with more services to register, this class will become overpopulated and hard to read for sure. To avoid that, we can extract the AuthorizationMessageHandler
configuration to a custom class.
So, let’s create a new MessageHandler
folder and a new CustomAuthorizationMessageHandler
class under it:
public class CustomAuthorizationMessageHandler : AuthorizationMessageHandler
{
public CustomAuthorizationMessageHandler(IAccessTokenProvider provider, NavigationManager navigation)
: base(provider, navigation)
{
ConfigureHandler(
authorizedUrls: new[] { "https://localhost:5001" },
scopes: new[] { "companyApi" });
}
}
Our new class must inherit from the AuthorizationMessageHanlder
class and implement a constructor with two parameters. Then, inside the constructor, we just call the ConfigureHanlder
method to do exactly that – configure message handler.
After that, we can return to the Program.cs
class and modify it:
public static async Task Main(string[] args)
{
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("app");
builder.Services.AddScoped<CustomAuthorizationMessageHandler>();
builder.Services.AddHttpClient("companiesAPI", cl =>
{
cl.BaseAddress = new Uri("https://localhost:5001/api/");
})
.AddHttpMessageHandler<CustomAuthorizationMessageHandler>();
...
}
As you can see, we register the CustomAuthorizationMessageHandler
class as a service and then use it with the AddHttpMessageHandler
method.
Now, if we try to log in and navigate to the FetchData page, we are going to be able to see our data. So, the result is the same, but we have a different implementation.
Sending Unauthorized HTTP Requests from Blazor WebAssembly
With this configuration in place, we have to attach the access token with each HTTP request. But in every application, we can find endpoints that are not protected and we should be able to access these resources without logging in.
That said, let’s check the behavior of our application now.
The first thing we are going to do is to add another endpoint in the CompaniesController
:
[HttpGet("unauthorized")]
public IActionResult UnauthorizedTestAction() =>
Ok(new { Message = "Access is allowed for unauthorized users" });
Now, on the client app, we are going to add a new Index.razor.cs
file under the Pages
folder:
public partial class Index
{
[Inject]
public HttpClient Http { get; set; }
private string _message;
protected override async Task OnInitializedAsync()
{
var result = await Http.GetFromJsonAsync<UnauthorizedTestDto>("companies/unauthorized");
_message = result.Message;
}
}
public class UnauthorizedTestDto
{
public string Message { get; set; }
}
Here, we are using the already registered HttpClient
to send a request to the UnauthorizedTestAction
.
Finally, let’s slightly modify the Index.razor
file:
@page "/"
<h1>Hello, world!</h1>
<div class="alert alert-warning" role="alert">
Before authentication will function correctly, you must configure your provider details in <code>Program.cs</code>
</div>
Welcome to your new app.
<SurveyPrompt Title="How is Blazor working for you?" />
<p>
@_message
</p>
So, what we expect here is to see this test message as soon as our application starts and navigates to the Index page.
Well, let’s try it:
As soon as we start our app, we hit the error. From the error message description, it is obvious what is the problem. We don’t have an access token.
There are two things we want to solve here. First, we don’t want our application to break if we don’t have the access token included in the HTTP request. Second, we want to allow unauthorized HTTP requests in our application.
To solve the first problem, we can modify the Index.razor.cs
file:
public partial class Index
{
[Inject]
public HttpClient Http { get; set; }
private string _message;
protected override async Task OnInitializedAsync()
{
try
{
var result = await Http.GetFromJsonAsync<UnauthorizedTestDto>("companies/unauthorized");
_message = result.Message;
}
catch (AccessTokenNotAvailableException ex )
{
ex.Redirect();
}
}
}
If the access token doesn’t exist, the application will throw the AccessTokenNotAvailableException
, which we can use to redirect to the Login screen.
Now if we test this, as soon as the app starts, it will navigate us to the Login page.
So, this is good, nothing breaks our app but still, we didn’t solve our problem of accessing unauthorized resources.
Additional HTTP Configuration
To solve that problem, we have to register another named HttpClient
in the Program.cs
class:
builder.Services.AddHttpClient("companyAPI.Unauthorized", client =>
client.BaseAddress = new Uri("https://localhost:5001/api/"));
Then, we can modify the Index.razor.cs
class:
public partial class Index
{
[Inject]
public IHttpClientFactory HttpClientFactory { get; set; }
private string _message;
protected override async Task OnInitializedAsync()
{
var client = HttpClientFactory.CreateClient("companyAPI.Unauthorized");
var result = await client.GetFromJsonAsync<UnauthorizedTestDto>("companies/unauthorized");
_message = result.Message;
}
}
Here, we inject the IHttpClientFactory
instead of HttpClient
and use that factory to create our named HttpClient
instance. Then, we are using that instance to send an HTTP request:
Thank you for your article! I reproduced an example of your article. Authentication works. On the client, I can get the username from the AuthenticationStateProvider. But in the controller on the server, I also see all the same Claims in the debugger as shown in the picture (https://i0.wp.com/puresourcecode.com/wp-content/uploads/2021/05/12-Inspecting-Attached-Access-Token-with-Blazor-WebAssembly-HttpClient.png?resize=768%2C396&ssl=1), but they do not have a Claim of the “name” type. In the controller in User.Identity.Name is also null for me too. How do I get the username on the server in the controller?
Hello! Happy the post was useful! You should have the User.Identity.Name in your controller. Remember that with the new browsers, the user token is not allowed to be read and save or share. You have to use BFF for the complete implementation with Identity Server (https://docs.duendesoftware.com/identityserver/v5/bff/). In my new post, I created the authentication with Microsoft Identity