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…
	
	Add table
		Add a link
		
	
		Reference in a new issue