Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
  • Loading branch information
Felipe Mattioli dos Santos committed Jul 10, 2024
2 parents 7f73e50 + ad1597d commit 0dfb9cc
Show file tree
Hide file tree
Showing 16 changed files with 224 additions and 113 deletions.
103 changes: 73 additions & 30 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,39 +5,51 @@
### Built With
<img src="https://img.shields.io/badge/dotnet8-blue" />

### 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": {
Expand All @@ -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<AuthSettings>();
```

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<JwtSecurityTokenHandler>();
services.AddKeyCloakAuth(authSettings!);
public static class AuthExtension
{
public static IServiceCollection AddApiAuthentication(this IServiceCollection services, AuthSettings authSettings)
{
services.AddHttpContextAccessor();
services.AddSingleton<JwtSecurityTokenHandler>();
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)
Expand All @@ -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"
}
}
```
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<IActionResult> 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<string>();
if (result.IsSuccess)
Expand All @@ -46,22 +47,20 @@ public async Task<IActionResult> CreateUser([FromRoute] string tenant, [FromBody
/// </summary>
/// <returns>A status code related to the operation.</returns>
[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<IActionResult> Login([FromBody] LoginUserRequest loginUserRequest, CancellationToken cancellationToken)
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<IActionResult> 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<TokenResponse>();
if (result.IsSuccess)
{
response.Result = result.Value;
response.DetailMessage = "Token generated with succesfully";
return Created();
return Ok(response);
}

response.DetailMessage = result.Error.Description;
Expand Down
38 changes: 24 additions & 14 deletions src/Feijuca.Keycloak.TokenManager/TokenManager.Api/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,32 +17,42 @@
.AddEnvironmentVariables();

var applicationSettings = builder.Configuration.GetApplicationSettings(builder.Environment);
Console.WriteLine(JsonConvert.SerializeObject(applicationSettings));

builder.Services.AddSingleton<ISettings>(applicationSettings);

builder.Services.AddControllers();
builder.Services
.AddSingleton<ISettings>(applicationSettings)
.AddControllers();

builder.Services.AddSwagger(applicationSettings!.AuthSettings!);
builder.Services
.AddExceptionHandler<GlobalExceptionHandler>()
.AddProblemDetails()
.AddApiAuthentication(applicationSettings.AuthSettings)
.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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<ImplicitUsings>enable</ImplicitUsings>
<GenerateDocumentationFile>True</GenerateDocumentationFile>
<NoWarn>1591</NoWarn>
<Version>1.3.1</Version>
<Version>1.5.1</Version>
</PropertyGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@

namespace TokenManager.Application.Services.Commands.Users
{
public record CreateUserCommand(AddUserRequest AddUserRequest) : IRequest<Result>;
public record CreateUserCommand(string Tenant, AddUserRequest AddUserRequest) : IRequest<Result>;
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ public class CreateUserCommandHandler(IUserRepository userRepository) : IRequest

public async Task<Result> 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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@

namespace TokenManager.Application.Services.Commands.Users
{
public record LoginUserCommand(LoginUserRequest LoginUser) : IRequest<Result<TokenResponse>>;
public record LoginUserCommand(string Tenant, LoginUserRequest LoginUser) : IRequest<Result<TokenResponse>>;
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
using Contracts.Common;
using MediatR;

using TokenManager.Application.Services.Mappers;
using TokenManager.Application.Services.Responses;
using TokenManager.Domain.Interfaces;
Expand All @@ -13,7 +12,8 @@ public class LoginUserCommandHandler(IUserRepository userRepository) : IRequestH

public async Task<Result<TokenResponse>> 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<TokenResponse>.Success(tokenDetailsResult.Value.ToTokenResponse());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Original file line number Diff line number Diff line change
@@ -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);
}
Original file line number Diff line number Diff line change
@@ -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);
}
Original file line number Diff line number Diff line change
@@ -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; }
Expand All @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading

0 comments on commit 0dfb9cc

Please sign in to comment.