add password-based login endpoint
This commit is contained in:
parent
7959b64fe6
commit
656eec81d8
5 changed files with 40 additions and 21 deletions
|
@ -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<string>? 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(
|
||||
|
|
|
@ -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<IActionResult> 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);
|
||||
}
|
|
@ -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();
|
||||
|
|
|
@ -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)]
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue