first
This commit is contained in:
@@ -0,0 +1,44 @@
|
||||
namespace Csob.Credentials;
|
||||
|
||||
/// <summary>
|
||||
/// Names of the HTTP headers that carry per-request ČSOB credentials and context.
|
||||
///
|
||||
/// This service is multi-tenant: it stores no credentials itself. Every sensitive value is
|
||||
/// supplied per request in a header (never the query string or body) and is forwarded to ČSOB.
|
||||
/// Headers must therefore only be sent over TLS. Nothing here is logged or persisted.
|
||||
/// </summary>
|
||||
public static class CredentialConstants
|
||||
{
|
||||
/// <summary>
|
||||
/// eIDAS client certificate (QWAC) as a Base64-encoded PKCS#12 / PFX bundle, including the
|
||||
/// private key and the full chain. Used to establish the mutual-TLS connection to ČSOB.
|
||||
/// Analogous to Node's <c>https.Agent({ pfx, passphrase })</c>.
|
||||
/// </summary>
|
||||
public const string CertificateHeader = "X-CSOB-Certificate";
|
||||
|
||||
/// <summary>Optional passphrase protecting the PFX in <see cref="CertificateHeader"/>.</summary>
|
||||
public const string CertificatePasswordHeader = "X-CSOB-Certificate-Password";
|
||||
|
||||
/// <summary>OAuth2 Bearer access token obtained for the PSU; forwarded as <c>Authorization: Bearer</c>.</summary>
|
||||
public const string AccessTokenHeader = "X-Access-Token";
|
||||
|
||||
/// <summary>ČSOB application API key; forwarded as the <c>APIKEY</c> header.</summary>
|
||||
public const string ApiKeyHeader = "X-API-Key";
|
||||
|
||||
/// <summary>TPP (third-party provider) organisation name; forwarded as the <c>TPP-Name</c> header.</summary>
|
||||
public const string TppNameHeader = "X-TPP-Name";
|
||||
|
||||
/// <summary>OAuth2 client id of the registered TPP application (used by the OAuth helper endpoints).</summary>
|
||||
public const string ClientIdHeader = "X-CSOB-Client-Id";
|
||||
|
||||
/// <summary>OAuth2 client secret of the registered TPP application (used by the OAuth helper endpoints).</summary>
|
||||
public const string ClientSecretHeader = "X-CSOB-Client-Secret";
|
||||
|
||||
// Optional PSU (end-user) context, forwarded verbatim to ČSOB when present.
|
||||
|
||||
/// <summary>Whether the PSU is online/involved in the request; forwarded as <c>User-Involved</c> (default false).</summary>
|
||||
public const string UserInvolvedHeader = "X-User-Involved";
|
||||
|
||||
/// <summary>PSU IP address; forwarded as <c>User-IP-Address</c>.</summary>
|
||||
public const string UserIpAddressHeader = "X-User-IP-Address";
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
|
||||
namespace Csob.Credentials;
|
||||
|
||||
/// <summary>
|
||||
/// Fully resolved set of per-request credentials and PSU context used to call the ČSOB PSD2 API.
|
||||
/// Built from request headers by <see cref="RequestCredentialsProvider"/>; never logged.
|
||||
/// </summary>
|
||||
public sealed record CsobCredentials
|
||||
{
|
||||
/// <summary>OAuth2 Bearer access token (forwarded as <c>Authorization: Bearer</c>).</summary>
|
||||
public required string AccessToken { get; init; }
|
||||
|
||||
/// <summary>ČSOB application API key (forwarded as <c>APIKEY</c>).</summary>
|
||||
public required string ApiKey { get; init; }
|
||||
|
||||
/// <summary>TPP organisation name (forwarded as <c>TPP-Name</c>).</summary>
|
||||
public required string TppName { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// eIDAS client certificate (with private key) for mutual TLS. Optional at the type level so
|
||||
/// metadata endpoints can resolve context, but required for any real upstream call.
|
||||
/// </summary>
|
||||
public X509Certificate2? Certificate { get; init; }
|
||||
|
||||
/// <summary>Whether the PSU is online for this request (<c>User-Involved</c>); defaults to false.</summary>
|
||||
public bool UserInvolved { get; init; }
|
||||
|
||||
/// <summary>Optional PSU IP address (<c>User-IP-Address</c>).</summary>
|
||||
public string? UserIpAddress { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
namespace Csob.Credentials;
|
||||
|
||||
/// <summary>
|
||||
/// Raised when a request does not provide the credential headers required to call ČSOB.
|
||||
/// Translated to HTTP 401 by the exception-handling middleware.
|
||||
/// </summary>
|
||||
public sealed class MissingCredentialsException : Exception
|
||||
{
|
||||
public IReadOnlyList<string> MissingHeaders { get; }
|
||||
|
||||
public MissingCredentialsException(IReadOnlyList<string> missingHeaders)
|
||||
: base("Incomplete ČSOB credentials. Provide the missing values as request headers (TLS only).")
|
||||
{
|
||||
MissingHeaders = missingHeaders;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
|
||||
namespace Csob.Credentials;
|
||||
|
||||
/// <summary>
|
||||
/// Resolves the ČSOB credentials and PSU context for the current request, exclusively from HTTP
|
||||
/// headers (this service stores no secrets). Missing required headers produce a 401; a malformed
|
||||
/// certificate or wrong passphrase produces a 400 (via <see cref="CryptographicException"/>).
|
||||
/// </summary>
|
||||
public sealed class RequestCredentialsProvider
|
||||
{
|
||||
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||
|
||||
public RequestCredentialsProvider(IHttpContextAccessor httpContextAccessor)
|
||||
{
|
||||
_httpContextAccessor = httpContextAccessor;
|
||||
}
|
||||
|
||||
/// <summary>Reads a single request header, returning null when absent or blank.</summary>
|
||||
public string? Header(string name)
|
||||
{
|
||||
var headers = _httpContextAccessor.HttpContext?.Request.Headers;
|
||||
if (headers is not null && headers.TryGetValue(name, out var value))
|
||||
{
|
||||
var raw = value.ToString();
|
||||
if (!string.IsNullOrWhiteSpace(raw))
|
||||
{
|
||||
return raw;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds the eIDAS client certificate from the Base64 PFX header (+ optional passphrase).
|
||||
/// Returns null when no certificate header is present.
|
||||
/// </summary>
|
||||
/// <exception cref="CryptographicException">The header is not valid Base64 or the PFX/passphrase is invalid.</exception>
|
||||
public X509Certificate2? TryBuildCertificate()
|
||||
{
|
||||
var base64 = Header(CredentialConstants.CertificateHeader);
|
||||
if (base64 is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
byte[] raw;
|
||||
try
|
||||
{
|
||||
raw = Convert.FromBase64String(base64.Trim());
|
||||
}
|
||||
catch (FormatException ex)
|
||||
{
|
||||
throw new CryptographicException($"{CredentialConstants.CertificateHeader} is not valid Base64.", ex);
|
||||
}
|
||||
|
||||
var password = Header(CredentialConstants.CertificatePasswordHeader);
|
||||
|
||||
// EphemeralKeySet keeps the private key in memory only — never written to the machine key store / disk.
|
||||
return new X509Certificate2(raw, password, X509KeyStorageFlags.EphemeralKeySet);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolves the full credential set required for an AISP/PISP/consent call. Throws
|
||||
/// <see cref="MissingCredentialsException"/> if any required header is absent.
|
||||
/// </summary>
|
||||
public CsobCredentials Resolve()
|
||||
{
|
||||
var accessToken = Header(CredentialConstants.AccessTokenHeader);
|
||||
var apiKey = Header(CredentialConstants.ApiKeyHeader);
|
||||
var tppName = Header(CredentialConstants.TppNameHeader);
|
||||
var certificate = TryBuildCertificate();
|
||||
|
||||
var missing = new List<string>();
|
||||
if (string.IsNullOrWhiteSpace(accessToken)) missing.Add(CredentialConstants.AccessTokenHeader);
|
||||
if (string.IsNullOrWhiteSpace(apiKey)) missing.Add(CredentialConstants.ApiKeyHeader);
|
||||
if (string.IsNullOrWhiteSpace(tppName)) missing.Add(CredentialConstants.TppNameHeader);
|
||||
if (certificate is null) missing.Add(CredentialConstants.CertificateHeader);
|
||||
if (missing.Count > 0)
|
||||
{
|
||||
certificate?.Dispose();
|
||||
throw new MissingCredentialsException(missing);
|
||||
}
|
||||
|
||||
return new CsobCredentials
|
||||
{
|
||||
AccessToken = accessToken!,
|
||||
ApiKey = apiKey!,
|
||||
TppName = tppName!,
|
||||
Certificate = certificate,
|
||||
UserInvolved = string.Equals(Header(CredentialConstants.UserInvolvedHeader), "true", StringComparison.OrdinalIgnoreCase),
|
||||
UserIpAddress = Header(CredentialConstants.UserIpAddressHeader),
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user