From d3ae3cdc773d513e72f2fb0ffe317bdff634d9a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Grzegorczyk?= Date: Fri, 16 Feb 2024 16:22:46 +0100 Subject: [PATCH] Closes #16 Backend integration with Keycloak tokens. --- README.md | 17 ++ backend/Dockerfile | 2 +- backend/src/api/Constants/ErrorMessages.cs | 6 + .../Controllers/AuthenticationController.cs | 92 +++--- .../api/Controllers/GreetingsController.cs | 4 +- backend/src/api/Data/DAO/RefreshToken.cs | 19 -- backend/src/api/Data/DAO/UserModel.cs | 9 +- backend/src/api/Data/DTO/AuthResultDTO.cs | 10 - backend/src/api/Data/DTO/TokenRequestDTO.cs | 12 - backend/src/api/Data/DTO/UserDTO.cs | 9 +- .../{JwtOptions.cs => KeycloakJwtOptions.cs} | 2 +- backend/src/api/Data/UsersDbContext.cs | 4 +- backend/src/api/LoggingExtensions.cs | 45 +++ .../20240214220927_KeycloakUser.Designer.cs | 50 ++++ .../Migrations/20240214220927_KeycloakUser.cs | 271 +++++++++++++++++ .../Migrations/UsersDbContextModelSnapshot.cs | 277 +----------------- backend/src/api/Program.cs | 156 +++++----- .../src/api/Services/Tokens/ITokenService.cs | 10 - .../src/api/Services/Tokens/TokenService.cs | 72 ----- .../src/api/Services/Users/IUserService.cs | 8 +- backend/src/api/Services/Users/UserService.cs | 91 +----- backend/src/api/api.csproj | 1 + backend/src/api/appsettings.json | 10 +- global.json | 2 +- 24 files changed, 552 insertions(+), 627 deletions(-) create mode 100644 backend/src/api/Constants/ErrorMessages.cs delete mode 100644 backend/src/api/Data/DAO/RefreshToken.cs delete mode 100644 backend/src/api/Data/DTO/AuthResultDTO.cs delete mode 100644 backend/src/api/Data/DTO/TokenRequestDTO.cs rename backend/src/api/Data/{JwtOptions.cs => KeycloakJwtOptions.cs} (82%) create mode 100644 backend/src/api/LoggingExtensions.cs create mode 100644 backend/src/api/Migrations/20240214220927_KeycloakUser.Designer.cs create mode 100644 backend/src/api/Migrations/20240214220927_KeycloakUser.cs delete mode 100644 backend/src/api/Services/Tokens/ITokenService.cs delete mode 100644 backend/src/api/Services/Tokens/TokenService.cs diff --git a/README.md b/README.md index 4da4ea6..fb49684 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,23 @@ To run this application locally it is recommended to have the following installe - dotnet sdk - entity framework tools +Firstly there is a need to configure environment variables: + +1. Copy `.env.example` as `.env` and populate the environment variables. +1. Copy `appsettings.json` as `appsettings.Development.json` and populate the variables. + +Next install dev-certs to use https in powershell + +```powershell +dotnet dev-certs https -ep ".aspnet\https\aspnetapp.pfx" -p devcertpasswd --trust +``` + +or in bash/zsh + +```bash +dotnet dev-certs https -ep .aspnet/https/aspnetapp.pfx -p devcertpasswd --trust +``` + Next go to the `scripts` directory and run `apply_migrations.ps1` Next You should go back to the main directory and run `docker compose up --build` This can be done with the following snippet. diff --git a/backend/Dockerfile b/backend/Dockerfile index 1f39250..5990f8d 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -1,6 +1,6 @@ #See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging. -FROM mcr.microsoft.com/dotnet/aspnet:7.0.2-alpine3.17-amd64 AS base +FROM mcr.microsoft.com/dotnet/aspnet:7.0.10-alpine3.18 AS base WORKDIR /app EXPOSE 80 EXPOSE 443 diff --git a/backend/src/api/Constants/ErrorMessages.cs b/backend/src/api/Constants/ErrorMessages.cs new file mode 100644 index 0000000..12f82b7 --- /dev/null +++ b/backend/src/api/Constants/ErrorMessages.cs @@ -0,0 +1,6 @@ +namespace api.Constants; + +public static class ErrorMessages +{ + public const string UserAlreadyExists = "User already exists"; +} diff --git a/backend/src/api/Controllers/AuthenticationController.cs b/backend/src/api/Controllers/AuthenticationController.cs index 480f258..c6af8b8 100644 --- a/backend/src/api/Controllers/AuthenticationController.cs +++ b/backend/src/api/Controllers/AuthenticationController.cs @@ -1,7 +1,9 @@ -using api.Data.DTO; +using api.Constants; +using api.Data.DTO; using api.Services.Users; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using System.IdentityModel.Tokens.Jwt; namespace api.Controllers; @@ -10,77 +12,53 @@ namespace api.Controllers; public class AuthenticationController : ControllerBase { private readonly IUserService _userService; + private readonly ILogger _logger; - public AuthenticationController(IUserService userService) + public AuthenticationController(IUserService userService, ILogger logger) { _userService = userService; + _logger = logger; } - [HttpPost("register")] - [ProducesResponseType(200)] - [ProducesResponseType(400)] - public async Task Register([FromBody] UserDTO user) + [HttpGet("create-user")] + [Authorize] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + public async Task CreateUser() { - if (!ModelState.IsValid) { return BadRequest("Invalid data provided!"); } + var accessToken = Request.Headers["Authorization"].ToString().Replace("Bearer ", ""); + var jsonTokenData = new JwtSecurityTokenHandler().ReadJwtToken(accessToken); + var keycloakUuid = jsonTokenData.Subject; + var userEmail = jsonTokenData.Claims.FirstOrDefault(claim => claim.Type == "email")?.Value; - var (IsSuccess, Error) = await _userService.Register(user); - if (IsSuccess) + if (userEmail == null) { - return Ok("User created"); - } - else - { - return BadRequest(Error); + _logger.LogInformation("User with keycloakUuid={keycloakUuid} tried to log in without email.", keycloakUuid); + return BadRequest("Required data missing"); } - } - [HttpPost("login")] - [ProducesResponseType(200)] - [ProducesResponseType(400)] - [ProducesResponseType(401)] - public async Task Login([FromBody] UserDTO user) - { - if (!ModelState.IsValid) { return BadRequest("Please provide login credentials!"); } + var (isSuccess, error) = await _userService.CreateKeycloakUser( + new UserDto + { + KeycloakUuid = keycloakUuid, + Email = userEmail + } + ); - var (IsSuccess, AuthResult, Error) = await _userService.Login(user); - if (IsSuccess) + if (isSuccess) { - return Ok(AuthResult); - } - else - { - return Unauthorized(Error); - } - } - - [HttpPost("refresh-token")] - [ProducesResponseType(200)] - [ProducesResponseType(400)] - [ProducesResponseType(401)] - public async Task RefreshToken([FromBody] TokenRequestDTO tokenRequestDTO) - { - if (!ModelState.IsValid) { return BadRequest("Invalid token request."); } - var (IsSuccess, AuthResult, Error) = await _userService.RefreshLogin(tokenRequestDTO); - if (IsSuccess) - { - return Ok(AuthResult); + _logger.LogInformation("The user {email} has been created successfully.", userEmail); + return Ok("User created"); } else { - return Unauthorized(Error); - } - } - - [HttpPost("logout")] - [ProducesResponseType(200)] - [Authorize()] - public async Task Logout() - { - if (HttpContext.User?.Identity?.Name is not null) - { - await _userService.Logout(HttpContext.User.Identity.Name); + if (error == ErrorMessages.UserAlreadyExists) + { + _logger.LogInformation("The user {email} has already been created.", userEmail); + return Ok("User is already created"); + } + _logger.LogInformation("Failure during creation of {email} user.", userEmail); + return BadRequest(error); } - return Ok("The user has been logged out."); } - } diff --git a/backend/src/api/Controllers/GreetingsController.cs b/backend/src/api/Controllers/GreetingsController.cs index 68d178b..665bed0 100644 --- a/backend/src/api/Controllers/GreetingsController.cs +++ b/backend/src/api/Controllers/GreetingsController.cs @@ -9,7 +9,7 @@ public class GreetingsController : ControllerBase { [HttpGet] [Authorize] - [ProducesResponseType(200)] + [ProducesResponseType(StatusCodes.Status200OK)] [Route("user")] public IActionResult Greet() { @@ -17,7 +17,7 @@ public IActionResult Greet() } [HttpGet] - [ProducesResponseType(200)] + [ProducesResponseType(StatusCodes.Status200OK)] [Route("HelloWorld")] public IActionResult HelloWorld() { diff --git a/backend/src/api/Data/DAO/RefreshToken.cs b/backend/src/api/Data/DAO/RefreshToken.cs deleted file mode 100644 index c63d6d2..0000000 --- a/backend/src/api/Data/DAO/RefreshToken.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System; -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; - -namespace api.Data.DAO; - -public class RefreshToken -{ - [Key] - public int Id { get; set; } - public string Token { get; set; } - public string JwtId { get; set; } - public bool IsRevoked { get; set; } - public DateTime DateAdded { get; set; } - public DateTime DateExpire { get; set; } - public string UserId { get; set; } - [ForeignKey(nameof(UserId))] - public UserModel User { get; set; } -} diff --git a/backend/src/api/Data/DAO/UserModel.cs b/backend/src/api/Data/DAO/UserModel.cs index 3b7faec..12253d3 100644 --- a/backend/src/api/Data/DAO/UserModel.cs +++ b/backend/src/api/Data/DAO/UserModel.cs @@ -1,7 +1,12 @@ -using Microsoft.AspNetCore.Identity; +using System.ComponentModel.DataAnnotations; namespace api.Data.DAO; -public class UserModel : IdentityUser +public class UserModel { + [Key] + public int Id { get; set; } + public required string KeycloakUuid { get; set; } + [EmailAddress] + public required string Email { get; set; } } diff --git a/backend/src/api/Data/DTO/AuthResultDTO.cs b/backend/src/api/Data/DTO/AuthResultDTO.cs deleted file mode 100644 index 2eb5168..0000000 --- a/backend/src/api/Data/DTO/AuthResultDTO.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System; - -namespace api.Data.DTO; - -public class AuthResultDTO -{ - public string Token { get; set; } - public string RefreshToken { get; set; } - public DateTime ExpiresAt { get; set; } -} diff --git a/backend/src/api/Data/DTO/TokenRequestDTO.cs b/backend/src/api/Data/DTO/TokenRequestDTO.cs deleted file mode 100644 index cd6b123..0000000 --- a/backend/src/api/Data/DTO/TokenRequestDTO.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System.ComponentModel.DataAnnotations; - -namespace api.Data.DTO; - -public class TokenRequestDTO -{ - - [Required] - public string Token { get; set; } - [Required] - public string RefreshToken { get; set; } -} diff --git a/backend/src/api/Data/DTO/UserDTO.cs b/backend/src/api/Data/DTO/UserDTO.cs index 70517f3..c7397d9 100644 --- a/backend/src/api/Data/DTO/UserDTO.cs +++ b/backend/src/api/Data/DTO/UserDTO.cs @@ -2,12 +2,11 @@ namespace api.Data.DTO; -public class UserDTO +public class UserDto { [Required] - [EmailAddress] - public string Email { get; set; } - + public required string KeycloakUuid { get; set; } [Required] - public string Password { get; set; } + [EmailAddress] + public required string Email { get; set; } } diff --git a/backend/src/api/Data/JwtOptions.cs b/backend/src/api/Data/KeycloakJwtOptions.cs similarity index 82% rename from backend/src/api/Data/JwtOptions.cs rename to backend/src/api/Data/KeycloakJwtOptions.cs index 2e3867e..27fd569 100644 --- a/backend/src/api/Data/JwtOptions.cs +++ b/backend/src/api/Data/KeycloakJwtOptions.cs @@ -1,5 +1,5 @@ namespace api.Data; -public class JwtOptions +public class KeycloakJwtOptions { public string? Issuer { get; set; } public string? Audience { get; set; } diff --git a/backend/src/api/Data/UsersDbContext.cs b/backend/src/api/Data/UsersDbContext.cs index f61e87d..ec58422 100644 --- a/backend/src/api/Data/UsersDbContext.cs +++ b/backend/src/api/Data/UsersDbContext.cs @@ -4,10 +4,10 @@ namespace api.Data; -public class UsersDbContext : IdentityDbContext +public class UsersDbContext : DbContext { public UsersDbContext(DbContextOptions options) : base(options) { } - public DbSet RefreshTokens { get; set; } + public DbSet Users { get; set; } } diff --git a/backend/src/api/LoggingExtensions.cs b/backend/src/api/LoggingExtensions.cs new file mode 100644 index 0000000..a359a02 --- /dev/null +++ b/backend/src/api/LoggingExtensions.cs @@ -0,0 +1,45 @@ +using Serilog; +using Serilog.Configuration; +using Serilog.Core; +using Serilog.Events; + +namespace api; + +public static class LoggingExtensions +{ + public static LoggerConfiguration WithCorrelationId(this LoggerEnrichmentConfiguration config) + => config.With(new CorrelationIdEnricher()); +} + +public class CorrelationIdEnricher : ILogEventEnricher +{ + private const string _propertyName = "CorelationId"; + private readonly IHttpContextAccessor _contextAccessor; + + public CorrelationIdEnricher() + { + _contextAccessor = new HttpContextAccessor(); + } + + public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory) + { + var httpContext = _contextAccessor.HttpContext; + if (httpContext == null) + { + return; + } + + if (httpContext.Items[_propertyName] is LogEventProperty logEventProperty) + { + logEvent.AddPropertyIfAbsent(logEventProperty); + return; + } + + var correlationId = Guid.NewGuid().ToString(); + + var correlationIdProperty = new LogEventProperty(_propertyName, new ScalarValue(correlationId)); + logEvent.AddOrUpdateProperty(correlationIdProperty); + + httpContext.Items.Add(_propertyName, correlationIdProperty); + } +} diff --git a/backend/src/api/Migrations/20240214220927_KeycloakUser.Designer.cs b/backend/src/api/Migrations/20240214220927_KeycloakUser.Designer.cs new file mode 100644 index 0000000..e4d23d1 --- /dev/null +++ b/backend/src/api/Migrations/20240214220927_KeycloakUser.Designer.cs @@ -0,0 +1,50 @@ +// +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using api.Data; + +#nullable disable + +namespace api.Migrations +{ + [DbContext(typeof(UsersDbContext))] + [Migration("20240214220927_KeycloakUser")] + partial class KeycloakUser + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "7.0.2") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("api.Data.DAO.UserModel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("KeycloakUuid") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Users"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/backend/src/api/Migrations/20240214220927_KeycloakUser.cs b/backend/src/api/Migrations/20240214220927_KeycloakUser.cs new file mode 100644 index 0000000..eeadbc1 --- /dev/null +++ b/backend/src/api/Migrations/20240214220927_KeycloakUser.cs @@ -0,0 +1,271 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace api.Migrations +{ + /// + public partial class KeycloakUser : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "AspNetRoleClaims"); + + migrationBuilder.DropTable( + name: "AspNetUserClaims"); + + migrationBuilder.DropTable( + name: "AspNetUserLogins"); + + migrationBuilder.DropTable( + name: "AspNetUserRoles"); + + migrationBuilder.DropTable( + name: "AspNetUserTokens"); + + migrationBuilder.DropTable( + name: "RefreshTokens"); + + migrationBuilder.DropTable( + name: "AspNetRoles"); + + migrationBuilder.DropTable( + name: "AspNetUsers"); + + migrationBuilder.CreateTable( + name: "Users", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + KeycloakUuid = table.Column(type: "text", nullable: false), + Email = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Users", x => x.Id); + }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Users"); + + migrationBuilder.CreateTable( + name: "AspNetRoles", + columns: table => new + { + Id = table.Column(type: "text", nullable: false), + ConcurrencyStamp = table.Column(type: "text", nullable: true), + Name = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + NormalizedName = table.Column(type: "character varying(256)", maxLength: 256, nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetRoles", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "AspNetUsers", + columns: table => new + { + Id = table.Column(type: "text", nullable: false), + AccessFailedCount = table.Column(type: "integer", nullable: false), + ConcurrencyStamp = table.Column(type: "text", nullable: true), + Email = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + EmailConfirmed = table.Column(type: "boolean", nullable: false), + LockoutEnabled = table.Column(type: "boolean", nullable: false), + LockoutEnd = table.Column(type: "timestamp with time zone", nullable: true), + NormalizedEmail = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + NormalizedUserName = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + PasswordHash = table.Column(type: "text", nullable: true), + PhoneNumber = table.Column(type: "text", nullable: true), + PhoneNumberConfirmed = table.Column(type: "boolean", nullable: false), + SecurityStamp = table.Column(type: "text", nullable: true), + TwoFactorEnabled = table.Column(type: "boolean", nullable: false), + UserName = table.Column(type: "character varying(256)", maxLength: 256, nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUsers", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "AspNetRoleClaims", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + ClaimType = table.Column(type: "text", nullable: true), + ClaimValue = table.Column(type: "text", nullable: true), + RoleId = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetRoleClaims", x => x.Id); + table.ForeignKey( + name: "FK_AspNetRoleClaims_AspNetRoles_RoleId", + column: x => x.RoleId, + principalTable: "AspNetRoles", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserClaims", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + ClaimType = table.Column(type: "text", nullable: true), + ClaimValue = table.Column(type: "text", nullable: true), + UserId = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserClaims", x => x.Id); + table.ForeignKey( + name: "FK_AspNetUserClaims_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserLogins", + columns: table => new + { + LoginProvider = table.Column(type: "text", nullable: false), + ProviderKey = table.Column(type: "text", nullable: false), + ProviderDisplayName = table.Column(type: "text", nullable: true), + UserId = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserLogins", x => new { x.LoginProvider, x.ProviderKey }); + table.ForeignKey( + name: "FK_AspNetUserLogins_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserRoles", + columns: table => new + { + UserId = table.Column(type: "text", nullable: false), + RoleId = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserRoles", x => new { x.UserId, x.RoleId }); + table.ForeignKey( + name: "FK_AspNetUserRoles_AspNetRoles_RoleId", + column: x => x.RoleId, + principalTable: "AspNetRoles", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_AspNetUserRoles_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserTokens", + columns: table => new + { + UserId = table.Column(type: "text", nullable: false), + LoginProvider = table.Column(type: "text", nullable: false), + Name = table.Column(type: "text", nullable: false), + Value = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserTokens", x => new { x.UserId, x.LoginProvider, x.Name }); + table.ForeignKey( + name: "FK_AspNetUserTokens_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "RefreshTokens", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + UserId = table.Column(type: "text", nullable: true), + DateAdded = table.Column(type: "timestamp with time zone", nullable: false), + DateExpire = table.Column(type: "timestamp with time zone", nullable: false), + IsRevoked = table.Column(type: "boolean", nullable: false), + JwtId = table.Column(type: "text", nullable: true), + Token = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_RefreshTokens", x => x.Id); + table.ForeignKey( + name: "FK_RefreshTokens_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id"); + }); + + migrationBuilder.CreateIndex( + name: "IX_AspNetRoleClaims_RoleId", + table: "AspNetRoleClaims", + column: "RoleId"); + + migrationBuilder.CreateIndex( + name: "RoleNameIndex", + table: "AspNetRoles", + column: "NormalizedName", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_AspNetUserClaims_UserId", + table: "AspNetUserClaims", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_AspNetUserLogins_UserId", + table: "AspNetUserLogins", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_AspNetUserRoles_RoleId", + table: "AspNetUserRoles", + column: "RoleId"); + + migrationBuilder.CreateIndex( + name: "EmailIndex", + table: "AspNetUsers", + column: "NormalizedEmail"); + + migrationBuilder.CreateIndex( + name: "UserNameIndex", + table: "AspNetUsers", + column: "NormalizedUserName", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_RefreshTokens_UserId", + table: "RefreshTokens", + column: "UserId"); + } + } +} diff --git a/backend/src/api/Migrations/UsersDbContextModelSnapshot.cs b/backend/src/api/Migrations/UsersDbContextModelSnapshot.cs index 9e5c911..2a552ba 100644 --- a/backend/src/api/Migrations/UsersDbContextModelSnapshot.cs +++ b/backend/src/api/Migrations/UsersDbContextModelSnapshot.cs @@ -1,5 +1,4 @@ // -using System; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; @@ -17,12 +16,12 @@ protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "6.0.1") + .HasAnnotation("ProductVersion", "7.0.2") .HasAnnotation("Relational:MaxIdentifierLength", 63); NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - modelBuilder.Entity("api.Data.RefreshToken", b => + modelBuilder.Entity("api.Data.DAO.UserModel", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -30,285 +29,17 @@ protected override void BuildModel(ModelBuilder modelBuilder) NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - b.Property("DateAdded") - .HasColumnType("timestamp with time zone"); - - b.Property("DateExpire") - .HasColumnType("timestamp with time zone"); - - b.Property("IsRevoked") - .HasColumnType("boolean"); - - b.Property("JwtId") - .HasColumnType("text"); - - b.Property("Token") - .HasColumnType("text"); - - b.Property("UserId") - .HasColumnType("text"); - - b.HasKey("Id"); - - b.HasIndex("UserId"); - - b.ToTable("RefreshTokens"); - }); - - modelBuilder.Entity("api.Data.UserModel", b => - { - b.Property("Id") - .HasColumnType("text"); - - b.Property("AccessFailedCount") - .HasColumnType("integer"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .HasColumnType("text"); - b.Property("Email") - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.Property("EmailConfirmed") - .HasColumnType("boolean"); - - b.Property("LockoutEnabled") - .HasColumnType("boolean"); - - b.Property("LockoutEnd") - .HasColumnType("timestamp with time zone"); - - b.Property("NormalizedEmail") - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.Property("NormalizedUserName") - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.Property("PasswordHash") - .HasColumnType("text"); - - b.Property("PhoneNumber") - .HasColumnType("text"); - - b.Property("PhoneNumberConfirmed") - .HasColumnType("boolean"); - - b.Property("SecurityStamp") - .HasColumnType("text"); - - b.Property("TwoFactorEnabled") - .HasColumnType("boolean"); - - b.Property("UserName") - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedEmail") - .HasDatabaseName("EmailIndex"); - - b.HasIndex("NormalizedUserName") - .IsUnique() - .HasDatabaseName("UserNameIndex"); - - b.ToTable("AspNetUsers", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => - { - b.Property("Id") - .HasColumnType("text"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .HasColumnType("text"); - - b.Property("Name") - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.Property("NormalizedName") - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedName") - .IsUnique() - .HasDatabaseName("RoleNameIndex"); - - b.ToTable("AspNetRoles", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("ClaimType") - .HasColumnType("text"); - - b.Property("ClaimValue") - .HasColumnType("text"); - - b.Property("RoleId") .IsRequired() .HasColumnType("text"); - b.HasKey("Id"); - - b.HasIndex("RoleId"); - - b.ToTable("AspNetRoleClaims", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("ClaimType") - .HasColumnType("text"); - - b.Property("ClaimValue") - .HasColumnType("text"); - - b.Property("UserId") + b.Property("KeycloakUuid") .IsRequired() .HasColumnType("text"); b.HasKey("Id"); - b.HasIndex("UserId"); - - b.ToTable("AspNetUserClaims", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => - { - b.Property("LoginProvider") - .HasColumnType("text"); - - b.Property("ProviderKey") - .HasColumnType("text"); - - b.Property("ProviderDisplayName") - .HasColumnType("text"); - - b.Property("UserId") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("LoginProvider", "ProviderKey"); - - b.HasIndex("UserId"); - - b.ToTable("AspNetUserLogins", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => - { - b.Property("UserId") - .HasColumnType("text"); - - b.Property("RoleId") - .HasColumnType("text"); - - b.HasKey("UserId", "RoleId"); - - b.HasIndex("RoleId"); - - b.ToTable("AspNetUserRoles", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => - { - b.Property("UserId") - .HasColumnType("text"); - - b.Property("LoginProvider") - .HasColumnType("text"); - - b.Property("Name") - .HasColumnType("text"); - - b.Property("Value") - .HasColumnType("text"); - - b.HasKey("UserId", "LoginProvider", "Name"); - - b.ToTable("AspNetUserTokens", (string)null); - }); - - modelBuilder.Entity("api.Data.RefreshToken", b => - { - b.HasOne("api.Data.UserModel", "User") - .WithMany() - .HasForeignKey("UserId"); - - b.Navigation("User"); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => - { - b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) - .WithMany() - .HasForeignKey("RoleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => - { - b.HasOne("api.Data.UserModel", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => - { - b.HasOne("api.Data.UserModel", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => - { - b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) - .WithMany() - .HasForeignKey("RoleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("api.Data.UserModel", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => - { - b.HasOne("api.Data.UserModel", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); + b.ToTable("Users"); }); #pragma warning restore 612, 618 } diff --git a/backend/src/api/Program.cs b/backend/src/api/Program.cs index 0c9a6d0..ef27dbb 100644 --- a/backend/src/api/Program.cs +++ b/backend/src/api/Program.cs @@ -1,94 +1,108 @@ -using api.Data.DAO; using api.Data; -using api.Services.Tokens; using api.Services.Users; using Microsoft.AspNetCore.Authentication.JwtBearer; -using Microsoft.AspNetCore.Identity; using Microsoft.IdentityModel.Tokens; using Microsoft.OpenApi.Models; -using System.Text; using Microsoft.EntityFrameworkCore; +using Serilog; +using api; +using System.Security.Cryptography; -var builder = WebApplication.CreateBuilder(args); +const string logFormat = "[{Timestamp:HH:mm:ss} {Level:u3}] {CorelationId} | {Message:lj}{NewLine}{Exception}"; +Log.Logger = new LoggerConfiguration().Enrich.WithCorrelationId() + .WriteTo + .Console(outputTemplate: logFormat) + .CreateLogger(); -builder.Services.AddDbContext(options => options.UseNpgsql(builder.Configuration.GetConnectionString("Users"))); - -var jwtOptions = new JwtOptions(); -builder.Configuration.Bind("Jwt", jwtOptions); -if(jwtOptions.Secret is null || jwtOptions.Issuer is null || jwtOptions.Audience is null) +try { - throw new Exception("Can't start application without JWT options"); -} + var builder = WebApplication.CreateBuilder(args); + builder.Services.AddHttpContextAccessor(); + builder.Host.UseSerilog(); + builder.Services.AddDbContext(options => options.UseNpgsql(builder.Configuration.GetConnectionString("Users"))); + var keycloakJwtOptions = new KeycloakJwtOptions(); + builder.Configuration.Bind("KeycloakJwt", keycloakJwtOptions); + if(keycloakJwtOptions.Secret is null || keycloakJwtOptions.Issuer is null || keycloakJwtOptions.Audience is null) + { + throw new Exception("Can't start application without JWT options"); + } -var tokenValidationParameters = new TokenValidationParameters() -{ - ValidateIssuerSigningKey = true, - IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(jwtOptions.Secret)), - ValidateAudience = true, - ValidAudience = jwtOptions.Audience, - ValidateIssuer = true, - ValidIssuer = jwtOptions.Issuer, - ValidateLifetime = true -}; -builder.Services.AddSingleton(tokenValidationParameters); -builder.Services.AddSingleton(jwtOptions); + // Create RSA key for offline validation of Keycloak token + RSA rsa = RSA.Create(); + rsa.ImportSubjectPublicKeyInfo(Convert.FromBase64String(keycloakJwtOptions.Secret), out _); + var rsaKeycloakSecurityKey = new RsaSecurityKey(rsa) + { + KeyId = Guid.NewGuid().ToString() + }; -builder.Services.AddIdentity().AddEntityFrameworkStores().AddDefaultTokenProviders(); -builder.Services.Configure(options => -{ - options.Password.RequiredLength = 8; - options.Password.RequireNonAlphanumeric = true; - options.Password.RequireLowercase = true; - options.Password.RequireUppercase = true; - options.Password.RequireDigit = true; - options.Lockout.MaxFailedAccessAttempts = 3; -}); -builder.Services.AddAuthentication(options => -{ - options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; - options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; - options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; -}).AddJwtBearer(options => -{ - options.SaveToken = true; - options.RequireHttpsMetadata = builder.Environment.IsProduction(); - options.TokenValidationParameters = tokenValidationParameters; + var tokenValidationParameters = new TokenValidationParameters() + { + ValidateIssuerSigningKey = true, + IssuerSigningKey = rsaKeycloakSecurityKey, + ValidAudience = keycloakJwtOptions.Audience, + ValidateAudience = true, + ValidIssuer = keycloakJwtOptions.Issuer, + ValidateIssuer = true, + ValidateLifetime = true + }; + builder.Services.AddSingleton(tokenValidationParameters); + builder.Services.AddSingleton(keycloakJwtOptions); -}); -builder.Services.AddControllers(); -builder.Services.AddSwaggerGen(c => -{ - c.SwaggerDoc("v1", new OpenApiInfo { Title = "api", Version = "v1" }); -}); -builder.Services.AddScoped(); -builder.Services.AddScoped(); + builder.Services.AddAuthentication(options => + { + options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; + options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; + }) + .AddJwtBearer(options => + { + options.SaveToken = true; + options.RequireHttpsMetadata = true; + options.TokenValidationParameters = tokenValidationParameters; + }); -builder.Services.AddCors(options => -{ - options.AddPolicy(name: "AllowAll", builder => + builder.Services.AddControllers(); + builder.Services.AddSwaggerGen(c => { - builder.WithOrigins("*"); + c.SwaggerDoc("v1", new OpenApiInfo { Title = "api", Version = "v1" }); }); -}); + builder.Services.AddScoped(); -var app = builder.Build(); + builder.Services.AddCors(options => + { + options.AddPolicy(name: "AllowAll", builder => + { + builder.AllowAnyOrigin().AllowAnyHeader().AllowAnyMethod(); + }); + }); -if (app.Environment.IsDevelopment()) -{ - app.UseDeveloperExceptionPage(); - app.UseSwagger(); - app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "api v1")); -} + var app = builder.Build(); -app.UseAuthentication(); + if (app.Environment.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + app.UseSwagger(); + app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "api v1")); + } -app.UseCors("AllowAll"); + app.UseCors("AllowAll"); + app.UseAuthentication(); -app.UseHttpsRedirection(); + app.UseHttpsRedirection(); + app.UseHsts(); -app.UseRouting(); -app.UseAuthorization(); + app.UseSerilogRequestLogging(); + app.UseRouting(); + app.UseAuthorization(); -app.MapControllers(); + app.MapControllers(); -app.Run(); \ No newline at end of file + app.Run(); +} +catch (Exception ex) +{ + Log.Fatal("Error starting the application: {Exception}", ex); +} +finally +{ + Log.CloseAndFlush(); +} diff --git a/backend/src/api/Services/Tokens/ITokenService.cs b/backend/src/api/Services/Tokens/ITokenService.cs deleted file mode 100644 index e5aa2c0..0000000 --- a/backend/src/api/Services/Tokens/ITokenService.cs +++ /dev/null @@ -1,10 +0,0 @@ -using api.Data.DAO; -using api.Data.DTO; - -namespace api.Services.Tokens; - -public interface ITokenService -{ - (AuthResultDTO authresult, RefreshToken refreshToken) Generate(UserModel user, RefreshToken? refreshToken = null); - (bool IsSuccess, AuthResultDTO? AuthResult, string? Error) Refresh(TokenRequestDTO tokenRequestDTO, UserModel user, RefreshToken storedToken); -} diff --git a/backend/src/api/Services/Tokens/TokenService.cs b/backend/src/api/Services/Tokens/TokenService.cs deleted file mode 100644 index c57c93c..0000000 --- a/backend/src/api/Services/Tokens/TokenService.cs +++ /dev/null @@ -1,72 +0,0 @@ -using api.Data; -using api.Data.DAO; -using api.Data.DTO; -using Microsoft.IdentityModel.Tokens; -using System.IdentityModel.Tokens.Jwt; -using System.Security.Claims; -using System.Text; - -namespace api.Services.Tokens -{ - public class TokenService : ITokenService - { - private readonly JwtOptions _jwtOptions; - private readonly TokenValidationParameters _validationParameters; - public TokenService(JwtOptions jwtOptions, TokenValidationParameters validationParameters) - { - _jwtOptions = jwtOptions; - _validationParameters = validationParameters; - } - - public (AuthResultDTO authresult, RefreshToken refreshToken) Generate(UserModel user, RefreshToken? refreshToken = null) - { - var claims = new List() - { - new Claim(ClaimTypes.Name, user.Email!), - new Claim(ClaimTypes.NameIdentifier, user.Id), - new Claim(JwtRegisteredClaimNames.Email, user.Email!), - new Claim(JwtRegisteredClaimNames.Sub, user.Email!), - new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()) - }; - var token = new JwtSecurityToken( - issuer: _jwtOptions.Issuer, - audience: _jwtOptions.Audience, - expires: DateTime.UtcNow.AddMinutes(10), - claims: claims, - signingCredentials: new SigningCredentials( - new SymmetricSecurityKey(Encoding.ASCII.GetBytes(_jwtOptions.Secret!)), - SecurityAlgorithms.HmacSha256 - )); - var jwt = new JwtSecurityTokenHandler().WriteToken(token); - - refreshToken ??= new RefreshToken() - { - JwtId = token.Id, - IsRevoked = false, - UserId = user.Id, - DateAdded = DateTime.UtcNow, - DateExpire = DateTime.UtcNow.AddMonths(6), - Token = $"{Guid.NewGuid()}{Guid.NewGuid()}" - }; - - return (new AuthResultDTO() { Token = jwt, RefreshToken = refreshToken.Token, ExpiresAt = token.ValidTo }, refreshToken); - } - - public (bool IsSuccess, AuthResultDTO? AuthResult, string? Error) Refresh(TokenRequestDTO tokenRequestDTO, UserModel user, RefreshToken storedToken) - { - var tokenHandler = new JwtSecurityTokenHandler(); - try - { - var tokenIsValid = tokenHandler.ValidateToken(tokenRequestDTO.Token, _validationParameters, out var validatedToken); - } - catch (SecurityTokenExpiredException) - { - if (storedToken.IsRevoked || storedToken.DateExpire <= DateTime.UtcNow) - { - return (false, null, "Refresh token has been revoked or expired"); - } - } - return (true, Generate(user, storedToken).authresult, null); - } - } -} diff --git a/backend/src/api/Services/Users/IUserService.cs b/backend/src/api/Services/Users/IUserService.cs index c10a837..6d459a1 100644 --- a/backend/src/api/Services/Users/IUserService.cs +++ b/backend/src/api/Services/Users/IUserService.cs @@ -1,14 +1,8 @@ using api.Data.DTO; -using System.Collections.Generic; -using System.Security.Claims; -using System.Threading.Tasks; namespace api.Services.Users; public interface IUserService { - Task<(bool IsSuccess, AuthResultDTO AuthResult, string Error)> Login(UserDTO user); - Task<(bool IsSuccess, AuthResultDTO AuthResult, string Error)> RefreshLogin(TokenRequestDTO tokenRequestDTO); - Task<(bool IsSuccess, IEnumerable Error)> Register(UserDTO user); - Task Logout(string username); + Task<(bool isSuccess, string? error)> CreateKeycloakUser(UserDto user); } diff --git a/backend/src/api/Services/Users/UserService.cs b/backend/src/api/Services/Users/UserService.cs index bbfb94f..bb54697 100644 --- a/backend/src/api/Services/Users/UserService.cs +++ b/backend/src/api/Services/Users/UserService.cs @@ -1,104 +1,33 @@ using api.Data; -using Microsoft.AspNetCore.Identity; -using System; -using System.Threading.Tasks; -using System.Linq; -using System.Collections.Generic; using api.Data.DTO; using api.Data.DAO; -using api.Services.Tokens; -using System.Security.Claims; +using api.Constants; namespace api.Services.Users; public class UserService : IUserService { - private readonly UserManager _userManager; - private readonly RoleManager _roleManager; private readonly UsersDbContext _usersDbContext; - private readonly ITokenService _tokenService; - public UserService(UserManager userManager, RoleManager roleManager, UsersDbContext usersDbContext, ITokenService tokenService) + public UserService(UsersDbContext usersDbContext) { - _userManager = userManager; - _roleManager = roleManager; _usersDbContext = usersDbContext; - _tokenService = tokenService; } - public async Task<(bool IsSuccess, IEnumerable Error)> Register(UserDTO user) + public async Task<(bool isSuccess, string? error)> CreateKeycloakUser(UserDto user) { - var userExists = await _userManager.FindByEmailAsync(user.Email); - if (userExists != null) + var userInstance = _usersDbContext.Users.FirstOrDefault(userInstance => userInstance.KeycloakUuid.Equals(user.KeycloakUuid)); + if (userInstance != null) { - return (false, new[] { "User already exists" }); + return (false, ErrorMessages.UserAlreadyExists); } var newUser = new UserModel() { - Email = user.Email, - UserName = user.Email, - SecurityStamp = Guid.NewGuid().ToString() + KeycloakUuid = user.KeycloakUuid, + Email = user.Email }; - var result = await _userManager.CreateAsync(newUser, user.Password); - if (result.Succeeded) - { - return (true, null); - } - else - { - return (false, result.Errors.Select(e => e.Description).ToArray()); - } - } - - public async Task<(bool IsSuccess, AuthResultDTO AuthResult, string Error)> Login(UserDTO user) - { - var existingUser = await _userManager.FindByEmailAsync(user.Email); - if (existingUser == null) - { - return (false, null, "User not found"); - } - else if (await _userManager.CheckPasswordAsync(existingUser, user.Password)) - { - var (authresult, refreshToken) = _tokenService.Generate(existingUser); - await _usersDbContext.RefreshTokens.AddAsync(refreshToken); - await _usersDbContext.SaveChangesAsync(); - return (true, authresult, null); - } - else - { - return (false, null, "Invalid password"); - } - } - - public async Task<(bool IsSuccess, AuthResultDTO AuthResult, string Error)> RefreshLogin(TokenRequestDTO tokenRequestDTO) - { - var storedToken = _usersDbContext.RefreshTokens.FirstOrDefault(t => t.Token.Equals(tokenRequestDTO.RefreshToken)); - if (storedToken == null) - { - return (false, null, "Refresh token is invalid!"); - } - var user = await _userManager.FindByIdAsync(storedToken.UserId); - if (user == null) - { - return (false, null, "Refresh token is invalid!"); - } - var (IsSuccess, AuthResult, Error) = _tokenService.Refresh(tokenRequestDTO, user, storedToken); - if (IsSuccess) - { - return (true, AuthResult, null); - } - else - { - return (false, null, Error); - } - - } - - public async Task Logout(string username) - { - var dbUser = await _userManager.FindByNameAsync(username); - var tokens = _usersDbContext.RefreshTokens.Where(token => token.UserId == dbUser.Id); - _usersDbContext.RefreshTokens.RemoveRange(tokens); + await _usersDbContext.Users.AddAsync(newUser); await _usersDbContext.SaveChangesAsync(); + return (true, null); } } diff --git a/backend/src/api/api.csproj b/backend/src/api/api.csproj index 1a93006..217deeb 100644 --- a/backend/src/api/api.csproj +++ b/backend/src/api/api.csproj @@ -18,6 +18,7 @@ + diff --git a/backend/src/api/appsettings.json b/backend/src/api/appsettings.json index d9d9a9b..f267ed6 100644 --- a/backend/src/api/appsettings.json +++ b/backend/src/api/appsettings.json @@ -6,5 +6,13 @@ "Microsoft.Hosting.Lifetime": "Information" } }, - "AllowedHosts": "*" + "AllowedHosts": "*", + "ConnectionStrings": { + "Users": "Host=localhost;Database=users;Username=postgres;Password=devdbpasswd" + }, + "KeycloakJwt": { + "Secret": "my-very-secret-key", + "Audience": "my-audience", + "Issuer": "my-issuer" + } } diff --git a/global.json b/global.json index 2b7d34d..8058dfb 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "6.0", + "version": "7.0.405", "rollForward": "latestFeature" } }