Compare commits
13 Commits
43edab2912
...
07111b9b61
Author | SHA1 | Date | |
---|---|---|---|
07111b9b61 | |||
538f1d67c8 | |||
233458ed89 | |||
7f87b4d856 | |||
0c6d1c9bfb | |||
516ba5bb8e | |||
9d5007ef3a | |||
c75ac60b0b | |||
5b7412f20f | |||
c4a4478b8c | |||
05166188be | |||
157708d00f | |||
36026b3afb |
17
ApiDto/Common/OAuthAction.cs
Normal file
17
ApiDto/Common/OAuthAction.cs
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
namespace Mirea.Api.Dto.Common;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Defines the actions that can be performed with an OAuth token.
|
||||||
|
/// </summary>
|
||||||
|
public enum OAuthAction
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The action to log in the user using the provided OAuth token.
|
||||||
|
/// </summary>
|
||||||
|
Login,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The action to bind an OAuth provider to the user's account.
|
||||||
|
/// </summary>
|
||||||
|
Bind
|
||||||
|
}
|
@ -1,17 +1,17 @@
|
|||||||
using Mirea.Api.Dto.Common;
|
using PasswordPolicy = Mirea.Api.Dto.Common.PasswordPolicy;
|
||||||
|
|
||||||
namespace Mirea.Api.Endpoint.Common.MapperDto;
|
namespace Mirea.Api.Endpoint.Common.MapperDto;
|
||||||
|
|
||||||
public static class PasswordPolicyConverter
|
public static class PasswordPolicyConverter
|
||||||
{
|
{
|
||||||
public static Security.Common.Domain.PasswordPolicy ConvertFromDto(this PasswordPolicy policy) =>
|
public static Security.Common.Model.PasswordPolicy ConvertFromDto(this PasswordPolicy policy) =>
|
||||||
new(policy.MinimumLength,
|
new(policy.MinimumLength,
|
||||||
policy.RequireLetter,
|
policy.RequireLetter,
|
||||||
policy.RequireLettersDifferentCase,
|
policy.RequireLettersDifferentCase,
|
||||||
policy.RequireDigit,
|
policy.RequireDigit,
|
||||||
policy.RequireSpecialCharacter);
|
policy.RequireSpecialCharacter);
|
||||||
|
|
||||||
public static PasswordPolicy ConvertToDto(this Security.Common.Domain.PasswordPolicy policy) =>
|
public static PasswordPolicy ConvertToDto(this Security.Common.Model.PasswordPolicy policy) =>
|
||||||
new()
|
new()
|
||||||
{
|
{
|
||||||
MinimumLength = policy.MinimumLength,
|
MinimumLength = policy.MinimumLength,
|
||||||
|
@ -1,24 +1,23 @@
|
|||||||
using Mirea.Api.Dto.Common;
|
using Mirea.Api.Dto.Common;
|
||||||
using Mirea.Api.Security.Common.Domain;
|
|
||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace Mirea.Api.Endpoint.Common.MapperDto;
|
namespace Mirea.Api.Endpoint.Common.MapperDto;
|
||||||
|
|
||||||
public static class TwoFactorAuthenticationConverter
|
public static class TwoFactorAuthenticationConverter
|
||||||
{
|
{
|
||||||
public static TwoFactorAuthentication ConvertToDto(this TwoFactorAuthenticator authenticator) =>
|
public static TwoFactorAuthentication ConvertToDto(this Security.Common.Model.TwoFactorAuthenticator authenticator) =>
|
||||||
authenticator switch
|
authenticator switch
|
||||||
{
|
{
|
||||||
TwoFactorAuthenticator.None => TwoFactorAuthentication.None,
|
Security.Common.Model.TwoFactorAuthenticator.None => TwoFactorAuthentication.None,
|
||||||
TwoFactorAuthenticator.Totp => TwoFactorAuthentication.TotpRequired,
|
Security.Common.Model.TwoFactorAuthenticator.Totp => TwoFactorAuthentication.TotpRequired,
|
||||||
_ => throw new ArgumentOutOfRangeException(nameof(authenticator), authenticator, null)
|
_ => throw new ArgumentOutOfRangeException(nameof(authenticator), authenticator, null)
|
||||||
};
|
};
|
||||||
|
|
||||||
public static TwoFactorAuthenticator ConvertFromDto(this TwoFactorAuthentication authentication) =>
|
public static Security.Common.Model.TwoFactorAuthenticator ConvertFromDto(this TwoFactorAuthentication authentication) =>
|
||||||
authentication switch
|
authentication switch
|
||||||
{
|
{
|
||||||
TwoFactorAuthentication.None => TwoFactorAuthenticator.None,
|
TwoFactorAuthentication.None => Security.Common.Model.TwoFactorAuthenticator.None,
|
||||||
TwoFactorAuthentication.TotpRequired => TwoFactorAuthenticator.Totp,
|
TwoFactorAuthentication.TotpRequired => Security.Common.Model.TwoFactorAuthenticator.Totp,
|
||||||
_ => throw new ArgumentOutOfRangeException(nameof(authentication), authentication, null)
|
_ => throw new ArgumentOutOfRangeException(nameof(authentication), authentication, null)
|
||||||
};
|
};
|
||||||
}
|
}
|
20
Endpoint/Common/MapperDto/UserConverter.cs
Normal file
20
Endpoint/Common/MapperDto/UserConverter.cs
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
using Mirea.Api.Endpoint.Configuration.Model;
|
||||||
|
using Mirea.Api.Security.Common.Model;
|
||||||
|
|
||||||
|
namespace Mirea.Api.Endpoint.Common.MapperDto;
|
||||||
|
|
||||||
|
public static class UserConverter
|
||||||
|
{
|
||||||
|
public static User ConvertToSecurity(this Admin data) =>
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Id = 1.ToString(),
|
||||||
|
Email = data.Email,
|
||||||
|
Username = data.Username,
|
||||||
|
PasswordHash = data.PasswordHash,
|
||||||
|
Salt = data.Salt,
|
||||||
|
SecondFactorToken = data.Secret,
|
||||||
|
TwoFactorAuthenticator = data.TwoFactorAuthenticator,
|
||||||
|
OAuthProviders = data.OAuthProviders
|
||||||
|
};
|
||||||
|
}
|
@ -34,10 +34,10 @@ public class CustomExceptionHandlerMiddleware(RequestDelegate next, ILogger<Cust
|
|||||||
|
|
||||||
var problemDetails = new ProblemDetails
|
var problemDetails = new ProblemDetails
|
||||||
{
|
{
|
||||||
Type = "https://tools.ietf.org/html/rfc9110#section-15.6",
|
Type = "https://tools.ietf.org/html/rfc9110#section-15.6.1",
|
||||||
Title = "An unexpected error occurred.",
|
Title = "An unexpected error occurred.",
|
||||||
Status = StatusCodes.Status500InternalServerError,
|
Status = StatusCodes.Status500InternalServerError,
|
||||||
Detail = exception.Message,
|
Detail = "Please provide this traceId to the administrator for further investigation.",
|
||||||
Extensions = new Dictionary<string, object?>()
|
Extensions = new Dictionary<string, object?>()
|
||||||
{
|
{
|
||||||
{ "traceId", traceId }
|
{ "traceId", traceId }
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
using Mirea.Api.Endpoint.Common.Services;
|
using Mirea.Api.Endpoint.Common.Services;
|
||||||
using Mirea.Api.Security.Common.Domain;
|
using Mirea.Api.Security.Common.Domain;
|
||||||
|
using Mirea.Api.Security.Common.Model;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
using Mirea.Api.Endpoint.Common.Services;
|
using Mirea.Api.Endpoint.Common.Services;
|
||||||
using Mirea.Api.Endpoint.Configuration.Model.GeneralSettings;
|
using Mirea.Api.Endpoint.Configuration.Model.GeneralSettings;
|
||||||
using Mirea.Api.Security.Common.Domain;
|
using Mirea.Api.Security.Common.Model;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
@ -18,7 +18,7 @@ using Mirea.Api.Endpoint.Common.Services;
|
|||||||
using Mirea.Api.Endpoint.Configuration.Model;
|
using Mirea.Api.Endpoint.Configuration.Model;
|
||||||
using Mirea.Api.Endpoint.Configuration.Model.GeneralSettings;
|
using Mirea.Api.Endpoint.Configuration.Model.GeneralSettings;
|
||||||
using Mirea.Api.Endpoint.Configuration.Validation.Validators;
|
using Mirea.Api.Endpoint.Configuration.Validation.Validators;
|
||||||
using Mirea.Api.Security.Common.Domain;
|
using Mirea.Api.Security.Common.Model;
|
||||||
using Mirea.Api.Security.Services;
|
using Mirea.Api.Security.Services;
|
||||||
using MySqlConnector;
|
using MySqlConnector;
|
||||||
using Npgsql;
|
using Npgsql;
|
||||||
@ -32,6 +32,7 @@ using System.Linq;
|
|||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using System.Security;
|
using System.Security;
|
||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
|
using CookieOptions = Microsoft.AspNetCore.Http.CookieOptions;
|
||||||
using PasswordPolicy = Mirea.Api.Dto.Common.PasswordPolicy;
|
using PasswordPolicy = Mirea.Api.Dto.Common.PasswordPolicy;
|
||||||
|
|
||||||
namespace Mirea.Api.Endpoint.Controllers.Configuration;
|
namespace Mirea.Api.Endpoint.Controllers.Configuration;
|
||||||
@ -539,7 +540,7 @@ public class SetupController(
|
|||||||
[TokenAuthentication]
|
[TokenAuthentication]
|
||||||
public ActionResult<bool> SetPasswordPolicy([FromBody] PasswordPolicy? policy = null)
|
public ActionResult<bool> SetPasswordPolicy([FromBody] PasswordPolicy? policy = null)
|
||||||
{
|
{
|
||||||
GeneralConfig.PasswordPolicy = policy?.ConvertFromDto() ?? new Security.Common.Domain.PasswordPolicy();
|
GeneralConfig.PasswordPolicy = policy?.ConvertFromDto() ?? new Security.Common.Model.PasswordPolicy();
|
||||||
cache.Set("password", true);
|
cache.Set("password", true);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -11,13 +11,15 @@ using Mirea.Api.Endpoint.Common.Exceptions;
|
|||||||
using Mirea.Api.Endpoint.Common.MapperDto;
|
using Mirea.Api.Endpoint.Common.MapperDto;
|
||||||
using Mirea.Api.Endpoint.Common.Services;
|
using Mirea.Api.Endpoint.Common.Services;
|
||||||
using Mirea.Api.Endpoint.Configuration.Model;
|
using Mirea.Api.Endpoint.Configuration.Model;
|
||||||
using Mirea.Api.Security.Common.Domain;
|
|
||||||
using Mirea.Api.Security.Services;
|
using Mirea.Api.Security.Services;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Security.Claims;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using CookieOptions = Mirea.Api.Security.Common.Model.CookieOptions;
|
||||||
using OAuthProvider = Mirea.Api.Security.Common.Domain.OAuthProvider;
|
using OAuthProvider = Mirea.Api.Security.Common.Domain.OAuthProvider;
|
||||||
|
|
||||||
namespace Mirea.Api.Endpoint.Controllers.V1;
|
namespace Mirea.Api.Endpoint.Controllers.V1;
|
||||||
@ -26,7 +28,7 @@ namespace Mirea.Api.Endpoint.Controllers.V1;
|
|||||||
public class AuthController(IOptionsSnapshot<Admin> user, IOptionsSnapshot<GeneralConfig> generalConfig, AuthService auth,
|
public class AuthController(IOptionsSnapshot<Admin> user, IOptionsSnapshot<GeneralConfig> generalConfig, AuthService auth,
|
||||||
PasswordHashService passwordService, OAuthService oAuthService) : BaseController
|
PasswordHashService passwordService, OAuthService oAuthService) : BaseController
|
||||||
{
|
{
|
||||||
private CookieOptionsParameters GetCookieParams() =>
|
private CookieOptions GetCookieParams() =>
|
||||||
new()
|
new()
|
||||||
{
|
{
|
||||||
Domain = HttpContext.GetCurrentDomain(),
|
Domain = HttpContext.GetCurrentDomain(),
|
||||||
@ -53,6 +55,19 @@ public class AuthController(IOptionsSnapshot<Admin> user, IOptionsSnapshot<Gener
|
|||||||
return $"<!DOCTYPE html><html lang=ru><head><meta charset=UTF-8><meta content=\"width=device-width,initial-scale=1\"name=viewport><link href=\"https://fonts.googleapis.com/css2?family=Roboto:wght@400;700&display=swap\"rel=stylesheet><style>body{{background-color:#121212;color:#fff;font-family:Roboto,sans-serif;display:flex;justify-content:center;align-items:center;height:100vh;margin:0;text-align:center}}.container{{max-width:600px;padding:20px;border-radius:8px;background-color:#1e1e1e;box-shadow:0 4px 20px rgba(0,0,0,.5)}}h1{{font-size:24px;margin-bottom:20px}}</style><title>{title}</title></head><body><div class=container><h1>{title}</h1>{blockInfo}<p style=font-size:14px;color:silver;>{message}</p><code style=font-size:12px;color:gray;>TraceId={traceId}</code></div>{script}</body></html>";
|
return $"<!DOCTYPE html><html lang=ru><head><meta charset=UTF-8><meta content=\"width=device-width,initial-scale=1\"name=viewport><link href=\"https://fonts.googleapis.com/css2?family=Roboto:wght@400;700&display=swap\"rel=stylesheet><style>body{{background-color:#121212;color:#fff;font-family:Roboto,sans-serif;display:flex;justify-content:center;align-items:center;height:100vh;margin:0;text-align:center}}.container{{max-width:600px;padding:20px;border-radius:8px;background-color:#1e1e1e;box-shadow:0 4px 20px rgba(0,0,0,.5)}}h1{{font-size:24px;margin-bottom:20px}}</style><title>{title}</title></head><body><div class=container><h1>{title}</h1>{blockInfo}<p style=font-size:14px;color:silver;>{message}</p><code style=font-size:12px;color:gray;>TraceId={traceId}</code></div>{script}</body></html>";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Handles the callback from an OAuth2 provider and finalizes the authorization process.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// This method processes the response from an OAuth provider after the user authorizes the application.
|
||||||
|
/// Upon successful authorization, it redirects the user back to the specified callback URL.
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="code">The authorization code returned by the OAuth provider.</param>
|
||||||
|
/// <param name="state">The state parameter to ensure the request's integrity and prevent CSRF attacks.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// An HTML response indicating the success or failure of the authorization process.
|
||||||
|
/// If a callback URL is provided, the user will be redirected to it.
|
||||||
|
/// </returns>
|
||||||
[HttpGet("OAuth2")]
|
[HttpGet("OAuth2")]
|
||||||
[BadRequestResponse]
|
[BadRequestResponse]
|
||||||
[Produces("text/html")]
|
[Produces("text/html")]
|
||||||
@ -69,7 +84,7 @@ public class AuthController(IOptionsSnapshot<Admin> user, IOptionsSnapshot<Gener
|
|||||||
traceId,
|
traceId,
|
||||||
true), "text/html");
|
true), "text/html");
|
||||||
|
|
||||||
var result = await oAuthService.LoginOAuth(HttpContext, GetCookieParams(),
|
var result = await oAuthService.LoginOAuth(GetCookieParams(), HttpContext,
|
||||||
HttpContext.GetApiUrl(Url.Action("OAuth2")!), code, state);
|
HttpContext.GetApiUrl(Url.Action("OAuth2")!), code, state);
|
||||||
|
|
||||||
string? callbackUrl = null;
|
string? callbackUrl = null;
|
||||||
@ -122,7 +137,7 @@ public class AuthController(IOptionsSnapshot<Admin> user, IOptionsSnapshot<Gener
|
|||||||
if (!callback.IsAbsoluteUri)
|
if (!callback.IsAbsoluteUri)
|
||||||
throw new ControllerArgumentException("The callback URL must be absolute.");
|
throw new ControllerArgumentException("The callback URL must be absolute.");
|
||||||
|
|
||||||
return Redirect(oAuthService.GetProviderRedirect(HttpContext, GetCookieParams(),
|
return Redirect(oAuthService.GetProviderRedirect(GetCookieParams(), HttpContext,
|
||||||
HttpContext.GetApiUrl(Url.Action("OAuth2")!),
|
HttpContext.GetApiUrl(Url.Action("OAuth2")!),
|
||||||
(OAuthProvider)provider,
|
(OAuthProvider)provider,
|
||||||
callback).AbsoluteUri);
|
callback).AbsoluteUri);
|
||||||
@ -150,6 +165,67 @@ public class AuthController(IOptionsSnapshot<Admin> user, IOptionsSnapshot<Gener
|
|||||||
})
|
})
|
||||||
.ConvertToDto());
|
.ConvertToDto());
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Processes the OAuth token
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="token">The OAuth token used for authentication or binding.</param>
|
||||||
|
/// <param name="action">The action to be performed: Login or Bind.</param>
|
||||||
|
/// <returns>If <see cref="OAuthAction.Bind"/> return Ok. If <see cref="OAuthAction.Login"/> return <see cref="TwoFactorAuthentication"/></returns>
|
||||||
|
[HttpGet("HandleToken")]
|
||||||
|
[MaintenanceModeIgnore]
|
||||||
|
[BadRequestResponse]
|
||||||
|
public async Task<ActionResult> HandleToken([FromQuery][MinLength(2)] string token, [FromQuery] OAuthAction action)
|
||||||
|
{
|
||||||
|
var (oAuthUser, error, isSuccess, provider) = await oAuthService.GetOAuthUser(GetCookieParams(), HttpContext, token);
|
||||||
|
|
||||||
|
if (!isSuccess || oAuthUser == null || provider == null)
|
||||||
|
throw new ControllerArgumentException(error ?? "Token processing error.");
|
||||||
|
|
||||||
|
switch (action)
|
||||||
|
{
|
||||||
|
case OAuthAction.Login:
|
||||||
|
return Ok(await auth.LoginOAuthAsync(GetCookieParams(), HttpContext, user.Value.ConvertToSecurity(), oAuthUser, provider.Value));
|
||||||
|
|
||||||
|
case OAuthAction.Bind:
|
||||||
|
var userId = HttpContext.User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||||
|
var admin = user.Value;
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(userId) || !int.TryParse(userId, out var result) || result != 1)
|
||||||
|
return Unauthorized(new ProblemDetails
|
||||||
|
{
|
||||||
|
Type = "https://tools.ietf.org/html/rfc9110#section-15.5.2",
|
||||||
|
Title = "Unauthorized",
|
||||||
|
Status = StatusCodes.Status401Unauthorized,
|
||||||
|
Detail = "The user is not logged in to link accounts.",
|
||||||
|
Extensions = new Dictionary<string, object?>()
|
||||||
|
{
|
||||||
|
{ "traceId", Activity.Current?.Id ?? HttpContext.TraceIdentifier }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (admin.OAuthProviders != null && admin.OAuthProviders.ContainsKey(provider.Value))
|
||||||
|
return Conflict(new ProblemDetails
|
||||||
|
{
|
||||||
|
Type = "https://tools.ietf.org/html/rfc9110#section-15.5.10",
|
||||||
|
Title = "Conflict",
|
||||||
|
Status = StatusCodes.Status409Conflict,
|
||||||
|
Detail = "This OAuth provider is already associated with the account.",
|
||||||
|
Extensions = new Dictionary<string, object?>()
|
||||||
|
{
|
||||||
|
{ "traceId", Activity.Current?.Id ?? HttpContext.TraceIdentifier }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
admin.OAuthProviders ??= [];
|
||||||
|
admin.OAuthProviders.Add(provider.Value, oAuthUser);
|
||||||
|
admin.SaveSetting();
|
||||||
|
|
||||||
|
return Ok();
|
||||||
|
default:
|
||||||
|
throw new ControllerArgumentException("The action cannot be processed.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Logs in a user using their username or email and password.
|
/// Logs in a user using their username or email and password.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -163,18 +239,9 @@ public class AuthController(IOptionsSnapshot<Admin> user, IOptionsSnapshot<Gener
|
|||||||
|
|
||||||
var tokenResult = await auth.LoginAsync(
|
var tokenResult = await auth.LoginAsync(
|
||||||
GetCookieParams(),
|
GetCookieParams(),
|
||||||
new User
|
HttpContext,
|
||||||
{
|
userEntity.ConvertToSecurity(),
|
||||||
Id = 1.ToString(),
|
request.Password, request.Username);
|
||||||
Username = userEntity.Username,
|
|
||||||
Email = userEntity.Email,
|
|
||||||
PasswordHash = userEntity.PasswordHash,
|
|
||||||
Salt = userEntity.Salt,
|
|
||||||
TwoFactorAuthenticator = userEntity.TwoFactorAuthenticator,
|
|
||||||
SecondFactorToken = userEntity.Secret,
|
|
||||||
OAuthProviders = userEntity.OAuthProviders
|
|
||||||
},
|
|
||||||
HttpContext, request.Password, request.Username);
|
|
||||||
|
|
||||||
return Ok(tokenResult.ConvertToDto());
|
return Ok(tokenResult.ConvertToDto());
|
||||||
}
|
}
|
||||||
@ -186,11 +253,8 @@ public class AuthController(IOptionsSnapshot<Admin> user, IOptionsSnapshot<Gener
|
|||||||
/// <returns>A boolean indicating whether the two-factor authentication was successful.</returns>
|
/// <returns>A boolean indicating whether the two-factor authentication was successful.</returns>
|
||||||
[HttpPost("2FA")]
|
[HttpPost("2FA")]
|
||||||
[BadRequestResponse]
|
[BadRequestResponse]
|
||||||
public async Task<ActionResult<bool>> TwoFactorAuth([FromBody] TwoFactorAuthRequest request)
|
public async Task<ActionResult<bool>> TwoFactorAuth([FromBody] TwoFactorAuthRequest request) =>
|
||||||
{
|
await auth.LoginAsync(GetCookieParams(), HttpContext, request.Method.ConvertFromDto(), request.Code);
|
||||||
var tokenResult = await auth.LoginAsync(GetCookieParams(), HttpContext, request.Method.ConvertFromDto(), request.Code);
|
|
||||||
return Ok(tokenResult);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Refreshes the authentication token using the existing refresh token.
|
/// Refreshes the authentication token using the existing refresh token.
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
namespace Mirea.Api.Security.Common.Domain.Caching;
|
using Mirea.Api.Security.Common.Model;
|
||||||
|
|
||||||
|
namespace Mirea.Api.Security.Common.Domain.Caching;
|
||||||
|
|
||||||
internal class FirstAuthToken
|
internal class FirstAuthToken
|
||||||
{
|
{
|
||||||
|
9
Security/Common/Domain/Caching/OAuthUserExtension.cs
Normal file
9
Security/Common/Domain/Caching/OAuthUserExtension.cs
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
namespace Mirea.Api.Security.Common.Domain.Caching;
|
||||||
|
|
||||||
|
internal class OAuthUserExtension
|
||||||
|
{
|
||||||
|
public string? Message { get; set; }
|
||||||
|
public bool IsSuccess { get; set; }
|
||||||
|
public required OAuthProvider? Provider { get; set; }
|
||||||
|
public OAuthUser? User { get; set; }
|
||||||
|
}
|
@ -1,6 +1,6 @@
|
|||||||
namespace Mirea.Api.Security.Common.Domain.OAuth2;
|
namespace Mirea.Api.Security.Common.Domain;
|
||||||
|
|
||||||
public class OAuthPayload
|
internal class OAuthPayload
|
||||||
{
|
{
|
||||||
public required OAuthProvider Provider { get; set; }
|
public required OAuthProvider Provider { get; set; }
|
||||||
public required string Callback { get; set; }
|
public required string Callback { get; set; }
|
@ -1,6 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace Mirea.Api.Security.Common.Domain.OAuth2;
|
namespace Mirea.Api.Security.Common.Domain;
|
||||||
|
|
||||||
internal readonly struct OAuthProviderUrisData
|
internal readonly struct OAuthProviderUrisData
|
||||||
{
|
{
|
@ -7,7 +7,7 @@ namespace Mirea.Api.Security.Common.Domain;
|
|||||||
|
|
||||||
internal class RequestContextInfo
|
internal class RequestContextInfo
|
||||||
{
|
{
|
||||||
public RequestContextInfo(HttpContext context, CookieOptionsParameters cookieOptions)
|
public RequestContextInfo(HttpContext context, Model.CookieOptions cookieOptions)
|
||||||
{
|
{
|
||||||
var ipEntity = context.Connection.RemoteIpAddress;
|
var ipEntity = context.Connection.RemoteIpAddress;
|
||||||
|
|
||||||
|
@ -1,16 +1,16 @@
|
|||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace Mirea.Api.Security.Common.Domain;
|
namespace Mirea.Api.Security.Common.Model;
|
||||||
|
|
||||||
public class CookieOptionsParameters
|
public class CookieOptions
|
||||||
{
|
{
|
||||||
public required string Domain { get; set; }
|
public required string Domain { get; set; }
|
||||||
public required string Path { get; set; }
|
public required string Path { get; set; }
|
||||||
|
|
||||||
internal void SetCookie(HttpContext context, string name, string value, DateTimeOffset? expires = null)
|
internal void SetCookie(HttpContext context, string name, string value, DateTimeOffset? expires = null)
|
||||||
{
|
{
|
||||||
var cookieOptions = new CookieOptions
|
var cookieOptions = new Microsoft.AspNetCore.Http.CookieOptions
|
||||||
{
|
{
|
||||||
Expires = expires,
|
Expires = expires,
|
||||||
Path = Path,
|
Path = Path,
|
@ -1,4 +1,4 @@
|
|||||||
namespace Mirea.Api.Security.Common.Domain;
|
namespace Mirea.Api.Security.Common.Model;
|
||||||
|
|
||||||
public class PasswordPolicy(
|
public class PasswordPolicy(
|
||||||
int minimumLength = 8,
|
int minimumLength = 8,
|
@ -1,4 +1,4 @@
|
|||||||
namespace Mirea.Api.Security.Common.Domain;
|
namespace Mirea.Api.Security.Common.Model;
|
||||||
|
|
||||||
public enum TwoFactorAuthenticator
|
public enum TwoFactorAuthenticator
|
||||||
{
|
{
|
@ -1,6 +1,7 @@
|
|||||||
using System.Collections.Generic;
|
using Mirea.Api.Security.Common.Domain;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace Mirea.Api.Security.Common.Domain;
|
namespace Mirea.Api.Security.Common.Model;
|
||||||
|
|
||||||
public class User
|
public class User
|
||||||
{
|
{
|
@ -1,8 +1,8 @@
|
|||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace Mirea.Api.Security.Common.Domain.OAuth2;
|
namespace Mirea.Api.Security.Common.OAuth2;
|
||||||
|
|
||||||
public class OAuthTokenResponse
|
internal class OAuthTokenResponse
|
||||||
{
|
{
|
||||||
[JsonPropertyName("access_token")]
|
[JsonPropertyName("access_token")]
|
||||||
public required string AccessToken { get; set; }
|
public required string AccessToken { get; set; }
|
@ -1,7 +1,8 @@
|
|||||||
using Mirea.Api.Security.Common.Interfaces;
|
using Mirea.Api.Security.Common.Domain;
|
||||||
|
using Mirea.Api.Security.Common.Interfaces;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace Mirea.Api.Security.Common.Domain.OAuth2.UserInfo;
|
namespace Mirea.Api.Security.Common.OAuth2.UserInfo;
|
||||||
|
|
||||||
internal class GoogleUserInfo : IUserInfo
|
internal class GoogleUserInfo : IUserInfo
|
||||||
{
|
{
|
@ -1,7 +1,8 @@
|
|||||||
using Mirea.Api.Security.Common.Interfaces;
|
using Mirea.Api.Security.Common.Domain;
|
||||||
|
using Mirea.Api.Security.Common.Interfaces;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace Mirea.Api.Security.Common.Domain.OAuth2.UserInfo;
|
namespace Mirea.Api.Security.Common.OAuth2.UserInfo;
|
||||||
|
|
||||||
internal class MailRuUserInfo : IUserInfo
|
internal class MailRuUserInfo : IUserInfo
|
||||||
{
|
{
|
@ -1,7 +1,8 @@
|
|||||||
using Mirea.Api.Security.Common.Interfaces;
|
using Mirea.Api.Security.Common.Domain;
|
||||||
|
using Mirea.Api.Security.Common.Interfaces;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace Mirea.Api.Security.Common.Domain.OAuth2.UserInfo;
|
namespace Mirea.Api.Security.Common.OAuth2.UserInfo;
|
||||||
|
|
||||||
internal class YandexUserInfo : IUserInfo
|
internal class YandexUserInfo : IUserInfo
|
||||||
{
|
{
|
@ -1,8 +1,8 @@
|
|||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace Mirea.Api.Security.Common.Domain;
|
namespace Mirea.Api.Security.Common.ViewModel;
|
||||||
|
|
||||||
public class LoginOAuthResult
|
public class LoginOAuth
|
||||||
{
|
{
|
||||||
public bool Success { get; set; }
|
public bool Success { get; set; }
|
||||||
public required string Token { get; set; }
|
public required string Token { get; set; }
|
@ -4,11 +4,13 @@ using Mirea.Api.Security.Common;
|
|||||||
using Mirea.Api.Security.Common.Domain;
|
using Mirea.Api.Security.Common.Domain;
|
||||||
using Mirea.Api.Security.Common.Domain.Caching;
|
using Mirea.Api.Security.Common.Domain.Caching;
|
||||||
using Mirea.Api.Security.Common.Interfaces;
|
using Mirea.Api.Security.Common.Interfaces;
|
||||||
|
using Mirea.Api.Security.Common.Model;
|
||||||
using System;
|
using System;
|
||||||
using System.Security;
|
using System.Security;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using CookieOptions = Mirea.Api.Security.Common.Model.CookieOptions;
|
||||||
|
|
||||||
namespace Mirea.Api.Security.Services;
|
namespace Mirea.Api.Security.Services;
|
||||||
|
|
||||||
@ -94,7 +96,7 @@ public class AuthService(ICacheService cache, IAccessToken accessTokenService, I
|
|||||||
throw new SecurityException("Authentication failed. Please check your credentials.");
|
throw new SecurityException("Authentication failed. Please check your credentials.");
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task GenerateAuthTokensAsync(CookieOptionsParameters cookieOptions, HttpContext context,
|
private async Task GenerateAuthTokensAsync(CookieOptions cookieOptions, HttpContext context,
|
||||||
RequestContextInfo requestContext, string userId, CancellationToken cancellation = default)
|
RequestContextInfo requestContext, string userId, CancellationToken cancellation = default)
|
||||||
{
|
{
|
||||||
var refreshToken = GenerateRefreshToken();
|
var refreshToken = GenerateRefreshToken();
|
||||||
@ -118,23 +120,7 @@ public class AuthService(ICacheService cache, IAccessToken accessTokenService, I
|
|||||||
authToken.Fingerprint);
|
authToken.Fingerprint);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<TwoFactorAuthenticator> LoginOAuthAsync(CookieOptionsParameters cookieOptions, HttpContext context, User user,
|
public async Task<bool> LoginAsync(CookieOptions cookieOptions, HttpContext context, TwoFactorAuthenticator authenticator, string code,
|
||||||
CancellationToken cancellation = default)
|
|
||||||
{
|
|
||||||
var requestContext = new RequestContextInfo(context, cookieOptions);
|
|
||||||
|
|
||||||
if (user.TwoFactorAuthenticator == TwoFactorAuthenticator.None)
|
|
||||||
{
|
|
||||||
await GenerateAuthTokensAsync(cookieOptions, context, requestContext, user.Id, cancellation);
|
|
||||||
return TwoFactorAuthenticator.None;
|
|
||||||
}
|
|
||||||
|
|
||||||
await StoreFirstAuthTokenInCache(user, requestContext, cancellation);
|
|
||||||
|
|
||||||
return user.TwoFactorAuthenticator;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<bool> LoginAsync(CookieOptionsParameters cookieOptions, HttpContext context, TwoFactorAuthenticator authenticator, string code,
|
|
||||||
CancellationToken cancellation = default)
|
CancellationToken cancellation = default)
|
||||||
{
|
{
|
||||||
var requestContext = new RequestContextInfo(context, cookieOptions);
|
var requestContext = new RequestContextInfo(context, cookieOptions);
|
||||||
@ -176,12 +162,12 @@ public class AuthService(ICacheService cache, IAccessToken accessTokenService, I
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<TwoFactorAuthenticator> LoginAsync(CookieOptionsParameters cookieOptions, User user, HttpContext context, string password,
|
private async Task<TwoFactorAuthenticator> LoginAsync(CookieOptions cookieOptions,
|
||||||
string username, CancellationToken cancellation = default)
|
HttpContext context,
|
||||||
|
User user,
|
||||||
|
CancellationToken cancellation = default)
|
||||||
{
|
{
|
||||||
var requestContext = new RequestContextInfo(context, cookieOptions);
|
var requestContext = new RequestContextInfo(context, cookieOptions);
|
||||||
username = username.Trim();
|
|
||||||
await VerifyUserOrThrowError(requestContext, user, password, username, cancellation);
|
|
||||||
|
|
||||||
if (user.TwoFactorAuthenticator == TwoFactorAuthenticator.None)
|
if (user.TwoFactorAuthenticator == TwoFactorAuthenticator.None)
|
||||||
{
|
{
|
||||||
@ -194,7 +180,37 @@ public class AuthService(ICacheService cache, IAccessToken accessTokenService, I
|
|||||||
return user.TwoFactorAuthenticator;
|
return user.TwoFactorAuthenticator;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task RefreshTokenAsync(CookieOptionsParameters cookieOptions, HttpContext context, CancellationToken cancellation = default)
|
public Task<TwoFactorAuthenticator> LoginOAuthAsync(CookieOptions cookieOptions,
|
||||||
|
HttpContext context,
|
||||||
|
User user,
|
||||||
|
OAuthUser oAuthUser,
|
||||||
|
OAuthProvider provider,
|
||||||
|
CancellationToken cancellation = default)
|
||||||
|
{
|
||||||
|
if (user.OAuthProviders == null || !user.OAuthProviders.TryGetValue(provider, out var value))
|
||||||
|
throw new SecurityException($"This provider '{Enum.GetName(provider)}' is not linked to the account.");
|
||||||
|
|
||||||
|
if (value.Id != oAuthUser.Id)
|
||||||
|
throw new SecurityException("This account was not linked");
|
||||||
|
|
||||||
|
return LoginAsync(cookieOptions, context, user, cancellation);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<TwoFactorAuthenticator> LoginAsync(CookieOptions cookieOptions,
|
||||||
|
HttpContext context,
|
||||||
|
User user,
|
||||||
|
string password,
|
||||||
|
string username,
|
||||||
|
CancellationToken cancellation = default)
|
||||||
|
{
|
||||||
|
var requestContext = new RequestContextInfo(context, cookieOptions);
|
||||||
|
username = username.Trim();
|
||||||
|
await VerifyUserOrThrowError(requestContext, user, password, username, cancellation);
|
||||||
|
|
||||||
|
return await LoginAsync(cookieOptions, context, user, cancellation);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task RefreshTokenAsync(CookieOptions cookieOptions, HttpContext context, CancellationToken cancellation = default)
|
||||||
{
|
{
|
||||||
const string defaultMessageError = "The session time has expired";
|
const string defaultMessageError = "The session time has expired";
|
||||||
var requestContext = new RequestContextInfo(context, cookieOptions);
|
var requestContext = new RequestContextInfo(context, cookieOptions);
|
||||||
@ -271,7 +287,7 @@ public class AuthService(ICacheService cache, IAccessToken accessTokenService, I
|
|||||||
cookieOptions.SetCookie(context, CookieNames.RefreshToken, authToken.RefreshToken, DateTime.UtcNow.Add(Lifetime));
|
cookieOptions.SetCookie(context, CookieNames.RefreshToken, authToken.RefreshToken, DateTime.UtcNow.Add(Lifetime));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task LogoutAsync(CookieOptionsParameters cookieOptions, HttpContext context, CancellationToken cancellation = default)
|
public async Task LogoutAsync(CookieOptions cookieOptions, HttpContext context, CancellationToken cancellation = default)
|
||||||
{
|
{
|
||||||
var requestContext = new RequestContextInfo(context, cookieOptions);
|
var requestContext = new RequestContextInfo(context, cookieOptions);
|
||||||
|
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Mirea.Api.Security.Common.Domain;
|
using Mirea.Api.Security.Common.Domain;
|
||||||
using Mirea.Api.Security.Common.Domain.OAuth2;
|
using Mirea.Api.Security.Common.Domain.Caching;
|
||||||
using Mirea.Api.Security.Common.Domain.OAuth2.UserInfo;
|
|
||||||
using Mirea.Api.Security.Common.Interfaces;
|
using Mirea.Api.Security.Common.Interfaces;
|
||||||
|
using Mirea.Api.Security.Common.OAuth2;
|
||||||
|
using Mirea.Api.Security.Common.OAuth2.UserInfo;
|
||||||
|
using Mirea.Api.Security.Common.ViewModel;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
@ -15,6 +17,7 @@ using System.Text;
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using CookieOptions = Mirea.Api.Security.Common.Model.CookieOptions;
|
||||||
|
|
||||||
namespace Mirea.Api.Security.Services;
|
namespace Mirea.Api.Security.Services;
|
||||||
|
|
||||||
@ -164,7 +167,15 @@ public class OAuthService(ILogger<OAuthService> logger, Dictionary<OAuthProvider
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public Uri GetProviderRedirect(HttpContext context, CookieOptionsParameters cookieOptions, string redirectUri,
|
private Task StoreOAuthUserInCache(string key, OAuthUserExtension data, CancellationToken cancellation) =>
|
||||||
|
cache.SetAsync(
|
||||||
|
key,
|
||||||
|
JsonSerializer.SerializeToUtf8Bytes(data),
|
||||||
|
slidingExpiration: TimeSpan.FromMinutes(15),
|
||||||
|
cancellationToken: cancellation);
|
||||||
|
|
||||||
|
|
||||||
|
public Uri GetProviderRedirect(CookieOptions cookieOptions, HttpContext context, string redirectUri,
|
||||||
OAuthProvider provider, Uri callback)
|
OAuthProvider provider, Uri callback)
|
||||||
{
|
{
|
||||||
var (clientId, _) = providers[provider];
|
var (clientId, _) = providers[provider];
|
||||||
@ -195,24 +206,37 @@ public class OAuthService(ILogger<OAuthService> logger, Dictionary<OAuthProvider
|
|||||||
public (OAuthProvider Provider, Uri Redirect)[] GetAvailableProviders(string redirectUri) =>
|
public (OAuthProvider Provider, Uri Redirect)[] GetAvailableProviders(string redirectUri) =>
|
||||||
[.. providers.Select(x => (x.Key, new Uri(redirectUri.TrimEnd('/') + "/?provider=" + (int)x.Key)))];
|
[.. providers.Select(x => (x.Key, new Uri(redirectUri.TrimEnd('/') + "/?provider=" + (int)x.Key)))];
|
||||||
|
|
||||||
public async Task<LoginOAuthResult> LoginOAuth(HttpContext context, CookieOptionsParameters cookieOptions,
|
public async Task<LoginOAuth> LoginOAuth(CookieOptions cookieOptions, HttpContext context,
|
||||||
string redirectUrl, string code, string state, CancellationToken cancellation = default)
|
string redirectUrl, string code, string state, CancellationToken cancellation = default)
|
||||||
{
|
{
|
||||||
var result = new LoginOAuthResult()
|
var result = new LoginOAuth()
|
||||||
{
|
{
|
||||||
Token = GeneratorKey.GenerateBase64(32)
|
Token = GeneratorKey.GenerateBase64(32)
|
||||||
};
|
};
|
||||||
|
|
||||||
var parts = state.Split('_');
|
var parts = state.Split('_');
|
||||||
|
|
||||||
if (parts.Length != 2)
|
if (parts.Length != 2)
|
||||||
{
|
{
|
||||||
result.ErrorMessage = "The request data is invalid or malformed.";
|
result.ErrorMessage = "The request data is invalid or malformed.";
|
||||||
|
|
||||||
|
await StoreOAuthUserInCache(result.Token, new OAuthUserExtension()
|
||||||
|
{
|
||||||
|
Message = result.ErrorMessage,
|
||||||
|
Provider = null
|
||||||
|
}, cancellation);
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
var payload = DecryptPayload(parts[0]);
|
var payload = DecryptPayload(parts[0]);
|
||||||
var checksum = parts[1];
|
var checksum = parts[1];
|
||||||
|
|
||||||
|
var cacheData = new OAuthUserExtension()
|
||||||
|
{
|
||||||
|
Provider = payload.Provider
|
||||||
|
};
|
||||||
|
|
||||||
result.Callback = new Uri(payload.Callback);
|
result.Callback = new Uri(payload.Callback);
|
||||||
|
|
||||||
if (!providers.TryGetValue(payload.Provider, out var providerInfo) ||
|
if (!providers.TryGetValue(payload.Provider, out var providerInfo) ||
|
||||||
@ -223,6 +247,10 @@ public class OAuthService(ILogger<OAuthService> logger, Dictionary<OAuthProvider
|
|||||||
state);
|
state);
|
||||||
|
|
||||||
result.ErrorMessage = "Invalid authorization request. Please try again later.";
|
result.ErrorMessage = "Invalid authorization request. Please try again later.";
|
||||||
|
cacheData.Message = result.ErrorMessage;
|
||||||
|
|
||||||
|
await StoreOAuthUserInCache(result.Token, cacheData, cancellation);
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -230,6 +258,7 @@ public class OAuthService(ILogger<OAuthService> logger, Dictionary<OAuthProvider
|
|||||||
var checksumRequest = GetHmacString(requestInfo);
|
var checksumRequest = GetHmacString(requestInfo);
|
||||||
|
|
||||||
result.ErrorMessage = "Authorization failed. Please try again later.";
|
result.ErrorMessage = "Authorization failed. Please try again later.";
|
||||||
|
cacheData.Message = result.ErrorMessage;
|
||||||
|
|
||||||
if (checksumRequest != checksum)
|
if (checksumRequest != checksum)
|
||||||
{
|
{
|
||||||
@ -240,6 +269,8 @@ public class OAuthService(ILogger<OAuthService> logger, Dictionary<OAuthProvider
|
|||||||
checksum
|
checksum
|
||||||
);
|
);
|
||||||
|
|
||||||
|
await StoreOAuthUserInCache(result.Token, cacheData, cancellation);
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -255,6 +286,8 @@ public class OAuthService(ILogger<OAuthService> logger, Dictionary<OAuthProvider
|
|||||||
payload.Provider,
|
payload.Provider,
|
||||||
checksum);
|
checksum);
|
||||||
|
|
||||||
|
await StoreOAuthUserInCache(result.Token, cacheData, cancellation);
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -272,6 +305,8 @@ public class OAuthService(ILogger<OAuthService> logger, Dictionary<OAuthProvider
|
|||||||
logger.LogWarning(ex, "Failed to retrieve user information from provider {Provider}",
|
logger.LogWarning(ex, "Failed to retrieve user information from provider {Provider}",
|
||||||
payload.Provider);
|
payload.Provider);
|
||||||
|
|
||||||
|
await StoreOAuthUserInCache(result.Token, cacheData, cancellation);
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -281,12 +316,75 @@ public class OAuthService(ILogger<OAuthService> logger, Dictionary<OAuthProvider
|
|||||||
result.ErrorMessage = null;
|
result.ErrorMessage = null;
|
||||||
result.Success = true;
|
result.Success = true;
|
||||||
|
|
||||||
await cache.SetAsync(
|
await StoreOAuthUserInCache(result.Token, new OAuthUserExtension
|
||||||
result.Token,
|
{
|
||||||
JsonSerializer.SerializeToUtf8Bytes(user),
|
IsSuccess = true,
|
||||||
absoluteExpirationRelativeToNow: TimeSpan.FromMinutes(15),
|
User = user,
|
||||||
cancellationToken: cancellation);
|
Provider = payload.Provider
|
||||||
|
}, cancellation);
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<(OAuthUser? User, string? Message, bool IsSuccess, OAuthProvider? Provider)>
|
||||||
|
GetOAuthUser(CookieOptions cookieOptions, HttpContext context, string token, CancellationToken cancellation = default)
|
||||||
|
{
|
||||||
|
var requestInfo = new RequestContextInfo(context, cookieOptions);
|
||||||
|
|
||||||
|
var result = await cache.GetAsync<OAuthUserExtension>(token, cancellation);
|
||||||
|
string tokenFailedKey = $"{requestInfo.Fingerprint}_oauth_token_failed";
|
||||||
|
|
||||||
|
if (result == null)
|
||||||
|
{
|
||||||
|
var failedTokenAttemptsCount = await cache.GetAsync<int?>(
|
||||||
|
tokenFailedKey,
|
||||||
|
cancellation) ?? 1;
|
||||||
|
|
||||||
|
var failedTokenCacheExpiration = TimeSpan.FromHours(1);
|
||||||
|
|
||||||
|
if (failedTokenAttemptsCount > 5)
|
||||||
|
{
|
||||||
|
logger.LogWarning(
|
||||||
|
"Multiple unsuccessful token attempts detected. Token {Token}, Fingerprint: {Fingerprint}. Attempt count: {AttemptCount}.",
|
||||||
|
token,
|
||||||
|
requestInfo.Fingerprint,
|
||||||
|
failedTokenAttemptsCount);
|
||||||
|
|
||||||
|
return (null, "Too many unsuccessful token attempts. Please try again later.", false, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.LogInformation(
|
||||||
|
"Cache data not found or expired for token: {Token}. Fingerprint: {Fingerprint}. Attempt count: {AttemptNumber}.",
|
||||||
|
token,
|
||||||
|
requestInfo.Fingerprint,
|
||||||
|
failedTokenAttemptsCount);
|
||||||
|
|
||||||
|
await cache.SetAsync(tokenFailedKey,
|
||||||
|
failedTokenAttemptsCount + 1,
|
||||||
|
slidingExpiration: failedTokenCacheExpiration,
|
||||||
|
cancellationToken: cancellation);
|
||||||
|
|
||||||
|
return (null, "Invalid or expired token.", false, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
await cache.RemoveAsync(tokenFailedKey, cancellation);
|
||||||
|
|
||||||
|
const string log = "Cache data retrieved for token: {Token}. Fingerprint: {Fingerprint}.";
|
||||||
|
|
||||||
|
if (result.User != null)
|
||||||
|
logger.LogInformation(log + " Provider: {Provider}. UserId: {UserId}.",
|
||||||
|
token,
|
||||||
|
requestInfo.Fingerprint,
|
||||||
|
result.User.Id,
|
||||||
|
result.Provider);
|
||||||
|
else if (result.Provider != null)
|
||||||
|
logger.LogInformation(log + " Provider: {Provider}.",
|
||||||
|
token,
|
||||||
|
requestInfo.Fingerprint,
|
||||||
|
result.Provider);
|
||||||
|
else
|
||||||
|
logger.LogInformation(log, token, requestInfo.Fingerprint);
|
||||||
|
|
||||||
|
return (result.User, result.Message, result.IsSuccess, result.Provider);
|
||||||
|
}
|
||||||
}
|
}
|
@ -1,4 +1,4 @@
|
|||||||
using Mirea.Api.Security.Common.Domain;
|
using Mirea.Api.Security.Common.Model;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Security;
|
using System.Security;
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user