diff --git a/README.md b/README.md index 8022109..668cc29 100644 --- a/README.md +++ b/README.md @@ -5,39 +5,51 @@ ### Built With -### Prerequisites +### Prerequisites 📋 This project was made with the purpose to attend only applications that follows the current [.Net Supported versions.](https://dotnet.microsoft.com/en-us/download/dotnet) -## Why feijuca? -Feijuca is a nickname for a famous Brazilian dish called [Feijoada](https://theculturetrip.com/south-america/brazil/articles/a-brief-introduction-to-feijoada-brazils-national-dish). I wanted to use a name representing my born country on this project and Feijuca was chosen. +## Why Feijuca 🫘? +Feijuca is a nickname for a famous Brazilian dish called [Feijoada](https://theculturetrip.com/south-america/brazil/articles/a-brief-introduction-to-feijoada-brazils-national-dish). I wanted to use a name representing my country on this project and Feijuca was chosen. -## About the Project +## **About the project🧾** This repository aims to provide a configuration option for .NET projects that are using or planning to use Keycloak for authentication and JWT token generation. The project consists of two distinct parts: 1. **Feijuca.Keycloak.Auth.MultiTenancy** 2. **Feijuca.Keycloak.TokenManager** -## Feijuca.Keycloak.Auth.MultiTenancy +**Attention: 🫵** +- The projects work in isolation way, there is no dependency between them. You do not need use one to use other, note that each project has different purpose, below you can understand better: + +## Feijuca.Keycloak.Auth.MultiTenancy 👨🏽‍💻 A [NuGet](https://www.nuget.org/packages/Feijuca.Keycloak.MultiTenancy) package that enables the implementation of multi-tenancy concepts using Keycloak. Each realm in Keycloak can represent a different tenant, allowing for unique configurations for each one. This ensures that each tenant within your application can have its own settings within Keycloak. -### Features -- Obtaining a tenant from a token. -- Extracting an ID from a token. -- Getting the URL where Keycloak is running. -- Custom properties for tenants (open a PR to discuss additional features). -- Simplification of managing actions in Keycloak. +### Features ⛲ +- You can use all existings keycloak features following a multi tenancy concept based on your realms, so you can handle different configurations based on each tenant (realm). +- With just one instance from your application you can handle different tenants using the same JWT token generation config +- Obtaining information such as a tenant, user id, url and so on from a token. (If you wanna implement a method do retrieve another thing related to the token, open a PR) -## Feijuca.Keycloak.TokenManager -Managing certain actions in Keycloak can be complicated. For instance, creating a user involves several steps: obtaining a token, creating the user, and setting a password. With **Feijuca.Keycloak.TokenManager**, you can create a user in a single request since all necessary actions are already integrated into the project. +## Feijuca.Keycloak.TokenManager 👨🏽‍💻 +Managing certain actions in Keycloak can be complicated. For instance, creating a new user using the keycloak api involves several steps: obtaining a token, creating the user, setting a password... +With **Feijuca.Keycloak.TokenManager**, you can create a user in a single request since all necessary actions are already integrated into the project. -### Features -- Resetting a user's password via email. -- Email confirmation. -- Checking a user's status to determine if they are valid (email confirmed). -- Custom endpoints (open a PR to discuss additional features). +### Features ⛲ +- Every action in one place. Forget about call multiples endpoints to do actions about users on keycloak. Do actions related to the user (Creation, remotion, e-mail confirming, password redefinition, and so on) based on predefined endpoints. +- Custom endpoints based on your necessities (If you think it could be helpful to the project, open a PR to discuss additional features). ## Getting Started - Multi tenancy configuration -If you wish use to accomplish the goal to use multi tenancy concept based on each realm on your keycloak instance, here is the steps to configure it: -1. Fill out the appsettings configs related to your realms (tenants) +To accomplish the goal to use multi tenancy concept based on each realm (Where each realm would be a different tenant), here is the steps to configure it: I assume that you already had the configurations on your keycloak instance, as for example, a client created with their configurations related (scopes and etc.) +Starting from this point, to use **Feijuca.Keycloak.Auth.MultiTenancy** follow the steps below: + +1. The tenant that each user belongs is stored on a user attribute, the tenant value should be the name of the realm. You can create a new attribute manually on Keycloak or using **Feijuca.Keycloak.TokenManager** you can create news users with these default attributes below: + +![image](https://github.com/fmattioli/Feijuca.Keycloak.AuthServices/assets/27566574/8dcf2109-2145-4e53-9487-ab8fe2582fff) + +2. Create a new audience related to the scopes used your client and include the audience on your client: + +![image](https://github.com/fmattioli/Feijuca.Keycloak.AuthServices/assets/27566574/6b7b437e-fa29-4776-b29f-4dba8e6d1f21) + +This step is important and mandatory because on each request received the tool will confirm the token audience following what was filled out on step 3. + +3. Filled out appsettings file on your application, relate all of yours realms (tenants) ```sh { "AuthSettings": { @@ -61,27 +73,58 @@ If you wish use to accomplish the goal to use multi tenancy concept based on ea "ClientSecret": "your-client-secret", "ClientId": "your-client-id", "Resource": "your-client-id", - "AuthServerUrl": "https://url-keycloakt/realms/10000/protocol/openid-connect/token" + "AuthServerUrl": "https://url-keycloak" } } ``` -2. Configure dependency injection (Note that AuthSettings is a model defined on **Feijuca.Keycloak.Auth.MultiTenancy**, usually I mapped it to variable. for example: +4. Configure dependency injection (Note that AuthSettings is a model defined on **Feijuca.Keycloak.Auth.MultiTenancy**, I recommend you use the GetSection method to map the appsettings configs to the AuthSettings model: ```sh var settings = configuration.GetSection("AuthSettings").Get(); + ``` +5. Add the service to the service collection from your application, I recommend you create a new extension method as below: + ```sh builder.Services .AddApiAuthentication(applicationSettings.AuthSettings!); - public static IServiceCollection AddApiAuthentication(this IServiceCollection services, AuthSettings authSettings) - { - services.AddHttpContextAccessor(); - services.AddSingleton(); - services.AddKeyCloakAuth(authSettings!); + public static class AuthExtension + { + public static IServiceCollection AddApiAuthentication(this IServiceCollection services, AuthSettings authSettings) + { + services.AddHttpContextAccessor(); + services.AddSingleton(); + services.AddKeyCloakAuth(authSettings!); + + return services; + } + } + ``` +6. Conclusion: + Following a default example, after generated, your token should have the following details: + Audience(s) related to the clients scopes: - return services; - } + ![image](https://github.com/fmattioli/Feijuca.Keycloak.AuthServices/assets/27566574/18da7c8b-81f7-4bd7-b794-8eb768db9d18) + + And your appsettings should be: + ```sh + "AuthSettings": { + "Realms": [ + { + "Name": "10000", + "Audience": "receipts-commandhander-api", + "Issuer": "https://url-keycloak/realms/10000" + } + ], + "ClientId": "receipts-commandhander-api", + "Resource": "receipts-commandhander-api", + "AuthServerUrl": "https://url-keycloak", + } ``` + With this configuration you should be able to use Keycloak following a multi tenancy contenxt using .NET. + Following this [link](https://github.com/fmattioli/Feijuca.Keycloak.AuthServices/blob/main/src/Feijuca.Keycloak.Auth.MultiTenancy/Feijuca.Keycloak.MultiTenancy/Extensions/AuthExtensions.cs) you can understand what is the logic used to validate the token received. + + ## Getting Started - Using Token Manager Api If you wish use to accomplish the goal to use multi tenancy concept based on each realm on your keycloak instance, here is the steps to configure it: 1. Fill out the appsettings configs related to your realms (tenants) @@ -108,7 +151,7 @@ If you wish use to accomplish the goal to use multi tenancy concept based on ea "ClientSecret": "your-client-secret", "ClientId": "your-client-id", "Resource": "your-client-id", - "AuthServerUrl": "https://url-keycloakt/realms/10000/protocol/openid-connect/token" + "AuthServerUrl": "https://url-keycloakt" } } ``` diff --git a/src/Feijuca.Keycloak.TokenManager/TokenManager.Api/Controllers/UserController.cs b/src/Feijuca.Keycloak.TokenManager/TokenManager.Api/Controllers/UserController.cs index 433b8e5..767fd5d 100644 --- a/src/Feijuca.Keycloak.TokenManager/TokenManager.Api/Controllers/UserController.cs +++ b/src/Feijuca.Keycloak.TokenManager/TokenManager.Api/Controllers/UserController.cs @@ -23,10 +23,11 @@ public class UserController(IMediator mediator) : Controller [ProducesResponseType(StatusCodes.Status201Created)] [ProducesResponseType(StatusCodes.Status500InternalServerError)] [ProducesResponseType(StatusCodes.Status400BadRequest)] + //[Authorize(Policy = "TokenManager")] + //[RequiredScope("tokenmanager-read")] public async Task CreateUser([FromRoute] string tenant, [FromBody] AddUserRequest addUserRequest, CancellationToken cancellationToken) { - addUserRequest.Tenant = tenant; - var result = await _mediator.Send(new CreateUserCommand(addUserRequest), cancellationToken); + var result = await _mediator.Send(new CreateUserCommand(tenant, addUserRequest), cancellationToken); var response = new ResponseResult(); if (result.IsSuccess) @@ -46,22 +47,20 @@ public async Task CreateUser([FromRoute] string tenant, [FromBody /// /// A status code related to the operation. [HttpPost] - [Route("login", Name = nameof(Login))] + [Route("login/{tenant}", Name = nameof(Login))] [ProducesResponseType(StatusCodes.Status201Created)] [ProducesResponseType(StatusCodes.Status500InternalServerError)] - [ProducesResponseType(StatusCodes.Status400BadRequest)] - [Authorize(Policy = "TokenManager")] - [RequiredScope("tokenmanager-read")] - public async Task Login([FromBody] LoginUserRequest loginUserRequest, CancellationToken cancellationToken) + [ProducesResponseType(StatusCodes.Status400BadRequest)] + public async Task Login([FromRoute] string tenant, [FromBody] LoginUserRequest loginUserRequest, CancellationToken cancellationToken) { - var result = await _mediator.Send(new LoginUserCommand(loginUserRequest), cancellationToken); + var result = await _mediator.Send(new LoginUserCommand(tenant, loginUserRequest), cancellationToken); var response = new ResponseResult(); if (result.IsSuccess) { response.Result = result.Value; response.DetailMessage = "Token generated with succesfully"; - return Created(); + return Ok(response); } response.DetailMessage = result.Error.Description; diff --git a/src/Feijuca.Keycloak.TokenManager/TokenManager.Api/Program.cs b/src/Feijuca.Keycloak.TokenManager/TokenManager.Api/Program.cs index 1c9ad86..d31973e 100644 --- a/src/Feijuca.Keycloak.TokenManager/TokenManager.Api/Program.cs +++ b/src/Feijuca.Keycloak.TokenManager/TokenManager.Api/Program.cs @@ -17,12 +17,12 @@ .AddEnvironmentVariables(); var applicationSettings = builder.Configuration.GetApplicationSettings(builder.Environment); -Console.WriteLine(JsonConvert.SerializeObject(applicationSettings)); -builder.Services.AddSingleton(applicationSettings); - -builder.Services.AddControllers(); +builder.Services + .AddSingleton(applicationSettings) + .AddControllers(); +builder.Services.AddSwagger(applicationSettings!.AuthSettings!); builder.Services .AddExceptionHandler() .AddProblemDetails() @@ -30,19 +30,29 @@ .AddLoggingDependency() .AddMediator() .AddRepositories(applicationSettings.AuthSettings) - .AddEndpointsApiExplorer() - .AddSwagger(applicationSettings!.AuthSettings!); + .AddEndpointsApiExplorer() + .AddCors(options => + { + options.AddPolicy("AllowAllOrigins", policy => + { + policy + .AllowAnyOrigin() + .AllowAnyMethod() + .AllowAnyHeader(); + }); + }); var app = builder.Build(); -app.UseExceptionHandler() - .UseSwagger() - .UseSwaggerUI(c => - { - c.SwaggerEndpoint("/swagger/v1/swagger.json", "TokenManager.Api"); - c.OAuthClientId(applicationSettings!.AuthSettings!.Resource); - c.OAuthUseBasicAuthenticationWithAccessCodeGrant(); - }); +app.UseCors("AllowAllOrigins") + .UseExceptionHandler() + .UseSwagger() + .UseSwaggerUI(c => + { + c.SwaggerEndpoint("/swagger/v1/swagger.json", "TokenManager.Api"); + c.OAuthClientId(applicationSettings!.AuthSettings!.Resource); + c.OAuthUseBasicAuthenticationWithAccessCodeGrant(); + }); app.UseHttpsRedirection() .UseAuthorization(); diff --git a/src/Feijuca.Keycloak.TokenManager/TokenManager.Api/TokenManager.Api.csproj b/src/Feijuca.Keycloak.TokenManager/TokenManager.Api/TokenManager.Api.csproj index e1d7df4..184b828 100644 --- a/src/Feijuca.Keycloak.TokenManager/TokenManager.Api/TokenManager.Api.csproj +++ b/src/Feijuca.Keycloak.TokenManager/TokenManager.Api/TokenManager.Api.csproj @@ -6,7 +6,7 @@ enable True 1591 - 1.3.1 + 1.5.1 diff --git a/src/Feijuca.Keycloak.TokenManager/TokenManager.Application.Services/Commands/Users/CreateUserCommand.cs b/src/Feijuca.Keycloak.TokenManager/TokenManager.Application.Services/Commands/Users/CreateUserCommand.cs index fcf720c..6edb755 100644 --- a/src/Feijuca.Keycloak.TokenManager/TokenManager.Application.Services/Commands/Users/CreateUserCommand.cs +++ b/src/Feijuca.Keycloak.TokenManager/TokenManager.Application.Services/Commands/Users/CreateUserCommand.cs @@ -4,5 +4,5 @@ namespace TokenManager.Application.Services.Commands.Users { - public record CreateUserCommand(AddUserRequest AddUserRequest) : IRequest; + public record CreateUserCommand(string Tenant, AddUserRequest AddUserRequest) : IRequest; } diff --git a/src/Feijuca.Keycloak.TokenManager/TokenManager.Application.Services/Commands/Users/CreateUserCommandHandler.cs b/src/Feijuca.Keycloak.TokenManager/TokenManager.Application.Services/Commands/Users/CreateUserCommandHandler.cs index 649139f..eb7edf5 100644 --- a/src/Feijuca.Keycloak.TokenManager/TokenManager.Application.Services/Commands/Users/CreateUserCommandHandler.cs +++ b/src/Feijuca.Keycloak.TokenManager/TokenManager.Application.Services/Commands/Users/CreateUserCommandHandler.cs @@ -11,7 +11,7 @@ public class CreateUserCommandHandler(IUserRepository userRepository) : IRequest public async Task Handle(CreateUserCommand request, CancellationToken cancellationToken) { - var resultUserCreated = await _userRepository.DoNewUserCreationActions(request. AddUserRequest.Tenant!, request.AddUserRequest.ToDomain()); + var resultUserCreated = await _userRepository.CreateNewUserActions(request.Tenant, request.AddUserRequest.ToDomain()); if (resultUserCreated.IsSuccess) { return Result.Success(); diff --git a/src/Feijuca.Keycloak.TokenManager/TokenManager.Application.Services/Commands/Users/LoginUserCommand.cs b/src/Feijuca.Keycloak.TokenManager/TokenManager.Application.Services/Commands/Users/LoginUserCommand.cs index d3f82ea..ec6f7b4 100644 --- a/src/Feijuca.Keycloak.TokenManager/TokenManager.Application.Services/Commands/Users/LoginUserCommand.cs +++ b/src/Feijuca.Keycloak.TokenManager/TokenManager.Application.Services/Commands/Users/LoginUserCommand.cs @@ -5,5 +5,5 @@ namespace TokenManager.Application.Services.Commands.Users { - public record LoginUserCommand(LoginUserRequest LoginUser) : IRequest>; + public record LoginUserCommand(string Tenant, LoginUserRequest LoginUser) : IRequest>; } diff --git a/src/Feijuca.Keycloak.TokenManager/TokenManager.Application.Services/Commands/Users/LoginUserCommandHandler.cs b/src/Feijuca.Keycloak.TokenManager/TokenManager.Application.Services/Commands/Users/LoginUserCommandHandler.cs index 2873080..1e79aa5 100644 --- a/src/Feijuca.Keycloak.TokenManager/TokenManager.Application.Services/Commands/Users/LoginUserCommandHandler.cs +++ b/src/Feijuca.Keycloak.TokenManager/TokenManager.Application.Services/Commands/Users/LoginUserCommandHandler.cs @@ -1,6 +1,5 @@ using Contracts.Common; using MediatR; - using TokenManager.Application.Services.Mappers; using TokenManager.Application.Services.Responses; using TokenManager.Domain.Interfaces; @@ -13,7 +12,8 @@ public class LoginUserCommandHandler(IUserRepository userRepository) : IRequestH public async Task> Handle(LoginUserCommand request, CancellationToken cancellationToken) { - var tokenDetailsResult = await _userRepository.LoginAsync(request.LoginUser.ToDomain()); + var user = request.LoginUser.ToDomain(request.Tenant); + var tokenDetailsResult = await _userRepository.LoginAsync(request.Tenant, user); if (tokenDetailsResult.IsSuccess) { return Result.Success(tokenDetailsResult.Value.ToTokenResponse()); diff --git a/src/Feijuca.Keycloak.TokenManager/TokenManager.Application.Services/Mappers/UserMapper.cs b/src/Feijuca.Keycloak.TokenManager/TokenManager.Application.Services/Mappers/UserMapper.cs index b5e5865..7c0b747 100644 --- a/src/Feijuca.Keycloak.TokenManager/TokenManager.Application.Services/Mappers/UserMapper.cs +++ b/src/Feijuca.Keycloak.TokenManager/TokenManager.Application.Services/Mappers/UserMapper.cs @@ -8,16 +8,16 @@ public static class UserMapper { public static User ToDomain(this AddUserRequest userRequest) { - var attributes = userRequest.Attributes!.ToDomain(userRequest.Tenant!); - return new User(userRequest.Username!, userRequest.Email!, userRequest.FirstName!, userRequest.LastName!, attributes); + var attributes = userRequest.Attributes!.ToDomain(); + return new User(userRequest.Username!, userRequest.Password, userRequest.Email!, userRequest.FirstName!, userRequest.LastName!, attributes); } - public static Attributes ToDomain(this AttributesRequest attributes, string tenant) + public static Attributes ToDomain(this AttributesRequest attributes) { - return new Attributes(attributes.ZoneInfo, attributes.Birthdate, attributes.PhoneNumber, attributes.Gender, attributes.Fullname, tenant, attributes.Picture); + return new Attributes(attributes.ZoneInfo, attributes.Birthdate, attributes.PhoneNumber, attributes.Gender, attributes.Fullname, attributes.Tenant, attributes.Picture); } - public static User ToDomain(this LoginUserRequest loginUserRequest) + public static User ToDomain(this LoginUserRequest loginUserRequest, string tenant) { return new User(loginUserRequest.Username, loginUserRequest.Password); } diff --git a/src/Feijuca.Keycloak.TokenManager/TokenManager.Application.Services/Requests/User/AddUserRequest.cs b/src/Feijuca.Keycloak.TokenManager/TokenManager.Application.Services/Requests/User/AddUserRequest.cs index bd06882..76cc9df 100644 --- a/src/Feijuca.Keycloak.TokenManager/TokenManager.Application.Services/Requests/User/AddUserRequest.cs +++ b/src/Feijuca.Keycloak.TokenManager/TokenManager.Application.Services/Requests/User/AddUserRequest.cs @@ -2,9 +2,5 @@ namespace TokenManager.Application.Services.Requests.User { - public record AddUserRequest(string? Username, string? Email, string? FirstName, string? LastName, AttributesRequest? Attributes) - { - [JsonIgnore] - public string? Tenant { get; set; } - } + public record AddUserRequest(string Username, string Password, string Email, string FirstName, string LastName, AttributesRequest? Attributes); } diff --git a/src/Feijuca.Keycloak.TokenManager/TokenManager.Application.Services/Requests/User/AttributesRequest.cs b/src/Feijuca.Keycloak.TokenManager/TokenManager.Application.Services/Requests/User/AttributesRequest.cs index de7c136..43943bd 100644 --- a/src/Feijuca.Keycloak.TokenManager/TokenManager.Application.Services/Requests/User/AttributesRequest.cs +++ b/src/Feijuca.Keycloak.TokenManager/TokenManager.Application.Services/Requests/User/AttributesRequest.cs @@ -1,4 +1,4 @@ namespace TokenManager.Application.Services.Requests.User { - public record AttributesRequest(string? ZoneInfo, string? Birthdate, string? PhoneNumber, string? Gender, string? Fullname, string? Picture); + public record AttributesRequest(string? Tenant, string? ZoneInfo, string? Birthdate, string? PhoneNumber, string? Gender, string? Fullname, string? Picture); } diff --git a/src/Feijuca.Keycloak.TokenManager/TokenManager.Application.Services/Requests/User/LoginUserRequest.cs b/src/Feijuca.Keycloak.TokenManager/TokenManager.Application.Services/Requests/User/LoginUserRequest.cs index 9b1246e..aa604ed 100644 --- a/src/Feijuca.Keycloak.TokenManager/TokenManager.Application.Services/Requests/User/LoginUserRequest.cs +++ b/src/Feijuca.Keycloak.TokenManager/TokenManager.Application.Services/Requests/User/LoginUserRequest.cs @@ -1,4 +1,6 @@ -namespace TokenManager.Application.Services.Requests.User +using Newtonsoft.Json; + +namespace TokenManager.Application.Services.Requests.User { public record LoginUserRequest(string Username, string Password); } diff --git a/src/Feijuca.Keycloak.TokenManager/TokenManager.Domain/Entities/User.cs b/src/Feijuca.Keycloak.TokenManager/TokenManager.Domain/Entities/User.cs index 3e7550f..260db5e 100644 --- a/src/Feijuca.Keycloak.TokenManager/TokenManager.Domain/Entities/User.cs +++ b/src/Feijuca.Keycloak.TokenManager/TokenManager.Domain/Entities/User.cs @@ -1,13 +1,16 @@ -namespace TokenManager.Domain.Entities +using Newtonsoft.Json; + +namespace TokenManager.Domain.Entities { public class User { + [JsonIgnore] + public string Password { get; set; } = null!; public string? Id { get; set; } public bool Enabled { get; set; } public bool EmailVerified { get; set; } public string? Username { get; set; } = null!; - public string? Email { get; set; } - public string? Password { get; set; } + public string? Email { get; set; } public string? FirstName { get; set; } public string? LastName { get; set; } public bool Totp { get; set; } @@ -29,11 +32,12 @@ public User(string userName, string password) Password = password; } - public User(string userName, string email, string firstName, string lastName, Attributes attributes) + public User(string userName, string password, string email, string firstName, string lastName, Attributes attributes) { Enabled = true; - EmailVerified = false; + EmailVerified = true; Email = email; + Password = password; FirstName = firstName; Username = userName; LastName = lastName; diff --git a/src/Feijuca.Keycloak.TokenManager/TokenManager.Domain/Errors/UserErrors.cs b/src/Feijuca.Keycloak.TokenManager/TokenManager.Domain/Errors/UserErrors.cs index 3035567..b1c1af4 100644 --- a/src/Feijuca.Keycloak.TokenManager/TokenManager.Domain/Errors/UserErrors.cs +++ b/src/Feijuca.Keycloak.TokenManager/TokenManager.Domain/Errors/UserErrors.cs @@ -16,6 +16,11 @@ public static class UserErrors $"An error occurred while trying to get JWT token. Please check username and password. {TechnicalMessage}" ); + public static Error WrongPasswordDefinition => new( + "User.WrongPasswordDefinition", + $"An error occurred while trying to add a new password to the user. {TechnicalMessage}" + ); + public static void SetTechnicalMessage(string technicalMessage) { TechnicalMessage = technicalMessage; diff --git a/src/Feijuca.Keycloak.TokenManager/TokenManager.Domain/Interfaces/IUserRepository.cs b/src/Feijuca.Keycloak.TokenManager/TokenManager.Domain/Interfaces/IUserRepository.cs index d4f50c3..f40db67 100644 --- a/src/Feijuca.Keycloak.TokenManager/TokenManager.Domain/Interfaces/IUserRepository.cs +++ b/src/Feijuca.Keycloak.TokenManager/TokenManager.Domain/Interfaces/IUserRepository.cs @@ -6,9 +6,11 @@ namespace TokenManager.Domain.Interfaces public interface IUserRepository { Task> GetAccessTokenAsync(string tenant); - Task DoNewUserCreationActions(string tenant, User user); + Task CreateNewUserActions(string tenant, User user); + Task> LoginAsync(string tenant, User user); Task CreateNewUserAsync(User user); Task> GetUserAsync(string userName); - Task> LoginAsync(User user); + Task ResetPasswordAsync(string userId, string password); + Task SendEmailVerificationAsync(string userId); } } diff --git a/src/Feijuca.Keycloak.TokenManager/TokenManager.Infra.Data/Repositories/UserRepository.cs b/src/Feijuca.Keycloak.TokenManager/TokenManager.Infra.Data/Repositories/UserRepository.cs index fbb5786..1ebb4c5 100644 --- a/src/Feijuca.Keycloak.TokenManager/TokenManager.Infra.Data/Repositories/UserRepository.cs +++ b/src/Feijuca.Keycloak.TokenManager/TokenManager.Infra.Data/Repositories/UserRepository.cs @@ -17,8 +17,12 @@ public class UserRepository : IUserRepository private readonly IAuthService _authService; private readonly TokenCredentials _tokenCredentials; private readonly HttpClient _httpClient; - - private string urlUserActions = ""; + private string _urlUserActions = ""; + private static readonly JsonSerializerSettings Settings = new() + { + ContractResolver = new CamelCasePropertyNamesContractResolver(), + NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore + }; public UserRepository(IHttpClientFactory httpClientFactory, IAuthService authService, TokenCredentials tokenCredentials) { @@ -26,13 +30,7 @@ public UserRepository(IHttpClientFactory httpClientFactory, IAuthService authSer _authService = authService; _tokenCredentials = tokenCredentials; _httpClient = _httpClientFactory.CreateClient("KeycloakClient"); - } - - private static readonly JsonSerializerSettings Settings = new() - { - ContractResolver = new CamelCasePropertyNamesContractResolver(), - NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore - }; + } public async Task> GetAccessTokenAsync(string tenant) { @@ -64,7 +62,7 @@ public async Task> GetAccessTokenAsync(string tenant) return Result.Failure(UserErrors.TokenGenerationError); } - public async Task DoNewUserCreationActions(string tenant, User user) + public async Task CreateNewUserActions(string tenant, User user) { var tokenBearerResult = await GetAccessTokenAsync(tenant); if (tokenBearerResult.IsSuccess) @@ -75,7 +73,8 @@ public async Task DoNewUserCreationActions(string tenant, User user) if (response.IsSuccessStatusCode) { - var userName = await GetUserAsync(user.Username); + var keycloakUser = await GetUserAsync(user.Username!); + await ResetPasswordAsync(keycloakUser.Value.Id!, user.Password!); return Result.Success(); } @@ -85,29 +84,19 @@ public async Task DoNewUserCreationActions(string tenant, User user) } return Result.Failure(UserErrors.InvalidUserNameOrPasswordError); - } - - private void ConfigureHttpClient(string tenant, string accessToken) - { - _httpClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", accessToken); - urlUserActions = _httpClient.BaseAddress - .AppendPathSegment("admin") - .AppendPathSegment("realms") - .AppendPathSegment(tenant) - .AppendPathSegment("users"); - } + } public async Task CreateNewUserAsync(User user) { var json = JsonConvert.SerializeObject(user, Settings); var httpContent = new StringContent(json, Encoding.UTF8, "application/json"); - var response = await _httpClient.PostAsync(urlUserActions, httpContent); + var response = await _httpClient.PostAsync(_urlUserActions, httpContent); return response; } public async Task> GetUserAsync(string userName) { - var url = urlUserActions.SetQueryParam("username", userName); + var url = _urlUserActions.SetQueryParam("username", userName); var response = await _httpClient.GetAsync(url); var keycloakUserContent = await response.Content.ReadAsStringAsync(); @@ -115,8 +104,67 @@ public async Task> GetUserAsync(string userName) return Result.Success(user[0]); } - public async Task> LoginAsync(User user) + public async Task ResetPasswordAsync(string userId, string password) { + var url = _urlUserActions + .AppendPathSegment(userId) + .AppendPathSegment("reset-password"); + + var passwordData = new + { + type = "password", + temporary = false, + value = password + }; + + var json = JsonConvert.SerializeObject(passwordData, Settings); + var httpContent = new StringContent(json, Encoding.UTF8, "application/json"); + var response = await _httpClient.PutAsync(url, httpContent); + + if (response.IsSuccessStatusCode) + { + return Result.Success(); + } + + var responseMessage = await response.Content.ReadAsStringAsync(); + UserErrors.SetTechnicalMessage(responseMessage); + return Result.Failure(UserErrors.InvalidUserNameOrPasswordError); + } + + public async Task SendEmailVerificationAsync(string userId) + { + var url = _urlUserActions + .AppendPathSegment(userId); + + var requestData = new + { + requiredActions = new string[] { "VERIFY_EMAIL" } + }; + + var json = JsonConvert.SerializeObject(requestData, Settings); + var httpContent = new StringContent(json, Encoding.UTF8, "application/json"); + var response = await _httpClient.PutAsync(url, httpContent); + + if (response.IsSuccessStatusCode) + { + url = url.AppendPathSegment("send-verify-email"); + await _httpClient.PutAsync(url, default!); + return Result.Success(); + } + + var responseMessage = await response.Content.ReadAsStringAsync(); + UserErrors.SetTechnicalMessage(responseMessage); + return Result.Failure(UserErrors.InvalidUserNameOrPasswordError); + } + + public async Task> LoginAsync(string tenant, User user) + { + var urlGetToken = _httpClient.BaseAddress.AppendPathSegment("realms") + .AppendPathSegment(tenant) + .AppendPathSegment("protocol") + .AppendPathSegment("openid-connect") + .AppendPathSegment("token"); + var requestData = new FormUrlEncodedContent( [ new KeyValuePair("grant_type", "password"), @@ -127,16 +175,7 @@ public async Task> LoginAsync(User user) new KeyValuePair("scope", "tokenmanager-write tokenmanager-read"), ]); - var tenant = _authService.GetTenantFromToken(); - - var url = _httpClient.BaseAddress - .AppendPathSegment("realms") - .AppendPathSegment(tenant) - .AppendPathSegment("protocol") - .AppendPathSegment("openid-connect") - .AppendPathSegment("token"); - - var response = await _httpClient.PostAsync(url, requestData); + var response = await _httpClient.PostAsync(urlGetToken, requestData); if (response.IsSuccessStatusCode) { @@ -149,5 +188,16 @@ public async Task> LoginAsync(User user) UserErrors.SetTechnicalMessage(responseMessage); return Result.Failure(UserErrors.InvalidUserNameOrPasswordError); } + + private void ConfigureHttpClient(string tenant, string accessToken) + { + _httpClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", accessToken); + + _urlUserActions = _httpClient.BaseAddress + .AppendPathSegment("admin") + .AppendPathSegment("realms") + .AppendPathSegment(tenant) + .AppendPathSegment("users"); + } } }