Merge pull request 'Add authentication methods to access protected resources' (#15) from feat/auth into release/v1.0.0
All checks were successful
.NET Test Pipeline / build-and-test (push) Successful in 3m48s
All checks were successful
.NET Test Pipeline / build-and-test (push) Successful in 3m48s
Reviewed-on: #15
This commit is contained in:
commit
42f0b8ee0e
12
ApiDto/Common/AuthRoles.cs
Normal file
12
ApiDto/Common/AuthRoles.cs
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
namespace Mirea.Api.Dto.Common;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// An enumeration that indicates which role the user belongs to
|
||||||
|
/// </summary>
|
||||||
|
public enum AuthRoles
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Administrator
|
||||||
|
/// </summary>
|
||||||
|
Admin
|
||||||
|
}
|
21
ApiDto/Requests/LoginRequest.cs
Normal file
21
ApiDto/Requests/LoginRequest.cs
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace Mirea.Api.Dto.Requests;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Request to receive protected content
|
||||||
|
/// </summary>
|
||||||
|
public class LoginRequest
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Login or Email to identify the client.
|
||||||
|
/// </summary>
|
||||||
|
[Required]
|
||||||
|
public required string Username { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The client's password.
|
||||||
|
/// </summary>
|
||||||
|
[Required]
|
||||||
|
public required string Password { get; set; }
|
||||||
|
}
|
23
ApiDto/Responses/TokenResponse.cs
Normal file
23
ApiDto/Responses/TokenResponse.cs
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
using System;
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace Mirea.Api.Dto.Responses;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Provides a JWT and RT token.
|
||||||
|
/// </summary>
|
||||||
|
public class TokenResponse
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A JWT token for accessing protected resources.
|
||||||
|
/// </summary>
|
||||||
|
[Required]
|
||||||
|
public required string AccessToken { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The date and time when the JWT token expires.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>After this date, a new JWT token must be requested.</remarks>
|
||||||
|
[Required]
|
||||||
|
public required DateTime ExpiresIn { get; set; }
|
||||||
|
}
|
@ -17,7 +17,7 @@ public class DistributedCacheService(IDistributedCache cache) : ICacheService
|
|||||||
SlidingExpiration = slidingExpiration
|
SlidingExpiration = slidingExpiration
|
||||||
};
|
};
|
||||||
|
|
||||||
var serializedValue = JsonSerializer.SerializeToUtf8Bytes(value);
|
var serializedValue = value as byte[] ?? JsonSerializer.SerializeToUtf8Bytes(value);
|
||||||
await cache.SetAsync(key, serializedValue, options, cancellationToken);
|
await cache.SetAsync(key, serializedValue, options, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
using Microsoft.Extensions.Caching.Memory;
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
using Mirea.Api.Security.Common.Interfaces;
|
using Mirea.Api.Security.Common.Interfaces;
|
||||||
using System;
|
using System;
|
||||||
|
using System.Text.Json;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
@ -16,14 +17,17 @@ public class MemoryCacheService(IMemoryCache cache) : ICacheService
|
|||||||
SlidingExpiration = slidingExpiration
|
SlidingExpiration = slidingExpiration
|
||||||
};
|
};
|
||||||
|
|
||||||
cache.Set(key, value, options);
|
cache.Set(key, value as byte[] ?? JsonSerializer.SerializeToUtf8Bytes(value), options);
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<T?> GetAsync<T>(string key, CancellationToken cancellationToken = default)
|
public Task<T?> GetAsync<T>(string key, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
cache.TryGetValue(key, out T? value);
|
return Task.FromResult(
|
||||||
return Task.FromResult(value);
|
cache.TryGetValue(key, out byte[]? value) ?
|
||||||
|
JsonSerializer.Deserialize<T>(value) :
|
||||||
|
default
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task RemoveAsync(string key, CancellationToken cancellationToken = default)
|
public Task RemoveAsync(string key, CancellationToken cancellationToken = default)
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
using Microsoft.AspNetCore.Mvc.ApiExplorer;
|
using Microsoft.AspNetCore.Mvc.ApiExplorer;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
|
using Microsoft.OpenApi.Models;
|
||||||
using Mirea.Api.Endpoint.Configuration.Swagger;
|
using Mirea.Api.Endpoint.Configuration.Swagger;
|
||||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||||
using System;
|
using System;
|
||||||
@ -19,6 +20,29 @@ public static class SwaggerConfiguration
|
|||||||
options.OperationFilter<SwaggerDefaultValues>();
|
options.OperationFilter<SwaggerDefaultValues>();
|
||||||
var basePath = AppDomain.CurrentDomain.BaseDirectory;
|
var basePath = AppDomain.CurrentDomain.BaseDirectory;
|
||||||
|
|
||||||
|
options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
|
||||||
|
{
|
||||||
|
In = ParameterLocation.Header,
|
||||||
|
Description = "Keep the JWT token in the field (Bearer token)",
|
||||||
|
Name = "Authorization",
|
||||||
|
Type = SecuritySchemeType.ApiKey
|
||||||
|
});
|
||||||
|
|
||||||
|
options.AddSecurityRequirement(new OpenApiSecurityRequirement
|
||||||
|
{
|
||||||
|
{
|
||||||
|
new OpenApiSecurityScheme
|
||||||
|
{
|
||||||
|
Reference = new OpenApiReference
|
||||||
|
{
|
||||||
|
Type = ReferenceType.SecurityScheme,
|
||||||
|
Id = "Bearer"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
options.IncludeXmlComments(Path.Combine(basePath, "docs.xml"));
|
options.IncludeXmlComments(Path.Combine(basePath, "docs.xml"));
|
||||||
options.IncludeXmlComments(Path.Combine(basePath, "ApiDtoDocs.xml"));
|
options.IncludeXmlComments(Path.Combine(basePath, "ApiDtoDocs.xml"));
|
||||||
});
|
});
|
||||||
|
158
Endpoint/Controllers/V1/AuthController.cs
Normal file
158
Endpoint/Controllers/V1/AuthController.cs
Normal file
@ -0,0 +1,158 @@
|
|||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.AspNetCore.Mvc.Filters;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using Mirea.Api.Dto.Common;
|
||||||
|
using Mirea.Api.Dto.Requests;
|
||||||
|
using Mirea.Api.Dto.Responses;
|
||||||
|
using Mirea.Api.Endpoint.Common.Model;
|
||||||
|
using Mirea.Api.Security.Common.Dto.Requests;
|
||||||
|
using Mirea.Api.Security.Services;
|
||||||
|
using System;
|
||||||
|
using System.Security;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Mirea.Api.Endpoint.Controllers.V1;
|
||||||
|
|
||||||
|
[ApiVersion("1.0")]
|
||||||
|
public class AuthController(IOptionsSnapshot<Admin> user, AuthService auth, PasswordHashService passwordService) : BaseController, IActionFilter
|
||||||
|
{
|
||||||
|
private string Fingerprint { get; set; } = string.Empty;
|
||||||
|
private string Ip { get; set; } = string.Empty;
|
||||||
|
private string UserAgent { get; set; } = string.Empty;
|
||||||
|
private string RefreshToken { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
private void SetCookie(string name, string value, DateTimeOffset? expires = null)
|
||||||
|
{
|
||||||
|
var cookieOptions = new CookieOptions
|
||||||
|
{
|
||||||
|
Expires = expires,
|
||||||
|
Path = "/api",
|
||||||
|
Domain = Request.Headers["X-Forwarded-Host"],
|
||||||
|
Secure = true,
|
||||||
|
HttpOnly = true
|
||||||
|
};
|
||||||
|
|
||||||
|
Response.Cookies.Append(name, value, cookieOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetRefreshToken(string value, DateTimeOffset? expires = null) =>
|
||||||
|
SetCookie("refresh_token", value, expires);
|
||||||
|
|
||||||
|
private void SetFirstToken(string value, DateTimeOffset? expires = null) =>
|
||||||
|
SetCookie("authentication_token", value, expires);
|
||||||
|
|
||||||
|
[ApiExplorerSettings(IgnoreApi = true)]
|
||||||
|
public void OnActionExecuting(ActionExecutingContext context)
|
||||||
|
{
|
||||||
|
Ip = context.HttpContext.Connection.RemoteIpAddress?.ToString()!;
|
||||||
|
UserAgent = context.HttpContext.Request.Headers.UserAgent.ToString();
|
||||||
|
Fingerprint = context.HttpContext.Request.Cookies["user_key"] ?? string.Empty;
|
||||||
|
RefreshToken = Request.Cookies["refresh_token"] ?? string.Empty;
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(Fingerprint)) return;
|
||||||
|
|
||||||
|
Fingerprint = Guid.NewGuid().ToString().Replace("-", "");
|
||||||
|
SetCookie("user_key", Fingerprint);
|
||||||
|
}
|
||||||
|
|
||||||
|
[ApiExplorerSettings(IgnoreApi = true)]
|
||||||
|
public void OnActionExecuted(ActionExecutedContext context) { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Handles user authentication by verifying the username/email and password,
|
||||||
|
/// then generating and returning an authentication token if successful.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request">The login request containing the username/email and password.</param>
|
||||||
|
/// <returns>A TokenResponse containing the access token and its expiry if successful, otherwise an Unauthorized response.</returns>
|
||||||
|
[HttpPost("Login")]
|
||||||
|
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||||
|
public async Task<ActionResult<TokenResponse>> Login([FromBody] LoginRequest request)
|
||||||
|
{
|
||||||
|
var userEntity = user.Value;
|
||||||
|
|
||||||
|
if (!userEntity.Username.Equals(request.Username, StringComparison.OrdinalIgnoreCase) &&
|
||||||
|
!userEntity.Email.Equals(request.Username, StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
!passwordService.VerifyPassword(request.Password, userEntity.Salt, userEntity.PasswordHash))
|
||||||
|
return Unauthorized("Invalid username/email or password");
|
||||||
|
|
||||||
|
var token = await auth.GenerateAuthTokensAsync(new TokenRequest
|
||||||
|
{
|
||||||
|
Fingerprint = Fingerprint,
|
||||||
|
Ip = Ip,
|
||||||
|
UserAgent = UserAgent
|
||||||
|
}, "1");
|
||||||
|
|
||||||
|
SetRefreshToken(token.RefreshToken, token.RefreshExpiresIn);
|
||||||
|
|
||||||
|
return Ok(new TokenResponse
|
||||||
|
{
|
||||||
|
AccessToken = token.AccessToken,
|
||||||
|
ExpiresIn = token.AccessExpiresIn
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Refreshes the authentication token using the existing refresh token.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>A TokenResponse containing the new access token and its expiry if successful, otherwise an Unauthorized response.</returns>
|
||||||
|
[HttpGet("ReLogin")]
|
||||||
|
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||||
|
public async Task<ActionResult<TokenResponse>> ReLogin()
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(RefreshToken))
|
||||||
|
return Unauthorized();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var token = await auth.RefreshTokenAsync(
|
||||||
|
new TokenRequest
|
||||||
|
{
|
||||||
|
Ip = Ip,
|
||||||
|
UserAgent = UserAgent,
|
||||||
|
Fingerprint = Fingerprint
|
||||||
|
},
|
||||||
|
RefreshToken
|
||||||
|
);
|
||||||
|
|
||||||
|
SetRefreshToken(token.RefreshToken, token.RefreshExpiresIn);
|
||||||
|
|
||||||
|
return Ok(new TokenResponse
|
||||||
|
{
|
||||||
|
AccessToken = token.AccessToken,
|
||||||
|
ExpiresIn = token.AccessExpiresIn
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (SecurityException)
|
||||||
|
{
|
||||||
|
return Unauthorized();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Logs the user out by clearing the refresh token and performing any necessary cleanup.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>An Ok response if the logout was successful.</returns>
|
||||||
|
[HttpGet("Logout")]
|
||||||
|
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||||
|
[Authorize]
|
||||||
|
public async Task<ActionResult> Logout()
|
||||||
|
{
|
||||||
|
SetRefreshToken("", DateTimeOffset.MinValue);
|
||||||
|
SetFirstToken("", DateTimeOffset.MinValue);
|
||||||
|
|
||||||
|
await auth.LogoutAsync(Fingerprint);
|
||||||
|
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieves the role of the authenticated user.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>The role of the authenticated user.</returns>
|
||||||
|
[HttpGet("GetRole")]
|
||||||
|
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||||
|
[Authorize]
|
||||||
|
public ActionResult<AuthRoles> GetRole() => Ok(AuthRoles.Admin);
|
||||||
|
}
|
23
Endpoint/Middleware/JwtRevocationMiddleware.cs
Normal file
23
Endpoint/Middleware/JwtRevocationMiddleware.cs
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Mirea.Api.Security.Common.Interfaces;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Mirea.Api.Endpoint.Middleware;
|
||||||
|
|
||||||
|
public class JwtRevocationMiddleware(RequestDelegate next)
|
||||||
|
{
|
||||||
|
public async Task Invoke(HttpContext context, IRevokedToken revokedTokenStore)
|
||||||
|
{
|
||||||
|
if (context.Request.Headers.ContainsKey("Authorization"))
|
||||||
|
{
|
||||||
|
var token = context.Request.Headers.Authorization.ToString().Replace("Bearer ", "");
|
||||||
|
if (await revokedTokenStore.IsTokenRevokedAsync(token))
|
||||||
|
{
|
||||||
|
context.Response.StatusCode = StatusCodes.Status401Unauthorized;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await next(context);
|
||||||
|
}
|
||||||
|
}
|
@ -6,6 +6,7 @@ using Mirea.Api.DataAccess.Application;
|
|||||||
using Mirea.Api.DataAccess.Persistence;
|
using Mirea.Api.DataAccess.Persistence;
|
||||||
using Mirea.Api.DataAccess.Persistence.Common;
|
using Mirea.Api.DataAccess.Persistence.Common;
|
||||||
using Mirea.Api.Endpoint.Common.Interfaces;
|
using Mirea.Api.Endpoint.Common.Interfaces;
|
||||||
|
using Mirea.Api.Endpoint.Common.Model;
|
||||||
using Mirea.Api.Endpoint.Common.Services;
|
using Mirea.Api.Endpoint.Common.Services;
|
||||||
using Mirea.Api.Endpoint.Configuration.AppConfig;
|
using Mirea.Api.Endpoint.Configuration.AppConfig;
|
||||||
using Mirea.Api.Endpoint.Configuration.General;
|
using Mirea.Api.Endpoint.Configuration.General;
|
||||||
@ -37,6 +38,8 @@ public class Program
|
|||||||
builder.Configuration.AddConfiguration(EnvironmentConfiguration.GetEnvironment());
|
builder.Configuration.AddConfiguration(EnvironmentConfiguration.GetEnvironment());
|
||||||
builder.Configuration.AddJsonFile(PathBuilder.Combine(GeneralConfig.FilePath), optional: true, reloadOnChange: true);
|
builder.Configuration.AddJsonFile(PathBuilder.Combine(GeneralConfig.FilePath), optional: true, reloadOnChange: true);
|
||||||
builder.Services.Configure<GeneralConfig>(builder.Configuration);
|
builder.Services.Configure<GeneralConfig>(builder.Configuration);
|
||||||
|
builder.Configuration.AddJsonFile(PathBuilder.Combine(Admin.PathToSave), optional: true, reloadOnChange: true);
|
||||||
|
builder.Services.Configure<Admin>(builder.Configuration);
|
||||||
|
|
||||||
builder.Host.AddCustomSerilog();
|
builder.Host.AddCustomSerilog();
|
||||||
AddDatabase(builder.Services, builder.Configuration);
|
AddDatabase(builder.Services, builder.Configuration);
|
||||||
@ -95,6 +98,7 @@ public class Program
|
|||||||
|
|
||||||
app.UseMiddleware<MaintenanceModeMiddleware>();
|
app.UseMiddleware<MaintenanceModeMiddleware>();
|
||||||
app.UseMiddleware<CustomExceptionHandlerMiddleware>();
|
app.UseMiddleware<CustomExceptionHandlerMiddleware>();
|
||||||
|
app.UseMiddleware<JwtRevocationMiddleware>();
|
||||||
|
|
||||||
app.UseHttpsRedirection();
|
app.UseHttpsRedirection();
|
||||||
|
|
||||||
|
@ -5,6 +5,7 @@ namespace Mirea.Api.Security.Common.Dto.Responses;
|
|||||||
public class AuthTokenResponse
|
public class AuthTokenResponse
|
||||||
{
|
{
|
||||||
public required string AccessToken { get; set; }
|
public required string AccessToken { get; set; }
|
||||||
|
public DateTime AccessExpiresIn { get; set; }
|
||||||
public required string RefreshToken { get; set; }
|
public required string RefreshToken { get; set; }
|
||||||
public DateTime ExpiresIn { get; set; }
|
public DateTime RefreshExpiresIn { get; set; }
|
||||||
}
|
}
|
@ -34,7 +34,7 @@ public class AuthService(ICacheService cache, IAccessToken accessTokenService, I
|
|||||||
public async Task<AuthTokenResponse> GenerateAuthTokensAsync(TokenRequest request, string userId, CancellationToken cancellation = default)
|
public async Task<AuthTokenResponse> GenerateAuthTokensAsync(TokenRequest request, string userId, CancellationToken cancellation = default)
|
||||||
{
|
{
|
||||||
var refreshToken = GenerateRefreshToken();
|
var refreshToken = GenerateRefreshToken();
|
||||||
var accessToken = GenerateAccessToken(userId);
|
var (token, expireIn) = GenerateAccessToken(userId);
|
||||||
|
|
||||||
var authTokenStruct = new AuthToken
|
var authTokenStruct = new AuthToken
|
||||||
{
|
{
|
||||||
@ -43,16 +43,17 @@ public class AuthService(ICacheService cache, IAccessToken accessTokenService, I
|
|||||||
RefreshToken = refreshToken,
|
RefreshToken = refreshToken,
|
||||||
UserAgent = request.UserAgent,
|
UserAgent = request.UserAgent,
|
||||||
UserId = userId,
|
UserId = userId,
|
||||||
AccessToken = accessToken.Token
|
AccessToken = token
|
||||||
};
|
};
|
||||||
|
|
||||||
await SetAuthTokenDataToCache(request.Fingerprint, authTokenStruct, cancellation);
|
await SetAuthTokenDataToCache(request.Fingerprint, authTokenStruct, cancellation);
|
||||||
|
|
||||||
return new AuthTokenResponse
|
return new AuthTokenResponse
|
||||||
{
|
{
|
||||||
AccessToken = accessToken.Token,
|
AccessToken = token,
|
||||||
ExpiresIn = accessToken.ExpireIn,
|
AccessExpiresIn = expireIn,
|
||||||
RefreshToken = authTokenStruct.RefreshToken
|
RefreshToken = authTokenStruct.RefreshToken,
|
||||||
|
RefreshExpiresIn = DateTime.UtcNow.Add(Lifetime),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -71,23 +72,28 @@ public class AuthService(ICacheService cache, IAccessToken accessTokenService, I
|
|||||||
authToken.UserAgent != request.UserAgent &&
|
authToken.UserAgent != request.UserAgent &&
|
||||||
authToken.Ip != request.Ip)
|
authToken.Ip != request.Ip)
|
||||||
{
|
{
|
||||||
await cache.RemoveAsync(request.Fingerprint, cancellation);
|
await cache.RemoveAsync(GetAuthCacheKey(request.Fingerprint), cancellation);
|
||||||
await RevokeAccessToken(authToken.AccessToken);
|
await RevokeAccessToken(authToken.AccessToken);
|
||||||
|
|
||||||
throw new SecurityException(request.Fingerprint);
|
throw new SecurityException(request.Fingerprint);
|
||||||
}
|
}
|
||||||
|
|
||||||
var accessToken = GenerateAccessToken(authToken.UserId);
|
var (token, expireIn) = GenerateAccessToken(authToken.UserId);
|
||||||
await RevokeAccessToken(authToken.AccessToken);
|
await RevokeAccessToken(authToken.AccessToken);
|
||||||
|
|
||||||
authToken.AccessToken = accessToken.Token;
|
var newRefreshToken = GenerateRefreshToken();
|
||||||
|
|
||||||
|
authToken.AccessToken = token;
|
||||||
|
authToken.RefreshToken = newRefreshToken;
|
||||||
|
|
||||||
await SetAuthTokenDataToCache(request.Fingerprint, authToken, cancellation);
|
await SetAuthTokenDataToCache(request.Fingerprint, authToken, cancellation);
|
||||||
|
|
||||||
return new AuthTokenResponse
|
return new AuthTokenResponse
|
||||||
{
|
{
|
||||||
AccessToken = accessToken.Token,
|
AccessToken = token,
|
||||||
ExpiresIn = accessToken.ExpireIn,
|
AccessExpiresIn = expireIn,
|
||||||
RefreshToken = GenerateRefreshToken()
|
RefreshToken = newRefreshToken,
|
||||||
|
RefreshExpiresIn = DateTime.UtcNow.Add(Lifetime)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user