115 lines
4.2 KiB
C#
115 lines
4.2 KiB
C#
using System.Net;
|
|
using System.Text.Json.Nodes;
|
|
using Csob.Client;
|
|
using Csob.Configuration;
|
|
using Csob.Credentials;
|
|
using Csob.Models;
|
|
|
|
namespace Csob.Services;
|
|
|
|
/// <summary>
|
|
/// OAuth2 Authorization Code helper for the ČSOB PSD2 flow. Builds the PSU authorization URL and
|
|
/// exchanges/refreshes tokens against the ČSOB token endpoint. The token endpoint is behind mutual
|
|
/// TLS, so the client certificate header is required for the token/refresh calls. Client id/secret
|
|
/// are taken from per-request headers (this service is multi-tenant and stores no app credentials).
|
|
/// </summary>
|
|
public sealed class OAuthService
|
|
{
|
|
private readonly RequestCredentialsProvider _credentials;
|
|
private readonly CsobHttpClientProvider _httpClientProvider;
|
|
private readonly CsobSettings _settings;
|
|
|
|
public OAuthService(
|
|
RequestCredentialsProvider credentials,
|
|
CsobHttpClientProvider httpClientProvider,
|
|
CsobSettings settings)
|
|
{
|
|
_credentials = credentials;
|
|
_httpClientProvider = httpClientProvider;
|
|
_settings = settings;
|
|
}
|
|
|
|
/// <summary>Builds the authorization URL to which the PSU must be redirected.</summary>
|
|
public AuthorizationUrlResponse BuildAuthorizationUrl(string redirectUri, string? scope, string? state)
|
|
{
|
|
var clientId = RequireHeader(CredentialConstants.ClientIdHeader);
|
|
|
|
var query = new Dictionary<string, string?>
|
|
{
|
|
["response_type"] = "code",
|
|
["client_id"] = clientId,
|
|
["redirect_uri"] = redirectUri,
|
|
["scope"] = scope,
|
|
["state"] = state,
|
|
};
|
|
|
|
var url = _settings.OAuthAuthorizeUrl + "?" + string.Join("&", query
|
|
.Where(kvp => !string.IsNullOrWhiteSpace(kvp.Value))
|
|
.Select(kvp => $"{Uri.EscapeDataString(kvp.Key)}={Uri.EscapeDataString(kvp.Value!)}"));
|
|
|
|
return new AuthorizationUrlResponse { AuthorizationUrl = url, State = state };
|
|
}
|
|
|
|
public Task<JsonNode?> ExchangeCodeAsync(TokenExchangeRequest request, CancellationToken ct)
|
|
=> PostTokenAsync(new Dictionary<string, string>
|
|
{
|
|
["grant_type"] = "authorization_code",
|
|
["code"] = request.Code,
|
|
["redirect_uri"] = request.RedirectUri,
|
|
}, ct);
|
|
|
|
public Task<JsonNode?> RefreshAsync(TokenRefreshRequest request, CancellationToken ct)
|
|
=> PostTokenAsync(new Dictionary<string, string>
|
|
{
|
|
["grant_type"] = "refresh_token",
|
|
["refresh_token"] = request.RefreshToken,
|
|
}, ct);
|
|
|
|
private async Task<JsonNode?> PostTokenAsync(Dictionary<string, string> form, CancellationToken ct)
|
|
{
|
|
var clientId = RequireHeader(CredentialConstants.ClientIdHeader);
|
|
var clientSecret = RequireHeader(CredentialConstants.ClientSecretHeader);
|
|
|
|
var certificate = _credentials.TryBuildCertificate()
|
|
?? throw new MissingCredentialsException(new[] { CredentialConstants.CertificateHeader });
|
|
|
|
form["client_id"] = clientId;
|
|
form["client_secret"] = clientSecret;
|
|
|
|
var http = _httpClientProvider.GetClient(certificate);
|
|
|
|
using var content = new FormUrlEncodedContent(form);
|
|
using var response = await http.PostAsync(_settings.OAuthTokenUrl, content, ct);
|
|
var payload = await response.Content.ReadAsStringAsync(ct);
|
|
|
|
if (!response.IsSuccessStatusCode)
|
|
{
|
|
throw new CsobApiException(response.StatusCode, ParseOAuthError(payload), payload);
|
|
}
|
|
|
|
return string.IsNullOrWhiteSpace(payload) ? null : JsonNode.Parse(payload);
|
|
}
|
|
|
|
private string RequireHeader(string name)
|
|
=> _credentials.Header(name) ?? throw new MissingCredentialsException(new[] { name });
|
|
|
|
private static IReadOnlyList<string> ParseOAuthError(string payload)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(payload))
|
|
{
|
|
return Array.Empty<string>();
|
|
}
|
|
|
|
try
|
|
{
|
|
// OAuth2 errors use { "error": "...", "error_description": "..." }.
|
|
var error = JsonNode.Parse(payload)?["error"]?.GetValue<string>();
|
|
return string.IsNullOrWhiteSpace(error) ? Array.Empty<string>() : new[] { error! };
|
|
}
|
|
catch (System.Text.Json.JsonException)
|
|
{
|
|
return Array.Empty<string>();
|
|
}
|
|
}
|
|
}
|