forked from gongxuegit/tiku-backend.net
feat(security): add distributed authorization foundation
This commit is contained in:
68
Tiku.Api/Middleware/BrowserCsrfMiddleware.cs
Normal file
68
Tiku.Api/Middleware/BrowserCsrfMiddleware.cs
Normal file
@@ -0,0 +1,68 @@
|
||||
using System.Security.Cryptography;
|
||||
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) =>
|
||||
!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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user