Merge branch 'feature/webapi/refresh-token' into develop
This commit is contained in:
9
Butter/Dtos/AuthResultDto.cs
Normal file
9
Butter/Dtos/AuthResultDto.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace Butter.Dtos;
|
||||
|
||||
public class AuthResultDto {
|
||||
public Guid? UserId { get; set; }
|
||||
public string? Token { get; set; }
|
||||
public string? RefreshToken { get; set; }
|
||||
public string? ErrorMessage { get; set; }
|
||||
public bool Success { get; set; }
|
||||
}
|
||||
6
Butter/Dtos/RefreshDto.cs
Normal file
6
Butter/Dtos/RefreshDto.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
namespace Butter.Dtos;
|
||||
|
||||
public class RefreshDto {
|
||||
public required Guid UserId { get; set; }
|
||||
public required string RefreshToken { get; set; }
|
||||
}
|
||||
65
Lactose/Authorization/RefreshTokenAuthorization.cs
Normal file
65
Lactose/Authorization/RefreshTokenAuthorization.cs
Normal file
@@ -0,0 +1,65 @@
|
||||
using Lactose.Repositories;
|
||||
using Lactose.Services;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using System.Security.Claims;
|
||||
|
||||
namespace Lactose.Authorization;
|
||||
|
||||
/*
|
||||
* Technical note:
|
||||
*
|
||||
* The RefreshTokenTransformation is used to add a claim to the user's claims principal that indicates if
|
||||
* the user's login is still valid. while UnexpiredLoginRequirement is used to check the claim.
|
||||
*
|
||||
* More Claim Transformation and Requirements can be added with IClaimsTransformation IAuthorizationRequirement and
|
||||
* added to Program.cs
|
||||
*/
|
||||
|
||||
public class UnexpiredLoginRequirement : IAuthorizationRequirement {
|
||||
|
||||
public class RefreshTokenValidityHandler : AuthorizationHandler<UnexpiredLoginRequirement> {
|
||||
protected override Task HandleRequirementAsync(
|
||||
AuthorizationHandlerContext context,
|
||||
UnexpiredLoginRequirement requirement
|
||||
) {
|
||||
Claim? firstOrDefault
|
||||
= context.User.Claims.FirstOrDefault(claim => claim.Type == RefreshTokenTransformation.ClaimType);
|
||||
|
||||
if (firstOrDefault is not { Value: "true" }) {
|
||||
context.Fail();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
context.Succeed(requirement);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class RefreshTokenTransformation(
|
||||
IUserRepository userRepository,
|
||||
LactoseAuthService authService
|
||||
) : IClaimsTransformation
|
||||
{
|
||||
public const string ClaimType = "LoginValid";
|
||||
public Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal)
|
||||
{
|
||||
ClaimsIdentity claimsIdentity = new ClaimsIdentity();
|
||||
if (principal.HasClaim(claim => claim.Type == ClaimType)) {
|
||||
foreach (ClaimsIdentity identity in principal.Identities) {
|
||||
if (identity.HasClaim(claim => claim.Type == ClaimType)) {
|
||||
identity.RemoveClaim(identity.FindFirst(ClaimType)!);
|
||||
}
|
||||
}
|
||||
}
|
||||
var userData = authService.GetUserData(principal);
|
||||
if (userData == null) return Task.FromResult(principal);
|
||||
var user = userRepository.Find(userData.Id);
|
||||
if (user == null) return Task.FromResult(principal);
|
||||
if (string.IsNullOrEmpty(user.RefreshToken) || user.RefreshTokenExpires == null || user.RefreshTokenExpires < DateTime.Now) return Task.FromResult(principal);
|
||||
claimsIdentity.AddClaim(new Claim(ClaimType, "true", ClaimValueTypes.Boolean));
|
||||
principal.AddIdentity(claimsIdentity);
|
||||
return Task.FromResult(principal);
|
||||
}
|
||||
}
|
||||
@@ -4,8 +4,8 @@ using System.Text;
|
||||
namespace Lactose.Configuration;
|
||||
|
||||
public class SignKeyConfiguration {
|
||||
public const string Position = "SignKey";
|
||||
public string Key { get; set; } = "32_CHARACTERS_KEY_IS_REQUIRED_TO_WORK";
|
||||
public const string SectionName = "SignKey";
|
||||
public string Key { get; set; } = string.Empty;
|
||||
|
||||
public SecurityKey GetSecurityKey() => new SymmetricSecurityKey(Encoding.UTF8.GetBytes(this.Key));
|
||||
}
|
||||
|
||||
21
Lactose/Configuration/SignKeyProvider.cs
Normal file
21
Lactose/Configuration/SignKeyProvider.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Lactose.Configuration;
|
||||
|
||||
public class SignKeyProvider(IConfiguration configuration) : IConfigureOptions<SignKeyConfiguration> {
|
||||
|
||||
const string SectionName = SignKeyConfiguration.SectionName+":Key";
|
||||
public void Configure(SignKeyConfiguration options) {
|
||||
|
||||
if (configuration[SectionName]!.Length <= 32 ) {
|
||||
throw new Exception("SignKey:Key is not 256 bits long");
|
||||
}
|
||||
options.Key = configuration[SectionName] ?? throw new Exception("SignKey:Key is not set");
|
||||
}
|
||||
|
||||
public SignKeyConfiguration Get() {
|
||||
var options = new SignKeyConfiguration();
|
||||
this.Configure(options);
|
||||
return options;
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ using Butter.Types;
|
||||
using Lactose.Models;
|
||||
using Lactose.Repositories;
|
||||
using Lactose.Services;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
@@ -17,9 +18,10 @@ public class AuthController(
|
||||
IUserRepository userRepository,
|
||||
IPasswordHasher<User> passwordHasher
|
||||
) : ControllerBase {
|
||||
//TODO: Reply with Authentication Result
|
||||
|
||||
|
||||
[HttpPost("login")]
|
||||
public ActionResult<string> Login([FromBody] CredentialsDto userDto) {
|
||||
public ActionResult<AuthResultDto> Login([FromBody] CredentialsDto userDto) {
|
||||
User? user;
|
||||
|
||||
if (userDto.Identifier.Contains('@')) {
|
||||
@@ -29,17 +31,32 @@ public class AuthController(
|
||||
//search for user in database (by username)
|
||||
user = userRepository.FindByUsername(userDto.Identifier);
|
||||
}
|
||||
if (user == null) {
|
||||
return NotFound(new AuthResultDto() {
|
||||
Success = false,
|
||||
ErrorMessage = "User or Password was wrong"
|
||||
}
|
||||
);
|
||||
}
|
||||
if (user.BannedAt != null) {
|
||||
return StatusCode(StatusCodes.Status100Continue,new AuthResultDto() {
|
||||
Success = false,
|
||||
ErrorMessage = "User is banned"
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if (user == null) { return NotFound("User or Password was wrong"); }
|
||||
|
||||
if (user.BannedAt != null) { return Forbid("User is banned"); }
|
||||
|
||||
if (user.DeletedAt != null) { return Forbid("User is disabled"); }
|
||||
|
||||
if (user.DeletedAt != null) {
|
||||
return StatusCode(StatusCodes.Status100Continue,new AuthResultDto() {
|
||||
Success = false,
|
||||
ErrorMessage = "User is disabled"
|
||||
}
|
||||
);
|
||||
}
|
||||
PasswordVerificationResult result = passwordHasher.VerifyHashedPassword(user, user.Password, userDto.Password);
|
||||
|
||||
switch (result) {
|
||||
case PasswordVerificationResult.Failed: return NotFound("User or Password was wrong");
|
||||
case PasswordVerificationResult.Failed: return NotFound(new AuthResultDto() { Success = false, ErrorMessage = "User or Password was wrong"});
|
||||
case PasswordVerificationResult.SuccessRehashNeeded:
|
||||
user.Password = passwordHasher.HashPassword(user, userDto.Password);
|
||||
userRepository.Save();
|
||||
@@ -48,8 +65,16 @@ public class AuthController(
|
||||
}
|
||||
|
||||
user.LastLogin = DateTime.Now;
|
||||
|
||||
var token = authService.GenerateAccessToken(user);
|
||||
var refreshToken = authService.GenerateRefreshToken(user);
|
||||
userRepository.Save();
|
||||
return authService.GenerateToken(user);
|
||||
return new AuthResultDto() {
|
||||
UserId = user.Id,
|
||||
Token = token,
|
||||
RefreshToken = refreshToken,
|
||||
Success = true
|
||||
};
|
||||
}
|
||||
|
||||
//TODO: Switch Guid reply with Authentication Result (giving a complete reason in case of failure or giving the authentication token)
|
||||
@@ -82,21 +107,90 @@ public class AuthController(
|
||||
return Ok(user.Id);
|
||||
}
|
||||
|
||||
[Authorize]
|
||||
[HttpPost()]
|
||||
public ActionResult<LactoseAuthService> Check() {
|
||||
public ActionResult<LactoseAuthenticatedUser> Check() {
|
||||
// this.User;
|
||||
//ClaimsPrincipal principal = User;
|
||||
LactoseAuthenticatedUser? identity = authService.GetUserData(User);
|
||||
return Ok(identity);
|
||||
}
|
||||
|
||||
[Authorize]
|
||||
[HttpPost("logout")]
|
||||
public ActionResult Logout() {
|
||||
LactoseAuthenticatedUser? identity = authService.GetUserData(User);
|
||||
|
||||
if (identity == null) { return Unauthorized(); }
|
||||
|
||||
authService.RevokeAccess(identity.Id);
|
||||
//Reset token from the database
|
||||
User? user = userRepository.Find(identity.Id);
|
||||
if (user == null) { return Unauthorized(); }
|
||||
user.RefreshToken = string.Empty;
|
||||
user.RefreshTokenExpires = null;
|
||||
userRepository.Save();
|
||||
|
||||
return Ok();
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Refresh access Token using the Refresh Token
|
||||
*/
|
||||
|
||||
[HttpPost("refresh")]
|
||||
public ActionResult<AuthResultDto> RefreshToken([FromBody] RefreshDto token) {
|
||||
User? user = userRepository.Find(token.UserId);
|
||||
|
||||
if (user == null) {
|
||||
return NotFound(
|
||||
new AuthResultDto() {
|
||||
Success = false,
|
||||
ErrorMessage = "No user found"
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if (user.BannedAt != null) {
|
||||
return StatusCode(
|
||||
StatusCodes.Status100Continue,
|
||||
new AuthResultDto() {
|
||||
Success = false,
|
||||
ErrorMessage = "User is banned"
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if (user.DeletedAt != null) {
|
||||
return StatusCode(
|
||||
StatusCodes.Status100Continue,
|
||||
new AuthResultDto() {
|
||||
Success = false,
|
||||
ErrorMessage = "User is disabled"
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if (!authService.ValidateRefreshToken(user, token.RefreshToken)) {
|
||||
return Unauthorized(
|
||||
new AuthResultDto() {
|
||||
Success = false,
|
||||
ErrorMessage = "Invalid refresh token"
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
var authDto = new AuthResultDto() {
|
||||
Success = true,
|
||||
Token = authService.GenerateAccessToken(user)
|
||||
};
|
||||
|
||||
// TODO: Decide if we want to change or extend refresh token
|
||||
if (user.RefreshTokenExpires > DateTime.Now - TimeSpan.FromMinutes(LactoseAuthService.BaseExpirationTime)) {
|
||||
user.RefreshTokenExpires = DateTime.Now.AddMinutes(LactoseAuthService.LongExpirationTime);
|
||||
userRepository.Save();
|
||||
}
|
||||
|
||||
return authDto;
|
||||
}
|
||||
}
|
||||
|
||||
452
Lactose/Migrations/20250604233318_AddRefreshToken.Designer.cs
generated
Normal file
452
Lactose/Migrations/20250604233318_AddRefreshToken.Designer.cs
generated
Normal file
@@ -0,0 +1,452 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Lactose.Context;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Lactose.Migrations
|
||||
{
|
||||
[DbContext(typeof(LactoseDbContext))]
|
||||
[Migration("20250604233318_AddRefreshToken")]
|
||||
partial class AddRefreshToken
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "8.0.15")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("AlbumAsset", b =>
|
||||
{
|
||||
b.Property<Guid>("AlbumsId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("AssetsId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("AlbumsId", "AssetsId");
|
||||
|
||||
b.HasIndex("AssetsId");
|
||||
|
||||
b.ToTable("AlbumAsset");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AssetTag", b =>
|
||||
{
|
||||
b.Property<Guid>("AssetsId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("TagsId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("AssetsId", "TagsId");
|
||||
|
||||
b.HasIndex("TagsId");
|
||||
|
||||
b.ToTable("AssetTag");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Album", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime?>("CreatedAt")
|
||||
.IsRequired()
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.Property<Guid?>("PersonOwnerId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(255)");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.Property<Guid?>("UserOwnerId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("PersonOwnerId");
|
||||
|
||||
b.HasIndex("UserOwnerId");
|
||||
|
||||
b.ToTable("Albums");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Asset", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.Property<DateTime?>("DeletedAt")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.Property<float?>("Duration")
|
||||
.HasColumnType("real");
|
||||
|
||||
b.Property<long>("FileSize")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<Guid?>("FolderId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<float?>("FrameRate")
|
||||
.HasColumnType("real");
|
||||
|
||||
b.Property<byte[]>("Hash")
|
||||
.IsRequired()
|
||||
.HasColumnType("BYTEA");
|
||||
|
||||
b.Property<bool>("IsPubliclyShared")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("MimeType")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(255)");
|
||||
|
||||
b.Property<string>("OriginalFilename")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(255)");
|
||||
|
||||
b.Property<string>("OriginalPath")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(2048)");
|
||||
|
||||
b.Property<Guid?>("OwnerId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("PreviewPath")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(2048)");
|
||||
|
||||
b.Property<int>("ResolutionHeight")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("ResolutionWidth")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("ThumbnailPath")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(2048)");
|
||||
|
||||
b.Property<int>("Type")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("FolderId");
|
||||
|
||||
b.HasIndex("OwnerId");
|
||||
|
||||
b.ToTable("Assets");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Face", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid?>("AssetId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<int>("BoundingBoxX1")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("BoundingBoxX2")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("BoundingBoxY1")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("BoundingBoxY2")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("ImageHeight")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("ImageWidth")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<Guid?>("PersonId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("AssetId");
|
||||
|
||||
b.HasIndex("PersonId");
|
||||
|
||||
b.ToTable("Faces");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Folder", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<bool>("Active")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("BasePath")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(2048)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Folders");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Person", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.Property<DateTime?>("DeletedAt")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(255)");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("People");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Settings", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<bool>("UserRegistrationOpen")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Settings");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Tag", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(255)");
|
||||
|
||||
b.Property<Guid?>("ParentId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ParentId");
|
||||
|
||||
b.ToTable("Tags");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.User", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<int>("AccessLevel")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<Guid?>("AssetId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime?>("BannedAt")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.Property<DateTime?>("DeletedAt")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.Property<string>("Email")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(128)");
|
||||
|
||||
b.Property<DateTime?>("LastLogin")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.Property<string>("Password")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(255)");
|
||||
|
||||
b.Property<string>("RefreshToken")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(255)");
|
||||
|
||||
b.Property<DateTime?>("RefreshTokenExpires")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.Property<string>("Username")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(64)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("AssetId");
|
||||
|
||||
b.ToTable("Users");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AlbumAsset", b =>
|
||||
{
|
||||
b.HasOne("Lactose.Models.Album", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("AlbumsId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Lactose.Models.Asset", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("AssetsId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AssetTag", b =>
|
||||
{
|
||||
b.HasOne("Lactose.Models.Asset", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("AssetsId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Lactose.Models.Tag", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("TagsId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Album", b =>
|
||||
{
|
||||
b.HasOne("Lactose.Models.Person", "PersonOwner")
|
||||
.WithMany("Albums")
|
||||
.HasForeignKey("PersonOwnerId");
|
||||
|
||||
b.HasOne("Lactose.Models.User", "UserOwner")
|
||||
.WithMany("OwnedAlbums")
|
||||
.HasForeignKey("UserOwnerId");
|
||||
|
||||
b.Navigation("PersonOwner");
|
||||
|
||||
b.Navigation("UserOwner");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Asset", b =>
|
||||
{
|
||||
b.HasOne("Lactose.Models.Folder", "Folder")
|
||||
.WithMany("Assets")
|
||||
.HasForeignKey("FolderId");
|
||||
|
||||
b.HasOne("Lactose.Models.User", "Owner")
|
||||
.WithMany("OwnedAssets")
|
||||
.HasForeignKey("OwnerId");
|
||||
|
||||
b.Navigation("Folder");
|
||||
|
||||
b.Navigation("Owner");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Face", b =>
|
||||
{
|
||||
b.HasOne("Lactose.Models.Asset", "Asset")
|
||||
.WithMany("Faces")
|
||||
.HasForeignKey("AssetId");
|
||||
|
||||
b.HasOne("Lactose.Models.Person", "Person")
|
||||
.WithMany("Faces")
|
||||
.HasForeignKey("PersonId");
|
||||
|
||||
b.Navigation("Asset");
|
||||
|
||||
b.Navigation("Person");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Tag", b =>
|
||||
{
|
||||
b.HasOne("Lactose.Models.Tag", "Parent")
|
||||
.WithMany()
|
||||
.HasForeignKey("ParentId");
|
||||
|
||||
b.Navigation("Parent");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.User", b =>
|
||||
{
|
||||
b.HasOne("Lactose.Models.Asset", null)
|
||||
.WithMany("SharedWith")
|
||||
.HasForeignKey("AssetId");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Asset", b =>
|
||||
{
|
||||
b.Navigation("Faces");
|
||||
|
||||
b.Navigation("SharedWith");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Folder", b =>
|
||||
{
|
||||
b.Navigation("Assets");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Person", b =>
|
||||
{
|
||||
b.Navigation("Albums");
|
||||
|
||||
b.Navigation("Faces");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.User", b =>
|
||||
{
|
||||
b.Navigation("OwnedAlbums");
|
||||
|
||||
b.Navigation("OwnedAssets");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
282
Lactose/Migrations/20250604233318_AddRefreshToken.cs
Normal file
282
Lactose/Migrations/20250604233318_AddRefreshToken.cs
Normal file
@@ -0,0 +1,282 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Lactose.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddRefreshToken : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AlterColumn<DateTime>(
|
||||
name: "UpdatedAt",
|
||||
table: "Users",
|
||||
type: "timestamp without time zone",
|
||||
nullable: true,
|
||||
oldClrType: typeof(DateTime),
|
||||
oldType: "timestamp with time zone",
|
||||
oldNullable: true);
|
||||
|
||||
migrationBuilder.AlterColumn<DateTime>(
|
||||
name: "LastLogin",
|
||||
table: "Users",
|
||||
type: "timestamp without time zone",
|
||||
nullable: true,
|
||||
oldClrType: typeof(DateTime),
|
||||
oldType: "timestamp with time zone",
|
||||
oldNullable: true);
|
||||
|
||||
migrationBuilder.AlterColumn<DateTime>(
|
||||
name: "DeletedAt",
|
||||
table: "Users",
|
||||
type: "timestamp without time zone",
|
||||
nullable: true,
|
||||
oldClrType: typeof(DateTime),
|
||||
oldType: "timestamp with time zone",
|
||||
oldNullable: true);
|
||||
|
||||
migrationBuilder.AlterColumn<DateTime>(
|
||||
name: "CreatedAt",
|
||||
table: "Users",
|
||||
type: "timestamp without time zone",
|
||||
nullable: false,
|
||||
oldClrType: typeof(DateTime),
|
||||
oldType: "timestamp with time zone");
|
||||
|
||||
migrationBuilder.AlterColumn<DateTime>(
|
||||
name: "BannedAt",
|
||||
table: "Users",
|
||||
type: "timestamp without time zone",
|
||||
nullable: true,
|
||||
oldClrType: typeof(DateTime),
|
||||
oldType: "timestamp with time zone",
|
||||
oldNullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "RefreshToken",
|
||||
table: "Users",
|
||||
type: "VARCHAR(255)",
|
||||
nullable: false,
|
||||
defaultValue: "");
|
||||
|
||||
migrationBuilder.AddColumn<DateTime>(
|
||||
name: "RefreshTokenExpires",
|
||||
table: "Users",
|
||||
type: "timestamp without time zone",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AlterColumn<DateTime>(
|
||||
name: "UpdatedAt",
|
||||
table: "People",
|
||||
type: "timestamp without time zone",
|
||||
nullable: true,
|
||||
oldClrType: typeof(DateTime),
|
||||
oldType: "timestamp with time zone",
|
||||
oldNullable: true);
|
||||
|
||||
migrationBuilder.AlterColumn<DateTime>(
|
||||
name: "DeletedAt",
|
||||
table: "People",
|
||||
type: "timestamp without time zone",
|
||||
nullable: true,
|
||||
oldClrType: typeof(DateTime),
|
||||
oldType: "timestamp with time zone",
|
||||
oldNullable: true);
|
||||
|
||||
migrationBuilder.AlterColumn<DateTime>(
|
||||
name: "CreatedAt",
|
||||
table: "People",
|
||||
type: "timestamp without time zone",
|
||||
nullable: false,
|
||||
oldClrType: typeof(DateTime),
|
||||
oldType: "timestamp with time zone");
|
||||
|
||||
migrationBuilder.AlterColumn<DateTime>(
|
||||
name: "UpdatedAt",
|
||||
table: "Assets",
|
||||
type: "timestamp without time zone",
|
||||
nullable: true,
|
||||
oldClrType: typeof(DateTime),
|
||||
oldType: "timestamp with time zone",
|
||||
oldNullable: true);
|
||||
|
||||
migrationBuilder.AlterColumn<byte[]>(
|
||||
name: "Hash",
|
||||
table: "Assets",
|
||||
type: "BYTEA",
|
||||
nullable: false,
|
||||
oldClrType: typeof(byte[]),
|
||||
oldType: "BLOB(128)");
|
||||
|
||||
migrationBuilder.AlterColumn<DateTime>(
|
||||
name: "DeletedAt",
|
||||
table: "Assets",
|
||||
type: "timestamp without time zone",
|
||||
nullable: true,
|
||||
oldClrType: typeof(DateTime),
|
||||
oldType: "timestamp with time zone",
|
||||
oldNullable: true);
|
||||
|
||||
migrationBuilder.AlterColumn<DateTime>(
|
||||
name: "CreatedAt",
|
||||
table: "Assets",
|
||||
type: "timestamp without time zone",
|
||||
nullable: false,
|
||||
oldClrType: typeof(DateTime),
|
||||
oldType: "timestamp with time zone");
|
||||
|
||||
migrationBuilder.AlterColumn<DateTime>(
|
||||
name: "UpdatedAt",
|
||||
table: "Albums",
|
||||
type: "timestamp without time zone",
|
||||
nullable: true,
|
||||
oldClrType: typeof(DateTime),
|
||||
oldType: "timestamp with time zone",
|
||||
oldNullable: true);
|
||||
|
||||
migrationBuilder.AlterColumn<DateTime>(
|
||||
name: "CreatedAt",
|
||||
table: "Albums",
|
||||
type: "timestamp without time zone",
|
||||
nullable: false,
|
||||
oldClrType: typeof(DateTime),
|
||||
oldType: "timestamp with time zone");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "RefreshToken",
|
||||
table: "Users");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "RefreshTokenExpires",
|
||||
table: "Users");
|
||||
|
||||
migrationBuilder.AlterColumn<DateTime>(
|
||||
name: "UpdatedAt",
|
||||
table: "Users",
|
||||
type: "timestamp with time zone",
|
||||
nullable: true,
|
||||
oldClrType: typeof(DateTime),
|
||||
oldType: "timestamp without time zone",
|
||||
oldNullable: true);
|
||||
|
||||
migrationBuilder.AlterColumn<DateTime>(
|
||||
name: "LastLogin",
|
||||
table: "Users",
|
||||
type: "timestamp with time zone",
|
||||
nullable: true,
|
||||
oldClrType: typeof(DateTime),
|
||||
oldType: "timestamp without time zone",
|
||||
oldNullable: true);
|
||||
|
||||
migrationBuilder.AlterColumn<DateTime>(
|
||||
name: "DeletedAt",
|
||||
table: "Users",
|
||||
type: "timestamp with time zone",
|
||||
nullable: true,
|
||||
oldClrType: typeof(DateTime),
|
||||
oldType: "timestamp without time zone",
|
||||
oldNullable: true);
|
||||
|
||||
migrationBuilder.AlterColumn<DateTime>(
|
||||
name: "CreatedAt",
|
||||
table: "Users",
|
||||
type: "timestamp with time zone",
|
||||
nullable: false,
|
||||
oldClrType: typeof(DateTime),
|
||||
oldType: "timestamp without time zone");
|
||||
|
||||
migrationBuilder.AlterColumn<DateTime>(
|
||||
name: "BannedAt",
|
||||
table: "Users",
|
||||
type: "timestamp with time zone",
|
||||
nullable: true,
|
||||
oldClrType: typeof(DateTime),
|
||||
oldType: "timestamp without time zone",
|
||||
oldNullable: true);
|
||||
|
||||
migrationBuilder.AlterColumn<DateTime>(
|
||||
name: "UpdatedAt",
|
||||
table: "People",
|
||||
type: "timestamp with time zone",
|
||||
nullable: true,
|
||||
oldClrType: typeof(DateTime),
|
||||
oldType: "timestamp without time zone",
|
||||
oldNullable: true);
|
||||
|
||||
migrationBuilder.AlterColumn<DateTime>(
|
||||
name: "DeletedAt",
|
||||
table: "People",
|
||||
type: "timestamp with time zone",
|
||||
nullable: true,
|
||||
oldClrType: typeof(DateTime),
|
||||
oldType: "timestamp without time zone",
|
||||
oldNullable: true);
|
||||
|
||||
migrationBuilder.AlterColumn<DateTime>(
|
||||
name: "CreatedAt",
|
||||
table: "People",
|
||||
type: "timestamp with time zone",
|
||||
nullable: false,
|
||||
oldClrType: typeof(DateTime),
|
||||
oldType: "timestamp without time zone");
|
||||
|
||||
migrationBuilder.AlterColumn<DateTime>(
|
||||
name: "UpdatedAt",
|
||||
table: "Assets",
|
||||
type: "timestamp with time zone",
|
||||
nullable: true,
|
||||
oldClrType: typeof(DateTime),
|
||||
oldType: "timestamp without time zone",
|
||||
oldNullable: true);
|
||||
|
||||
migrationBuilder.AlterColumn<byte[]>(
|
||||
name: "Hash",
|
||||
table: "Assets",
|
||||
type: "BLOB(128)",
|
||||
nullable: false,
|
||||
oldClrType: typeof(byte[]),
|
||||
oldType: "BYTEA");
|
||||
|
||||
migrationBuilder.AlterColumn<DateTime>(
|
||||
name: "DeletedAt",
|
||||
table: "Assets",
|
||||
type: "timestamp with time zone",
|
||||
nullable: true,
|
||||
oldClrType: typeof(DateTime),
|
||||
oldType: "timestamp without time zone",
|
||||
oldNullable: true);
|
||||
|
||||
migrationBuilder.AlterColumn<DateTime>(
|
||||
name: "CreatedAt",
|
||||
table: "Assets",
|
||||
type: "timestamp with time zone",
|
||||
nullable: false,
|
||||
oldClrType: typeof(DateTime),
|
||||
oldType: "timestamp without time zone");
|
||||
|
||||
migrationBuilder.AlterColumn<DateTime>(
|
||||
name: "UpdatedAt",
|
||||
table: "Albums",
|
||||
type: "timestamp with time zone",
|
||||
nullable: true,
|
||||
oldClrType: typeof(DateTime),
|
||||
oldType: "timestamp without time zone",
|
||||
oldNullable: true);
|
||||
|
||||
migrationBuilder.AlterColumn<DateTime>(
|
||||
name: "CreatedAt",
|
||||
table: "Albums",
|
||||
type: "timestamp with time zone",
|
||||
nullable: false,
|
||||
oldClrType: typeof(DateTime),
|
||||
oldType: "timestamp without time zone");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,7 @@ namespace Lactose.Migrations
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "9.0.0")
|
||||
.HasAnnotation("ProductVersion", "8.0.15")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
@@ -58,8 +58,9 @@ namespace Lactose.Migrations
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
b.Property<DateTime?>("CreatedAt")
|
||||
.IsRequired()
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.Property<Guid?>("PersonOwnerId")
|
||||
.HasColumnType("uuid");
|
||||
@@ -69,7 +70,7 @@ namespace Lactose.Migrations
|
||||
.HasColumnType("VARCHAR(255)");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.Property<Guid?>("UserOwnerId")
|
||||
.HasColumnType("uuid");
|
||||
@@ -90,10 +91,10 @@ namespace Lactose.Migrations
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.Property<DateTime?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.Property<float?>("Duration")
|
||||
.HasColumnType("real");
|
||||
@@ -109,7 +110,7 @@ namespace Lactose.Migrations
|
||||
|
||||
b.Property<byte[]>("Hash")
|
||||
.IsRequired()
|
||||
.HasColumnType("BLOB(128)");
|
||||
.HasColumnType("BYTEA");
|
||||
|
||||
b.Property<bool>("IsPubliclyShared")
|
||||
.HasColumnType("boolean");
|
||||
@@ -147,7 +148,7 @@ namespace Lactose.Migrations
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
@@ -222,17 +223,17 @@ namespace Lactose.Migrations
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.Property<DateTime?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(255)");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
@@ -286,27 +287,34 @@ namespace Lactose.Migrations
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime?>("BannedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.Property<DateTime?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.Property<string>("Email")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(128)");
|
||||
|
||||
b.Property<DateTime?>("LastLogin")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.Property<string>("Password")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(255)");
|
||||
|
||||
b.Property<string>("RefreshToken")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(255)");
|
||||
|
||||
b.Property<DateTime?>("RefreshTokenExpires")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.Property<string>("Username")
|
||||
.IsRequired()
|
||||
|
||||
@@ -64,6 +64,11 @@ public class User {
|
||||
/// </summary>
|
||||
public EAccessLevel AccessLevel { get; set; }
|
||||
|
||||
[Column(TypeName = "VARCHAR(255)")][JsonIgnore]
|
||||
public string RefreshToken { get; set; } = string.Empty;
|
||||
|
||||
public DateTime? RefreshTokenExpires { get; set; }
|
||||
|
||||
#region Navigation Properties
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
using Lactose.Authorization;
|
||||
using Lactose.Configuration;
|
||||
using Lactose.Context;
|
||||
using Lactose.Models;
|
||||
using Lactose.Repositories;
|
||||
using Lactose.Services;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using Microsoft.OpenApi.Models;
|
||||
@@ -17,13 +20,7 @@ builder.Configuration.AddJsonFile("appsettings.json", true, true)
|
||||
|
||||
builder.Services.Configure<RouteOptions>(options => options.LowercaseUrls = true);
|
||||
|
||||
//TODO: Security Issue, Switch to a more secrete way to pass the key
|
||||
//TODO: Switch to use a binder to Fix configuration not being loaded with GetSection
|
||||
builder.Services.AddOptions<SignKeyConfiguration>().Bind(builder.Configuration.GetSection(SignKeyConfiguration.Position));
|
||||
if (builder.Configuration.Get<SignKeyConfiguration>()!.Key.Length<32) {
|
||||
throw new Exception("Key is not 256 bits long");
|
||||
}
|
||||
|
||||
builder.Services.ConfigureOptions<SignKeyProvider>();
|
||||
|
||||
string? user = builder.Configuration["DatabaseCredentials:UserID"];
|
||||
string? password = builder.Configuration["DatabaseCredentials:Password"];
|
||||
@@ -76,6 +73,7 @@ builder.Services.AddTransient<ITagRepository, TagRepository>();
|
||||
builder.Services.AddTransient<IAssetRepository, AssetRepository>();
|
||||
builder.Services.AddTransient<IAlbumRepository, AlbumRepository>();
|
||||
builder.Services.AddTransient<IMediaRepository, MediaRepository>();
|
||||
builder.Services.AddTransient<ITokenService, TokenService>();
|
||||
builder.Services.AddTransient<LactoseAuthService>();
|
||||
builder.Services.AddEndpointsApiExplorer();
|
||||
builder.Services.AddControllers();
|
||||
@@ -106,7 +104,7 @@ builder.Services.AddSwaggerGen(
|
||||
},
|
||||
|
||||
},
|
||||
new string [] {}
|
||||
[]
|
||||
}
|
||||
});
|
||||
|
||||
@@ -115,7 +113,24 @@ builder.Services.AddSwaggerGen(
|
||||
options.UseInlineDefinitionsForEnums();
|
||||
}
|
||||
);
|
||||
SecurityKey securityKey = new SignKeyProvider(builder.Configuration).Get().GetSecurityKey();
|
||||
|
||||
builder.Services.AddTransient<IClaimsTransformation, RefreshTokenTransformation>();
|
||||
|
||||
var policy = new AuthorizationPolicyBuilder()
|
||||
.AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme)
|
||||
.RequireAuthenticatedUser()
|
||||
.RequireClaim(RefreshTokenTransformation.ClaimType)
|
||||
.Build();
|
||||
|
||||
builder.Services.AddAuthorization(options =>
|
||||
{
|
||||
AuthorizationPolicy defaultPolicy =
|
||||
new AuthorizationPolicyBuilder(JwtBearerDefaults.AuthenticationScheme)
|
||||
.RequireAuthenticatedUser()
|
||||
.RequireClaim(RefreshTokenTransformation.ClaimType,"true")
|
||||
.Build();
|
||||
});
|
||||
|
||||
builder.Services
|
||||
.AddAuthentication(x =>
|
||||
@@ -129,14 +144,13 @@ builder.Services
|
||||
x.SaveToken = true;
|
||||
x.TokenValidationParameters = new TokenValidationParameters
|
||||
{
|
||||
//TODO Load from configuration or file
|
||||
|
||||
IssuerSigningKey = builder.Configuration.Get<SignKeyConfiguration>()!.GetSecurityKey(),
|
||||
IssuerSigningKey = securityKey,
|
||||
ValidateIssuer = false,
|
||||
ValidateAudience = false
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
WebApplication app = builder.Build();
|
||||
|
||||
using (var scope = app.Services.CreateScope()) {
|
||||
|
||||
@@ -2,36 +2,24 @@ using Butter.Types;
|
||||
using Lactose.Configuration;
|
||||
using Lactose.Models;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Security.Claims;
|
||||
|
||||
namespace Lactose.Services;
|
||||
|
||||
public class LactoseAuthService(ILogger<LactoseAuthService> logger,IOptions<SignKeyConfiguration> keyOption) {
|
||||
public class LactoseAuthService(
|
||||
ILogger<LactoseAuthService> logger,
|
||||
ITokenService tokenService)
|
||||
{
|
||||
public const int BaseExpirationTime = 10;
|
||||
public const int LongExpirationTime = 60;
|
||||
|
||||
public string GenerateToken(User user) {
|
||||
var handler = new JwtSecurityTokenHandler();
|
||||
public string GenerateAccessToken(User user) => tokenService.GenerateToken(GenerateClaims(user), BaseExpirationTime);
|
||||
|
||||
SecurityKey key = keyOption.Value.GetSecurityKey();
|
||||
|
||||
if (key.KeySize < 256) {
|
||||
logger.LogError("Signing key is not 256 bits long");
|
||||
throw new Exception("Key is not 256 bits long");
|
||||
}
|
||||
|
||||
var credentials = new SigningCredentials(
|
||||
keyOption.Value.GetSecurityKey(),
|
||||
SecurityAlgorithms.HmacSha256Signature);
|
||||
|
||||
var tokenDescriptor = new SecurityTokenDescriptor {
|
||||
Subject = GenerateClaims(user),
|
||||
Expires = DateTime.UtcNow.AddMinutes(30),
|
||||
SigningCredentials = credentials,
|
||||
};
|
||||
|
||||
var token = handler.CreateToken(tokenDescriptor);
|
||||
return handler.WriteToken(token);
|
||||
public string GenerateRefreshToken(User user) {
|
||||
var token =tokenService.GenerateToken(LongExpirationTime);
|
||||
user.RefreshToken = token;
|
||||
user.RefreshTokenExpires = DateTime.Now.AddMinutes(LongExpirationTime);
|
||||
return token;
|
||||
}
|
||||
|
||||
static ClaimsIdentity GenerateClaims(User user) {
|
||||
@@ -61,8 +49,10 @@ public class LactoseAuthService(ILogger<LactoseAuthService> logger,IOptions<Sign
|
||||
);
|
||||
}
|
||||
|
||||
public void RevokeAccess(Guid id) {
|
||||
//TODO: Revoke access to user
|
||||
public bool ValidateRefreshToken(User user, string tokenRefreshToken) {
|
||||
if (user.RefreshToken != tokenRefreshToken) return false;
|
||||
if (user.RefreshTokenExpires < DateTime.Now) return false;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
36
Lactose/Services/TokenService.cs
Normal file
36
Lactose/Services/TokenService.cs
Normal file
@@ -0,0 +1,36 @@
|
||||
using Lactose.Configuration;
|
||||
using Lactose.Models;
|
||||
using Lactose.Repositories;
|
||||
using Lactose.Utils;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using System.Collections.Concurrent;
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Security.Claims;
|
||||
|
||||
namespace Lactose.Services;
|
||||
|
||||
public interface ITokenService {
|
||||
string GenerateToken(ClaimsIdentity identity, int expireTime);
|
||||
string GenerateToken(int expireTime);
|
||||
}
|
||||
|
||||
public class TokenService (
|
||||
IOptions<SignKeyConfiguration> signKey
|
||||
) : ITokenService {
|
||||
|
||||
public const int BaseExpirationTime = 30;
|
||||
|
||||
readonly TokenGenerator tokenGenerator = new TokenGenerator(
|
||||
new SigningCredentials(signKey.Value.GetSecurityKey(), SecurityAlgorithms.HmacSha256Signature),
|
||||
descriptor => {
|
||||
var handler = new JwtSecurityTokenHandler();
|
||||
var token = handler.CreateToken(descriptor);
|
||||
return handler.WriteToken(token);
|
||||
}
|
||||
);
|
||||
|
||||
public string GenerateToken(ClaimsIdentity identity, int expireTime = BaseExpirationTime) => tokenGenerator.Generate(identity, expireTime);
|
||||
|
||||
public string GenerateToken(int expireTime = BaseExpirationTime) => tokenGenerator.Generate(expireTime);
|
||||
}
|
||||
66
Lactose/Utils/TokenGeneratorProvvider.cs
Normal file
66
Lactose/Utils/TokenGeneratorProvvider.cs
Normal file
@@ -0,0 +1,66 @@
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using System.Security.Claims;
|
||||
|
||||
namespace Lactose.Utils;
|
||||
|
||||
public class TokenBuilder {
|
||||
|
||||
SigningCredentials? credentials;
|
||||
int expireTime;
|
||||
ClaimsIdentity? claims;
|
||||
|
||||
public TokenBuilder(SigningCredentials? signingCredentials = null, int expireTime = 10) {
|
||||
this.credentials = signingCredentials;
|
||||
this.expireTime = expireTime;
|
||||
}
|
||||
|
||||
public TokenBuilder ExpireTime(int minutes) {
|
||||
this.expireTime = expireTime;
|
||||
return this;
|
||||
}
|
||||
|
||||
public TokenBuilder SigningCredentials(SigningCredentials signingCredentials) {
|
||||
this.credentials = credentials;
|
||||
return this;
|
||||
}
|
||||
|
||||
public TokenBuilder SetClaims(ClaimsIdentity identity) {
|
||||
this.claims = identity;
|
||||
return this;
|
||||
}
|
||||
|
||||
private SecurityTokenDescriptor PrepareSecurityTokenDescriptor() {
|
||||
var descriptor = new SecurityTokenDescriptor {
|
||||
SigningCredentials = credentials,
|
||||
Expires = DateTime.UtcNow
|
||||
};
|
||||
if (expireTime>0) descriptor.Expires = DateTime.UtcNow.AddMinutes(expireTime);
|
||||
if (claims != null) descriptor.Subject = claims;
|
||||
return descriptor;
|
||||
}
|
||||
|
||||
public SecurityToken GenerateToken(SecurityTokenHandler handler) => GenerateToken(handler.CreateToken);
|
||||
|
||||
|
||||
public T GenerateToken<T>(Func<SecurityTokenDescriptor, T> handler) {
|
||||
T token = handler(PrepareSecurityTokenDescriptor());
|
||||
this.claims = null;
|
||||
return token;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public class TokenGenerator(SigningCredentials credentials, Func<SecurityTokenDescriptor, string> handler) {
|
||||
readonly TokenBuilder builder = new TokenBuilder(credentials);
|
||||
|
||||
public string Generate(ClaimsIdentity claims, int expireTime) {
|
||||
builder.ExpireTime(expireTime);
|
||||
builder.SetClaims(claims);
|
||||
return builder.GenerateToken(handler);
|
||||
}
|
||||
public string Generate(int expireTime) {
|
||||
builder.ExpireTime(expireTime);
|
||||
return builder.GenerateToken(handler);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -44,9 +44,10 @@ Content-Type: application/json
|
||||
}
|
||||
|
||||
> {%
|
||||
client.global.set("auth_token", response.body);
|
||||
client.global.set("auth_token", jsonPath(response.body, "$.token"));
|
||||
client.test("Login rng user", function () {
|
||||
client.assert(response.status === 200)
|
||||
//client.assert(jsonPath(response.body, "$.success") != true,jsonPath(response.body, "$.errorMessage")??"null")
|
||||
});
|
||||
%}
|
||||
|
||||
@@ -59,7 +60,7 @@ Content-Type: application/json
|
||||
|
||||
> {%
|
||||
client.test("API Authenticated Empty", function () {
|
||||
client.assert(response.status === 204)
|
||||
client.assert(response.status === 401)
|
||||
});
|
||||
%}
|
||||
|
||||
@@ -95,9 +96,10 @@ Content-Type: application/json
|
||||
}
|
||||
|
||||
> {%
|
||||
client.global.set("admin_token", response.body);
|
||||
client.global.set("admin_token", jsonPath(response.body, "$.token"));
|
||||
client.test("Login as admin", function () {
|
||||
client.assert(response.status === 200)
|
||||
//client.assert(jsonPath(response.body, "$.success") != true,jsonPath(response.body, "$.errorMessage")??"null")
|
||||
});
|
||||
%}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user