using System.Security.Cryptography; using Microsoft.Extensions.Options; using Tiku.Api.Options; namespace Tiku.Api.Middleware; public sealed class BrowserCsrfMiddleware( RequestDelegate next, IOptions options) { public async Task InvokeAsync(HttpContext context) { if (!IsUnsafe(context.Request.Method) || !IsBrowserCookieRequest(context.Request)) { await next(context); return; } if (!IsTrustedOrigin(context.Request) || !HasValidCsrfToken(context.Request)) { context.Response.StatusCode = StatusCodes.Status403Forbidden; await context.Response.WriteAsJsonAsync(new { title = "Browser request origin or CSRF token is invalid.", status = StatusCodes.Status403Forbidden, code = "browser_csrf_rejected", traceId = context.TraceIdentifier }, context.RequestAborted); return; } await next(context); } private static bool IsUnsafe(string method) => !HttpMethods.IsGet(method) && !HttpMethods.IsHead(method) && !HttpMethods.IsOptions(method); private static bool IsBrowserCookieRequest(HttpRequest request) => request.Cookies.ContainsKey(BrowserAuthOptions.AccessCookie) || request.Cookies.ContainsKey(BrowserAuthOptions.RefreshCookie); private bool IsTrustedOrigin(HttpRequest request) { var origin = request.Headers.Origin.ToString().Trim().TrimEnd('/'); if (string.IsNullOrWhiteSpace(origin) || !Uri.TryCreate(origin, UriKind.Absolute, out var uri)) { return false; } var sameOrigin = string.Equals(uri.Scheme, request.Scheme, StringComparison.OrdinalIgnoreCase) && string.Equals(uri.Authority, request.Host.Value, StringComparison.OrdinalIgnoreCase); return sameOrigin || options.Value.AllowedOrigins.Any(allowed => string.Equals(allowed.Trim().TrimEnd('/'), origin, StringComparison.OrdinalIgnoreCase)); } private static bool HasValidCsrfToken(HttpRequest request) { var cookie = request.Cookies[BrowserAuthOptions.CsrfCookie]; var header = request.Headers[BrowserAuthOptions.CsrfHeader].ToString(); if (string.IsNullOrWhiteSpace(cookie) || string.IsNullOrWhiteSpace(header)) { return false; } var left = System.Text.Encoding.UTF8.GetBytes(cookie); var right = System.Text.Encoding.UTF8.GetBytes(header); return left.Length == right.Length && CryptographicOperations.FixedTimeEquals(left, right); } }