top of page
Writer's pictureThe Tech Platform

JWT and Refresh Tokens in ASP.NET Core



In this article we’ll go through a simple example of how to implement JWT(Json Web Token) authentication and Refresh tokens in ASP.NET Core web api with c#.


The example api has several auth endpoints and user related endpoints for deleting and updating user information.


The project is available on GitHub at https://github.com/Revazashvili/UserManagement


Required Tools

  • .Net Core SDK

  • IDE — your code editor of choice, but i recommend use Rider

After this tools create empty web api project on c# and try running it by dotnet run .


Appsettings.json

In your configuration file add some properties needed for jwt and refresh token to configure.

"JwtSettings":{
  "AccessTokenSecret":"my_too_strong_access_secret_key",
  "RefreshTokenSecret":"my_too_strong_refresh_secret_key",
  "AccessTokenExpirationMinutes":0.3,
  "RefreshTokenExpirationMinutes":60,
  "Issuer":"https://localhost:5001",
  "Audience":"https://localhost:5001"
}

and create class with this fields also.

public class JwtSettings
{
    public string AccessTokenSecret { get; set; }
    public string RefreshTokenSecret { get; set; }
    public double AccessTokenExpirationMinutes { get; set; }
    public double RefreshTokenExpirationMinutes { get; set; }
    public string Issuer { get; set; }
    public string Audience { get; set; }
}

then all you need to bind configuration and inject into DI container(this will be needed in other services also).

var jwtSettings = new JwtSettings();
Configuration.Bind(nameof(JwtSettings), jwtSettings);
services.AddSingleton(jwtSettings);


Configure JWT

Configure jwt bearer scheme in Startup.cs class. use issuer, audience and secret key to configure parameters for validating tokens.

services.AddAuthentication(x =>
    {
        x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
        x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
    })
    .AddJwtBearer(x =>
    {
        x.SaveToken = true;
        x.RequireHttpsMetadata = false;
        x.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuerSigningKey = true,
            ValidateIssuer = true,
            ValidateAudience = true,
            ValidateLifetime = true,
            IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSettings.AccessTokenSecret)),
            ValidIssuer = jwtSettings.Issuer,
            ValidAudience = jwtSettings.Audience,
            ClockSkew = TimeSpan.Zero
        };
    });

optionally you can add security definition to swagger…

services.AddSwaggerGen(c =>
{
    c.SwaggerDoc("v1", new OpenApiInfo {Title = "API", Version = "v1"});
    c.EnableAnnotations();

    c.AddSecurityDefinition(JwtBearerDefaults.AuthenticationScheme, new OpenApiSecurityScheme
    {
        Description = "JWT Authorization header using the Bearer scheme (Example: 'Bearer 12345abcdef')",
        Name = "Authorization",
        In = ParameterLocation.Header,
        Type = SecuritySchemeType.ApiKey,
        Scheme = JwtBearerDefaults.AuthenticationScheme
    });

    c.AddSecurityRequirement(new OpenApiSecurityRequirement
    {
        {
            new OpenApiSecurityScheme
            {
                Reference = new OpenApiReference
                {
                    Type = ReferenceType.SecurityScheme,
                    Id = JwtBearerDefaults.AuthenticationScheme
                }
            },
            Array.Empty<string>()
        }
    });
});


Add middlewares

Then you must need to add middlewares in Configure method in Startup.cs class.

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

This must be after UseRouting method and with this sequence.(first you need to know identity of user and then you can authrize,allow something or not).


Generate JWT and refresh tokens

Then you need to add token service interface and individual service for access and refresh token.

/// <summary>
/// Interface for generating token.
/// </summary>
public interface ITokenService
{
    /// <summary>
    /// Generates token based on user information.
    /// </summary>
    /// <param name="user"><see cref="User"/> instance.</param>
    /// <returns>Generated token.</returns>
    string Generate(User user);
}/// <inheritdoc cref="ITokenService"/>
public interface IRefreshTokenService : ITokenService { }
/// <inheritdoc cref="ITokenService"/>
public interface IAccessTokenService : ITokenService { }

there will be different service for generating token, not to duplicate code in access and refresh token services.

/// <summary>
/// Interface for generating token.
/// </summary>
public interface ITokenGenerator
{
    /// <summary>
    /// Generates jwt token.
    /// </summary>
    /// <param name="secretKey">The secret key for security.</param>
    /// <param name="issuer">The issuer.</param>
    /// <param name="audience">The audience.</param>
    /// <param name="expires">The expire time.</param>
    /// <param name="claims"><see cref="IEnumerable{T}"/></param>
    /// <returns>Generated token.</returns>
    string Generate(string secretKey, string issuer, string audience, double expires,
        IEnumerable<Claim> claims = null);
}/// <inheritdoc cref="ITokenGenerator"/>
public class TokenGenerator : ITokenGenerator
{
    public string Generate(string secretKey, string issuer, string audience, double expires, IEnumerable<Claim> claims = null)
    {
        var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secretKey));
        var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
        JwtSecurityToken securityToken = new(issuer,audience,
            claims,
            DateTime.UtcNow,
            DateTime.UtcNow.AddMinutes(expires),
            credentials);
        return new JwtSecurityTokenHandler().WriteToken(securityToken);
    }
}

Now lets implement token services…

public class AccessTokenService : IAccessTokenService
{
    private readonly ITokenGenerator _tokenGenerator;
    private readonly JwtSettings _jwtSettings;

    public AccessTokenService(ITokenGenerator tokenGenerator, JwtSettings jwtSettings) =>
        (_tokenGenerator, _jwtSettings) = (tokenGenerator, jwtSettings);

    public string Generate(User user)
    {
        List<Claim> claims = new()
        {
            new Claim("id", user.Id),
            new Claim(ClaimTypes.Email, user.Email),
            new Claim(ClaimTypes.Name, user.UserName),
        };
        return _tokenGenerator.Generate(_jwtSettings.AccessTokenSecret, _jwtSettings.Issuer, _jwtSettings.Audience,
            _jwtSettings.AccessTokenExpirationMinutes, claims);
    }
}

to generate access token you need JWTSettings and ITokenGenerator to inject via constructor. for access token you must provider claims to generator service, but for refresh token it’s not needed…

public class RefreshTokenService : IRefreshTokenService
{
    private readonly ITokenGenerator _tokenGenerator;
    private readonly JwtSettings _jwtSettings;

    public RefreshTokenService(ITokenGenerator tokenGenerator, JwtSettings jwtSettings) =>
        (_tokenGenerator, _jwtSettings) = (tokenGenerator, jwtSettings);

    public string Generate(User user) => _tokenGenerator.Generate(_jwtSettings.RefreshTokenSecret,
        _jwtSettings.Issuer, _jwtSettings.Audience,
        _jwtSettings.RefreshTokenExpirationMinutes);
}


Refresh Token Validation

In order to validate refresh token, you need to add simple service and implement it.

/// <summary>
/// Interface for validating refresh token.
/// </summary>
public interface IRefreshTokenValidator
{
    /// <summary>
    /// Validates refresh token.
    /// </summary>
    /// <param name="refreshToken">The refresh token.</param>
    /// <returns>True if token is valid,otherwise false.</returns>
    bool Validate(string refreshToken);
}

To implement you need to create TokenValidationParameters like in Startup.cs and use JwtSecurityTokenHandler to validate token.

public class RefreshTokenValidator : IRefreshTokenValidator
{
    private readonly JwtSettings _jwtSettings;

    public RefreshTokenValidator(JwtSettings jwtSettings) => _jwtSettings = jwtSettings;

    public bool Validate(string refreshToken)
    {
        var validationParameters = new TokenValidationParameters
        {
            ValidateIssuerSigningKey = true,
            ValidateIssuer = true,
            ValidateAudience = true,
            ValidateLifetime = true,
            IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_jwtSettings.RefreshTokenSecret)),
            ValidIssuer = _jwtSettings.Issuer,
            ValidAudience = _jwtSettings.Audience,
            ClockSkew = TimeSpan.Zero
        };

        JwtSecurityTokenHandler jwtSecurityTokenHandler = new();
        try
        {
            jwtSecurityTokenHandler.ValidateToken(refreshToken, validationParameters,
                out SecurityToken validatedToken);
            return true;
        }
        catch (Exception)
        {
            return false;
        }
    }
}


Authenticate

authenticating user will be needed in to places: sign in and refresh token endpoints. so i’m gonna create service that saves refresh token in database and then returns access and refresh token for current user.

/// <summary>
/// Interface for authentication.
/// </summary>
public interface IAuthenticateService
{
    /// <summary>
    /// Authenticates user.
    /// Takes responsibilities to generate access and refresh token, save refresh token in database
    /// and return instance of <see cref="AuthenticateResponse"/> class. 
    /// </summary>
    /// <param name="user">The user.</param>
    /// <param name="cancellationToken">Instance of <see cref="CancellationToken"/>.</param>
    Task<AuthenticateResponse> Authenticate(User user,CancellationToken cancellationToken);
}

here you use previously created service IAccessTokenService,IRefreshTokenService and abstraction for database IApplicationDbContext service.

public class AuthenticateService : IAuthenticateService
{
    private readonly IAccessTokenService _accessTokenService;
    private readonly IRefreshTokenService _refreshTokenService;
    private readonly IApplicationDbContext _context;
    public AuthenticateService(IAccessTokenService accessTokenService, IRefreshTokenService refreshTokenService, IApplicationDbContext context)
    {
        _accessTokenService = accessTokenService;
        _refreshTokenService = refreshTokenService;
        _context = context;
    }

    public async Task<AuthenticateResponse> Authenticate(User user,CancellationToken cancellationToken)
    {
        var refreshToken = _refreshTokenService.Generate(user);
        await _context.RefreshTokens.AddAsync(new RefreshToken(user.Id, refreshToken), cancellationToken);
        await _context.SaveChangesAsync(cancellationToken);
        return new AuthenticateResponse
        {
            AccessToken = _accessTokenService.Generate(user),
            RefreshToken = refreshToken
        };
    }
}


Endpoints

In this example i’m gonna use Mediator pattern and CQRS to implement this functionality.


Login handler:

public record LoginUserCommand(LoginUserRequest LoginUserRequest) : IRequest<AuthenticateResponse>{}

public class LoginUserCommandHandler : IRequestHandler<LoginUserCommand,AuthenticateResponse>
{
    private readonly IAuthenticateService _authenticateService;
    private readonly UserManager<User> _userManager;
    private readonly SignInManager<User> _signInManager;

    public LoginUserCommandHandler(UserManager<User> userManager, SignInManager<User> signInManager, IAuthenticateService authenticateService)
    {
        _userManager = userManager;
        _signInManager = signInManager;
        _authenticateService = authenticateService;
    }

    public async Task<AuthenticateResponse> Handle(LoginUserCommand request, CancellationToken cancellationToken)
    {
        var user = await _userManager.FindByEmailAsync(request.LoginUserRequest.Email);
        if (user is null) throw new UserNotFoundException();
        var signInResult =
            await _signInManager.PasswordSignInAsync(user, request.LoginUserRequest.Password, false, false);
        if (!signInResult.Succeeded) throw new SignInException();
        return await _authenticateService.Authenticate(user, cancellationToken);
    }
}

Login endpoint:

[HttpPost]
[SwaggerOperation(Description = "Signs in provided user and return token",
    Summary = "Sign In",
    OperationId = "Auth.Login",
    Tags = new []{ "Auth" })]
[SwaggerResponse(200,"User logged in successfully",typeof(string))]
[SwaggerResponse(400,"User with provided email can not be found",typeof(string))]
[Produces("application/json")]
[Consumes("application/json")]
public override async Task<ActionResult<string>> HandleAsync(
    [SwaggerRequestBody("User login payload",Required = true)]LoginUserRequest loginUserRequest, 
    CancellationToken cancellationToken = new())
{
    return Ok(await _mediator.Send(new LoginUserCommand(loginUserRequest), cancellationToken));
}

Login request object:

public class LoginUserRequest
{
    [SwaggerSchema(Required = new[] { "The User Email" })]
    public string Email { get; set; }
    [SwaggerSchema(Required = new[] { "The User Password" })]
    public string Password { get; set; }
}

Refresh handler:

public record RefreshCommand(RefreshRequest RefreshRequest) : IRequest<AuthenticateResponse>{}

public class RefreshCommandHandler : IRequestWrapper<RefreshCommand,AuthenticateResponse>
{
    private readonly IAuthenticateService _authenticateService;
    private readonly IRefreshTokenValidator _refreshTokenValidator;
    private readonly IApplicationDbContext _context;
    private readonly UserManager<User> _userManager;

    public RefreshCommandHandler(IRefreshTokenValidator refreshTokenValidator, IApplicationDbContext context,
        UserManager<User> userManager,IAuthenticateService authenticateService)
    {
        _refreshTokenValidator = refreshTokenValidator;
        _context = context;
        _userManager = userManager;
        _authenticateService = authenticateService;
    }

    public async Task<AuthenticateResponse> Handle(RefreshCommand request, CancellationToken cancellationToken)
    {
        var refreshRequest = request.RefreshRequest;
        var isValidRefreshToken = _refreshTokenValidator.Validate(refreshRequest.RefreshToken);
        if (!isValidRefreshToken)
            throw new InvalidRefreshTokenException();
        var refreshToken =
            await _context.RefreshTokens.FirstOrDefaultAsync(x => x.Token == refreshRequest.RefreshToken,
                cancellationToken);
        if(refreshToken is null)
            throw new InvalidRefreshTokenException();

        _context.RefreshTokens.Remove(refreshToken);
        await _context.SaveChangesAsync(cancellationToken);
        
        var user = await _userManager.FindByIdAsync(refreshToken.UserId);
        if (user is null) throw new UserNotFoundException();

        return await _authenticateService.Authenticate(user, cancellationToken);
    }

Refresh endpoint:

[HttpPost]
[SwaggerOperation(Description = "Refreshes token",
    Summary = "Refresh token",
    OperationId = "Auth.Refresh",
    Tags = new []{ "Auth" })]
[SwaggerResponse(200,"token refreshed in successfully")]
[Produces("application/json")]
[Consumes("application/json")]
public override async Task<ActionResult<AuthenticateResponse>> HandleAsync(
    [FromBody,SwaggerRequestBody("Refresh token payload")]RefreshRequest refreshRequest, 
    CancellationToken cancellationToken = new())
{
    return Ok(await _mediator.Send(new RefreshCommand(refreshRequest), cancellationToken));
}

Refresh request object:

public class RefreshRequest
{
    /// <summary>
    /// The refresh token.
    /// </summary>
    public string RefreshToken { get; set; }
}


Dependency Injection

all you have to do is inject your services in DI Container and start you project…

services.AddScoped<IApplicationDbContext>(x => x.GetService<ApplicationDbContext>()!);
services.AddScoped<ITokenGenerator, TokenGenerator>();
services.AddScoped<IAccessTokenService, AccessTokenService>();
services.AddScoped<IRefreshTokenService, RefreshTokenService>();
services.AddScoped<IRefreshTokenValidator, RefreshTokenValidator>();
services.AddScoped<IAuthenticateService, AuthenticateService>();


Summary

We have implemented JWT and Refresh token functionality in ASP.NET Core web api. you should see source code and get better understanding of this concept.



Source: Medium - Levan Revazashvili


The Tech Platform

0 comments

Comments


bottom of page