67 lines
2.6 KiB
C#
67 lines
2.6 KiB
C#
using System.Security.Cryptography;
|
|
using System.Text;
|
|
using Microsoft.Extensions.Options;
|
|
using Tiku.Api.Options;
|
|
|
|
namespace Tiku.Api.Middleware;
|
|
|
|
public sealed class BrowserCsrfMiddleware(
|
|
RequestDelegate next,
|
|
IOptions<BrowserAuthOptions> 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)
|
|
{
|
|
return !HttpMethods.IsGet(method) && !HttpMethods.IsHead(method) && !HttpMethods.IsOptions(method);
|
|
}
|
|
|
|
private static bool IsBrowserCookieRequest(HttpRequest request)
|
|
{
|
|
return 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 = Encoding.UTF8.GetBytes(cookie);
|
|
var right = Encoding.UTF8.GetBytes(header);
|
|
return left.Length == right.Length && CryptographicOperations.FixedTimeEquals(left, right);
|
|
}
|
|
} |