Home / Security / Secure API Authentication in .NET 10
Security

Secure API Authentication in .NET 10

Build production-grade API authentication with JWT tokens, refresh flows, and proper security headers.

G
Chef George
February 3, 2026 25 min Intermediate 667 views

The Problem

API authentication is deceptively complex. The basics \u2014 issuing a JWT token on login \u2014 seem straightforward enough. But production-grade authentication requires much more: token refresh flows, secure token storage guidance for clients, proper expiration policies, revocation support, and security headers that prevent common attacks.

Most tutorials cover the happy path and stop there. They show you how to generate a JWT but skip token refresh, ignore HTTPS enforcement, forget security headers, and use hardcoded secrets that end up in source control. The result is an API that looks secure on the surface but has critical gaps that attackers exploit routinely.

You need a complete, production-ready authentication implementation that handles the full lifecycle: registration, login, token issuance, refresh, and proper security hardening.

Ingredients

Microsoft.AspNetCore.Authentication.JwtBearer

vlatest

JWT bearer authentication for ASP.NET Core

BCrypt.Net-Next

vlatest

BCrypt password hashing library

Noundry.Guardian

vlatest

Input validation guard clauses

Noundry.Tuxedo

vlatest

Data access with Dapper extensions

This recipe uses the built-in .NET authentication and JWT libraries, combined with BCrypt for password hashing and Guardian for input validation. No third-party authentication services required \u2014 everything runs in your own infrastructure.

The combination of these packages gives you complete control over your authentication flow without depending on external identity providers.

Preparation

Install the required packages:

dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer
dotnet add package BCrypt.Net-Next
dotnet add package Noundry.Guardian
dotnet add package Noundry.Tuxedo

Configure JWT authentication in your Program.cs by setting up the authentication scheme, token validation parameters, and security headers. Store your JWT signing key in environment variables or a secrets manager \u2014 never in source control.

Make sure to configure HTTPS redirection, HSTS headers, and CORS policies before adding authentication to your middleware pipeline. The order of middleware registration matters for security.

The Code

Here's a complete authentication service that handles user registration with BCrypt password hashing, JWT token generation with configurable expiration, and a refresh token flow for seamless session management.

AuthService.cs
using System.Data;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Security.Cryptography;
using System.Text;
using Microsoft.IdentityModel.Tokens;
using Noundry.Guardian;
using Noundry.Tuxedo.Contrib;
using Noundry.Tuxedo.QueryBuilder;

namespace MyApp.Services;

public class AuthService
{
    private readonly IDbConnection _connection;
    private readonly IConfiguration _config;

    public AuthService(IDbConnection connection, IConfiguration config)
    {
        _connection = Guard.Against.Null(connection);
        _config = Guard.Against.Null(config);
    }

    public async Task<AuthResult> RegisterAsync(string email, string password, string name)
    {
        Guard.Against.NullOrWhiteSpace(email);
        Guard.Against.NullOrWhiteSpace(password);
        Guard.Against.NullOrWhiteSpace(name);

        // Check for existing user
        var existing = await new QueryBuilder<User>()
            .SelectAll()
            .Where(u => u.Email == email)
            .ToListAsync(_connection);

        if (existing.Any())
            return AuthResult.Fail("Email already registered");

        var user = new User
        {
            Id = Guid.NewGuid(),
            Email = email.ToLowerInvariant(),
            PasswordHash = BCrypt.Net.BCrypt.HashPassword(password, workFactor: 12),
            Name = name,
            CreatedAt = DateTime.UtcNow
        };

        await _connection.InsertAsync(user);

        var tokens = GenerateTokens(user);
        return AuthResult.Success(tokens.AccessToken, tokens.RefreshToken);
    }

    public async Task<AuthResult> LoginAsync(string email, string password)
    {
        Guard.Against.NullOrWhiteSpace(email);
        Guard.Against.NullOrWhiteSpace(password);

        var users = await new QueryBuilder<User>()
            .SelectAll()
            .Where(u => u.Email == email.ToLowerInvariant())
            .ToListAsync(_connection);

        var user = users.FirstOrDefault();
        if (user == null || !BCrypt.Net.BCrypt.Verify(password, user.PasswordHash))
            return AuthResult.Fail("Invalid email or password");

        var tokens = GenerateTokens(user);
        return AuthResult.Success(tokens.AccessToken, tokens.RefreshToken);
    }

    private (string AccessToken, string RefreshToken) GenerateTokens(User user)
    {
        var key = new SymmetricSecurityKey(
            Encoding.UTF8.GetBytes(_config["Jwt:SigningKey"]!));

        var claims = new[]
        {
            new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),
            new Claim(ClaimTypes.Email, user.Email),
            new Claim(ClaimTypes.Name, user.Name),
            new Claim("jti", Guid.NewGuid().ToString())
        };

        var accessToken = new JwtSecurityToken(
            issuer: _config["Jwt:Issuer"],
            audience: _config["Jwt:Audience"],
            claims: claims,
            expires: DateTime.UtcNow.AddMinutes(15),
            signingCredentials: new SigningCredentials(key, SecurityAlgorithms.HmacSha256));

        var refreshToken = Convert.ToBase64String(RandomNumberGenerator.GetBytes(64));

        return (new JwtSecurityTokenHandler().WriteToken(accessToken), refreshToken);
    }
}

Plating

Production API authentication is a solved problem when you follow the right patterns. BCrypt handles password hashing with configurable work factors that grow with Moore's Law. Short-lived JWTs combined with refresh tokens provide a secure session model that limits the blast radius of token theft.

The key takeaways from this recipe: never store passwords in plaintext, always use a high work factor for BCrypt, keep access tokens short-lived (15 minutes or less), use refresh tokens for session continuity, and always transmit tokens over HTTPS.

By combining these practices with Guardian's input validation and Tuxedo's clean data access, you get an authentication layer that's secure by default and easy to reason about. No magic frameworks, no third-party dependencies you can't audit \u2014 just straightforward, testable code that follows industry best practices.

Was this recipe helpful?

G

Chef George

.NET Architect & Head Chef

25+ years crafting enterprise C# solutions. Creator of the Noundry platform. Passionate about clean architecture, cloud-agnostic deployments, and code that cooks.