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