sec: return the token instead of performing actions with the user
This commit is contained in:
parent
dcdd43469b
commit
43edab2912
@ -33,94 +33,73 @@ public class AuthController(IOptionsSnapshot<Admin> user, IOptionsSnapshot<Gener
|
|||||||
Path = UrlHelper.GetSubPathWithoutFirstApiName + "api"
|
Path = UrlHelper.GetSubPathWithoutFirstApiName + "api"
|
||||||
};
|
};
|
||||||
|
|
||||||
private static string GenerateHtmlResponse(string title, string message, OAuthProvider? provider, bool isError = false, string? traceId = null)
|
private static string GenerateHtmlResponse(
|
||||||
|
string title,
|
||||||
|
string message,
|
||||||
|
Uri? callback,
|
||||||
|
string traceId,
|
||||||
|
bool isError)
|
||||||
{
|
{
|
||||||
var messageColor = isError ? "red" : "white";
|
var callbackUrl = callback?.ToString();
|
||||||
var script = "<script>setTimeout(()=>{if(window.opener){window.opener.postMessage(" +
|
|
||||||
"{success:" + (!isError).ToString().ToLower() +
|
|
||||||
",provider:'" + (provider == null ? "null" : (int)provider) +
|
|
||||||
"',providerName:'" + (provider == null ? "null" : Enum.GetName(provider.Value)) +
|
|
||||||
"',message:'" + message.Replace("'", "\\'") +
|
|
||||||
"'},'*');}window.close();}, 15000);</script>";
|
|
||||||
|
|
||||||
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}}p{{font-size:16px;color:{messageColor}}}</style><title>{title}</title></head><body><div class=container><h1>{title}</h1><p>{message}</p><p style=font-size:14px;color:silver;>Это информационная страница. Вы можете закрыть её.</p>{(!string.IsNullOrEmpty(traceId) ? $"<code style=font-size:12px;color:gray;>TraceId={traceId}</code>" : string.Empty)}</div>{script}</body></html>";
|
var script = callback == null ? string.Empty :
|
||||||
|
$"<script>setTimeout(()=>{{window.location.href='{callbackUrl}';}}, {(isError ? 15000 : 5000)});</script>";
|
||||||
|
|
||||||
|
var blockInfo = "<p>" + (callback == null ?
|
||||||
|
"Вернитесь обратно и попробуйте снова позже.</p>" :
|
||||||
|
$"Если вы не будете автоматически перенаправлены, нажмите ниже.</p>" +
|
||||||
|
$"<a href=\"{callbackUrl}\" style=\"color:inherit;text-decoration:underline;\">Перейти вручную</a>");
|
||||||
|
|
||||||
|
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>";
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("OAuth2")]
|
[HttpGet("OAuth2")]
|
||||||
[BadRequestResponse]
|
[BadRequestResponse]
|
||||||
[Produces("text/html")]
|
[Produces("text/html")]
|
||||||
[MaintenanceModeIgnore]
|
[MaintenanceModeIgnore]
|
||||||
public async Task<ContentResult> OAuth2([FromQuery] string code, [FromQuery] string state)
|
public async Task<ContentResult> OAuth2([FromQuery] string? code, [FromQuery] string? state)
|
||||||
{
|
{
|
||||||
var userId = HttpContext.User.FindFirstValue(ClaimTypes.NameIdentifier);
|
|
||||||
string title;
|
|
||||||
string message;
|
|
||||||
OAuthProvider provider;
|
|
||||||
OAuthUser oAuthUser;
|
|
||||||
var traceId = Activity.Current?.Id ?? HttpContext.TraceIdentifier;
|
var traceId = Activity.Current?.Id ?? HttpContext.TraceIdentifier;
|
||||||
|
|
||||||
try
|
if (string.IsNullOrEmpty(code) || string.IsNullOrEmpty(state))
|
||||||
{
|
return Content(GenerateHtmlResponse(
|
||||||
(provider, oAuthUser) = await oAuthService.LoginOAuth(HttpContext, GetCookieParams(),
|
"Ошибка передачи данных!",
|
||||||
|
"Провайдер OAuth не передал нужных данных.",
|
||||||
|
null,
|
||||||
|
traceId,
|
||||||
|
true), "text/html");
|
||||||
|
|
||||||
|
var result = await oAuthService.LoginOAuth(HttpContext, GetCookieParams(),
|
||||||
HttpContext.GetApiUrl(Url.Action("OAuth2")!), code, state);
|
HttpContext.GetApiUrl(Url.Action("OAuth2")!), code, state);
|
||||||
}
|
|
||||||
catch (Exception e)
|
string? callbackUrl = null;
|
||||||
|
|
||||||
|
if (result.Callback != null)
|
||||||
|
callbackUrl = result.Callback + (result.Callback.Query.Length > 0 ? "&" : "?") +
|
||||||
|
$"result={Uri.EscapeDataString(result.Token)}";
|
||||||
|
|
||||||
|
string title, message;
|
||||||
|
|
||||||
|
if (!result.Success)
|
||||||
{
|
{
|
||||||
title = "Произошла ошибка при общении с провайдером OAuth!";
|
if (callbackUrl != null)
|
||||||
message = e.Message;
|
callbackUrl += $"&traceId={Uri.EscapeDataString(traceId)}";
|
||||||
return Content(GenerateHtmlResponse(title, message, null, true, traceId), "text/html");
|
|
||||||
|
title = "Ошибка авторизации!";
|
||||||
|
message = result.ErrorMessage ?? "Произошла ошибка. Попробуйте ещё раз.";
|
||||||
}
|
}
|
||||||
|
else
|
||||||
var userEntity = user.Value;
|
|
||||||
|
|
||||||
if (userId != null)
|
|
||||||
{
|
{
|
||||||
userEntity.OAuthProviders ??= [];
|
title = "Авторизация завершена!";
|
||||||
|
message = "Вы будете перенаправлены обратно через несколько секунд.";
|
||||||
if (!userEntity.OAuthProviders.TryAdd(provider, oAuthUser))
|
|
||||||
{
|
|
||||||
title = "Ошибка связи аккаунта!";
|
|
||||||
message = "Этот OAuth провайдер уже связан с вашей учетной записью. " +
|
|
||||||
"Пожалуйста, используйте другого провайдера или удалите связь с аккаунтом.";
|
|
||||||
return Content(GenerateHtmlResponse(title, message, provider, true, traceId), "text/html");
|
|
||||||
}
|
|
||||||
|
|
||||||
userEntity.SaveSetting();
|
|
||||||
|
|
||||||
title = "Учетная запись успешно связана.";
|
|
||||||
message = "Вы успешно связали свою учетную запись с провайдером OAuth. Вы можете продолжить использовать приложение.";
|
|
||||||
return Content(GenerateHtmlResponse(title, message, provider), "text/html");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (userEntity.OAuthProviders != null &&
|
return Content(GenerateHtmlResponse(
|
||||||
userEntity.OAuthProviders.TryGetValue(provider, out var userOAuth) &&
|
title,
|
||||||
userOAuth.Id == oAuthUser.Id)
|
message,
|
||||||
{
|
callbackUrl == null ? null : new Uri(callbackUrl),
|
||||||
await auth.LoginOAuthAsync(GetCookieParams(), HttpContext, new User
|
traceId,
|
||||||
{
|
!result.Success), "text/html");
|
||||||
Id = 1.ToString(),
|
|
||||||
Username = userEntity.Username,
|
|
||||||
Email = userEntity.Email,
|
|
||||||
PasswordHash = userEntity.PasswordHash,
|
|
||||||
Salt = userEntity.Salt,
|
|
||||||
TwoFactorAuthenticator = userEntity.TwoFactorAuthenticator,
|
|
||||||
SecondFactorToken = userEntity.Secret,
|
|
||||||
OAuthProviders = userEntity.OAuthProviders
|
|
||||||
});
|
|
||||||
|
|
||||||
title = "Успешный вход в аккаунт.";
|
|
||||||
message = "Вы успешно вошли в свою учетную запись. Добро пожаловать!";
|
|
||||||
return Content(GenerateHtmlResponse(title, message, provider), "text/html");
|
|
||||||
}
|
|
||||||
|
|
||||||
title = "Вы успешно зарегистрированы.";
|
|
||||||
message = "Процесс завершен. Вы можете закрыть эту страницу.";
|
|
||||||
userEntity.Email = string.IsNullOrEmpty(oAuthUser.Email) ? string.Empty : oAuthUser.Email;
|
|
||||||
userEntity.Username = string.IsNullOrEmpty(oAuthUser.Username) ? string.Empty : oAuthUser.Username;
|
|
||||||
userEntity.OAuthProviders ??= [];
|
|
||||||
userEntity.OAuthProviders.Add(provider, oAuthUser);
|
|
||||||
userEntity.SaveSetting();
|
|
||||||
return Content(GenerateHtmlResponse(title, message, provider), "text/html");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
11
Security/Common/Domain/LoginOAuthResult.cs
Normal file
11
Security/Common/Domain/LoginOAuthResult.cs
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Mirea.Api.Security.Common.Domain;
|
||||||
|
|
||||||
|
public class LoginOAuthResult
|
||||||
|
{
|
||||||
|
public bool Success { get; set; }
|
||||||
|
public required string Token { get; set; }
|
||||||
|
public Uri? Callback { get; set; }
|
||||||
|
public string? ErrorMessage { get; set; }
|
||||||
|
}
|
@ -10,7 +10,6 @@ using System.IO;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Net.Http.Headers;
|
using System.Net.Http.Headers;
|
||||||
using System.Security;
|
|
||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
@ -19,7 +18,8 @@ using System.Threading.Tasks;
|
|||||||
|
|
||||||
namespace Mirea.Api.Security.Services;
|
namespace Mirea.Api.Security.Services;
|
||||||
|
|
||||||
public class OAuthService(ILogger<OAuthService> logger, Dictionary<OAuthProvider, (string ClientId, string Secret)> providers, string secretKey)
|
public class OAuthService(ILogger<OAuthService> logger, Dictionary<OAuthProvider, (string ClientId, string Secret)> providers,
|
||||||
|
ICacheService cache)
|
||||||
{
|
{
|
||||||
public required ReadOnlyMemory<byte> SecretKey { private get; init; }
|
public required ReadOnlyMemory<byte> SecretKey { private get; init; }
|
||||||
|
|
||||||
@ -195,21 +195,26 @@ 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<(OAuthProvider provider, OAuthUser User)> LoginOAuth(HttpContext context, CookieOptionsParameters cookieOptions,
|
public async Task<LoginOAuthResult> LoginOAuth(HttpContext context, CookieOptionsParameters cookieOptions,
|
||||||
string redirectUrl, string code, string state, CancellationToken cancellation = default)
|
string redirectUrl, string code, string state, CancellationToken cancellation = default)
|
||||||
{
|
{
|
||||||
|
var result = new LoginOAuthResult()
|
||||||
|
{
|
||||||
|
Token = GeneratorKey.GenerateBase64(32)
|
||||||
|
};
|
||||||
var parts = state.Split('_');
|
var parts = state.Split('_');
|
||||||
|
|
||||||
if (parts.Length != 2)
|
if (parts.Length != 2)
|
||||||
{
|
{
|
||||||
throw new SecurityException("The request data is invalid or malformed.");
|
result.ErrorMessage = "The request data is invalid or malformed.";
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
var payload = DecryptPayload(parts[0]);
|
var payload = DecryptPayload(parts[0]);
|
||||||
var checksum = parts[1];
|
var checksum = parts[1];
|
||||||
|
|
||||||
|
result.Callback = new Uri(payload.Callback);
|
||||||
|
|
||||||
if (!providers.TryGetValue(payload.Provider, out var providerInfo) ||
|
if (!providers.TryGetValue(payload.Provider, out var providerInfo) ||
|
||||||
!ProviderData.TryGetValue(payload.Provider, out var currentProviderStruct))
|
!ProviderData.TryGetValue(payload.Provider, out var currentProviderStruct))
|
||||||
{
|
{
|
||||||
@ -217,12 +222,15 @@ public class OAuthService(ILogger<OAuthService> logger, Dictionary<OAuthProvider
|
|||||||
"is not registered as a possible data recipient from state: {State}",
|
"is not registered as a possible data recipient from state: {State}",
|
||||||
state);
|
state);
|
||||||
|
|
||||||
throw new SecurityException("Invalid authorization request. Please try again later.");
|
result.ErrorMessage = "Invalid authorization request. Please try again later.";
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
var requestInfo = new RequestContextInfo(context, cookieOptions);
|
var requestInfo = new RequestContextInfo(context, cookieOptions);
|
||||||
var checksumRequest = GetHmacString(requestInfo);
|
var checksumRequest = GetHmacString(requestInfo);
|
||||||
|
|
||||||
|
result.ErrorMessage = "Authorization failed. Please try again later.";
|
||||||
|
|
||||||
if (checksumRequest != checksum)
|
if (checksumRequest != checksum)
|
||||||
{
|
{
|
||||||
logger.LogWarning(
|
logger.LogWarning(
|
||||||
@ -231,10 +239,11 @@ public class OAuthService(ILogger<OAuthService> logger, Dictionary<OAuthProvider
|
|||||||
checksumRequest,
|
checksumRequest,
|
||||||
checksum
|
checksum
|
||||||
);
|
);
|
||||||
throw new SecurityException("Suspicious activity detected. Please try again.");
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
OAuthTokenResponse? accessToken = null;
|
OAuthTokenResponse? accessToken;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
accessToken = await ExchangeCodeForTokensAsync(currentProviderStruct.TokenUrl, redirectUrl, code, providerInfo.ClientId,
|
accessToken = await ExchangeCodeForTokensAsync(currentProviderStruct.TokenUrl, redirectUrl, code, providerInfo.ClientId,
|
||||||
@ -243,27 +252,41 @@ public class OAuthService(ILogger<OAuthService> logger, Dictionary<OAuthProvider
|
|||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
logger.LogWarning(ex, "Failed to exchange code for access token with provider {Provider}. State: {State}",
|
logger.LogWarning(ex, "Failed to exchange code for access token with provider {Provider}. State: {State}",
|
||||||
provider,
|
payload.Provider,
|
||||||
secretStateData);
|
checksum);
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (accessToken == null)
|
if (accessToken == null)
|
||||||
throw new SecurityException("Unable to complete authorization with the provider. Please try again later.");
|
return result;
|
||||||
|
|
||||||
OAuthUser? result = null;
|
OAuthUser? user;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
result = await GetUserProfileAsync(currentProviderStruct.UserInfoUrl, currentProviderStruct.AuthHeader, accessToken.AccessToken,
|
user = await GetUserProfileAsync(currentProviderStruct.UserInfoUrl, currentProviderStruct.AuthHeader, accessToken.AccessToken,
|
||||||
provider, cancellation);
|
payload.Provider, cancellation);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
logger.LogWarning(ex, "Failed to retrieve user information from provider {Provider}", provider);
|
logger.LogWarning(ex, "Failed to retrieve user information from provider {Provider}",
|
||||||
|
payload.Provider);
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result == null)
|
if (user == null)
|
||||||
throw new SecurityException("Unable to retrieve user information. Please check the details and try again.");
|
return result;
|
||||||
|
|
||||||
return (provider, result);
|
result.ErrorMessage = null;
|
||||||
|
result.Success = true;
|
||||||
|
|
||||||
|
await cache.SetAsync(
|
||||||
|
result.Token,
|
||||||
|
JsonSerializer.SerializeToUtf8Bytes(user),
|
||||||
|
absoluteExpirationRelativeToNow: TimeSpan.FromMinutes(15),
|
||||||
|
cancellationToken: cancellation);
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
Loading…
x
Reference in New Issue
Block a user