Add CSRF Middleware

Create a middleware to protect your app from Cross-Site Request Forgery (CSRF) attacks.

When using session cookies for authentication, you must protect against CSRF attacks. CSRF is a type of web security vulnerability where an attacker tricks a user into unknowingly submitting a request to a web application where they're already authenticated. Since browsers automatically include cookies with requests, the malicious request appears legitimate to the server. This allows attackers to perform actions on behalf of the user without their consent.

For modern browsers, CSRF attacks can be prevented by using the SameSite cookie attribute. Setting the SameSite cookie attribute to Lax or Strict will ensure that the browser won't send the cookie for cross-site requests, eliminating the threat of CSRF. However, for older browsers that don't support the SameSite cookie attribute, you'll need to implement your own CSRF protection. One common approach for building CSRF protection is to use the Signed Double Submit Cookie pattern.


Implement Signed Double Submit Cookie Pattern

The Signed Double Submit Cookie pattern is a stateless solution for preventing CSRF attacks. For a high-level overview of how the Signed Double Submit Cookie pattern works, please refer to the following documentation.

๐Ÿ“˜

Why Not Use ASP.NET's Built-in Anti-CSRF Functionality

ASP.NET Core's built-in antiforgery system is primarily designed for traditional requests sent through form posts. Protecting session cookies in APIs accessed via JavaScript typically requires additional setup. The method described on this page offers a simpler approach for securing authenticated API calls from JavaScript clients.


Add CSRF Utility Functions

First, you'll need to create a couple of utility functions for use both within the CSRF middleware and the Callback Endpoint.

// CsrfUtils.cs

using System.Security.Cryptography;
using System.Text;

using Microsoft.AspNetCore.Http;

public static class CsrfUtils
{
    private const string _csrfCookieName = "CSRF-TOKEN";
    private const string _csrfTokenHeaderName = "X-CSRF-TOKEN";

    // Generates a new CSRF secret.  The secret will be used to compute the HMAC
    // which in turn will be used as the CSRF token.
    public static string GenerateCsrfSecret()
    {
        var secretBytes = RandomNumberGenerator.GetBytes(32);
        return Convert.ToBase64String(secretBytes);
    }

    // Updates the CSRF cookie with a new CSRF token. The CSRF token is created by computing
    // the HMAC for the given CSRF secret.
    public static void UpdateCsrfTokenCookie(HttpContext httpContext, string csrfSecret)
    {
        using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(csrfSecret));
        var tokenBytes = hmac.ComputeHash(Encoding.UTF8.GetBytes(csrfSecret));
        var csrfToken = Convert.ToBase64String(tokenBytes);

        httpContext.Response.Cookies.Append(_csrfCookieName, csrfToken, new CookieOptions
        {
            HttpOnly = false, // Must be false so that your frontend JavaScript code can access the value.
            Secure = false, // IMPORTANT: Only set this to false if your server isn't using HTTPS.
            SameSite = SameSiteMode.Strict, // This should match the SameSite settings of your session cookie.
            Path = "/",
            Expires = DateTimeOffset.UtcNow.AddMinutes(30), // This value should match your session cookie expiration.
            MaxAge = TimeSpan.FromMinutes(30)
        });
    }

    // Verifies that the CSRF token in the request header was cryptographically generated from 
    // the CSRF secret in your application session.
    public static bool IsCsrfTokenValid(HttpContext httpContext, string csrfSecret)
    {
        var csrfToken = string.Empty;
        if (httpContext.Request.Headers.TryGetValue(_csrfTokenHeaderName, out var token))
        {
            csrfToken = token.FirstOrDefault()?.ToString() ?? string.Empty;
        }

        if (string.IsNullOrEmpty(csrfSecret) || string.IsNullOrEmpty(csrfToken))
        {
            return false;
        }

        using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(csrfSecret));
        var computedHash = hmac.ComputeHash(Encoding.UTF8.GetBytes(csrfSecret));
        var computedToken = Convert.ToBase64String(computedHash);
        return csrfToken == computedToken;
    }
}

Implement CSRF Middleware

Next, we'll create a middleware that will check whether the CSRF token in the request header can be computed using the CSRF secret in the session cookie. If the CSRF token verification fails, then a 403 response will be returned.

// CsrfMiddleware.cs
using Microsoft.AspNetCore.Http;
using Wristband.AspNet.Auth;

public class CsrfMiddleware
{
    private readonly RequestDelegate _next;

    public CsrfMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        // Skip CSRF token validation for endpoints that don't require authentication.
        var endpoint = context.GetEndpoint();
        if (endpoint?.Metadata.GetMetadata<RequireWristbandAuth>() == null)
        {
            await _next(context);
            return;
        }

        // Use the CSRF secret from the session to verify the CSRF token from the 
        // request header.
        var csrfSecret = context.User.Claims.FirstOrDefault(c => c.Type == "csrfSecret")?.Value;
        if (string.IsNullOrEmpty(csrfSecret) || !CsrfUtils.IsCsrfTokenValid(context, csrfSecret))
        {
            context.Response.StatusCode = StatusCodes.Status403Forbidden;
            return;
        }

        // If the CSRF token is successfully verified, then a new CSRF token is generated
        // to extend its expiration period.
        CsrfUtils.UpdateCsrfTokenCookie(context, csrfSecret);
        await _next(context);
    }
}

๐Ÿ“˜

Apply CSRF Middleware to All Protected Endpoints

The above CSRF middleware implementation ensures that the CSRF Token is verified for all endpoints that are annotated with the RequireWristbandAuth metadata attribute. This ensures that any endpoint that requires an authenticated session also requires a valid CSRF Token.


Enable CSRF Middleware

Finally, configure the middleware in your Program.cs file:

// Program.cs

...

app.UseAuthentication();
app.UseMiddleware<AuthMiddleware>();
app.UseMiddleware<CsrfMiddleware>(); // CsrfMiddleware must come after AuthMiddleware.

// API routes
app.MapAuthEndpoints();

...

Whatโ€™s Next

To complete our CSRF protection logic, we'll need to make adjustments to our existing auth endpoints.