add more authentication code

This commit is contained in:
sam 2024-05-19 23:51:53 +02:00
parent aca83fa1ef
commit 04b7cf624d
Signed by: sam
GPG key ID: B4EF20DDE721CAA1
7 changed files with 151 additions and 55 deletions

View file

@ -30,7 +30,7 @@ public partial class RequestSigningService
if (!resp.IsSuccessStatusCode) if (!resp.IsSuccessStatusCode)
{ {
var error = await resp.Content.ReadAsStringAsync(); var error = await resp.Content.ReadAsStringAsync();
throw new FoxchatError.OutgoingFederationError($"Request to {domain}{requestPath} returned an error", DeserializeObject<ApiError>(error)); throw new FoxchatError.OutgoingFederationError($"Request to {domain}{requestPath} returned an error", DeserializeObject<Models.ApiError>(error));
} }
var bodyString = await resp.Content.ReadAsStringAsync(); var bodyString = await resp.Content.ReadAsStringAsync();

View file

@ -1,15 +1,31 @@
using System.Net;
namespace Foxchat.Core; namespace Foxchat.Core;
public class FoxchatError(string message) : Exception(message) public class FoxchatError(string message) : Exception(message)
{ {
public class ApiError(string message) : FoxchatError(message); public class BadRequest(string message) : ApiError(message, statusCode: HttpStatusCode.BadRequest);
public class BadRequest(string message) : ApiError(message); public class IncomingFederationError(string message) : ApiError(message, statusCode: HttpStatusCode.BadRequest);
public class IncomingFederationError(string message) : FoxchatError(message);
public class OutgoingFederationError(string message, Models.ApiError? innerError = null) : FoxchatError(message) public class OutgoingFederationError(
string message, Models.ApiError? innerError = null
) : ApiError(message, statusCode: HttpStatusCode.InternalServerError)
{ {
public Models.ApiError? InnerError => innerError; public Models.ApiError? InnerError => innerError;
} }
public class DatabaseError(string message) : FoxchatError(message); public class DatabaseError(string message) : FoxchatError(message);
public class AuthenticationError(string message) : ApiError(message, statusCode: HttpStatusCode.BadRequest);
}
public class ApiError(string message, HttpStatusCode? statusCode = null) : FoxchatError(message)
{
public readonly HttpStatusCode StatusCode = statusCode ?? HttpStatusCode.InternalServerError;
public class Unauthorized(string message) : ApiError(message, statusCode: HttpStatusCode.Unauthorized);
public class Forbidden(string message, IEnumerable<string>? scopes = null) : ApiError(message, statusCode: HttpStatusCode.Forbidden)
{
public readonly string[] Scopes = scopes?.ToArray() ?? [];
}
} }

View file

@ -1,50 +0,0 @@
using System.Security.Cryptography;
using System.Text.Encodings.Web;
using Foxchat.Core;
using Foxchat.Core.Utils;
using Foxchat.Identity.Database;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.BearerToken;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;
using NodaTime;
namespace Foxchat.Identity.Authorization;
public static class AuthenticationHandlerExtensions
{
public static void AddAuthenticationHandler(this IServiceCollection services)
{
}
}
public class FoxchatAuthenticationHandler(
IOptionsMonitor<BearerTokenOptions> options,
ILoggerFactory logger,
UrlEncoder encoder,
IdentityContext context,
IClock clock
) : AuthenticationHandler<AuthenticationSchemeOptions>(options, logger, encoder)
{
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
var header = Request.Headers.Authorization.ToString();
if (!header.StartsWith("bearer ", StringComparison.InvariantCultureIgnoreCase))
return AuthenticateResult.NoResult();
var token = header[7..];
if (!CryptoUtils.TryFromBase64String(token, out var rawToken))
return AuthenticateResult.Fail(new FoxchatError.BadRequest("Invalid token format"));
var hash = SHA512.HashData(rawToken);
var oauthToken = await context.Tokens
.Include(t => t.Account)
.Include(t => t.Application)
.FirstOrDefaultAsync(t => t.Hash == hash && t.Expires > clock.GetCurrentInstant());
if (oauthToken == null)
return AuthenticateResult.NoResult();
throw new NotImplementedException();
}
}

View file

@ -0,0 +1,77 @@
using System.Security.Cryptography;
using Foxchat.Core;
using Foxchat.Core.Utils;
using Foxchat.Identity.Database;
using Foxchat.Identity.Database.Models;
using Microsoft.EntityFrameworkCore;
using NodaTime;
namespace Foxchat.Identity.Authorization;
public class AuthenticationMiddleware(
IdentityContext db,
IClock clock
) : IMiddleware
{
public async Task InvokeAsync(HttpContext ctx, RequestDelegate next)
{
var endpoint = ctx.GetEndpoint();
var metadata = endpoint?.Metadata.GetMetadata<AuthenticateAttribute>();
if (metadata == null)
{
await next(ctx);
return;
}
var header = ctx.Request.Headers.Authorization.ToString();
if (!header.StartsWith("bearer ", StringComparison.InvariantCultureIgnoreCase))
{
await next(ctx);
return;
}
var token = header[7..];
if (!CryptoUtils.TryFromBase64String(token, out var rawToken))
{
await next(ctx);
return;
}
var hash = SHA512.HashData(rawToken);
var oauthToken = await db.Tokens
.Include(t => t.Account)
.Include(t => t.Application)
.FirstOrDefaultAsync(t => t.Hash == hash && t.Expires > clock.GetCurrentInstant());
if (oauthToken == null)
{
await next(ctx);
return;
}
ctx.SetToken(oauthToken);
await next(ctx);
}
}
public static class HttpContextExtensions
{
private const string Key = "token";
public static void SetToken(this HttpContext ctx, Token token) => ctx.Items.Add(Key, token);
public static Account? GetAccount(this HttpContext ctx) => ctx.GetToken()?.Account;
public static Account GetAccountOrThrow(this HttpContext ctx) =>
ctx.GetAccount() ?? throw new FoxchatError.AuthenticationError("No account in HttpContext");
public static Token? GetToken(this HttpContext ctx)
{
if (ctx.Items.TryGetValue(Key, out var token))
return token as Token;
return null;
}
}
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class AuthenticateAttribute : Attribute;

View file

@ -0,0 +1,38 @@
using System.Net;
using Foxchat.Core;
using Foxchat.Identity.Database;
using NodaTime;
namespace Foxchat.Identity.Authorization;
public class AuthorizationMiddleware(
IdentityContext db,
IClock clock
) : IMiddleware
{
public async Task InvokeAsync(HttpContext ctx, RequestDelegate next)
{
var endpoint = ctx.GetEndpoint();
var attribute = endpoint?.Metadata.GetMetadata<AuthorizeAttribute>();
if (attribute == null)
{
await next(ctx);
return;
}
var token = ctx.GetToken();
if (token == null || token.Expires > clock.GetCurrentInstant())
throw new ApiError.Unauthorized("This endpoint requires an authenticated user.");
if (attribute.Scopes.Length > 0 && attribute.Scopes.Except(token.Scopes).Any())
throw new ApiError.Forbidden("This endpoint requires ungranted scopes.", attribute.Scopes.Except(token.Scopes));
await next(ctx);
}
}
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class AuthorizeAttribute(params string[] scopes) : Attribute
{
public readonly string[] Scopes = scopes;
}

View file

@ -0,0 +1,13 @@
using Foxchat.Identity.Authorization;
namespace Foxchat.Identity.Extensions;
public static class WebApplicationExtensions
{
public static IApplicationBuilder UseCustomMiddleware(this IApplicationBuilder app)
{
return app
.UseMiddleware<AuthenticationMiddleware>()
.UseMiddleware<AuthorizationMiddleware>();
}
}

View file

@ -4,6 +4,7 @@ using Foxchat.Core;
using Foxchat.Identity; using Foxchat.Identity;
using Foxchat.Identity.Database; using Foxchat.Identity.Database;
using Foxchat.Identity.Services; using Foxchat.Identity.Services;
using Foxchat.Identity.Extensions;
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
@ -31,6 +32,7 @@ builder.Services
var app = builder.Build(); var app = builder.Build();
app.UseSerilogRequestLogging(); app.UseSerilogRequestLogging();
app.UseCustomMiddleware();
app.UseRouting(); app.UseRouting();
app.UseSwagger(); app.UseSwagger();
app.UseSwaggerUI(); app.UseSwaggerUI();