Skip to content
This repository has been archived by the owner on Aug 30, 2024. It is now read-only.

Commit

Permalink
FEAT: Adding new endpoint to refresh token
Browse files Browse the repository at this point in the history
  • Loading branch information
fmattioli committed Jul 23, 2024
1 parent 3f3764e commit a36df97
Show file tree
Hide file tree
Showing 13 changed files with 196 additions and 76 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -24,18 +24,15 @@ public class UserController(IMediator mediator) : Controller
public async Task<IActionResult> CreateUser([FromRoute] string tenant, [FromBody] AddUserRequest addUserRequest, CancellationToken cancellationToken)
{
var result = await _mediator.Send(new CreateUserCommand(tenant, addUserRequest), cancellationToken);

var response = new ResponseResult<string>();

if (result.IsSuccess)
{
response.Result = addUserRequest.Username;
response.DetailMessage = "User created successfully";
var response = ResponseResult<string>.Success("User created successfully");
return Created("/createUser", response);
}

response.Result = "Some error occured while trying executing the operation";
response.DetailMessage = result.Error.Description;
return BadRequest(response);
var responseError = ResponseResult<string>.Failure(result.Error);
return BadRequest(responseError);
}

/// <summary>
Expand All @@ -50,17 +47,38 @@ public async Task<IActionResult> CreateUser([FromRoute] string tenant, [FromBody
public async Task<IActionResult> Login([FromRoute] string tenant, [FromBody] LoginUserRequest loginUserRequest, CancellationToken cancellationToken)
{
var result = await _mediator.Send(new LoginUserCommand(tenant, loginUserRequest), cancellationToken);

if (result.IsSuccess)
{
var response = ResponseResult<TokenDetailsResponse>.Success(result.Result);
return Ok(response);
}

var responseError = ResponseResult<TokenDetailsResponse>.Failure(result.Error);
return BadRequest(responseError);
}

/// <summary>
/// Return a valid JWT token and details about them refreshed.
/// </summary>
/// <returns>A status code related to the operation.</returns>
[HttpPost]
[Route("refreshToken/{tenant}", Name = nameof(RefreshToken))]
[ProducesResponseType(StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<IActionResult> RefreshToken([FromRoute] string tenant, [FromBody] string refreshToken, CancellationToken cancellationToken)
{
var result = await _mediator.Send(new RefreshTokenCommand(tenant, refreshToken), cancellationToken);

var response = new ResponseResult<TokenResponse>();
if (result.IsSuccess)
{
response.Result = result.Value;
response.DetailMessage = "Token generated with succesfully";
var response = ResponseResult<TokenDetailsResponse>.Success(result.Result);
return Ok(response);
}

response.DetailMessage = result.Error.Description;
return BadRequest(response);
var responseError = ResponseResult<TokenDetailsResponse>.Failure(result.Error);
return BadRequest(responseError);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"ClientSecret": "qSGxtu0CFOmZ6Yzr5ntPK2iXppmKeerS",
"ClientId": "smartconsig-api",
"Resource": "smartconsig-api",
"AuthServerUrl": "https://services-keycloak.ul0sru.easypanel.host",
"AuthServerUrl": "https://services-keycloak.ul0sru.easypanel.host"
}
}
}
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(string Tenant, LoginUserRequest LoginUser) : IRequest<Result<TokenResponse>>;
public record LoginUserCommand(string Tenant, LoginUserRequest LoginUser) : IRequest<ResponseResult<TokenDetailsResponse>>;
}
Original file line number Diff line number Diff line change
@@ -1,25 +1,24 @@
using MediatR;
using TokenManager.Application.Services.Mappers;
using TokenManager.Application.Services.Responses;
using TokenManager.Domain.Entities;
using TokenManager.Domain.Interfaces;

namespace TokenManager.Application.Services.Commands.Users
{
public class LoginUserCommandHandler(IUserRepository userRepository) : IRequestHandler<LoginUserCommand, Result<TokenResponse>>
public class LoginUserCommandHandler(IUserRepository userRepository) : IRequestHandler<LoginUserCommand, ResponseResult<TokenDetailsResponse>>
{
private readonly IUserRepository _userRepository = userRepository;

public async Task<Result<TokenResponse>> Handle(LoginUserCommand request, CancellationToken cancellationToken)
public async Task<ResponseResult<TokenDetailsResponse>> Handle(LoginUserCommand request, CancellationToken cancellationToken)
{
var user = request.LoginUser.ToDomain();
var tokenDetailsResult = await _userRepository.LoginAsync(request.Tenant, user);
if (tokenDetailsResult.IsSuccess)
{
return Result<TokenResponse>.Success(tokenDetailsResult.Value.ToTokenResponse());
return tokenDetailsResult.ToTokenResponse();
}

return Result<TokenResponse>.Failure(tokenDetailsResult.Error);
return ResponseResult<TokenDetailsResponse>.Failure(tokenDetailsResult.Error);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
using MediatR;
using TokenManager.Application.Services.Responses;

namespace TokenManager.Application.Services.Commands.Users
{
public record RefreshTokenCommand(string Tenant, string RefreshToken) : IRequest<ResponseResult<TokenDetailsResponse>>;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using MediatR;
using TokenManager.Application.Services.Mappers;
using TokenManager.Application.Services.Responses;
using TokenManager.Domain.Interfaces;

namespace TokenManager.Application.Services.Commands.Users
{
public class RefreshTokenCommandHandler(IUserRepository userRepository) : IRequestHandler<RefreshTokenCommand, ResponseResult<TokenDetailsResponse>>
{
private readonly IUserRepository _userRepository = userRepository;

public async Task<ResponseResult<TokenDetailsResponse>> Handle(RefreshTokenCommand request, CancellationToken cancellationToken)
{
var tokeDetails = await _userRepository.RefreshTokenAsync(request.Tenant, request.RefreshToken);
return tokeDetails.ToTokenResponse();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,23 +11,24 @@ public static User ToDomain(this AddUserRequest userRequest)
return new User(userRequest.Username!, userRequest.Password, userRequest.Email!, userRequest.FirstName!, userRequest.LastName!, userRequest.Attributes);
}


public static User ToDomain(this LoginUserRequest loginUserRequest)
{
return new User(loginUserRequest.Username, loginUserRequest.Password);
}

public static TokenResponse ToTokenResponse(this TokenDetails tokenDetails)
public static ResponseResult<TokenDetailsResponse> ToTokenResponse(this Result<TokenDetails> tokenDetails)
{
return new TokenResponse
var tokenDetailsResponse = new TokenDetailsResponse
{
AccessToken = tokenDetails.Access_Token,
ExpiresIn = tokenDetails.Expires_In,
RefreshToken = tokenDetails.Refresh_Token,
RefreshExpiresIn = tokenDetails.Refresh_Expires_In,
TokenType = tokenDetails.Token_Type,
Scope = tokenDetails.Scope
AccessToken = tokenDetails.Value.Access_Token,
ExpiresIn = tokenDetails.Value.Expires_In,
RefreshToken = tokenDetails.Value.Refresh_Token,
RefreshExpiresIn = tokenDetails.Value.Refresh_Expires_In,
TokenType = tokenDetails.Value.Token_Type,
Scopes = tokenDetails.Value.Scopes
};

return ResponseResult<TokenDetailsResponse>.Success(tokenDetailsResponse);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,57 @@
namespace TokenManager.Application.Services.Responses
using TokenManager.Domain.Entities;

namespace TokenManager.Application.Services.Responses
{
public class ResponseResult<T>
public class ResponseResult
{
public T? Result { get; set; }
public string? DetailMessage { get; set; }
protected ResponseResult(bool isSuccess, Error error)
{
if (isSuccess && error != Error.None ||
!isSuccess && error == Error.None)
{
throw new ArgumentException("Invalid error", nameof(error));
}

IsSuccess = isSuccess;
Error = error;
}

public bool IsSuccess { get; }

public bool IsFailure => !IsSuccess;

public Error Error { get; }

public static ResponseResult Success() => new(true, Error.None);

public static ResponseResult Failure(Error error) => new(false, error);
}

public class ResponseResult<T> : Result
{
private readonly T _result;

private ResponseResult(T value, bool isSuccess, Error error)
: base(isSuccess, error)
{
_result = value;
}

public T Result
{
get
{
if (!IsSuccess)
{
throw new InvalidOperationException("No value available for failure result.");
}

return _result;
}
}

public static ResponseResult<T> Success(T value) => new(value, true, Error.None);

public static new ResponseResult<T> Failure(Error error) => new(default!, false, error);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

namespace TokenManager.Application.Services.Responses
{
public class TokenResponse
public class TokenDetailsResponse
{
[JsonProperty("access_token")]
public string? AccessToken { get; set; }
Expand All @@ -26,6 +26,6 @@ public class TokenResponse
public string? SessionState { get; set; }

[JsonProperty("scope")]
public string? Scope { get; set; }
public string? Scopes { get; set; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@ public class TokenDetails
public int Refresh_Expires_In { get; set; }
public string Refresh_Token { get; set; } = null!;
public string Token_Type { get; set; } = null!;
public string Scope { get; set; } = null!;
public string Scopes { get; set; } = null!;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ public interface IUserRepository
{
Task<Result<TokenDetails>> GetAccessTokenAsync(string tenant);
Task<Result<TokenDetails>> LoginAsync(string tenant, User user);
Task<Result<TokenDetails>> RefreshTokenAsync(string tenant, string refreshToken);
Task<(bool result, string content)> CreateNewUserAsync(string tenant, User user);
Task<Result<User>> GetUserAsync(string userName);
Task<Result> ResetPasswordAsync(string userId, string password);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
using Flurl;

using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;

using System.Text;

using TokenManager.Domain.Entities;
using TokenManager.Domain.Errors;
using TokenManager.Domain.Interfaces;
Expand Down Expand Up @@ -63,7 +60,68 @@ public async Task<Result<TokenDetails>> GetAccessTokenAsync(string tenant)
UserErrors.SetTechnicalMessage(responseMessage);
return Result<TokenDetails>.Failure(UserErrors.TokenGenerationError);
}


public async Task<Result<TokenDetails>> 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<string, string>("grant_type", "password"),
new KeyValuePair<string, string>("client_id", _tokenCredentials.Client_Id),
new KeyValuePair<string, string>("client_secret", _tokenCredentials.Client_Secret),
new KeyValuePair<string, string>("username", user.Username!),
new KeyValuePair<string, string>("password", user.Password!)
]);

var response = await _httpClient.PostAsync(urlGetToken, requestData);

if (response.IsSuccessStatusCode)
{
var content = await response.Content.ReadAsStringAsync();
var result = JsonConvert.DeserializeObject<TokenDetails>(content);
return Result<TokenDetails>.Success(result!);
}

var responseMessage = await response.Content.ReadAsStringAsync();
UserErrors.SetTechnicalMessage(responseMessage);
return Result<TokenDetails>.Failure(UserErrors.InvalidUserNameOrPasswordError);
}

public async Task<Result<TokenDetails>> RefreshTokenAsync(string tenant, string refreshToken)
{
var urlGetToken = _httpClient.BaseAddress.AppendPathSegment("realms")
.AppendPathSegment(tenant)
.AppendPathSegment("protocol")
.AppendPathSegment("openid-connect")
.AppendPathSegment("token");

var requestData = new FormUrlEncodedContent(
[
new KeyValuePair<string, string>("grant_type", "refresh_token"),
new KeyValuePair<string, string>("client_id", _tokenCredentials.Client_Id),
new KeyValuePair<string, string>("client_secret", _tokenCredentials.Client_Secret),
new KeyValuePair<string, string>("refresh_token", refreshToken),
]);

var response = await _httpClient.PostAsync(urlGetToken, requestData);

if (response.IsSuccessStatusCode)
{
var content = await response.Content.ReadAsStringAsync();
var result = JsonConvert.DeserializeObject<TokenDetails>(content);
return Result<TokenDetails>.Success(result!);
}

var responseMessage = await response.Content.ReadAsStringAsync();
UserErrors.SetTechnicalMessage(responseMessage);
return Result<TokenDetails>.Failure(UserErrors.InvalidUserNameOrPasswordError);
}

public async Task<(bool result, string content)> CreateNewUserAsync(string tenant, User user)
{
SetBaseUrlUserAction(tenant);
Expand Down Expand Up @@ -135,38 +193,7 @@ public async Task<Result> SendEmailVerificationAsync(string userId)
var responseMessage = await response.Content.ReadAsStringAsync();
UserErrors.SetTechnicalMessage(responseMessage);
return Result.Failure(UserErrors.InvalidUserNameOrPasswordError);
}

public async Task<Result<TokenDetails>> 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<string, string>("grant_type", "password"),
new KeyValuePair<string, string>("client_id", _tokenCredentials.Client_Id),
new KeyValuePair<string, string>("client_secret", _tokenCredentials.Client_Secret),
new KeyValuePair<string, string>("username", user.Username!),
new KeyValuePair<string, string>("password", user.Password!)
]);

var response = await _httpClient.PostAsync(urlGetToken, requestData);

if (response.IsSuccessStatusCode)
{
var content = await response.Content.ReadAsStringAsync();
var result = JsonConvert.DeserializeObject<TokenDetails>(content);
return Result<TokenDetails>.Success(result!);
}

var responseMessage = await response.Content.ReadAsStringAsync();
UserErrors.SetTechnicalMessage(responseMessage);
return Result<TokenDetails>.Failure(UserErrors.InvalidUserNameOrPasswordError);
}
}

private void SetBaseUrlUserAction(string tenant)
{
Expand Down
Loading

0 comments on commit a36df97

Please sign in to comment.