using HopFrame.Api.Logic; using HopFrame.Api.Models; using HopFrame.Database; using HopFrame.Database.Models.Entries; using HopFrame.Security.Authentication; using HopFrame.Security.Authorization; using HopFrame.Security.Claims; using HopFrame.Security.Models; using HopFrame.Security.Services; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; namespace HopFrame.Api.Controller; [ApiController] [Route("authentication")] public class SecurityController(TDbContext context, IUserService users, ITokenContext tokenContext) : ControllerBase where TDbContext : HopDbContextBase { [HttpPut("login")] public async Task>> Login([FromBody] UserLogin login) { var user = await users.GetUserByEmail(login.Email); if (user is null) return LogicResult>.NotFound("The provided email address was not found"); if (await users.CheckUserPassword(user, login.Password)) return LogicResult>.Forbidden("The provided password is not correct"); var refreshToken = new TokenEntry { CreatedAt = DateTime.Now, Token = Guid.NewGuid().ToString(), Type = TokenEntry.RefreshTokenType, UserId = user.Id.ToString() }; var accessToken = new TokenEntry { CreatedAt = DateTime.Now, Token = Guid.NewGuid().ToString(), Type = TokenEntry.AccessTokenType, UserId = user.Id.ToString() }; HttpContext.Response.Cookies.Append(ITokenContext.RefreshTokenType, refreshToken.Token, new CookieOptions { MaxAge = HopFrameAuthentication.RefreshTokenTime, HttpOnly = true, Secure = true }); await context.Tokens.AddRangeAsync(refreshToken, accessToken); await context.SaveChangesAsync(); return LogicResult>.Ok(accessToken.Token); } [HttpPost("register")] public async Task>> Register([FromBody] UserRegister register) { if (register.Password.Length < 8) return LogicResult>.Conflict("Password needs to be at least 8 characters long"); var allUsers = await users.GetUsers(); if (allUsers.Any(user => user.Username == register.Username || user.Email == register.Email)) return LogicResult>.Conflict("Username or Email is already registered"); var user = await users.AddUser(register); var refreshToken = new TokenEntry { CreatedAt = DateTime.Now, Token = Guid.NewGuid().ToString(), Type = TokenEntry.RefreshTokenType, UserId = user.Id.ToString() }; var accessToken = new TokenEntry { CreatedAt = DateTime.Now, Token = Guid.NewGuid().ToString(), Type = TokenEntry.AccessTokenType, UserId = user.Id.ToString() }; await context.Tokens.AddRangeAsync(refreshToken, accessToken); await context.SaveChangesAsync(); HttpContext.Response.Cookies.Append(ITokenContext.RefreshTokenType, refreshToken.Token, new CookieOptions { MaxAge = HopFrameAuthentication.RefreshTokenTime, HttpOnly = true, Secure = true }); HttpContext.Response.Cookies.Append(ITokenContext.AccessTokenType, accessToken.Token, new CookieOptions { MaxAge = HopFrameAuthentication.AccessTokenTime, HttpOnly = false, Secure = true }); return LogicResult>.Ok(accessToken.Token); } [HttpGet("authenticate")] public async Task>> Authenticate() { var refreshToken = HttpContext.Request.Cookies[ITokenContext.RefreshTokenType]; if (string.IsNullOrEmpty(refreshToken)) return LogicResult>.Conflict("Refresh token not provided"); var token = await context.Tokens.SingleOrDefaultAsync(token => token.Token == refreshToken && token.Type == TokenEntry.RefreshTokenType); if (token is null) return LogicResult>.NotFound("Refresh token not valid"); if (token.CreatedAt + HopFrameAuthentication.RefreshTokenTime < DateTime.Now) return LogicResult>.Conflict("Refresh token is expired"); var accessToken = new TokenEntry { CreatedAt = DateTime.Now, Token = Guid.NewGuid().ToString(), Type = TokenEntry.AccessTokenType, UserId = token.UserId }; await context.Tokens.AddAsync(accessToken); await context.SaveChangesAsync(); HttpContext.Response.Cookies.Append(ITokenContext.AccessTokenType, accessToken.Token, new CookieOptions { MaxAge = HopFrameAuthentication.AccessTokenTime, HttpOnly = false, Secure = true }); return LogicResult>.Ok(accessToken.Token); } [HttpDelete("logout"), Authorized] public async Task Logout() { var accessToken = HttpContext.User.GetAccessTokenId(); var refreshToken = HttpContext.Request.Cookies[ITokenContext.RefreshTokenType]; if (string.IsNullOrEmpty(accessToken) || string.IsNullOrEmpty(refreshToken)) return LogicResult.Conflict("access or refresh token not provided"); var tokenEntries = await context.Tokens.Where(token => (token.Token == accessToken && token.Type == TokenEntry.AccessTokenType) || (token.Token == refreshToken && token.Type == TokenEntry.RefreshTokenType)) .ToArrayAsync(); if (tokenEntries.Length != 2) return LogicResult.NotFound("One or more of the provided tokens was not found"); context.Tokens.Remove(tokenEntries[0]); context.Tokens.Remove(tokenEntries[1]); await context.SaveChangesAsync(); HttpContext.Response.Cookies.Delete(ITokenContext.RefreshTokenType); HttpContext.Response.Cookies.Delete(ITokenContext.AccessTokenType); return LogicResult.Ok(); } [HttpDelete("delete"), Authorized] public async Task Delete([FromBody] UserPasswordValidation validation) { var user = tokenContext.User; if (await users.CheckUserPassword(user, validation.Password)) return LogicResult.Forbidden("The provided password is not correct"); await users.DeleteUser(user); HttpContext.Response.Cookies.Delete(ITokenContext.RefreshTokenType); return LogicResult.Ok(); } }