add more authentication code
This commit is contained in:
parent
aca83fa1ef
commit
04b7cf624d
7 changed files with 151 additions and 55 deletions
|
@ -30,7 +30,7 @@ public partial class RequestSigningService
|
|||
if (!resp.IsSuccessStatusCode)
|
||||
{
|
||||
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();
|
||||
|
|
|
@ -1,15 +1,31 @@
|
|||
using System.Net;
|
||||
|
||||
namespace Foxchat.Core;
|
||||
|
||||
public class FoxchatError(string message) : Exception(message)
|
||||
{
|
||||
public class ApiError(string message) : FoxchatError(message);
|
||||
public class BadRequest(string message) : ApiError(message);
|
||||
public class IncomingFederationError(string message) : FoxchatError(message);
|
||||
public class BadRequest(string message) : ApiError(message, statusCode: HttpStatusCode.BadRequest);
|
||||
public class IncomingFederationError(string message) : ApiError(message, statusCode: HttpStatusCode.BadRequest);
|
||||
|
||||
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 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() ?? [];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
77
Foxchat.Identity/Authorization/AuthenticationMiddleware.cs
Normal file
77
Foxchat.Identity/Authorization/AuthenticationMiddleware.cs
Normal 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;
|
38
Foxchat.Identity/Authorization/AuthorizationMiddleware.cs
Normal file
38
Foxchat.Identity/Authorization/AuthorizationMiddleware.cs
Normal 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;
|
||||
}
|
13
Foxchat.Identity/Extensions/WebApplicationExtensions.cs
Normal file
13
Foxchat.Identity/Extensions/WebApplicationExtensions.cs
Normal 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>();
|
||||
}
|
||||
}
|
|
@ -4,6 +4,7 @@ using Foxchat.Core;
|
|||
using Foxchat.Identity;
|
||||
using Foxchat.Identity.Database;
|
||||
using Foxchat.Identity.Services;
|
||||
using Foxchat.Identity.Extensions;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
|
@ -31,6 +32,7 @@ builder.Services
|
|||
var app = builder.Build();
|
||||
|
||||
app.UseSerilogRequestLogging();
|
||||
app.UseCustomMiddleware();
|
||||
app.UseRouting();
|
||||
app.UseSwagger();
|
||||
app.UseSwaggerUI();
|
||||
|
|
Loading…
Reference in a new issue