diff --git a/sources/ClientSdk.Grpc/Protos/authentication.proto b/sources/ClientSdk.Grpc/Protos/authentication.proto index 10510db..827eca0 100644 --- a/sources/ClientSdk.Grpc/Protos/authentication.proto +++ b/sources/ClientSdk.Grpc/Protos/authentication.proto @@ -23,8 +23,9 @@ message LoginRequest { message LoginResponse { bool isSuccess = 1; string accessToken = 2; - google.protobuf.Timestamp expiration = 3; + google.protobuf.Timestamp accessTokenExpiration = 3; string refreshToken = 4; + google.protobuf.Timestamp refreshTokenExpiration = 5; } message LogoutResponse { diff --git a/sources/Clients.Identity.Api.Contracts/Authentications/LoginProxyResponse.cs b/sources/Clients.Identity.Api.Contracts/Authentications/LoginProxyResponse.cs index f44392e..d2500bb 100644 --- a/sources/Clients.Identity.Api.Contracts/Authentications/LoginProxyResponse.cs +++ b/sources/Clients.Identity.Api.Contracts/Authentications/LoginProxyResponse.cs @@ -4,6 +4,7 @@ public class LoginProxyResponse { public bool IsSuccess { get; set; } public string AccessToken { get; set; } = string.Empty; - public DateTime Expiration { get; set; } + public DateTime AccessTokenExpiration { get; set; } public string RefreshToken { get; set; } = string.Empty; + public DateTime RefreshTokenExpiration { get; set; } } \ No newline at end of file diff --git a/sources/Clients.Identity.Api.Contracts/Authentications/RefreshTokenProxyRequest.cs b/sources/Clients.Identity.Api.Contracts/Authentications/RefreshTokenProxyRequest.cs new file mode 100644 index 0000000..4f375cf --- /dev/null +++ b/sources/Clients.Identity.Api.Contracts/Authentications/RefreshTokenProxyRequest.cs @@ -0,0 +1,6 @@ +namespace MadWorldNL.Clients.Identity.Api.Contracts.Authentications; + +public class RefreshTokenProxyRequest +{ + public string AccessToken { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/sources/Clients.Identity.Api.Shared/Endpoints/EndpointsExtensions.cs b/sources/Clients.Identity.Api.Shared/Endpoints/EndpointsExtensions.cs index cfb1ef3..2f72a19 100644 --- a/sources/Clients.Identity.Api.Shared/Endpoints/EndpointsExtensions.cs +++ b/sources/Clients.Identity.Api.Shared/Endpoints/EndpointsExtensions.cs @@ -27,5 +27,10 @@ private static void AddAuthenticationEndpoints(this WebApplication app) authenticationService.LogoutAsync()) .RequireAuthorization() .WithName("Logout"); + + authenticationEndpoints.MapPost("/RefreshToken", + ([FromBody] RefreshTokenProxyRequest request, [FromServices] AuthenticationService authenticationService) => + authenticationService.RefreshTokenAsync(request)) + .WithName("RefreshToken"); } } \ No newline at end of file diff --git a/sources/Clients.Identity.Api.Shared/Services/AuthenticationService.cs b/sources/Clients.Identity.Api.Shared/Services/AuthenticationService.cs index 656b579..295e86e 100644 --- a/sources/Clients.Identity.Api.Shared/Services/AuthenticationService.cs +++ b/sources/Clients.Identity.Api.Shared/Services/AuthenticationService.cs @@ -1,8 +1,10 @@ using Google.Protobuf.WellKnownTypes; using MadWorldNL.Clients.Identity.Api.Contracts.Authentications; using MadWorldNL.Clients.Identity.Api.Shared.Settings; +using Microsoft.AspNetCore.Identity.Data; using Microsoft.Extensions.Options; using Server.Presentation.Grpc.Authentication.V1; +using LoginRequest = Server.Presentation.Grpc.Authentication.V1.LoginRequest; namespace MadWorldNL.Clients.Identity.Api.Shared.Services; @@ -27,8 +29,9 @@ public async Task AuthenticateAsync(LoginProxyRequest proxyR { IsSuccess = response.IsSuccess, AccessToken = response.AccessToken, - Expiration = response.Expiration.ToDateTime(), - RefreshToken = response.RefreshToken + AccessTokenExpiration = response.AccessTokenExpiration.ToDateTime(), + RefreshToken = response.RefreshToken, + RefreshTokenExpiration = response.RefreshTokenExpiration.ToDateTime() }; } @@ -41,4 +44,24 @@ public async Task LogoutAsync() IsSuccess = response.IsSuccess }; } + + public async Task RefreshTokenAsync(RefreshTokenProxyRequest proxyRequest) + { + var request = new TokenRefreshRequest() + { + Audience = _identitySettings.Audience, + RefreshToken = proxyRequest.AccessToken + }; + + var response = await client.TokenRefreshAsync(request); + + return new LoginProxyResponse() + { + IsSuccess = response.IsSuccess, + AccessToken = response.AccessToken, + AccessTokenExpiration = response.AccessTokenExpiration.ToDateTime(), + RefreshToken = response.RefreshToken, + RefreshTokenExpiration = response.RefreshTokenExpiration.ToDateTime() + }; + } } \ No newline at end of file diff --git a/sources/Clients.Identity.Blazor.Shared/Authentications/AuthenticationManager.cs b/sources/Clients.Identity.Blazor.Shared/Authentications/AuthenticationManager.cs index 858d178..d1c6f1a 100644 --- a/sources/Clients.Identity.Blazor.Shared/Authentications/AuthenticationManager.cs +++ b/sources/Clients.Identity.Blazor.Shared/Authentications/AuthenticationManager.cs @@ -25,6 +25,20 @@ public async Task LoginAsync(LoginProxyRequest request) return response; } + public async Task RefreshAsync(string accessToken) + { + var request = new RefreshTokenProxyRequest() + { + AccessToken = accessToken + }; + + var response = await _authenticationService.RefreshTokenAsync(request); + await _authenticationStorage.SetAccessTokenAsync(response); + await _authenticationStateProvider.GetAuthenticationStateAsync(); + + return response; + } + public async Task GetActiveTokenFromSession() { return await _authenticationStorage.GetAccessTokenAsync(); diff --git a/sources/Clients.Identity.Blazor.Shared/Authentications/AuthenticationService.cs b/sources/Clients.Identity.Blazor.Shared/Authentications/AuthenticationService.cs index b246bce..b32eec8 100644 --- a/sources/Clients.Identity.Blazor.Shared/Authentications/AuthenticationService.cs +++ b/sources/Clients.Identity.Blazor.Shared/Authentications/AuthenticationService.cs @@ -20,4 +20,11 @@ public async Task LoginAsync(LoginProxyRequest request) return await response.Content.ReadFromJsonAsync() ?? new LoginProxyResponse(); } + + public async Task RefreshTokenAsync(RefreshTokenProxyRequest request) + { + var response = await _httpClient.PostAsJsonAsync($"{Endpoint}/RefreshToken", request); + return await response.Content.ReadFromJsonAsync() + ?? new LoginProxyResponse(); + } } \ No newline at end of file diff --git a/sources/Clients.Identity.Blazor.Shared/Authentications/IAuthenticationManager.cs b/sources/Clients.Identity.Blazor.Shared/Authentications/IAuthenticationManager.cs index 4a59058..6da8c14 100644 --- a/sources/Clients.Identity.Blazor.Shared/Authentications/IAuthenticationManager.cs +++ b/sources/Clients.Identity.Blazor.Shared/Authentications/IAuthenticationManager.cs @@ -5,5 +5,6 @@ namespace MadWorldNL.Clients.Identity.Blazor.Shared.Authentications; public interface IAuthenticationManager { Task LoginAsync(LoginProxyRequest request); + Task RefreshAsync(string accessToken); Task GetActiveTokenFromSession(); } \ No newline at end of file diff --git a/sources/Clients.Identity.Blazor.Shared/Authentications/IAuthenticationService.cs b/sources/Clients.Identity.Blazor.Shared/Authentications/IAuthenticationService.cs index fba1bd6..ea57d1c 100644 --- a/sources/Clients.Identity.Blazor.Shared/Authentications/IAuthenticationService.cs +++ b/sources/Clients.Identity.Blazor.Shared/Authentications/IAuthenticationService.cs @@ -5,4 +5,5 @@ namespace MadWorldNL.Clients.Identity.Blazor.Shared.Authentications; public interface IAuthenticationService { Task LoginAsync(LoginProxyRequest request); + Task RefreshTokenAsync(RefreshTokenProxyRequest request); } \ No newline at end of file diff --git a/sources/Clients.Identity.Blazor.Shared/Authentications/MyHttpMessageHandler.cs b/sources/Clients.Identity.Blazor.Shared/Authentications/MyHttpMessageHandler.cs index 939361f..aea19db 100644 --- a/sources/Clients.Identity.Blazor.Shared/Authentications/MyHttpMessageHandler.cs +++ b/sources/Clients.Identity.Blazor.Shared/Authentications/MyHttpMessageHandler.cs @@ -1,5 +1,6 @@ using System.Net; using System.Net.Http.Headers; +using MadWorldNL.Clients.Identity.Api.Contracts.Authentications; namespace MadWorldNL.Clients.Identity.Blazor.Shared.Authentications; @@ -46,9 +47,19 @@ private async Task AddAuthorizationHeader(HttpRequestMessage request) { throw new RefreshTokenInvalidException(); } + + if (accessToken.AccessTokenExpiration.AddMinutes(-5) < DateTimeOffset.UtcNow) + { + accessToken = await RefreshAccessToken(accessToken); + } AddBearerToken(request, accessToken.AccessToken); } + + private async Task RefreshAccessToken(LoginProxyResponse accessToken) + { + return await _authenticationManager.RefreshAsync(accessToken.RefreshToken); + } private static void AddBearerToken(HttpRequestMessage request, string token) { diff --git a/sources/Server.Application/Users/LoginUseCase.cs b/sources/Server.Application/Users/LoginUseCase.cs index 73a9f1d..805ebfd 100644 --- a/sources/Server.Application/Users/LoginUseCase.cs +++ b/sources/Server.Application/Users/LoginUseCase.cs @@ -38,15 +38,17 @@ public async Task Login(Login request) var jwt = _jwtGenerator.GenerateToken(user!, request.Audience, roles); var token = GenerateRefreshToken(); - var refreshToken = new RefreshToken(token, request.Audience, expires: DateTime.UtcNow.AddDays(7), user!.Id); + var refreshTokenExpires = DateTime.UtcNow.AddDays(7); + var refreshToken = new RefreshToken(token, request.Audience, refreshTokenExpires, user!.Id); await _userRepository.AddRefreshToken(refreshToken); return new TokenResponse() { IsSuccess = true, Jwt = jwt.Token, - Expires = jwt.Expires, - RefreshToken = token + JwtExpires = jwt.Expires, + RefreshToken = token, + RefreshTokenExpires = refreshTokenExpires }; } diff --git a/sources/Server.Application/Users/RefreshTokenUseCase.cs b/sources/Server.Application/Users/RefreshTokenUseCase.cs index ba23df1..1e523ee 100644 --- a/sources/Server.Application/Users/RefreshTokenUseCase.cs +++ b/sources/Server.Application/Users/RefreshTokenUseCase.cs @@ -38,8 +38,9 @@ public async Task RefreshToken(Token request) { IsSuccess = true, Jwt = jwt.Token, - Expires = jwt.Expires, - RefreshToken = request.RefreshToken + JwtExpires = jwt.Expires, + RefreshToken = request.RefreshToken, + RefreshTokenExpires = refreshToken.Expires }; } } \ No newline at end of file diff --git a/sources/Server.Domain/Users/Login/TokenResponse.cs b/sources/Server.Domain/Users/Login/TokenResponse.cs index 7f88798..fb76d9d 100644 --- a/sources/Server.Domain/Users/Login/TokenResponse.cs +++ b/sources/Server.Domain/Users/Login/TokenResponse.cs @@ -4,8 +4,9 @@ public class TokenResponse { public bool IsSuccess { get; init; } public string Jwt { get; init; } = string.Empty; - public DateTimeOffset Expires { get; init; } + public DateTimeOffset JwtExpires { get; init; } public string RefreshToken { get; init; } = string.Empty; + public DateTimeOffset RefreshTokenExpires { get; init; } public string Message { get; set; } = string.Empty; @@ -14,7 +15,7 @@ public static TokenResponse AccessDenied() return new TokenResponse() { Message = "Access denied", - Expires = DateTimeOffset.MinValue, + JwtExpires = DateTimeOffset.MinValue, }; } } \ No newline at end of file diff --git a/sources/Server.Presentation.Grpc/Mappers/Authentication/TokenResponseMappers.cs b/sources/Server.Presentation.Grpc/Mappers/Authentication/TokenResponseMappers.cs index 7501c77..7a9c577 100644 --- a/sources/Server.Presentation.Grpc/Mappers/Authentication/TokenResponseMappers.cs +++ b/sources/Server.Presentation.Grpc/Mappers/Authentication/TokenResponseMappers.cs @@ -12,8 +12,9 @@ public static LoginResponse ToLoginResponse(this TokenResponse response) { AccessToken = response.Jwt, RefreshToken = response.RefreshToken, - Expiration = response.Expires.ToTimestamp(), - IsSuccess = response.IsSuccess + AccessTokenExpiration = response.JwtExpires.ToTimestamp(), + IsSuccess = response.IsSuccess, + RefreshTokenExpiration = response.RefreshTokenExpires.ToTimestamp() }; } } \ No newline at end of file