Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 72 additions & 12 deletions AuthorizeNET/AuthorizeNET/Api/Controllers/Bases/ApiOperationBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ namespace AuthorizeNet.Api.Controllers.Bases
using System.Collections.Generic;
using System.Globalization;
using System;
using System.Threading;
using Contracts.V1;
using Utilities;
using Microsoft.Extensions.Logging;
Expand All @@ -11,10 +12,67 @@ public abstract class ApiOperationBase<TQ, TS> : IApiOperation<TQ, TS>
where TQ : ANetApiRequest
where TS : ANetApiResponse
{
protected static ILogger Logger = LogFactory.getLog(typeof(ApiOperationBase<TQ, TS>));
protected static ILogger Logger = LogFactory.getLog(typeof(ApiOperationBase<TQ, TS>));

// AsyncLocal backing storage for thread-safe access
private static AsyncLocal<AuthorizeNet.Environment> _runEnvironment = new AsyncLocal<AuthorizeNet.Environment>();
private static AsyncLocal<merchantAuthenticationType> _merchantAuthentication = new AsyncLocal<merchantAuthenticationType>();

/// <summary>
/// Gets or sets the runtime environment for API requests.
/// This property is now thread-safe using AsyncLocal storage, preventing cross-tenant credential bleed.
///
/// RECOMMENDED: Pass environment parameter to Execute() method instead of using this static property.
/// </summary>
/// <example>
/// RECOMMENDED (per-request):
/// <code>
/// controller.Execute(Environment.PRODUCTION); // DO THIS
/// </code>
///
/// LEGACY (now thread-safe but discouraged):
/// <code>
/// ApiOperationBase.RunEnvironment = Environment.PRODUCTION;
/// controller.Execute();
/// </code>
/// </example>
[Obsolete("Static RunEnvironment is discouraged in multi-tenant applications. " +
"Pass environment parameter to Execute() method instead.", true)]
public static AuthorizeNet.Environment RunEnvironment
{
get => _runEnvironment.Value;
set => _runEnvironment.Value = value;
}

public static AuthorizeNet.Environment RunEnvironment { get; set; }
public static merchantAuthenticationType MerchantAuthentication { get; set; }
/// <summary>
/// Gets or sets the merchant authentication credentials for API requests.
/// This property is now thread-safe using AsyncLocal storage, preventing cross-tenant credential bleed.
///
/// RECOMMENDED: Set merchantAuthentication on the request object instead of using this static property.
/// </summary>
/// <example>
/// RECOMMENDED (per-request):
/// <code>
/// var request = new createTransactionRequest();
/// request.merchantAuthentication = merchantAuth; // DO THIS
/// var controller = new createTransactionController(request);
/// controller.Execute();
/// </code>
///
/// LEGACY (now thread-safe but discouraged):
/// <code>
/// ApiOperationBase.MerchantAuthentication = merchantAuth;
/// var controller = new createTransactionController(request);
/// controller.Execute();
/// </code>
/// </example>
[Obsolete("Static MerchantAuthentication is discouraged in multi-tenant applications. " +
"Set merchantAuthentication on the request object instead.", true)]
public static merchantAuthenticationType MerchantAuthentication
{
get => _merchantAuthentication.Value;
set => _merchantAuthentication.Value = value;
}

private TQ _apiRequest;
private TS _apiResponse;
Expand Down Expand Up @@ -87,37 +145,39 @@ public void Execute(AuthorizeNet.Environment environment = null)
{
BeforeExecute();

if (null == environment) { environment = ApiOperationBase<ANetApiRequest, ANetApiResponse>.RunEnvironment; }
if (null == environment) { environment = _runEnvironment.Value; }
if (null == environment) throw new ArgumentException(NullEnvironmentErrorMessage);

var httpApiResponse = HttpUtility.PostData<TQ, TS>(environment, GetApiRequest());

if (null != httpApiResponse)
{
Logger.LogDebug("Received Response:'{0}' for request:'{1}'", httpApiResponse, GetApiRequest());
// SECURITY: Log only type names, not full DTOs which may contain
// merchantAuthentication credentials, card numbers, or session tokens.
Logger.LogDebug("Received Response type:'{0}' for request type:'{1}'", httpApiResponse.GetType().Name, _requestClass.Name);
if (httpApiResponse.GetType() == _responseClass)
{
var response = (TS)httpApiResponse;
SetApiResponse(response);
Logger.LogDebug("Setting response: '{0}'", response);
Logger.LogDebug("Setting response type: '{0}'", _responseClass.Name);
}
else if (httpApiResponse.GetType() == typeof(ErrorResponse))
{
SetErrorResponse(httpApiResponse);
Logger.LogDebug("Received ErrorResponse:'{0}'", httpApiResponse);
Logger.LogDebug("Received ErrorResponse for request type:'{0}'", _requestClass.Name);
}
else
{
SetErrorResponse(httpApiResponse);
Logger.LogError("Invalid response:'{0}'", httpApiResponse);
Logger.LogError("Invalid response type:'{0}' for request type:'{1}'", httpApiResponse.GetType().Name, _requestClass.Name);
}
Logger.LogDebug("Response obtained: {0}", GetApiResponse());
Logger.LogDebug("Response obtained for request type: {0}", _requestClass.Name);
SetResultStatus();

}
else
{
Logger.LogDebug("Got a 'null' Response for request:'{0}'\n", GetApiRequest());
Logger.LogDebug("Got a 'null' Response for request type:'{0}'", _requestClass.Name);
}
AfterExecute();
}
Expand Down Expand Up @@ -191,9 +251,9 @@ private void ValidateAndSetMerchantAuthentication()

if (null == request.merchantAuthentication)
{
if (null != ApiOperationBase<ANetApiRequest, ANetApiResponse>.MerchantAuthentication)
if (null != _merchantAuthentication.Value)
{
request.merchantAuthentication = ApiOperationBase<ANetApiRequest, ANetApiResponse>.MerchantAuthentication;
request.merchantAuthentication = _merchantAuthentication.Value;
}
else
{
Expand Down
93 changes: 65 additions & 28 deletions AuthorizeNET/AuthorizeNET/Environment.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,19 +19,61 @@ public class Environment
public static readonly Environment HOSTED_VM = new Environment(null, null, null);
public static Environment CUSTOM = new Environment(null, null, null);

public bool HttpUseProxy { get; set; }
public string HttpsProxyUsername { get; set; }
public string HttpsProxyPassword { get; set; }
public string HttpProxyHost { get; set; }
public int HttpProxyPort { get; set; }
/// <summary>
/// Gets whether to use HTTP proxy. Immutable - set via constructor for thread safety.
/// </summary>
public bool HttpUseProxy { get; }

/// <summary>
/// Gets the HTTPS proxy username. Immutable - set via constructor for thread safety.
/// </summary>
public string HttpsProxyUsername { get; }

/// <summary>
/// Gets the HTTPS proxy password. Immutable - set via constructor for thread safety.
/// </summary>
public string HttpsProxyPassword { get; }

/// <summary>
/// Gets the HTTP proxy host. Immutable - set via constructor for thread safety.
/// </summary>
public string HttpProxyHost { get; }

/// <summary>
/// Gets the HTTP proxy port. Immutable - set via constructor for thread safety.
/// </summary>
public int HttpProxyPort { get; }


private Environment(string baseUrl, string xmlBaseUrl, string cardPresentUrl)
{
BaseUrl = baseUrl;
XmlBaseUrl = xmlBaseUrl;
CardPresentUrl = cardPresentUrl;
}
public Environment(string baseUrl, string xmlBaseUrl, string cardPresentUrl)
: this(baseUrl, xmlBaseUrl, cardPresentUrl, false, null, 0, null, null)
{
}

/// <summary>
/// Creates a new Environment with the specified URLs and optional proxy settings.
/// </summary>
/// <param name="baseUrl">Base URL</param>
/// <param name="xmlBaseUrl">XML base URL</param>
/// <param name="cardPresentUrl">Card present URL</param>
/// <param name="httpUseProxy">Whether to use HTTP proxy</param>
/// <param name="proxyHost">Proxy host address</param>
/// <param name="proxyPort">Proxy port number</param>
/// <param name="proxyUsername">Proxy username for authentication</param>
/// <param name="proxyPassword">Proxy password for authentication</param>
public Environment(string baseUrl, string xmlBaseUrl, string cardPresentUrl,
bool httpUseProxy = false, string proxyHost = null, int proxyPort = 0,
string proxyUsername = null, string proxyPassword = null)
{
BaseUrl = baseUrl;
XmlBaseUrl = xmlBaseUrl;
CardPresentUrl = cardPresentUrl;
HttpUseProxy = httpUseProxy;
HttpProxyHost = proxyHost;
HttpProxyPort = proxyPort;
HttpsProxyUsername = proxyUsername;
HttpsProxyPassword = proxyPassword;
}

/// <summary>
/// Gets the base url
Expand Down Expand Up @@ -61,22 +103,17 @@ public static Environment createEnvironment(string baseUrl, string xmlBaseUrl)
}


/// <summary>
/// Create a custom environment with the specified base url
/// </summary>
/// <param name="baseUrl">Base url</param>
/// <param name="xmlBaseUrl">Xml base url</param>
/// <param name="cardPresentUrl">Card present url</param>
/// <returns>The custom environment</returns>
public static Environment createEnvironment(string baseUrl, string xmlBaseUrl, string cardPresentUrl)
{
var environment = CUSTOM;
environment.BaseUrl = baseUrl;
environment.XmlBaseUrl = xmlBaseUrl;
environment.CardPresentUrl = cardPresentUrl;

return environment;
}
/// <summary>
/// Create a custom environment with the specified base url
/// </summary>
/// <param name="baseUrl">Base url</param>
/// <param name="xmlBaseUrl">Xml base url</param>
/// <param name="cardPresentUrl">Card present url</param>
/// <returns>The custom environment</returns>
public static Environment createEnvironment(string baseUrl, string xmlBaseUrl, string cardPresentUrl)
{
return new Environment(baseUrl, xmlBaseUrl, cardPresentUrl);
}

/// <summary>
/// Reads an integer value from the environment
Expand Down Expand Up @@ -122,4 +159,4 @@ public static string GetProperty(string propertyName)
return System.Environment.GetEnvironmentVariable(propertyName);
}
}
}
}
20 changes: 10 additions & 10 deletions AuthorizeNET/AuthorizeNET/Utilities/HttpUtility.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,11 @@ public static ANetApiResponse PostData<TQ, TS>(AuthorizeNet.Environment env, TQ
{
ANetApiResponse response = null;
if (null == request)
{
throw new ArgumentNullException("request");
}
Logger.LogDebug("MerchantInfo->LoginId/TransactionKey: '{0}':'{1}'->{2}", request.merchantAuthentication.name, request.merchantAuthentication.ItemElementName, request.merchantAuthentication.Item);
{
throw new ArgumentNullException("request");
}

var postUrl = GetPostUrl(env);
var postUrl = GetPostUrl(env);

string responseAsString = null;
using (var clientHandler = new HttpClientHandler())
Expand All @@ -46,12 +45,13 @@ public static ANetApiResponse PostData<TQ, TS>(AuthorizeNet.Environment env, TQ
client.Timeout = TimeSpan.FromMilliseconds(httpConnectionTimeout != 0 ? httpConnectionTimeout : Constants.HttpConnectionDefaultTimeout);
var content = new StringContent(XmlUtility.Serialize(request), Encoding.UTF8, "text/xml");
var webResponse = client.PostAsync(postUrl, content).Result;
Logger.LogDebug("Retrieving Response from Url: '{0}'", postUrl);
Logger.LogDebug("Retrieving Response from Url: '{0}'", postUrl);

// Get the response
Logger.LogDebug("Received Response: '{0}'", webResponse);
responseAsString = webResponse.Content.ReadAsStringAsync().Result;
Logger.LogDebug("Response from Stream: '{0}'", responseAsString);
// Get the response — SECURITY: Log only HTTP status, never raw body
// (response may contain PAN, transactionKey, session tokens)
Logger.LogDebug("Received Response: StatusCode='{0}', ReasonPhrase='{1}'", webResponse.StatusCode, webResponse.ReasonPhrase);
responseAsString = webResponse.Content.ReadAsStringAsync().Result;
Logger.LogDebug("Response received, ContentLength='{0}', ContentType='{1}'", responseAsString?.Length, webResponse.Content?.Headers?.ContentType);

}
}
Expand Down
50 changes: 47 additions & 3 deletions AuthorizeNET/AuthorizeNET/Utilities/LogFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,57 @@
using System;
using Microsoft.Extensions.Logging;

/// <summary>
/// Factory for creating SDK loggers. Consumers can inject their own ILoggerFactory
/// via SetLoggerFactory() to control log routing, sinks, and levels.
///
/// SECURITY NOTE: The default log level is Warning (not Debug) to prevent
/// accidental exposure of sensitive data (merchantAuthentication credentials,
/// card numbers, session tokens) in request/response DTOs. No raw XML or
/// response body content is ever logged at any level — only metadata such as
/// HTTP status codes, content lengths, type names, and error messages.
/// </summary>
public static class LogFactory
{
private static ILoggerFactory LoggerFactory => new LoggerFactory().AddDebug(LogLevel.Debug);
private static readonly object _lock = new object();
private static volatile ILoggerFactory _loggerFactory;

/// <summary>
/// Allows consumers to inject their own ILoggerFactory for full control
/// over log routing, sinks, and filtering levels.
/// Thread-safe: uses volatile read + lock on write.
/// </summary>
/// <param name="loggerFactory">The ILoggerFactory to use for all SDK logging.</param>
public static void SetLoggerFactory(ILoggerFactory loggerFactory)
{
lock (_lock)
{
_loggerFactory = loggerFactory;
}
}

private static ILoggerFactory GetLoggerFactory()
{
// Volatile read — no lock needed for read path (double-checked pattern)
var factory = _loggerFactory;
if (factory != null)
return factory;

lock (_lock)
{
if (_loggerFactory == null)
{
// Default: Warning level via Debug output (only captured when debugger is attached).
// Consumers should call SetLoggerFactory() to wire up their own sinks/levels.
_loggerFactory = new LoggerFactory().AddDebug(LogLevel.Warning);
}
return _loggerFactory;
}
}

public static ILogger getLog(Type classType)
{
return LoggerFactory.CreateLogger(classType.FullName);
return GetLoggerFactory().CreateLogger(classType.FullName);
}
}
}
}
4 changes: 3 additions & 1 deletion AuthorizeNET/AuthorizeNET/Utilities/XmlUtility.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,9 @@ public static T Deserialize<T>(string xml)
}
catch (Exception e)
{
Logger.LogError("Error:'{0}' when deserializing the into object:'{1}' from xml:'{2}'", e.Message, responseType, xml);
// SECURITY: Never log raw XML — it may contain PAN, transactionKey,
// session tokens, or other sensitive payment data (PCI A3.2.6, KC 7.10.9).
Logger.LogError("Error:'{0}' when deserializing into object:'{1}' (xmlLength={2})", e.Message, responseType, xml?.Length);
throw;
}
}
Expand Down
Loading