Add CSRF Protection to Backend
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 toStrict
will ensure that the browser won't send the cookie for cross-site requests, eliminating the threat of CSRF. However, for looser SameSite
settings or 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 Synchronizer Token pattern.
Below, we'll show how to use the Synchronizer Token pattern to prevent CSRF attacks. For a high-level overview of how this 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, create a couple of utility functions somewhere in your backend for use within the Callback Endpoint and the auth middleware.
// CsrfUtils.cs
using System.Security.Cryptography;
public static class CsrfUtils
{
private const string _csrfCookieName = "CSRF-TOKEN";
// Generates a new CSRF token.
public static string CreateCsrfSecret()
{
var tokenBytes = RandomNumberGenerator.GetBytes(32);
return Convert.ToHexString(tokenBytes).ToLower();
}
// Updates the CSRF cookie with the CSRF token.
public static void UpdateCsrfCookie(HttpContext httpContext, string csrfToken)
{
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)
});
}
}
Update Callback Endpoint
Next, your Callback Endpoint needs to be updated to create a CSRF token. Once the CSRF token is generated, it will be stored in a CSRF cookie so that the frontend JavaScript code can read it. In addition, the CSRF token will be stored in the session cookie so the CSRF middleware can access and validate it.
// AuthRoutes.cs
...
app.MapGet("/auth/callback", async (HttpContext httpContext, IWristbandAuthService wristbandAuth) =>
{
try
{
var callbackResult = await wristbandAuth.Callback(httpContext);
if (callbackResult.Type == CallbackResultType.REDIRECT_REQUIRED)
{
return Results.Redirect(callbackResult.RedirectUrl);
}
var callbackData = callbackResult.CallbackData;
var userinfo = callbackData.Userinfo;
var claims = new List<Claim>
{
new("accessToken", callbackData.AccessToken),
new("refreshToken", callbackData.RefreshToken ?? string.Empty),
new("expiresAt", $"{DateTimeOffset.Now.ToUnixTimeMilliseconds() + (callbackData.ExpiresIn * 1000)}"),
new("tenantDomainName", callbackData.TenantDomainName),
new("tenantCustomDomain", callbackData.TenantCustomDomain ?? string.Empty),
new("userId", userinfo.TryGetValue("sub", out var userId) ? userId.GetString() : string.Empty),
new("tenantId", userinfo.TryGetValue("tnt_id", out var tenantId) ? tenantId.GetString() : string.Empty),
};
/* ***** BEGIN NEW CSRF LOGIC ***** */
// Generate the CSRF token.
var csrfToken = CsrfUtils.CreateCsrfToken();
// Make sure to add the CSRF token to the session claims.
claims.Add(new Claim("csrfToken", csrfToken));
// Initialize the auth session cookie, which now includes the CSRF csrfToken.
await httpContext.SignInAsync(
CookieAuthenticationDefaults.AuthenticationScheme,
new ClaimsPrincipal(new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme)),
new AuthenticationProperties { IsPersistent = true });
// Create the CSRF token cookie.
CsrfUtils.UpdateCsrfCookie(httpContext, csrfToken);
/* ***** END NEW CSRF LOGIC ***** */
var appUrl = string.IsNullOrWhiteSpace(callbackResult.CallbackData.ReturnUrl)
? "<replace_with_a_default_return_url>"
: callbackResult.CallbackData.ReturnUrl;
return Results.Redirect(appUrl);
} catch (Exception ex)
{
return Results.Problem(detail: $"Unexpected error: {ex.Message}", statusCode: 500);
}
});
...
Update Logout Endpoint
Now, update your Logout Endpoint to delete the CSRF cookie before redirecting to the Wristband Logout Endpoint.
// AuthRoutes.cs
...
app.MapGet("/auth/logout", async (HttpContext httpContext, IWristbandAuthService wristbandAuth) =>
{
try
{
/* ***** BEGIN NEW CSRF LOGIC ***** */
httpContext.Response.Cookies.Delete("CSRF-TOKEN");
/* ***** END NEW CSRF LOGIC ***** */
var refreshToken = httpContext.User.FindFirst("refreshToken")?.Value;
var tenantCustomDomain = httpContext.User.FindFirst("tenantCustomDomain")?.Value;
var tenantDomainName = httpContext.User.FindFirst("tenantDomainName")?.Value;
var logoutConfig = new LogoutConfig
{
RefreshToken = refreshToken,
TenantCustomDomain = tenantCustomDomain,
TenantDomainName = tenantDomainName,
};
await httpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
var wristbandLogoutUrl = await wristbandAuth.Logout(httpContext, logoutConfig);
return Results.Redirect(wristbandLogoutUrl);
}
catch (Exception ex)
{
return Results.Problem(detail: $"Unexpected error: {ex.Message}", statusCode: 500);
}
});
...
Add CSRF Validation to Auth Middleware
Finally, update the auth middleware to check whether the CSRF token in the request header matches the CSRF token that comes from the session cookie. If the CSRF token verification fails, then a 403 response will be returned.
// AuthMiddleware.cs
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Wristband.AspNet.Auth;
public class AuthMiddleware
{
private const string _csrfHeaderName = "X-CSRF-TOKEN";
private readonly RequestDelegate _next;
public AuthMiddleware(RequestDelegate next) => _next = next;
public async Task InvokeAsync(HttpContext context, IWristbandAuthService wristbandAuth)
{
if (context.GetEndpoint()?.Metadata.GetMetadata<RequireWristbandAuth>() == null)
{
await _next(context);
return;
}
if (!await IsAuthenticated(context))
{
context.Response.StatusCode = StatusCodes.Status401Unauthorized;
return;
}
/* ***** BEGIN NEW CSRF LOGIC ***** */
// Validate that the CSRF token is valid.
var csrfToken = context.User.Claims.FirstOrDefault(c => c.Type == "csrfToken")?.Value;
var csrfHeaderValue = context.Request.Headers[_csrfHeaderName].FirstOrDefault();
if (string.IsNullOrEmpty(csrfToken) || csrfToken != csrfHeaderValue)
{
context.Response.StatusCode = StatusCodes.Status403Forbidden;
return;
}
// If CSRF token is successfully verified, update the CSRF cookie to extend its
// expiration period.
CsrfUtils.UpdateCsrfCookie(context, csrfToken);
/* ***** END NEW CSRF LOGIC ***** */
await context.SignInAsync(
CookieAuthenticationDefaults.AuthenticationScheme,
context.User,
new AuthenticationProperties { IsPersistent = true });
await _next(context);
}
private async Task<bool> IsAuthenticated(HttpContext context)
{
var authResult = await context.AuthenticateAsync(CookieAuthenticationDefaults.AuthenticationScheme);
return authResult.Succeeded && authResult.Principal != null;
}
}
Updated 4 days ago
Next, let's enhance the frontend to be able to pass a CSRF request header when making API calls to your backend.