diff --git a/Foxchat.Core/FoxchatError.cs b/Foxchat.Core/FoxchatError.cs index 6c5324b..d2f5304 100644 --- a/Foxchat.Core/FoxchatError.cs +++ b/Foxchat.Core/FoxchatError.cs @@ -13,15 +13,13 @@ public class FoxchatError(string message, Exception? inner = null) : Exception(m 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? scopes = null) : ApiError(message, statusCode: HttpStatusCode.Forbidden) { public readonly string[] Scopes = scopes?.ToArray() ?? []; } - public class BadRequest(string message) : ApiError(message, statusCode: HttpStatusCode.BadRequest); + public class NotFound(string message) : ApiError(message, statusCode: HttpStatusCode.NotFound); public class IncomingFederationError(string message) : ApiError(message, statusCode: HttpStatusCode.BadRequest); public class OutgoingFederationError( diff --git a/Foxchat.Identity/Controllers/Oauth/PasswordAuthController.cs b/Foxchat.Identity/Controllers/Oauth/PasswordAuthController.cs index 66df4bf..927fadf 100644 --- a/Foxchat.Identity/Controllers/Oauth/PasswordAuthController.cs +++ b/Foxchat.Identity/Controllers/Oauth/PasswordAuthController.cs @@ -1,12 +1,12 @@ using Foxchat.Identity.Middleware; using Foxchat.Identity.Database; -using Foxchat.Identity.Utils; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Identity; using Foxchat.Identity.Database.Models; using Foxchat.Core; using System.Diagnostics; using NodaTime; +using Microsoft.EntityFrameworkCore; namespace Foxchat.Identity.Controllers.Oauth; @@ -36,13 +36,41 @@ public class PasswordAuthController(ILogger logger, IdentityContext db, IClock c await db.AddAsync(acct); var hashedPassword = await Task.Run(() => _passwordHasher.HashPassword(acct, req.Password)); acct.Password = hashedPassword; + // TODO: make token expiry configurable var (tokenStr, token) = Token.Create(acct, app, req.Scopes, clock.GetCurrentInstant() + Duration.FromDays(365)); await db.AddAsync(token); await db.SaveChangesAsync(); - return Ok(new RegisterResponse(acct.Id, acct.Username, acct.Email, tokenStr)); + return Ok(new AuthResponse(acct.Id, acct.Username, acct.Email, tokenStr)); + } + + [HttpPost("login")] + public async Task Login([FromBody] LoginRequest req) + { + var app = HttpContext.GetApplicationOrThrow(); + var appToken = HttpContext.GetToken() ?? throw new UnreachableException(); + + if (req.Scopes.Except(appToken.Scopes).Any()) + throw new ApiError.Forbidden("Cannot request token scopes that are not allowed for this token", req.Scopes.Except(appToken.Scopes)); + + var acct = await db.Accounts.FirstOrDefaultAsync(a => a.Email == req.Email) + ?? throw new ApiError.NotFound("No user with that email found, or password is incorrect"); + + var pwResult = await Task.Run(() => _passwordHasher.VerifyHashedPassword(acct, acct.Password, req.Password)); + if (pwResult == PasswordVerificationResult.Failed) + throw new ApiError.NotFound("No user with that email found, or password is incorrect"); + if (pwResult == PasswordVerificationResult.SuccessRehashNeeded) + acct.Password = await Task.Run(() => _passwordHasher.HashPassword(acct, req.Password)); + + + var (tokenStr, token) = Token.Create(acct, app, req.Scopes, clock.GetCurrentInstant() + Duration.FromDays(365)); + await db.AddAsync(token); + await db.SaveChangesAsync(); + + return Ok(new AuthResponse(acct.Id, acct.Username, acct.Email, tokenStr)); } public record RegisterRequest(string Username, string Password, string Email, string[] Scopes); - public record RegisterResponse(Ulid Id, string Username, string Email, string Token); + public record LoginRequest(string Email, string Password, string[] Scopes); + public record AuthResponse(Ulid Id, string Username, string Email, string Token); } \ No newline at end of file diff --git a/Foxchat.Identity/Controllers/Oauth/TokenController.cs b/Foxchat.Identity/Controllers/Oauth/TokenController.cs index 5cff74e..829f9ec 100644 --- a/Foxchat.Identity/Controllers/Oauth/TokenController.cs +++ b/Foxchat.Identity/Controllers/Oauth/TokenController.cs @@ -39,14 +39,7 @@ public class TokenController(ILogger logger, IdentityContext db, IClock clock) : { // TODO: make this configurable var expiry = clock.GetCurrentInstant() + Duration.FromDays(365); - var (token, hash) = Token.Generate(); - var tokenObj = new Token - { - Hash = hash, - Scopes = scopes, - Expires = expiry, - ApplicationId = app.Id - }; + var (token, tokenObj) = Token.Create(null, app, scopes, expiry); await db.AddAsync(tokenObj); await db.SaveChangesAsync(); diff --git a/Foxchat.Identity/Middleware/AuthenticationMiddleware.cs b/Foxchat.Identity/Middleware/AuthenticationMiddleware.cs index 663515d..224e485 100644 --- a/Foxchat.Identity/Middleware/AuthenticationMiddleware.cs +++ b/Foxchat.Identity/Middleware/AuthenticationMiddleware.cs @@ -71,6 +71,13 @@ public static class HttpContextExtensions return token as Token; return null; } + + public static Application GetApplicationOrThrow(this HttpContext context) + { + var token = context.GetToken(); + if (token is not { Account: null }) throw new ApiError.Forbidden("This endpoint requires a client token."); + return token.Application; + } } [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] diff --git a/Foxchat.Identity/Utils/OauthUtils.cs b/Foxchat.Identity/Utils/OauthUtils.cs index ae37b1a..26188bb 100644 --- a/Foxchat.Identity/Utils/OauthUtils.cs +++ b/Foxchat.Identity/Utils/OauthUtils.cs @@ -24,11 +24,4 @@ public static class OauthUtils return false; } } - - public static Application GetApplicationOrThrow(this HttpContext context) - { - var token = context.GetToken(); - if (token is not { Account: null }) throw new ApiError.Forbidden("This endpoint requires a client token."); - return token.Application; - } }