It is a good practice to have a separate endpoint for the refresh token action, and that’s exactly what we will do now. Let’s start by creating a new TokenController
in the Presentation
project:
using CompanyEmployees.Core.Services.Abstractions; using Microsoft.AspNetCore.Mvc; namespace CompanyEmployees.Infrastructure.Presentation.Controllers; [Route("api/token")] [ApiController] public class TokenController : ControllerBase { private readonly IServiceManager _service; public TokenController(IServiceManager service) => _service = service; }
Before we continue with the controller modification, we are going to modify the IAuthenticationService
interface:
public interface IAuthenticationService { Task<IdentityResult> RegisterUser(UserForRegistrationDto userForRegistration); Task<bool> ValidateUser(UserForAuthenticationDto userForAuth); Task<TokenDto> CreateToken(bool populateExp); Task<TokenDto> RefreshToken(TokenDto tokenDto); }
We also have to implement this method:
public async Task<TokenDto> RefreshToken(TokenDto tokenDto) { var principal = GetPrincipalFromExpiredToken(tokenDto.AccessToken); var user = await _userManager.FindByNameAsync(principal.Identity.Name); if (user == null || user.RefreshToken != tokenDto.RefreshToken || user.RefreshTokenExpiryTime <= DateTime.Now) throw new RefreshTokenBadRequest(); _user = user; return await CreateToken(populateExp: false); }
We first extract the principal from the expired token and use the Identity.Name
property, which is the user’s username, 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 stop the flow and return the BadRequest response to the user. Then we just populate the _user
field and call the CreateToken()
method to generate new Access and Refresh tokens.
This time, we don’t want to update the expiry time of the refresh token thus sending false
as an argument. Since we don’t have the RefreshTokenBadRequest
class, let’s create it in the DomainExceptions
folder: And add a required using
directive in the AuthenticationService
class to remove the current error:
using CompanyEmployees.Core.Domain.Exceptions;
Finally, let’s create a new action in the TokenController
:
[HttpPost("refresh")] [ServiceFilter(typeof(ValidationFilterAttribute))] public async Task<IActionResult> Refresh([FromBody] TokenDto tokenDto) { var tokenDtoToReturn = await _service.AuthenticationService.RefreshToken(tokenDto); return Ok(tokenDtoToReturn); }
That’s it. Our refresh token logic is prepared and ready for testing. Let’s first send the POST authentication request:
curl --location 'https://localhost:5001/api/authentication/login' --header 'Content-Type: application/json' --header 'Accept: application/json' --data '{ "username": "JDoe", "password": "Password1000" }'
As before, we have both tokens in the response body. Now, let’s send the POST refresh request with these tokens as the request body:
curl --location 'https://localhost:5001/api/token/refresh' --header 'Content-Type: application/json' --header 'Accept: application/json' --data '{ "accessToken":"your access token", "refreshToken": "your refresh token" }'
And we can see new tokens in the response body. Additionally, if we inspect the database, we will find the same refresh token value. Usually, in your client application (if you are the one creating the client for the API), you would inspect the exp
claim of the access token and if it is about to expire, your client app will send the request to the api/token
endpoint and get a new set of valid tokens.