In this article, we are going to show you how to implement a refresh token with Blazor WebAssembly and ASP.NET Core Web API. We are going to change our solution from the previous articles, on both API and Blazor sides, to support refresh token actions.
With step-by-step explanations and modifications, we are going to have a fully functional solution that silently refreshes our access token before it expires.
If you have followed our entire Blazor series, then you have the starting code for this article. If not, you can visit the previous article and download the source code as a starting point for this project.
For the complete navigation for this series, you can visit the Blazor Series page.
So, let’s get started.
User Class Creation, Class Modifications, and New Migration
Because we want to add a new refresh token functionality for our users, we have to extend the AspNetUsers
table. To do that, we are going to modify our Web API project and create a new User
class in the Context
folder:
public class User : IdentityUser { public string RefreshToken { get; set; } public DateTime RefreshTokenExpiryTime { get; set; } }
For more information regarding this User class and extending the tables from ASP.NET Core Identity, you can read our article that covers this topic in great detail.
After that, we have to modify the ProductContext
class:
public class ProductContext : IdentityDbContext<User> { public ProductContext(DbContextOptions options) :base(options) { } ... }
As you can see, our context class doesn’t inherit from the IdentityDbContext<IdentityUser>
class anymore, but from the IdentityDbContext<User>
class.
Also, we have to modify the Identity
registration inside the Startup
class:
services.AddIdentity<User, IdentityRole>() .AddEntityFrameworkStores<ProductContext>();
Or in .NET 6 and above:
builder.Services.AddIdentity<User, IdentityRole>() .AddEntityFrameworkStores<ProductContext>();
Here as well, we are not using the IdentityUser
class anymore but the User
class instead.
Then, we have to modify our AccountsController
class. Basically, we have to replace all IdentityUser occurrences with the User class. The easiest way to do that is to press CTRL+H and enter the required terms for the replace action:
We should replace four IdentityUser occurrences in this document.
Finally, to reflect these changes in our database, we are going to create and execute migration:
Add-Migration AdditionalUserFiledsForRefreshToken
Update-Database
As soon as we do that, our AspNetUsers table will have two additional columns:
Token Service Implementation
Now, we came to the point where we need to implement a refresh token logic. For that, we are going to create a separate service and use it inside the Accounts controller. But, let’s go step by step.
First, we are going to create a TokenHelpers
folder and inside a new ITokenService
interface:
public interface ITokenService { SigningCredentials GetSigningCredentials(); Task<List<Claim>> GetClaims(User user); JwtSecurityToken GenerateTokenOptions(SigningCredentials signingCredentials, List<Claim> claims); string GenerateRefreshToken(); ClaimsPrincipal GetPrincipalFromExpiredToken(string token); }
The first three members are the methods that we already have in the AccountsController
– we are going to move them in a bit. The other two are the new ones.
So, let’s create a new TokenService
class in the same folder.
And let’s register this service in the Startup
class:
services.AddScoped<ITokenService, TokenService>();
Or in .NET 6 and above in the Program
class:
builder.Services.AddScoped<ITokenService, TokenService>();
Before we add implementation to the TokenService
class, let’s go to the AccountsController
, cut the GetSigningCredentials
, GetClaims
, and GenerateTokenOptions
methods, and paste them in the TokenService
class. Also, we can move the IConfiguration
and IConfiguratoinSection
variables from the AccountsController
to this class.
Lastly, we can inject the ITokenService
inside the AccountsController
, and fix the errors in the Login
action:
[Route("api/accounts")] [ApiController] public class AccountsController : ControllerBase { private readonly UserManager<User> _userManager; private readonly ITokenService _tokenService; public AccountsController(UserManager<User> userManager, ITokenService tokenService) { _userManager = userManager; _tokenService = tokenService; } [HttpPost("Registration")] public async Task<IActionResult> RegisterUser([FromBody] UserForRegistrationDto userForRegistration) { ... } [HttpPost("Login")] public async Task<IActionResult> Login([FromBody] UserForAuthenticationDto userForAuthentication) { var user = await _userManager.FindByNameAsync(userForAuthentication.Email); if (user == null || !await _userManager.CheckPasswordAsync(user, userForAuthentication.Password)) return Unauthorized(new AuthResponseDto { ErrorMessage = "Invalid Authentication" }); var signingCredentials = _tokenService.GetSigningCredentials(); var claims = await _tokenService.GetClaims(user); var tokenOptions = _tokenService.GenerateTokenOptions(signingCredentials, claims); var token = new JwtSecurityTokenHandler().WriteToken(tokenOptions); return Ok(new AuthResponseDto { IsAuthSuccessful = true, Token = token }); } }
As we already have some implementation in the TokenService
class, we have to add two more methods:
public class TokenService : ITokenService { private readonly IConfiguration _configuration; private readonly IConfigurationSection _jwtSettings; private readonly UserManager<User> _userManager; public TokenService(IConfiguration configuration, UserManager<User> userManager) { _configuration = configuration; _jwtSettings = _configuration.GetSection("JwtSettings"); _userManager = userManager; } public SigningCredentials GetSigningCredentials() { var key = Encoding.UTF8.GetBytes(_jwtSettings["securityKey"]); var secret = new SymmetricSecurityKey(key); return new SigningCredentials(secret, SecurityAlgorithms.HmacSha256); } public async Task<List<Claim>> GetClaims(User user) { var claims = new List<Claim> { new Claim(ClaimTypes.Name, user.Email) }; var roles = await _userManager.GetRolesAsync(user); foreach (var role in roles) { claims.Add(new Claim(ClaimTypes.Role, role)); } return claims; } public JwtSecurityToken GenerateTokenOptions(SigningCredentials signingCredentials, List<Claim> claims) { var tokenOptions = new JwtSecurityToken( issuer: _jwtSettings["validIssuer"], audience: _jwtSettings["validAudience"], claims: claims, expires: DateTime.Now.AddMinutes(Convert.ToDouble(_jwtSettings["expiryInMinutes"])), signingCredentials: signingCredentials); return tokenOptions; } public string GenerateRefreshToken() { var randomNumber = new byte[32]; using (var rng = RandomNumberGenerator.Create()) { rng.GetBytes(randomNumber); return Convert.ToBase64String(randomNumber); } } public ClaimsPrincipal GetPrincipalFromExpiredToken(string token) { var tokenValidationParameters = new TokenValidationParameters { ValidateAudience = true, ValidateIssuer = true, ValidateIssuerSigningKey = true, IssuerSigningKey = new SymmetricSecurityKey( Encoding.UTF8.GetBytes(_jwtSettings["securityKey"])), ValidateLifetime = false, ValidIssuer = _jwtSettings["validIssuer"], ValidAudience = _jwtSettings["validAudience"], }; var tokenHandler = new JwtSecurityTokenHandler(); SecurityToken securityToken; var principal = tokenHandler.ValidateToken(token, tokenValidationParameters, out securityToken); var jwtSecurityToken = securityToken as JwtSecurityToken; if (jwtSecurityToken == null || !jwtSecurityToken.Header.Alg.Equals(SecurityAlgorithms.HmacSha256, StringComparison.InvariantCultureIgnoreCase)) { throw new SecurityTokenException("Invalid token"); } return principal; } }
GenerateRefreshToken()
contains the logic to generate the refresh token. We use the RandomNumberGenerator
class to generate a cryptographic random number for this purpose.
We also use the GetPrincipalFromExpiredToken()
method to get the user principal from the expired access token. We make use of the ValidateToken()
method of JwtSecurityTokenHandler
class for this purpose. This method validates the token and returns the ClaimsPrincipal
object. Other methods in this class are the same as they were in the AccountsController
.
Login Action Update to Support Refresh Token Flow
With the TokenService in place, we can modify our Login action to create a refresh token and its expiration period for newly logged-in users.
Just before we do that, let’s modify the AuthResponseDto
class (Entities/DTO folder) to support a refresh token in the response to the client :
public class AuthResponseDto { public bool IsAuthSuccessful { get; set; } public string ErrorMessage { get; set; } public string Token { get; set; } public string RefreshToken { get; set; } }
Now, we can modify the Login action:
[HttpPost("Login")] public async Task<IActionResult> Login([FromBody] UserForAuthenticationDto userForAuthentication) { var user = await _userManager.FindByNameAsync(userForAuthentication.Email); if (user == null || !await _userManager.CheckPasswordAsync(user, userForAuthentication.Password)) return Unauthorized(new AuthResponseDto { ErrorMessage = "Invalid Authentication" }); var signingCredentials = _tokenService.GetSigningCredentials(); var claims = await _tokenService.GetClaims(user); var tokenOptions = _tokenService.GenerateTokenOptions(signingCredentials, claims); var token = new JwtSecurityTokenHandler().WriteToken(tokenOptions); user.RefreshToken = _tokenService.GenerateRefreshToken(); user.RefreshTokenExpiryTime = DateTime.Now.AddDays(7); await _userManager.UpdateAsync(user); return Ok(new AuthResponseDto { IsAuthSuccessful = true, Token = token, RefreshToken = user.RefreshToken }); }
So, here we populate additional properties with the refresh token and its expiration period, and then just update the logged-in user.
That’s all regarding the AccountsController
.
Implementing Refresh Token Action in the TokenController
Let’s continue by creating a new RefreshTokenDto
class in the Entities/DTO
folder:
public class RefreshTokenDto { public string Token { get; set; } public string RefreshToken { get; set; } }
When the client application sends the refresh token request, it must provide a request body with the access and refresh tokens. So, with this class, we are going to accept that request body.
Now, let’s create a new TokenController
in the Controller
folder and modify it:
[Route("api/token")] [ApiController] public class TokenController : ControllerBase { private readonly UserManager<User> _userManager; private readonly ITokenService _tokenService; public TokenController(UserManager<User> userManager, ITokenService tokenService) { _userManager = userManager; _tokenService = tokenService; } [HttpPost] [Route("refresh")] public async Task<IActionResult> Refresh([FromBody]RefreshTokenDto tokenDto) { if (tokenDto is null) { return BadRequest(new AuthResponseDto { IsAuthSuccessful = false, ErrorMessage = "Invalid client request" }); } var principal = _tokenService.GetPrincipalFromExpiredToken(tokenDto.Token); var username = principal.Identity.Name; var user = await _userManager.FindByEmailAsync(username); if (user == null || user.RefreshToken != tokenDto.RefreshToken || user.RefreshTokenExpiryTime <= DateTime.Now) return BadRequest(new AuthResponseDto { IsAuthSuccessful = false, ErrorMessage = "Invalid client request" }); var signingCredentials = _tokenService.GetSigningCredentials(); var claims = await _tokenService.GetClaims(user); var tokenOptions = _tokenService.GenerateTokenOptions(signingCredentials, claims); var token = new JwtSecurityTokenHandler().WriteToken(tokenOptions); user.RefreshToken = _tokenService.GenerateRefreshToken(); await _userManager.UpdateAsync(user); return Ok(new AuthResponseDto { Token = token, RefreshToken = user.RefreshToken, IsAuthSuccessful = true }); } }
In the Refresh action, we check if the model is valid and if it is not, we return a BadRequest. Then, we extract the principal from the expired token and use the Identity.Name
property, which is the email of the user, to fetch that user from the database. If the user doesn’t exist, or the refresh tokens are not equal, or the refresh token has expired, we return BadRequest. Otherwise, we use the methods from TokenService
to create access and refresh tokens and update the user in the database. Finally, we return a response with the Token and RefreshToken.
That’s it.
Let’s move on to the BlazorWebassembly project.
Refresh Token Implementation with Blazor WebAssembly
After we are done with the server-side implementation, we are going to continue with the client-side.
Now, once we log in, we are not getting only the access token from the Web API but also the refresh token. Due to that, we have to store both tokens in the storage and also remove both of them during the logout action. To do that, we have to modify the Login
method in the AuthenticationService
class:
public async Task<AuthResponseDto> Login(UserForAuthenticationDto userForAuthentication) { var content = JsonSerializer.Serialize(userForAuthentication); var bodyContent = new StringContent(content, Encoding.UTF8, "application/json"); var authResult = await _client.PostAsync("accounts/login", bodyContent); var authContent = await authResult.Content.ReadAsStringAsync(); var result = JsonSerializer.Deserialize<AuthResponseDto>(authContent, _options); if (!authResult.IsSuccessStatusCode) return result; await _localStorage.SetItemAsync("authToken", result.Token); await _localStorage.SetItemAsync("refreshToken", result.RefreshToken); ((AuthStateProvider)_authStateProvider).NotifyUserAuthentication(result.Token); _client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("bearer", result.Token); return new AuthResponseDto { IsAuthSuccessful = true }; }
Also, we have to modify the Logout
method from the same class:
public async Task Logout() { await _localStorage.RemoveItemAsync("authToken"); await _localStorage.RemoveItemAsync("refreshToken"); ((AuthStateProvider)_authStateProvider).NotifyUserLogout(); _client.DefaultRequestHeaders.Authorization = null; }
Good.
At this point, we are storing and removing both access and refresh tokens to/from our storage. But, this only works for the Login and Logout actions. What about the refresh action itself?
Well, let’s start working on that.
The first thing we have to do is to modify the IAuthenticationService
interface:
public interface IAuthenticationService { Task<RegistrationResponseDto> RegisterUser(UserForRegistrationDto userForRegistration); Task<AuthResponseDto> Login(UserForAuthenticationDto userForAuthentication); Task Logout(); Task<string> RefreshToken(); }
After that, we have to implement this new member in the AuthenticationService
class:
public async Task<string> RefreshToken() { var token = await _localStorage.GetItemAsync<string>("authToken"); var refreshToken = await _localStorage.GetItemAsync<string>("refreshToken"); var tokenDto = JsonSerializer.Serialize(new RefreshTokenDto { Token = token, RefreshToken = refreshToken }); var bodyContent = new StringContent(tokenDto, Encoding.UTF8, "application/json"); var refreshResult = await _client.PostAsync("token/refresh", bodyContent); var refreshContent = await refreshResult.Content.ReadAsStringAsync(); var result = JsonSerializer.Deserialize<AuthResponseDto>(refreshContent, _options); if (!refreshResult.IsSuccessStatusCode) throw new ApplicationException("Something went wrong during the refresh token action"); await _localStorage.SetItemAsync("authToken", result.Token); await _localStorage.SetItemAsync("refreshToken", result.RefreshToken); _client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("bearer", result.Token); return result.Token; }
Because our Refresh
action on the API level accepts the DTO that contains both access and refresh tokens, we have to provide that from this method. As you can see, we are doing exactly that by fetching the tokens from the storage and placing them in the tokenDto
variable. Then, we just send the request and deserialize the response from the API. If it is not successful, we just throw an Application exception (of course, you can add additional actions here, but this is not the focus of this article). If it’s successful, we store our new tokens in the storage and set the Authorization header for our HTTP Client with a new access token. Finally, we return a new token.
Adding a Service to Check Whether to Refresh Token with Blazor WebAssembly
We are having the RefreshToken
method to send that request to the API. But, we don’t want to do that for every single HTTP request. What we want to do is to check our access token first, and then if it is expired or about to expire, send the refresh request.
To do that, we are going to create a new RefreshTokenService
class in the HttpRepository
folder, and modify it:
public class RefreshTokenService { private readonly AuthenticationStateProvider _authProvider; private readonly IAuthenticationService _authService; public RefreshTokenService(AuthenticationStateProvider authProvider, IAuthenticationService authService) { _authProvider = authProvider; _authService = authService; } public async Task<string> TryRefreshToken() { var authState = await _authProvider.GetAuthenticationStateAsync(); var user = authState.User; var exp = user.FindFirst(c => c.Type.Equals("exp")).Value; var expTime = DateTimeOffset.FromUnixTimeSeconds(Convert.ToInt64(exp)); var timeUTC = DateTime.UtcNow; var diff = expTime - timeUTC; if (diff.TotalMinutes <= 2) return await _authService.RefreshToken(); return string.Empty; } }
Here, we use the AuthenticationStateProvider
object to extract the user’s authentication state with the GetAuthenticationStateAsync
method. From the authState
variable of type AuthenticationState
, we can extract the ClaimsPrincipal
object by using the User
property. Once we have the claims in the user
variable, we extract the expiry claim in the exp
variable and convert it to the DateTimeOffset
type inside the expTime
variable. We also get the current date in the timeUTC
variable. After that, we subtract the current time from the expiration time. If the result in minutes is less than equal to two, we want to execute the RefreshToken
method from the AuthenticationService
and return the result. Otherwise, we just return an empty string.
It is a common practice to refresh a token if it is about to expire, and that’s the reason why we are using the value of two minutes. Of course, you will fit this value to your needs.
Now, before we continue, we are going to register this service in the Program.cs
class:
builder.Services.AddScoped<RefreshTokenService>();
Good.
Let’s move on.
Intercepting HTTP Requests Using HTTP Interceptor
Right now, we have our refresh token logic that will work if we call our TryRefreshToken
method before sending HTTP requests. In this case, we would have to call this method in all the methods from the ProductHttpRepository
class. This should work just fine, but we wan to do something different here.
The thing we want here is to intercept our outgoing HTTP requests and check if we should refresh our token. The advantage of this approach is that we can implement this only in one place and our logic will execute for each request we send to the server.
Let’s see how to do that.
First, we have to install the HttpClientInterceptor
library:
After the installation, we have to register the interceptor service and attach it to the specific client in the Program.cs
class:
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) } .EnableIntercept(sp)); ... builder.Services.AddHttpClientInterceptor(); ... await builder.Build().RunAsync();
With the AddHttpClientInterceptor
method, we are registering the interceptor service in the IOC. Also, we are using the EnableIntercept
method to attach interception functionality for this specific HttpClient
. This means that our interceptor will intercept any HTTP request sent with this HttpClient. It also means if we register another client and don’t attach the interceptor to it, any request from that client won’t be intercepted.
Now, to use this interceptor, we are going to create another class HttpInterceptorService
in the HttpRepository
folder:
public class HttpInterceptorService { private readonly HttpClientInterceptor _interceptor; private readonly RefreshTokenService _refreshTokenService; public HttpInterceptorService(HttpClientInterceptor interceptor, RefreshTokenService refreshTokenService) { _interceptor = interceptor; _refreshTokenService = refreshTokenService; } public void RegisterEvent() => _interceptor.BeforeSendAsync += InterceptBeforeHttpAsync; public async Task InterceptBeforeHttpAsync(object sender, HttpClientInterceptorEventArgs e) { var absPath = e.Request.RequestUri.AbsolutePath; if (!absPath.Contains("token") && !absPath.Contains("accounts")) { var token = await _refreshTokenService.TryRefreshToken(); if(!string.IsNullOrEmpty(token)) { e.Request.Headers.Authorization = new AuthenticationHeaderValue("bearer", token); } } } public void DisposeEvent() => _interceptor.BeforeSendAsync -= InterceptBeforeHttpAsync; }
Let’s Explain the Code
First, we inject our registered HttpClientInterceptor
and RefreshTokenService
services. Then we have three methods.
The HttpClientInterceptor
contains four event handlers that enable us to control when the interception will happen. Two of them are synchronous and two of them are asynchronous (BeforeSend/Async, AfterSend/Async). In this class, we are using asynchronous event handlers.
So, in the RegisterEvent
method, we are registering our event to the BeforeSendAsync
event handler. This means, that before our HTTP request is sent, we are going to intercept it and fire our event.
In the DisposeEvent
method, we are doing exactly that. We remove our event subscription from the event handler.
Finally, in our InterceptBeforeHttpAsync
event, we first check the request URI of the intercepted request. If it contains token
or accounts
, we don’t want to invoke refresh token actions, because that request could already be the one we use for the refresh token or login/logout action. But if it doesn’t contain these words in the URI, we call the TryRefreshToken
method, and if that method returns a refreshed token, we attach it to the current request before we let it move on towards the API.
The last thing we have to do is to register this class as a service in the Program.cs
class:
builder.Services.AddScoped<HttpInterceptorService>();
That’s it.
Using the Interceptor Service
Now, all we have to do is to use this interceptor in the component that we use to fetch the data from the server. It is the Products
component.
So, let’s modify the Products.razor.cs
file:
public partial class Products : IDisposable { public List<Product> ProductList { get; set; } = new List<Product>(); public MetaData MetaData { get; set; } = new MetaData(); private ProductParameters _productParameters = new ProductParameters(); [Inject] public IProductHttpRepository ProductRepo { get; set; } [Inject] public HttpInterceptorService Interceptor { get; set; } protected async override Task OnInitializedAsync() { Interceptor.RegisterEvent(); await GetProducts(); } //All the other methods in this class - Get, Sort, Search, Delete public void Dispose() => Interceptor.DisposeEvent(); }
So, we inject our interceptor service here and as soon as our component initializes, we register our event to the event handler. This means as long as we are on this component, every request will be intercepted and checked for the refresh token action. Also, we have the Dispose
method where we dispose of our event as soon as we navigate away from this component.
Testing Refresh Token Functionality
Before you test this, you should be aware of the expiration period of the token. It is set in the Web API’s appsettings.json
file to five minutes.
Token is Valid
Now, as soon as we log in, we will get our access token with the mentioned expiration period. If we navigate to the Products
menu, the app is going to send an HTTP request to fetch the products from the server. At that moment, our interceptor will intercept the request and call the TryRefreshToken
method, since the URI contains neither token nor accounts word. But because the token is not even close to expiring, the TryRefreshToken
method will return an empty string, and our request will just move forward to the API.
The token is About to Expire
After three minutes we can try navigating to the Products page again, or if we are on the Products page, we can just click another link on the pagination. This is going to trigger another HTTP request to fetch the products from the server. But at that point, our token expiration period is fewer than two minutes. This means the TryRefreshToken
method will call the RefreshToken
method where we send an HTTP request to refresh the token. Our interceptor will intercept that request as well, but since its URI contains the token
word, it will be released towards the API. As soon as we get a new token back, our HttpInterceptorService
will store it in the Authorization
header of a previous request and let it move on towards the API. Also, in the RefreshToken
method, this new token will be stored in the Authorization header for all the other requests from our HttpClient.
Testing with the Expired Token
Finally, if you want to test the functionality with the expired token, you will have to wait for about ten or slightly more minutes. That’s because the server adds additional five minutes to the token expiration when validating the access token sent from the client. But after ten minutes, you will see that you will get a new token back from the server, as long as the refresh token hasn’t expired.
Conclusion
That’s it.
We have learned how to implement refresh token functionality with both Blazor WebAssembly and ASP.NET Core Web API applications. Also, we’ve learned some neat tricks to intercept our requests on the client-side, thus allowing us to make changes only in one place in the app.
So, until another article…
All the best.
Hi, I have an issue with this implementation. I will probably be able to resolve this myself, but wanted to notify people of it (plus any help resolving this is welcome). The method of intercepting requests and then refreshing tokens if needed, can cause issues when multiple requests are launched at once. In the screenshot you can see an occurrence in the console of multiple calls to the RefreshToken() method of the authentication service at once. The order in which the refresh tokens are saved to the database and in which the tokens are stored in local storage seems to be different and then I get an issue in the Refresh() method of the TokenController because user.RefreshToken != tokenDto.RefreshToken .
Although I’ll probably be able to solve this issue myself, any help is welcome or a change in this tutorial to tackle this potential issue.
https://i.imgur.com/ERcOsfX.png
Hello Roger. Thank you for pointing out this issue, but to be honest, we are trying to show the general solution, and the specific implementations are always up to a developer to handle. There is no way for us to cover all those specific cases as every developer has a different project.
For your case, if you think that the interceptor is causing the issue, maybe you can lock it somehow to allow a single request at a time, or on the API level to lock the update action. Or maybe you can use Delegating Handler instead of the interceptor if you think the interceptor is the problem. I am really not sure about the solution, but if you resolve it, it would be great to share the solution with us, or at least the idea of how you solved it.
It is indeed true that everyone has a different project. However, I don’t know what the cause is for simultaneous calls to the refreshtoken, as my application didn’t launch several http calls at once.
Anyway, the solution I used (inspired by a colleague who used the debounce function in typescript for her own implementation of a refresh token for her project) and this article: https://blog.jeremylikness.com/blog/an-easier-blazor-debounce/
I modified the HttpInterceptorService as follows: (if still issues, maybe increase interval of 100 ms to 300 ms or so):
I do have to recommend your series of articles on Blazor webassembly by the way. They have been really helpful.
Thank you very much for the solution and for the kind words. I’m glad you’ve been able to resolve your issue.
Nice tutorials ! Very generous! Learned a lot. Question comes to mind though (not a pro!), though: Since we are not using SignalR, this HttpInterceptor might come in handy, and so might for instance filtering unauthorized request before hitting authorized parts of the Web API, but let’s say in the end we implement SignalR, which I guess is the whole idea – just sending diffs – would u say it’s still valid choice using Interceptors? Save a lot of traffic still? And how about complexity? Let’s say u have a lot of action going on, using Localstorage to get not only tokens, but a lot of other stuff, such as products, related stuff, things can get complicated, right? Any thoughts and best practices? A lot to digest..
Done.
Best Wishes/CK.
Hi Chirsto. First of all, there are situations where you can use SignalR and when you shouldn’t. Even though you can implement SignalR to fetch things from your API, this is not the way you should be doing it. For those actions use HTTP requests. That said, you can authorize your SignalR messages as well, but I think this interceptor has nothing to do with it since it intercepts HTTP requests/responses. SingalR has its own hubs/publishers/subscribers, which are separate from this.
Hi I have a question.
The last part where you are calling the TryRefreshToken method by intercepting http requests. I’m using Blazor server side as the front-end, is it a good idea to have a background task running that calls the TryRefreshToken method every n minute, instead of intercepting http requests?
Hi. Well, not that I tried it, but it can be acceptable in some form. Again, because I didn’t use this that way, I can’t tell if it has some drawbacks or not.
Hi Marinko
I have had a look at IdentityServer4, in order to implement the refresh token, however IdentityServer4 is to extensive and I will have to rewrite my application, which is not possible right now.
I have the jwt authentication working for my application backend and frontend. Its only the refresh token that is missing.
As I see it I have these options:
Are there other options?
Currently, I see no other option. Maybe try with 1 and see how it works.
Has this article been updated to latests version of .net and blazor
Well all is the same. Just small differences in the Web API project which we cover in this article.
Hi
Can I use the procedure in this article if I’m using Blazor server side?
I have two repositories, first asp.net core web api and the second is the Blazor server side which is calling the the first repository. Both are using .NET 6
If you have two server-side applications, it is better to use some sort of token providers like IdentityServer4 or Duende.
Halu, can we just used Delegating Handler in intercepting http request? Thanks…
Yes you can. You can find the comment from Michael, he used it as well
I thinks we return {token,expireAfterMinute} in login action and wasm write the expireAfterMinute (int) in localStorage and compare DateTime.Now with DateTime.Now.AddMinute(expireAfterMinute) , because the local machine dateTime maybe wrong.
Hi Marinko, After completing the video tutorial relating to this blog, I can no longer register a new user because of Cors blocking the register request. The cors policy setup is as per the tutorial. I can login however. What may be causing this?
I am sorry David. But what video tutorial are we talking about? Do you mean our Blazor WASM video course? If that is the case, please visit the source code and compare it to your solution. This shouldn’t be causing any troubles regarding cors. At least it didn’t for this article and for the video course.
Apologies Marinko, Yes the Blazor Wasm course, after implementing the code in the RefreshToken module, I can no longer register a new user due to Cors policy. I’m hosting the server project at abc-api.xyz.domain.com and the client at xyz.domain.com.
Well I am not sure why would refresh logic affect the CORS policy. That’s why I said to compare your code to ours, you have it in the course. Also, pay attention to have the Cors registered in the correct order. I believe we mentioned that in the course or even added a file for that. Finally, you can try to be more specific with Cors, not to use any origin but to specify the origin from your client app.
It’s ok I’ve reverted my code to the code to the lesson prior to Refreshing tokens and all is good. Maybe I got some typo somewhere. I’ll try and find out why?
After delving into the response, the CORS issue persists as a result of a 500 server error thrown in the “Registration” logic which implies the issue is not directly a CORS problem. I noticed that after a request to the account controller’s register method the cors specifics in the response header are missing – obviously because of the 500 server error in the registration logic. But without the “RefreshToken” logic this error disappears.
Hello David. I am really sorry that you face this issue, but I must say that I’ve tested module 14 (the one where we implement the complete registration/login/refresh logic) and everything is working fine. I’ve just tested user registration after the refresh token logic is implemented and I have managed to register a new user, log in with credentials and receive the token and refresh token in response. Basically, I can’t understand why would refresh logic affect the registration part.
Hello Marinko. I downloaded the code from the repo (module15end) changed the bits in appsettings etc to suit my environment. I publish the api project at api.xyz.domain.com and the client at xyz.domain.com, both projects are published to IIS – the application stops working. I take out the code in the refreshtoken lesson and re-publish and the application works again. When the app fails I check the reponse headers and the cors info is missing. Very strange. Maybe I should be looking at the Interceptor.
Hello David. Well, first of all, when I said today that I’ve tested the app, I did it locally, I didn’t publish it, just to be clear there. Now, that said, I am still not sure why would you have that error. Yeah, you can inspect the interceptor, but as you can see, in that interceptor we are ignoring all the requests towards the /token or /account URIs, so it shouldn’t be affecting the registration request at all. Even more, we didn’t register the BeforeSend event for the registration component, so it won’t be fired at all. This is really confusing.
SOLVED: The issue was with the CORS setup in Startup.cs. When publishing across different domains the MS documentation states that you must configure your CORS with headers ie:
.WithHeaders(HeaderNames.ContentType, HeaderNames.Authorization)
Ignore this.
You didn’t resolve the issue?
Correct, but haven’t given up yet 🙂
SOLVED: In the User class I had ‘public string RefreshToken { get; set; }’ so in the DB the RefreshToken field was marked as Not Null. Obviously the registration logic didn’t account for this and hence the 500 error after the register request. So changed the field to nullable in the class, did a migration and and all is good.
Awesome. I’m glad you resolved this.
Hello Marinko,
thanks a lot for your series on Blazor WebAssembly.
I want to ask you a question about Google Authentication. Is it possible to integrate Google Login with the JWT Token (same way you implemented in this section) without using Identity pages on server side?
Hello Safa.
I’ve managed to do that only in WebAssembly hosted app: https://code-maze.com/google-authentication-in-blazor-webassembly-hosted-applications/
I didn’t try to do it with this setup having separate WASM and API projects.
Is there any reason why you don’t use a DelegatingHandler instead of the Interceptor to refresh the token directly in the HttpClient on HTTP call?
No. To be honest, I was working with Interceptor on something else and got an idea about using it with the refresh token flow. So, I wrote the article. But, even though I didn’t test the logic with the DelegatingHandler, as much as I remember, you can do it that way as well – again, I didn’t test it.
Thank you for your response 🙂
For my personal project I used your tutorial (which is very good btw) but switched to using a DelegatingHandler instead of the Interceptor. Also works very good. Just wanted to ask if there was a specific reason not to use it 🙂
Thanks again for this great tutorial!
Thank you for sharing that info with us here. Now, if someone else wants to use Handler instead of the interceptor, they know it works.
Hi Michael, is any way that you can share how you did it? I’m very curious hoy can be implemented.
Have a great day.
Hello Luis,
sure 🙂
Program.cs:
RefreshTokenHandler.cs:
Thank you Michael for sharing your code. That’s quite an awesome thing to do.
Puedes compartirnos tu código a través de un repositorio, lo implemente y me da un error!
Hello Alexander.
Please use English for your comments. Yes, we can use Google Translate, but that is not a point.
Now, you can find the source code for our code at the beginning of the article. Regarding Michael’s code, well this is up to him. You can always download our code and implement his changes.
Gracias, saludos desde Chimbote, Perú. Excelente aportación de conocimiento, seria excelente que habilites una comunidad para temas de conversación. saludos.
I had problems with circular references in my project using Michaels solution – essentially the HTTP client needing the HTTP client further down the chain and so throwing DI value errors. I’m not sure if somethings changed in .NET 7 or I was doing something wrong.
To get around it I eventually settled for piping in the service provider to where I needed the HTTPClient
My code ended up slightly different but I piped the service provider into RefreshTokenService:
Then in TryRefreshToken:
I’m sure there might be better ways but this resolved it for me.
Hello, just want to ask about injecting the interceptor in the page component.
How come you injected it in the Products.razor.cs rather than the layout component? I mean what about other authorized components? should we inject the interceptor service in every authorized component?
I might have missed something in the article here.. anyhow I really appreciate the work here.. I managed to follow all your steps with .NET 6 without any problem so thanks again! 🙂
Hello Rygar. I see your point and it is a good one. To be honest, I didn’t test it with the layout, since we just have a single component in this project, I’ve injected it there. You can read the official documents (on a GitHub page) about the interceptor as well to learn a bit more about it. If you are willing, you can try it with the layout component and let us know here 🙂
Thanks for a great article!
I have a question regarding the Refresh endpoint of the TokenController – shoildn’t it update the RefreshTokenExpiryTime after the user.RefreshToken = _tokenService.GenerateRefreshToken(); line?
Well no. We want after the refresh token expires to force users to login again.
Thank you for an excellent article.
I have got the Blazor WebAssembly working with the Web API and SQL Server.
I would now like to publish all three components to Azure.
Can you please guide me?
Regards
Mul
Hello. Well, it is a process to publish all three on Azure but of course, it can easily be done. In our Blazor WebAssembly course https://code-maze.com/blazor-webassembly-course/?source=nav (section 12) we are covering exactly that (among other topics of course).
I have a security question:
Refresh tokens are not recommended to be store on client side:
https://docs.microsoft.com/en-us/aspnet/core/blazor/security/webassembly/?view=aspnetcore-5.0#refresh-tokens-1
As they say: “Refresh tokens can’t be secured client-side in Blazor WebAssembly apps. Therefore, refresh tokens shouldn’t be sent to the app for direct use.”
As far as i understood you store the refresh tokens inside the localStorage on client side(blazor-webassembly).
Any comments are welcome
To answer all your questions – Yes Microsoft doesn’t recommend storing tokens on the client side (they say for security reasons) and for the solution they recommend using third-party solutions like Azure.
If you have an internal application, this is a perfectly safe option. Also, it is safe enough for the public apps as well, but it would be even safer with using the session storage instead of the local storage.
There is a problem with the interceptor code and the suggested use of RegisterEvent in OnInitializedAsync and DisposeEvent in a razor page.
When you have two razor pages and for each of these pages one implements the suggested RegisterEvent and DisposeEvent then when you switch pages in the browsser the Dispose of the page you are leaving occurs after the OnInitializedAsync of the new page. This means that there is a moment when 2 Eventhandlers are registered that intercept the HTTP request When the bearer token expires the server method is called twice. When the server call is executed a new refresh token is calculated and stored in the database (for the authenticated user).. Both server calls return a new RefreshToken to the client HTTP method. The client then updates the browser storage. There is no ‘atomic’ action in the server code so it is possibel that the refreshtoken stored in the database by the server is different from the refreshtoken stored by the client in the browser (dependent on the sheduling of the threads). The next http call that triggers a refresh fails because the refreshtokens no longer match.
I discoverd this when setting the expire time of the bearer token to 1 minute. Each http method then triggers a refresh.
Hello Jean. Thanks for the comment. Why do you have an expiry time of the bearer token set to 1 minute? Honestly, I am pretty sure it should be set to a much higher value, for example one hour. Usually no more than that. In that case, even though you have two registered events (which will be very shortly, since the first one is going to be disposed right away), the second event won’t trigger the refresh action since the token is still valid. I assume that in the second component, you are having the Http call inside the OnInitializedAsync method.
Hi Marinko, I used the expire time of 1 minute to make sure every HTTP request would result in a token refresh (this is of course only for testing the code and this should work). In the test code I use there is indeed a HTTP request in the OnInitializedAsync method which fetches some data from the server. Even with a token expire time of 1 hour the problem can occur; this happens when there is a page change immediately followed by a HTTP call with an expired token. Since there are two event handler registered at that moment (Dispose occurs after OnInitializedAsync), these event handler handle the same HTTP call and in both cases the refreshToken method is called since both handlers detect that the token is expired. This results in 2 server calls and each call creates a different RefreshToken. It then depends on which thread is handled first for the error to occur.
Hello Jean. Yes, to be honest, it suprised me a bit that the Dispose method of the current component is executed after the OnInitialize in the component you are navigating on. You can solve this problem with a bit of a hack. In the RegisterEvent method you can remove the InterceptBeforeHttpAsync before you add it:
_interceptor.BeforeSendAsync -= InterceptBeforeHttpAsync;
_interceptor.BeforeSendAsync += InterceptBeforeHttpAsync;
This will basically delete the need for the DisposeEvent method completely. But it works, at least with what I tested so far.
Hi Marinko, The hack is working. Thanks.
Another question: Suppose an authenticated user logs in using browser 1 and later on logs on to the same application using another browser.. When tokens expire one of the applications will fail (because the refreshToken in the database is linked to the browser that did the latest refresh). How would you fix this?
I am not sure about that, maybe there is nothing to think about, since if you are using another browser, that client is a valid one and the previous one shouldn’t have a token refreshed.
Thanks for the article!. Do you know why I need [Authorize(AuthenticationSchemes = “Bearer”)] in the controller to make it work? Just using [Authorize] I get an html response saying “An unhandled error has ocurred”
Maybe you are missing something in the jwt configuration in the startup class. You can check our source code to compare it with yours.
Thanks! For future reference it looks like the issue was having services.AddIdentity after services.AddAuthentication. Changing the order made it work.
Great, thanks for that. It is like that in the source code, but it is good to mention it.
I just breezed through the article and code and I’m certainly not an expert, but this is really just your personal implementation of what IdentityServer4 offers? “Refresh Token Flow” isn’t even part of the OIDC spec? Why would you invent your own authentication/authorization protocol? Or is this an existing one? Implementing security stuff on your own is a really big bad practice.
Just an example: A login failed count is not used. You are exposing a public login API method, that can be called an infinite amount of times by an attaker. Worse, anyone can call RegisterUser() and create users as he wishes? There’s probably plenty more issues that I don’t realize.
This complete series is with Blazor WASM client app and external Web API, so we have implemented a custom authentication on top of ASP.NET Core Identity on the Web API side. OAuth is implemented in the Blazor WASM hosted solution. Lock out and other features can be easilly implemented because we already use ASP.NET Core Identity. And yes you are right, OAuth flow is not supporting recresh token by default, so I provided a custom solution if anyone needs it when they create blazor and web api as separate solutions. We will cover all the additional Identity features in our course making our app even more secure.
So why go through all that custom implementation? What reason is there not to use IdentityServer?
For example, when you have a private API with a single client app to consume it, it could be overload using entire token provider server like IdentityServer4 to provide an auth mechanism. Usually, you use an external token provider for a public API’s consumed by multiple clients. Of course, this doesn’t mean you will make a mistake if you use IS4 with a private API and a single Client App – you can do that for sure. We have that covered as well. You can find all the articles here: https://code-maze.com/blazor-webassembly-series/, under the Securing the Blazor WebAssembly Application with IdentityServer4 section.
I believe there is still something wrong, with your example. I took your code, and the situation is exactly the same, it doesn’t show the Product page, even if I login with admin role. (of course, the database is updated with an admin user)
Hi Zoltan. Well, I really don’t know what to say. I hate this type of problems. I just run my code and everything works. When log in with a Viewer I can’t see the Product page, but when I log in with an Administrator, the product page is there and I can fetch the data. Basically, if my code is wrong, I wouldn’t be able to test it with HTTP requests while writing this article, but I did test it (It would be so bad from my side to write the “Testing Refresh Token Functionality” section without really having application tested – I will never do such a thing). Have you maybe tried removing cache or something like that. As I said, I really hate this type of problems (it works on my machine but not on yours) since it should be working on all machines, it doesn’t make sense differently.
Never mind, I completely understand. If you can’t reproduce the issue there, nothing can be done. Could be the cache or something else with my setup. Just ignore this. I just wanted to review Blazor WebAssembly these days. Believe it or not, my current workplace requested me to prepare for Oqtane, which is based on Blazor, where the WA part was completely missing for me.
Ok Zoltan, I figured it out. First of all thanks a lot for pointing to this issue, I totally forgot to migrate changes from the Role repository to the Refresh token repository. Once I did it, everything works and it also supports single user with a multiple roles (which was the main change in the Role repository). So, you can try my code, and now it should work. But the really strange thing is that your code works on my machine. I compare it to my solution (updated one) and both are the same. Then I started your projects, created two users, one with a Viewer role and one with a Viewer and Admin roles. As soon as I log in with a user with multiple roles I can see the Product menu and I can fetch the data. After I log out and log in with a Viewer, I can’t see that page anymore. So, it seems that everything is working as it supposed to. Why do you have a problem with your code (I have established why you had problem with mine) is a big question for me.
Thank you for the excellent article. I will take your latest code and check it, now that you updated it.
Yes, I can confirm that it works now. Anyway, its tremendous work on your side and you are among the best for C# tutorials.
Thank you very much Zoltan for the kind words and for your support. Also, thanks a lot for pointing to this issue. Have a great day and of course, have a great New Year.
Hi Marinko, I studied this article also, but the final outcome doesn’t work. When I open the client, the Admin user has no access to the Products page, as in the previous article (which worked for me).
I even copied a major portion of your repo’s code into mine, to make sure I didn’t miss something. Same issue, Products page not displayed.
Its not urgent, but maybe you have an idea what I missed: my zipped repo: https://drive.google.com/file/d/1qiFa_i4LE5sAyreA_oEwlvsAuo6zTYNI/view?usp=sharing
Hi Zoltan. This is strange since this implementation has nothing to do with roles, so if it worked in a previous article, I can’t say whay it doesn’t work now. Anyhow, this is not a simple topic, so what I can do is to compare your solution with mine. I can do it, but question is when I can find a spare time for that. Basically this is my advice to you, it will speed up the process for sure. Try going through each step of this article, and compare my solution with yours. Maybe something misses and you can’t see it at the moment. Also, my access is denied for the link you posted. I’ve sent the access request.
No issue. I will check it later. Don’t spend time on it.