Implémentation d'une authentification personnalisée par en-tête HTTP dans ASP.NET Core

Dans le cadre du développement d'API internes ou de microservices exposés sur des réseaux de confiance, il est souvent nécessaire de mettre en place un mécanisme d'authentification léger. Cet article détaille la création d'un schéma d'authentification sur mesure basé sur la lecture d'un en-tête HTTP spécifique, en implémentant l'interface IAuthenticationHandler d'ASP.NET Core.

Implémentation du gestionnaire d'authentification

L'interface IAuthenticationHandler requiert l'implémentation de quatre méthodes principales. Le cycle de vie commence par InitializeAsync pour capturer le contexte, suivi de AuthenticateAsync où la logique de validation de l'en-tête est exécutée. Selon le résultat, le framework peut ensuite déléguer le traitement à ChallengeAsync ou ForbidAsync.

public class HeaderBasedAuthHandler : IAuthenticationHandler
{
    private HttpContext _httpContext;
    private AuthenticationScheme _scheme;

    public Task InitializeAsync(AuthenticationScheme scheme, HttpContext context)
    {
        _scheme = scheme;
        _httpContext = context;
        return Task.CompletedTask;
    }

    public Task<AuthenticateResult> AuthenticateAsync()
    {
        // Extraction du jeton depuis l'en-tête personnalisé
        if (!_httpContext.Request.Headers.TryGetValue("X-Api-Session-Token", out var extractedToken))
        {
            return Task.FromResult(AuthenticateResult.NoResult());
        }

        // Validation du jeton via notre gestionnaire de sessions
        var sessionData = EphemeralSessionToken.ValidateToken(extractedToken.ToString());

        if (sessionData == null)
        {
            return Task.FromResult(AuthenticateResult.Fail("Jeton invalide ou session expirée."));
        }

        var ticket = new AuthenticationTicket(sessionData.BuildClaimsPrincipal(), _scheme.Name);
        return Task.FromResult(AuthenticateResult.Success(ticket));
    }

    public Task ChallengeAsync(AuthenticationProperties properties)
    {
        _httpContext.Response.StatusCode = StatusCodes.Status401Unauthorized;
        return Task.CompletedTask;
    }

    public Task ForbidAsync(AuthenticationProperties properties)
    {
        _httpContext.Response.StatusCode = StatusCodes.Status403Forbidden;
        return Task.CompletedTask;
    }
}

Conception du gestionnaire de jetons en mémoire

Pour gérer les sessions de manière éphémère sans base de données externe, nous utilisons une structure en mémoire. L'utilisation de ConcurrentDictionary garantit la sécurité des threads lors des accès simultanés. Ce gestionnaire s'occupe de la génération, de la validation et de la conversion des jetons en ClaimsPrincipal.

public class EphemeralSessionToken
{
    public string UserId { get; set; }
    public string UserRole { get; set; }
    public DateTime ExpirationTime { get; set; }

    private static readonly ConcurrentDictionary<string, EphemeralSessionToken> _activeSessions = new();
    private static readonly TimeSpan DefaultLifespan = TimeSpan.FromMinutes(30);

    public static EphemeralSessionToken ValidateToken(string token)
    {
        if (string.IsNullOrWhiteSpace(token) || token.Length != 32)
        {
            return null;
        }

        if (_activeSessions.TryGetValue(token, out var session))
        {
            if (session.ExpirationTime > DateTime.UtcNow)
            {
                // Rafraîchissement de la durée de vie (sliding expiration)
                session.ExpirationTime = DateTime.UtcNow.Add(DefaultLifespan);
                return session;
            }
            _activeSessions.TryRemove(token, out _);
        }
        return null;
    }

    public static string GenerateToken(string userId = "anonymous", string role = "reader")
    {
        var tokenKey = Guid.NewGuid().ToString("N");
        var newSession = new EphemeralSessionToken
        {
            UserId = userId,
            UserRole = role,
            ExpirationTime = DateTime.UtcNow.Add(DefaultLifespan)
        };

        _activeSessions.TryAdd(tokenKey, newSession);
        return tokenKey;
    }

    public ClaimsPrincipal BuildClaimsPrincipal()
    {
        var claims = new[]
        {
            new Claim(ClaimTypes.NameIdentifier, UserId),
            new Claim(ClaimTypes.Role, UserRole)
        };

        var identity = new ClaimsIdentity(claims, "CustomHeaderAuth");
        return new ClaimsPrincipal(identity);
    }
}

Configuraton du middleware dans le pipeline

Une fois le gestionnaire et le provider de jetons créés, le schéma d'authentification doit être enregistré dans le conteneur de services et activé dans le pipeline de requêtes.

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        // Enregistrement du schéma d'authentification personnalisé
        services.AddAuthentication("CustomHeaderScheme")
            .AddScheme<AuthenticationSchemeOptions, HeaderBasedAuthHandler>("CustomHeaderScheme", null);

        services.AddControllers();
    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        app.UseRouting();
        
        // Activation de l'authentification et de l'autorisation
        app.UseAuthentication();
        app.UseAuthorization();

        app.UseEndpoints(endpoints =>
        {
            endpoints.MapControllers();
        });
    }
}

Sécurisation des contrôleurs

Le mécanisme est désormais opérationnel. Il suffit d'appliquer l'attribut [Authorize] sur les contrôleurs ou les actions spécifiques en précisant le schéma configuré pour restreindre l'accès.

[Authorize(AuthenticationSchemes = "CustomHeaderScheme")]
[ApiController]
[Route("api/[controller]")]
public class DataController : ControllerBase
{
    [HttpGet]
    public IActionResult GetSecureData()
    {
        return Ok("Accès autorisé aux données sécurisées.");
    }
}

Étiquettes: asp-net-core CSharp authentication iauthenticationhandler api-security

Publié le 7 juillet à 02h07