1
0

178 lines
6.4 KiB
C#

using System.Security.Claims;
using Flawless.Communication.Request;
using Flawless.Communication.Response;
using Flawless.Communication.Shared;
using Flawless.Server.Models;
using Flawless.Server.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace Flawless.Server.Controllers;
[ApiController, Route("api/auth")]
public class AuthenticationController(
UserManager<AppUser> userManager,
SignInManager<AppUser> signInManager,
TokenGenerationService tokenService,
AppDbContext dbContext,
IConfiguration config,
ILogger<AuthenticationController> logger)
: ControllerBase
{
[HttpGet("status")]
public Task<ActionResult<ServerStatusResponse>> GetServerStatusAsync()
{
return Task.FromResult<ActionResult<ServerStatusResponse>>(Ok(new ServerStatusResponse()
{
AllowPublicRegister = true
}));
}
[HttpPost("register")]
public async Task<ActionResult> PublicRegisterAsync(RegisterRequest request)
{
if (!config.GetValue("User:PublicRegister", true))
return BadRequest(new FailedResponse("Not opened for public register."));
var user = new AppUser
{
UserName = request.Username,
Email = request.Email,
EmailConfirmed = true,
CreatedOn = DateTime.UtcNow,
SecurityStamp = Guid.NewGuid().ToString()
};
var result = await userManager.CreateAsync(user, request.Password);
if (result.Succeeded)
{
logger.LogInformation("User '{0}' created (PUBLIC REGISTER)", user.UserName);
return Ok();
}
logger.LogInformation("User '{0}' NOT created (PUBLIC REGISTER) : {1}", user.UserName, result.Errors);
return BadRequest(new FailedResponse(result.Errors));
}
[HttpPost("login")]
public async Task<ActionResult<TokenInfo>> LoginAsync(LoginRequest r)
{
var user = await userManager.FindByNameAsync(r.Username);
if (user == null) return BadRequest(new FailedResponse("Invalid username or password."));
var result = await signInManager.CheckPasswordSignInAsync(user, r.Password, false);
if (result.Succeeded)
{
var refreshToken = tokenService.GenerateRefreshToken();
var claims = await GetClaimsAsync(user, refreshToken);
var jwtToken = tokenService.GenerateToken(claims);
var exp = DateTime.UtcNow.AddDays(tokenService.RefreshTokenLifeTime);
var refKey = new AppUserRefreshKey
{
UserId = user.Id,
RefreshToken = refreshToken,
ExpireIn = exp,
};
dbContext.RefreshTokens.Add(refKey);
await dbContext.SaveChangesAsync();
await userManager.AddLoginAsync(user, new UserLoginInfo("login-interface", "", null));
return Ok(new TokenInfo { Token = jwtToken, Expiration = exp });
}
if (result.IsLockedOut)
{
return BadRequest(new FailedResponse("Account is locked out."));
}
return BadRequest(new FailedResponse("Invalid username or password."));
}
[HttpPost("refresh")]
public async Task<ActionResult<TokenInfo>> RefreshAsync(TokenInfo r)
{
var now = DateTime.UtcNow;
var set = dbContext.RefreshTokens;
var principal = tokenService.GetPrincipalFromExpiredToken(r.Token);
var user = await userManager.GetUserAsync(principal);
if (user == null) return BadRequest(new FailedResponse("Token is ban. Please login again."));
try
{
// Remove timeout guys
await set.Where(k => k.ExpireIn < now).ExecuteDeleteAsync();
// Find valid expired refresh token
var refreshToken = principal.FindFirst(FlawlessClaimsType.RefreshToken)?.Value;
var tk = await set.FirstOrDefaultAsync(k => k.RefreshToken == refreshToken && k.UserId.ToString() == user.Id.ToString());
if (tk == null) return BadRequest(new FailedResponse("Token is ban. Please login again."));
// Renew keys
refreshToken = tokenService.GenerateRefreshToken();
var claims = await GetClaimsAsync(user, refreshToken);
var newJwtToken = tokenService.GenerateToken(claims);
var exp = DateTime.UtcNow.AddDays(tokenService.RefreshTokenLifeTime);
// Reassign a new key.
set.Remove(tk);
set.Add(new AppUserRefreshKey
{
UserId = user.Id,
RefreshToken = refreshToken,
ExpireIn = exp,
});
return Ok(new TokenInfo { Token = newJwtToken, Expiration = exp });
}
finally
{
await dbContext.SaveChangesAsync();
}
}
[HttpPost("logout_all")]
[Authorize]
public async Task<IActionResult> LogoutAllAsync()
{
var u = (await userManager.GetUserAsync(User))!;
u.RenewSecurityStamp();
// Do not let tokens can be refresh
await dbContext.RefreshTokens.Where(k => k.UserId == u.Id).ExecuteDeleteAsync();
await dbContext.SaveChangesAsync();
return Ok();
}
[HttpPost("renew_password")]
[Authorize]
public async Task<IActionResult> RenewPasswordAsync(ResetPasswordRequest r)
{
var u = (await userManager.GetUserAsync(User))!;
if (string.IsNullOrEmpty(r.OldPassword)) return Unauthorized(new FailedResponse("Old password is empty."));
var result = await userManager.ChangePasswordAsync(u, r.OldPassword, r.NewPassword);
if (!result.Succeeded) return BadRequest(new FailedResponse(result.Errors));
u.RenewSecurityStamp();
await dbContext.SaveChangesAsync();
return Ok();
}
private async ValueTask<IList<Claim>> GetClaimsAsync(AppUser user, string refreshToken)
{
var c = await userManager.GetClaimsAsync(user);
c.Add(new (FlawlessClaimsType.SecurityStamp, user.SecurityStamp ?? string.Empty));
c.Add(new (ClaimTypes.NameIdentifier, user.Id.ToString()));
c.Add(new (FlawlessClaimsType.RefreshToken, refreshToken));
return c;
}
}