sec: add payload
This commit is contained in:
parent
97187a8e45
commit
17fd260068
@ -16,7 +16,7 @@ using Mirea.Api.Security.Services;
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Security.Claims;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using OAuthProvider = Mirea.Api.Security.Common.Domain.OAuthProvider;
|
using OAuthProvider = Mirea.Api.Security.Common.Domain.OAuthProvider;
|
||||||
|
|
||||||
@ -130,17 +130,23 @@ public class AuthController(IOptionsSnapshot<Admin> user, IOptionsSnapshot<Gener
|
|||||||
/// This method generates a redirect URL for the selected provider and redirects the user to it.
|
/// This method generates a redirect URL for the selected provider and redirects the user to it.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
/// <param name="provider">The identifier of the OAuth provider to authorize with.</param>
|
/// <param name="provider">The identifier of the OAuth provider to authorize with.</param>
|
||||||
|
/// <param name="callback">The address where the user will need to be redirected after the end of communication with the OAuth provider</param>
|
||||||
/// <returns>A redirect to the OAuth provider's authorization URL.</returns>
|
/// <returns>A redirect to the OAuth provider's authorization URL.</returns>
|
||||||
/// <exception cref="ControllerArgumentException">Thrown if the specified provider is not valid.</exception>
|
/// <exception cref="ControllerArgumentException">Thrown if the specified provider is not valid.</exception>
|
||||||
[HttpGet("AuthorizeOAuth2")]
|
[HttpGet("AuthorizeOAuth2")]
|
||||||
[MaintenanceModeIgnore]
|
[MaintenanceModeIgnore]
|
||||||
public ActionResult AuthorizeOAuth2([FromQuery] int provider)
|
public ActionResult AuthorizeOAuth2([FromQuery] int provider, [FromQuery] Uri callback)
|
||||||
{
|
{
|
||||||
if (!Enum.IsDefined(typeof(OAuthProvider), provider))
|
if (!Enum.IsDefined(typeof(OAuthProvider), provider))
|
||||||
throw new ControllerArgumentException("There is no selected provider");
|
throw new ControllerArgumentException("There is no selected provider");
|
||||||
|
|
||||||
return Redirect(oAuthService.GetProviderRedirect(HttpContext, GetCookieParams(), HttpContext.GetApiUrl(Url.Action("OAuth2")!),
|
if (!callback.IsAbsoluteUri)
|
||||||
(OAuthProvider)provider).AbsoluteUri);
|
throw new ControllerArgumentException("The callback URL must be absolute.");
|
||||||
|
|
||||||
|
return Redirect(oAuthService.GetProviderRedirect(HttpContext, GetCookieParams(),
|
||||||
|
HttpContext.GetApiUrl(Url.Action("OAuth2")!),
|
||||||
|
(OAuthProvider)provider,
|
||||||
|
callback).AbsoluteUri);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -152,9 +158,17 @@ public class AuthController(IOptionsSnapshot<Admin> user, IOptionsSnapshot<Gener
|
|||||||
/// <returns>A list of available providers and their redirect URLs.</returns>
|
/// <returns>A list of available providers and their redirect URLs.</returns>
|
||||||
[HttpGet("AvailableProviders")]
|
[HttpGet("AvailableProviders")]
|
||||||
[MaintenanceModeIgnore]
|
[MaintenanceModeIgnore]
|
||||||
public ActionResult<List<AvailableOAuthProvidersResponse>> AvailableProviders() =>
|
public ActionResult<List<AvailableOAuthProvidersResponse>> AvailableProviders([FromQuery] Uri callback) =>
|
||||||
Ok(oAuthService
|
Ok(oAuthService
|
||||||
.GetAvailableProviders(HttpContext.GetApiUrl(Url.Action("AuthorizeOAuth2")!))
|
.GetAvailableProviders(HttpContext.GetApiUrl(Url.Action("AuthorizeOAuth2")!))
|
||||||
|
.Select(x =>
|
||||||
|
{
|
||||||
|
if (!callback.IsAbsoluteUri)
|
||||||
|
throw new ControllerArgumentException("The callback URL must be absolute.");
|
||||||
|
|
||||||
|
x.Redirect = new Uri(x.Redirect + "&callback=" + Uri.EscapeDataString(callback.AbsoluteUri));
|
||||||
|
return x;
|
||||||
|
})
|
||||||
.ConvertToDto());
|
.ConvertToDto());
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -6,6 +6,7 @@ using Mirea.Api.Security.Common.Domain.OAuth2.UserInfo;
|
|||||||
using Mirea.Api.Security.Common.Interfaces;
|
using Mirea.Api.Security.Common.Interfaces;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
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;
|
||||||
@ -110,23 +111,83 @@ public class OAuthService(ILogger<OAuthService> logger, Dictionary<OAuthProvider
|
|||||||
Encoding.UTF8.GetBytes($"{contextInfo.Fingerprint}_{contextInfo.Ip}_{contextInfo.UserAgent}")));
|
Encoding.UTF8.GetBytes($"{contextInfo.Fingerprint}_{contextInfo.Ip}_{contextInfo.UserAgent}")));
|
||||||
}
|
}
|
||||||
|
|
||||||
public Uri GetProviderRedirect(HttpContext context, CookieOptionsParameters cookieOptions, string redirectUri, OAuthProvider provider)
|
private string EncryptPayload(OAuthPayload payload)
|
||||||
|
{
|
||||||
|
var data = JsonSerializer.Serialize(payload);
|
||||||
|
|
||||||
|
var aes = Aes.Create();
|
||||||
|
aes.Key = SecretKey.ToArray();
|
||||||
|
aes.GenerateIV();
|
||||||
|
|
||||||
|
using var encryptor = aes.CreateEncryptor(aes.Key, aes.IV);
|
||||||
|
using var ms = new MemoryStream();
|
||||||
|
|
||||||
|
ms.Write(aes.IV, 0, aes.IV.Length);
|
||||||
|
|
||||||
|
using (var cs = new CryptoStream(ms, encryptor, CryptoStreamMode.Write))
|
||||||
|
using (var writer = new StreamWriter(cs))
|
||||||
|
{
|
||||||
|
writer.Write(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Convert.ToBase64String(ms.ToArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
private OAuthPayload DecryptPayload(string encryptedData)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var cipherBytes = Convert.FromBase64String(encryptedData);
|
||||||
|
|
||||||
|
using var aes = Aes.Create();
|
||||||
|
aes.Key = SecretKey.ToArray();
|
||||||
|
|
||||||
|
var iv = new byte[16];
|
||||||
|
Array.Copy(cipherBytes, 0, iv, 0, iv.Length);
|
||||||
|
aes.IV = iv;
|
||||||
|
|
||||||
|
using var ms = new MemoryStream(cipherBytes, 16, cipherBytes.Length - 16);
|
||||||
|
using var decryptor = aes.CreateDecryptor(aes.Key, aes.IV);
|
||||||
|
|
||||||
|
using var cs = new CryptoStream(ms, decryptor, CryptoStreamMode.Read);
|
||||||
|
using var reader = new StreamReader(cs);
|
||||||
|
|
||||||
|
var data = reader.ReadToEnd();
|
||||||
|
|
||||||
|
return JsonSerializer.Deserialize<OAuthPayload>(data) ??
|
||||||
|
throw new NullReferenceException($"Couldn't convert data to {nameof(OAuthPayload)}.");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogWarning(ex, "Couldn't decrypt the data OAuth request.");
|
||||||
|
throw new InvalidOperationException("Couldn't decrypt the data.", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Uri GetProviderRedirect(HttpContext context, CookieOptionsParameters cookieOptions, string redirectUri,
|
||||||
|
OAuthProvider provider, Uri callback)
|
||||||
{
|
{
|
||||||
var (clientId, _) = providers[provider];
|
var (clientId, _) = providers[provider];
|
||||||
|
|
||||||
var requestInfo = new RequestContextInfo(context, cookieOptions);
|
var requestInfo = new RequestContextInfo(context, cookieOptions);
|
||||||
var state = GetHmacString(requestInfo, secretKey);
|
var payload = EncryptPayload(new OAuthPayload()
|
||||||
|
{
|
||||||
|
Provider = provider,
|
||||||
|
Callback = callback.AbsoluteUri
|
||||||
|
});
|
||||||
|
|
||||||
|
var checksum = GetHmacString(requestInfo);
|
||||||
|
|
||||||
var redirectUrl = $"?client_id={clientId}" +
|
var redirectUrl = $"?client_id={clientId}" +
|
||||||
"&response_type=code" +
|
"&response_type=code" +
|
||||||
$"&redirect_uri={redirectUri}" +
|
$"&redirect_uri={redirectUri}" +
|
||||||
$"&scope={ProviderData[provider].Scope}" +
|
$"&scope={ProviderData[provider].Scope}" +
|
||||||
$"&state={Uri.EscapeDataString(state + "_" + Enum.GetName(provider))}";
|
$"&state={Uri.EscapeDataString(payload + "_" + checksum)}";
|
||||||
|
|
||||||
logger.LogInformation("Redirecting user Fingerprint: {Fingerprint} to OAuth provider {Provider} with state: {State}",
|
logger.LogInformation("Redirecting user Fingerprint: {Fingerprint} to OAuth provider {Provider} with state: {State}",
|
||||||
requestInfo.Fingerprint,
|
requestInfo.Fingerprint,
|
||||||
provider,
|
provider,
|
||||||
state);
|
checksum);
|
||||||
|
|
||||||
return new Uri(ProviderData[provider].RedirectUrl + redirectUrl);
|
return new Uri(ProviderData[provider].RedirectUrl + redirectUrl);
|
||||||
}
|
}
|
||||||
@ -137,27 +198,38 @@ public class OAuthService(ILogger<OAuthService> logger, Dictionary<OAuthProvider
|
|||||||
public async Task<(OAuthProvider provider, OAuthUser User)> LoginOAuth(HttpContext context, CookieOptionsParameters cookieOptions,
|
public async Task<(OAuthProvider provider, OAuthUser User)> LoginOAuth(HttpContext context, CookieOptionsParameters cookieOptions,
|
||||||
string redirectUrl, string code, string state, CancellationToken cancellation = default)
|
string redirectUrl, string code, string state, CancellationToken cancellation = default)
|
||||||
{
|
{
|
||||||
var partsState = state.Split('_');
|
|
||||||
|
|
||||||
if (!Enum.TryParse<OAuthProvider>(partsState.Last(), true, out var provider) ||
|
var parts = state.Split('_');
|
||||||
!providers.TryGetValue(provider, out var providerInfo) ||
|
|
||||||
!ProviderData.TryGetValue(provider, out var currentProviderStruct))
|
if (parts.Length != 2)
|
||||||
{
|
{
|
||||||
logger.LogWarning("Failed to parse OAuth provider from state: {State}", state);
|
throw new SecurityException("The request data is invalid or malformed.");
|
||||||
throw new InvalidOperationException("Invalid authorization request.");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var secretStateData = string.Join("_", partsState.SkipLast(1));
|
var payload = DecryptPayload(parts[0]);
|
||||||
var requestInfo = new RequestContextInfo(context, cookieOptions);
|
var checksum = parts[1];
|
||||||
var secretData = GetHmacString(requestInfo, secretKey);
|
|
||||||
|
|
||||||
if (secretData != secretStateData)
|
if (!providers.TryGetValue(payload.Provider, out var providerInfo) ||
|
||||||
|
!ProviderData.TryGetValue(payload.Provider, out var currentProviderStruct))
|
||||||
|
{
|
||||||
|
logger.LogWarning("The OAuth provider specified in the payload " +
|
||||||
|
"is not registered as a possible data recipient from state: {State}",
|
||||||
|
state);
|
||||||
|
|
||||||
|
throw new SecurityException("Invalid authorization request. Please try again later.");
|
||||||
|
}
|
||||||
|
var requestInfo = new RequestContextInfo(context, cookieOptions);
|
||||||
|
var checksumRequest = GetHmacString(requestInfo);
|
||||||
|
|
||||||
|
|
||||||
|
if (checksumRequest != checksum)
|
||||||
{
|
{
|
||||||
logger.LogWarning(
|
logger.LogWarning(
|
||||||
"Fingerprint mismatch. Possible CSRF attack detected. Fingerprint: {Fingerprint}, State: {State}, ExpectedState: {ExpectedState}",
|
"Fingerprint mismatch. Possible CSRF attack detected. Fingerprint: {Fingerprint}, State: {State}, ExpectedState: {ExpectedState}",
|
||||||
requestInfo.Fingerprint,
|
requestInfo.Fingerprint,
|
||||||
secretData,
|
checksumRequest,
|
||||||
secretStateData
|
checksum
|
||||||
);
|
);
|
||||||
throw new SecurityException("Suspicious activity detected. Please try again.");
|
throw new SecurityException("Suspicious activity detected. Please try again.");
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user