feat: implement token generation and issuing on successful login
This commit is contained in:
@@ -3,6 +3,7 @@
|
|||||||
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
|
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<PackageVersion Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.2" />
|
||||||
<PackageVersion Include="Microsoft.AspNetCore.OpenApi" Version="10.0.1" />
|
<PackageVersion Include="Microsoft.AspNetCore.OpenApi" Version="10.0.1" />
|
||||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.2">
|
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.2">
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" />
|
<PackageReference Include="Microsoft.AspNetCore.OpenApi" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design">
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Design">
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
|||||||
@@ -14,26 +14,12 @@ internal static class Endpoint
|
|||||||
|
|
||||||
private static async Task<IResult> HandleAsync(
|
private static async Task<IResult> HandleAsync(
|
||||||
[FromBody] LoginRequest loginRequest,
|
[FromBody] LoginRequest loginRequest,
|
||||||
|
[FromServices] HttpContext httpContext,
|
||||||
[FromServices] AppDbContext appDbContext,
|
[FromServices] AppDbContext appDbContext,
|
||||||
[FromServices] IPasswordHasher passwordHasher
|
[FromServices] IPasswordHasher passwordHasher,
|
||||||
|
[FromServices] TokenService tokenService
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
// TODO: Implement actual auth flow
|
|
||||||
// 1. We want to make sure that
|
|
||||||
// the user exists
|
|
||||||
// 2. We want to make sure that
|
|
||||||
// tha the password is correct
|
|
||||||
// 3. We want to issue an access token
|
|
||||||
// with a refresh token
|
|
||||||
// 4. We want to store the refresh
|
|
||||||
// token
|
|
||||||
// 5. We want to set the refresh
|
|
||||||
// token in a cookie
|
|
||||||
|
|
||||||
// TODO: Things we need
|
|
||||||
// 1. We need a user model
|
|
||||||
// 2. We need a refresh token model
|
|
||||||
|
|
||||||
var user = await appDbContext.Users.SingleOrDefaultAsync(u => u.Username == loginRequest.Username);
|
var user = await appDbContext.Users.SingleOrDefaultAsync(u => u.Username == loginRequest.Username);
|
||||||
|
|
||||||
if (user is null)
|
if (user is null)
|
||||||
@@ -48,6 +34,26 @@ internal static class Endpoint
|
|||||||
return Results.Unauthorized();
|
return Results.Unauthorized();
|
||||||
}
|
}
|
||||||
|
|
||||||
return Results.Ok();
|
var accessToken = tokenService.GenerateAccessToken(user);
|
||||||
|
|
||||||
|
var refreshToken = tokenService.GenerateRefreshToken(user);
|
||||||
|
await appDbContext.RefreshTokens.AddAsync(refreshToken);
|
||||||
|
await appDbContext.SaveChangesAsync();
|
||||||
|
|
||||||
|
httpContext.Response.Cookies.Append(
|
||||||
|
"fiscalos_refresh_cookie",
|
||||||
|
refreshToken.Token,
|
||||||
|
new CookieOptions
|
||||||
|
{
|
||||||
|
HttpOnly = true,
|
||||||
|
Expires = refreshToken.ExpiresAt,
|
||||||
|
// TODO: Revisit when decided
|
||||||
|
// on hosting setup
|
||||||
|
SameSite = SameSiteMode.None,
|
||||||
|
Secure = true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return Results.Ok(new Response(accessToken));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
|
namespace FiscalOS.API.Login;
|
||||||
|
|
||||||
|
internal sealed record JwtOptions
|
||||||
|
{
|
||||||
|
public string Issuer { get; init; } = string.Empty;
|
||||||
|
public string Audience { get; init; } = string.Empty;
|
||||||
|
public string Secret { get; init; } = string.Empty;
|
||||||
|
public int ExpiryInMinutes { get; init; } = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed record JwtOptionsSetup : IConfigureOptions<JwtOptions>
|
||||||
|
{
|
||||||
|
private const string SectionName = nameof(JwtOptions);
|
||||||
|
private readonly IConfiguration _configuration;
|
||||||
|
|
||||||
|
public JwtOptionsSetup(IConfiguration configuration)
|
||||||
|
{
|
||||||
|
_configuration = configuration;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Configure(JwtOptions options)
|
||||||
|
{
|
||||||
|
_configuration.GetSection(SectionName).Bind(options);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
namespace FiscalOS.API.Login;
|
||||||
|
|
||||||
|
internal sealed record Response(string AccessToken);
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
using System.IdentityModel.Tokens.Jwt;
|
||||||
|
using System.Security.Claims;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
using FiscalOS.Core.Identity;
|
||||||
|
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using Microsoft.IdentityModel.Tokens;
|
||||||
|
|
||||||
|
namespace FiscalOS.API.Login;
|
||||||
|
|
||||||
|
internal sealed class TokenService(
|
||||||
|
TimeProvider timeProvider,
|
||||||
|
IOptions<JwtOptions> jwtOptions
|
||||||
|
)
|
||||||
|
{
|
||||||
|
private const int RefreshTokenExpiryInHours = 12;
|
||||||
|
private readonly JwtOptions _jwtOptions = jwtOptions.Value;
|
||||||
|
private readonly TimeProvider _timeProvider = timeProvider;
|
||||||
|
|
||||||
|
public string GenerateAccessToken(User user)
|
||||||
|
{
|
||||||
|
var tokenHandler = new JwtSecurityTokenHandler();
|
||||||
|
var secretKeyBytes = Encoding.UTF8.GetBytes(_jwtOptions.Secret);
|
||||||
|
var issuedAt = _timeProvider.GetUtcNow();
|
||||||
|
var expiresAt = issuedAt.AddMinutes(_jwtOptions.ExpiryInMinutes);
|
||||||
|
List<Claim> claims = [
|
||||||
|
new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
|
||||||
|
new(JwtRegisteredClaimNames.Sub, user.Id.ToString()),
|
||||||
|
];
|
||||||
|
|
||||||
|
var descriptor = new SecurityTokenDescriptor()
|
||||||
|
{
|
||||||
|
Subject = new(claims),
|
||||||
|
IssuedAt = issuedAt.UtcDateTime,
|
||||||
|
Expires = expiresAt.UtcDateTime,
|
||||||
|
Issuer = _jwtOptions.Issuer,
|
||||||
|
Audience = _jwtOptions.Audience,
|
||||||
|
SigningCredentials = new(
|
||||||
|
new SymmetricSecurityKey(secretKeyBytes),
|
||||||
|
SecurityAlgorithms.HmacSha256Signature
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
var securityToken = tokenHandler.CreateJwtSecurityToken(descriptor);
|
||||||
|
var jwtToken = tokenHandler.WriteToken(securityToken);
|
||||||
|
|
||||||
|
return jwtToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
public RefreshToken GenerateRefreshToken(User user)
|
||||||
|
{
|
||||||
|
var expiresAt = _timeProvider
|
||||||
|
.GetUtcNow()
|
||||||
|
.AddHours(RefreshTokenExpiryInHours);
|
||||||
|
|
||||||
|
return RefreshToken.From(user.Id, GenerateToken(), expiresAt);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GenerateToken()
|
||||||
|
{
|
||||||
|
var randomBytes = RandomNumberGenerator.GetBytes(32);
|
||||||
|
var token = Convert.ToBase64String(randomBytes);
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,22 @@ namespace FiscalOS.Core.Identity;
|
|||||||
|
|
||||||
public sealed class RefreshToken : Entity
|
public sealed class RefreshToken : Entity
|
||||||
{
|
{
|
||||||
|
public string Token { get; init; } = string.Empty;
|
||||||
|
public DateTimeOffset ExpiresAt { get; init; }
|
||||||
public Guid UserId { get; init; }
|
public Guid UserId { get; init; }
|
||||||
public User User { get; init; } = new();
|
public User User { get; init; } = new();
|
||||||
|
|
||||||
|
private RefreshToken()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public static RefreshToken From(Guid userId, string token, DateTimeOffset expiresAt)
|
||||||
|
{
|
||||||
|
return new()
|
||||||
|
{
|
||||||
|
UserId = userId,
|
||||||
|
Token = token,
|
||||||
|
ExpiresAt = expiresAt,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user