Ova

How to implement token based authentication in net core?

Published in ASP.NET Core Authentication 9 mins read

Implementing token-based authentication in .NET Core typically involves using JSON Web Tokens (JWTs) to secure your APIs, allowing clients to receive an access token after successful login, which they then use to authenticate subsequent requests. This process includes generating, validating, and managing tokens, often alongside refresh tokens for enhanced security and user experience.

How to Implement Token-Based Authentication in .NET Core

Token-based authentication provides a stateless and scalable way to secure web APIs by issuing signed tokens that verify a user's identity and authorization claims without requiring session management on the server. Here’s a detailed guide on how to implement this using JWTs in .NET Core.

Understanding Token-Based Authentication and JWTs

Before diving into implementation, it's crucial to grasp the core components:

  • Access Token (JWT): A compact, URL-safe means of representing claims to be transferred between two parties. It contains information about the user and their permissions, signed to prevent tampering.
  • Refresh Token: A long-lived token used to obtain a new access token once the current one expires, without requiring the user to re-authenticate with their credentials. This enhances security by keeping access tokens short-lived.

A JWT consists of three parts, separated by dots: Header.Payload.Signature.

Part Description Example Content
Header Specifies the token type (JWT) and the signing algorithm used. {"alg": "HS256", "typ": "JWT"}
Payload Contains the claims (assertions about the entity and additional data). {"sub": "1234567890", "name": "John Doe", "iat": 1516239022, "exp": 1516242622}
Signature Used to verify that the sender of the JWT is who it says it is and that the message hasn't been changed along the way. Cryptographic hash of header, payload, and a secret key.

Step-by-Step Implementation

The implementation process covers creating a web application, setting up token-based authorization, and incorporating refresh tokens.

1. Create a .NET Core Web API Project

Begin by setting up a new ASP.NET Core Web API project. This will serve as the foundation for your authentication system.

dotnet new webapi -n MyAuthApi
cd MyAuthApi

2. Install Necessary NuGet Packages

You'll need the Microsoft.AspNetCore.Authentication.JwtBearer package to enable JWT authentication in your application.

dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer
dotnet add package Microsoft.EntityFrameworkCore.InMemory # For simplicity, can use for user/refresh token storage
dotnet add package Microsoft.AspNetCore.Identity.EntityFrameworkCore # If using ASP.NET Core Identity

3. Configure JWT Authentication

In your Program.cs (for .NET 6+ Minimal APIs) or Startup.cs (for older versions), configure the JWT Bearer authentication scheme. This involves defining how to validate incoming tokens.

// Program.cs
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using System.Text;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

// --- JWT Configuration ---
var jwtSettings = builder.Configuration.GetSection("Jwt");
var key = Encoding.ASCII.GetBytes(jwtSettings["Key"]);

builder.Services.AddAuthentication(options =>
{
    options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
    options.RequireHttpsMetadata = false; // Set to true in production
    options.SaveToken = true;
    options.TokenValidationParameters = new TokenValidationParameters
    {
        ValidateIssuerSigningKey = true,
        IssuerSigningKey = new SymmetricSecurityKey(key),
        ValidateIssuer = true,
        ValidIssuer = jwtSettings["Issuer"],
        ValidateAudience = true,
        ValidAudience = jwtSettings["Audience"],
        ValidateLifetime = true,
        ClockSkew = TimeSpan.Zero // The maximum allowed timespan between the token's expiration and the current time
    };
});

builder.Services.AddAuthorization(); // Ensure authorization services are added

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();

// --- Enable Authentication and Authorization ---
app.UseAuthentication();
app.UseAuthorization();

app.MapControllers();

app.Run();

You also need to add JWT configuration to your appsettings.json:

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "Jwt": {
    "Key": "ThisIsMySuperSecretKeyForJWTAuth_DoNotShare!", // Keep this secret and long
    "Issuer": "https://yourdomain.com",
    "Audience": "https://yourdomain.com"
  }
}

4. Implement User Registration and Login

Create an authentication controller (e.g., AuthController.cs) with endpoints for user registration and login. Upon successful login, you'll generate a JWT for the user.

// Models for request/response
public class LoginRequest
{
    public string Username { get; set; }
    public string Password { get; set; }
}

public class AuthResponse
{
    public string AccessToken { get; set; }
    public string RefreshToken { get; set; } // Will be added later
}

// AuthController.cs
using Microsoft.AspNetCore.Mvc;
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;

[ApiController]
[Route("api/[controller]")]
public class AuthController : ControllerBase
{
    private readonly IConfiguration _configuration;

    public AuthController(IConfiguration configuration)
    {
        _configuration = configuration;
    }

    [HttpPost("register")]
    public async Task<IActionResult> Register([FromBody] LoginRequest model)
    {
        // For demonstration: In a real app, hash password and save to DB
        if (model.Username == "test" && model.Password == "password")
        {
            return Ok("User registered successfully (demo)");
        }
        return BadRequest("Registration failed (demo)");
    }

    [HttpPost("login")]
    public async Task<IActionResult> Login([FromBody] LoginRequest model)
    {
        // For demonstration: Validate user against a mock database
        if (model.Username != "user" || model.Password != "pass")
        {
            return Unauthorized("Invalid credentials.");
        }

        var claims = new List<Claim>
        {
            new Claim(ClaimTypes.NameIdentifier, "1"),
            new Claim(ClaimTypes.Name, model.Username),
            new Claim(ClaimTypes.Role, "Admin") // Example role
        };

        var accessToken = GenerateAccessToken(claims);
        var refreshToken = GenerateRefreshToken(); // Placeholder for now

        return Ok(new AuthResponse { AccessToken = accessToken, RefreshToken = refreshToken });
    }

    private string GenerateAccessToken(IEnumerable<Claim> claims)
    {
        var jwtSettings = _configuration.GetSection("Jwt");
        var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSettings["Key"]));
        var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);

        var tokenDescriptor = new SecurityTokenDescriptor
        {
            Subject = new ClaimsIdentity(claims),
            Expires = DateTime.UtcNow.AddMinutes(15), // Short-lived access token
            Issuer = jwtSettings["Issuer"],
            Audience = jwtSettings["Audience"],
            SigningCredentials = credentials
        };

        var tokenHandler = new JwtSecurityTokenHandler();
        var token = tokenHandler.CreateToken(tokenDescriptor);
        return tokenHandler.WriteToken(token);
    }

    private string GenerateRefreshToken()
    {
        // Implement refresh token generation and storage here
        // For now, return a simple GUID
        return Guid.NewGuid().ToString();
    }
}

5. Secure API Endpoints

Apply the [Authorize] attribute to controllers or individual action methods you want to protect. This ensures that only requests with a valid JWT can access these resources.

// WeatherForecastController.cs (or any new secured controller)
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

[ApiController]
[Route("[controller]")]
[Authorize] // This controller requires authentication
public class SecuredDataController : ControllerBase
{
    [HttpGet]
    public IActionResult GetSecuredData()
    {
        var userName = User.Identity.Name; // Access user claims from the token
        return Ok($"Hello {userName}! You have access to secured data.");
    }

    [HttpGet("admin-only")]
    [Authorize(Roles = "Admin")] // Requires 'Admin' role
    public IActionResult GetAdminData()
    {
        return Ok("This data is only for administrators.");
    }
}

6. Implement Refresh Tokens

Refresh tokens are crucial for improving security and user experience by allowing access tokens to be short-lived.

  1. Generate and Store Refresh Tokens: When a user logs in, generate both an access token and a refresh token. The refresh token should be stored securely, typically in a database, along with its expiration date and associated user ID.

    // Inside AuthController.cs (modified Login method)
    // ...
    private string GenerateRefreshToken(string userId)
    {
        var randomNumber = new byte[32];
        using (var rng = RandomNumberGenerator.Create())
        {
            rng.GetBytes(randomNumber);
            return Convert.ToBase64String(randomNumber);
        }
        // In a real app, store this token with userId, creation date, expiry date, and revoked status in DB.
        // Example: _dbContext.RefreshTokens.Add(new RefreshToken { Token = token, UserId = userId, ExpiryDate = DateTime.UtcNow.AddDays(7) });
        // _dbContext.SaveChanges();
    }
    // ...
    // Update AuthResponse to hold RefreshToken
    public class AuthResponse
    {
        public string AccessToken { get; set; }
        public string RefreshToken { get; set; }
    }
    // In Login:
    var refreshToken = GenerateRefreshToken(claims.FirstOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value);
    // ... return new AuthResponse { AccessToken = accessToken, RefreshToken = refreshToken };
  2. Refresh Token Endpoint: Create an endpoint (/refresh) where clients can send their expired access token and valid refresh token to obtain a new pair.

    using System.Security.Cryptography; // For RandomNumberGenerator
    
    // ... inside AuthController.cs
    
    public class RefreshTokenRequest
    {
        public string AccessToken { get; set; }
        public string RefreshToken { get; set; }
    }
    
    [HttpPost("refresh")]
    public async Task<IActionResult> Refresh([FromBody] RefreshTokenRequest request)
    {
        var principal = GetPrincipalFromExpiredToken(request.AccessToken);
        if (principal == null)
        {
            return BadRequest("Invalid access token.");
        }
    
        var userId = principal.Claims.FirstOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value;
        if (string.IsNullOrEmpty(userId))
        {
            return BadRequest("Invalid access token claims.");
        }
    
        // --- DEMO ONLY: In a real app, retrieve refresh token from DB and validate ---
        // var storedRefreshToken = _dbContext.RefreshTokens.SingleOrDefault(rt => rt.Token == request.RefreshToken && rt.UserId == userId);
        // if (storedRefreshToken == null || !storedRefreshToken.IsActive)
        // {
        //     return BadRequest("Invalid or expired refresh token.");
        // }
    
        // Generate new access token and refresh token
        var newAccessToken = GenerateAccessToken(principal.Claims);
        var newRefreshToken = GenerateRefreshToken(userId);
    
        // --- DEMO ONLY: In a real app, update stored refresh token ---
        // storedRefreshToken.RevokedDate = DateTime.UtcNow; // Revoke old refresh token
        // _dbContext.RefreshTokens.Add(new RefreshToken { Token = newRefreshToken, UserId = userId, ExpiryDate = DateTime.UtcNow.AddDays(7) });
        // await _dbContext.SaveChangesAsync();
    
        return Ok(new AuthResponse { AccessToken = newAccessToken, RefreshToken = newRefreshToken });
    }
    
    private ClaimsPrincipal GetPrincipalFromExpiredToken(string token)
    {
        var jwtSettings = _configuration.GetSection("Jwt");
        var tokenValidationParameters = new TokenValidationParameters
        {
            ValidateAudience = true,
            ValidAudience = jwtSettings["Audience"],
            ValidateIssuer = true,
            ValidIssuer = jwtSettings["Issuer"],
            ValidateIssuerSigningKey = true,
            IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSettings["Key"])),
            ValidateLifetime = false // We need to validate expiration here, but not reject if expired
        };
    
        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;
    }

7. Testing the API

Use tools like Postman or Swagger UI (which you enabled with AddSwaggerGen()) to test your authentication flow:

  1. Register: Send a POST request to /api/auth/register to create a mock user.
  2. Login: Send a POST request to /api/auth/login with user credentials. You should receive an accessToken and refreshToken.
  3. Access Secured Endpoint: Take the accessToken and include it in the Authorization header as a Bearer token (e.g., Authorization: Bearer <your_access_token>) for a GET request to /secureddata.
  4. Refresh Token: Once the access token expires (or if you manually set a very short expiry for testing), send a POST request to /api/auth/refresh with the expired access token and the valid refresh token to get a new pair.
  5. Access Admin-Only Endpoint: If your user has the "Admin" role, try accessing /secureddata/admin-only with the appropriate access token.

Practical Considerations and Best Practices

  • HTTPS: Always use HTTPS in production to protect tokens from interception.
  • Secret Key Management: The JWT secret key (Jwt:Key) must be strong and kept confidential. Never hardcode it in production; use environment variables or a secure key vault.
  • Token Expiration:
    • Access Tokens: Keep them short-lived (e.g., 15-30 minutes) to minimize the impact of compromise.
    • Refresh Tokens: Make them longer-lived (e.g., days or weeks) and implement a robust revocation mechanism.
  • Refresh Token Revocation: Allow users to revoke their refresh tokens (e.g., when logging out of all devices). This is critical for security.
  • Token Storage on Client: Store access tokens and refresh tokens securely on the client-side. For web applications, HttpOnly cookies for refresh tokens and localStorage/sessionStorage for access tokens (with XSS precautions) are common approaches.
  • Claims: Be mindful of what information you put into JWT claims. Avoid sensitive data, and keep the token payload minimal to reduce its size.
  • Error Handling: Implement robust error handling for authentication failures and token validation issues.

By following these steps, you can effectively implement secure token-based authentication, including refresh tokens, in your ASP.NET Core applications, providing a robust and scalable solution for API security.