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
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<Version>0.0.1.19</Version>
<Version>0.0.1.20</Version>
<Authors>CyberSource</Authors>
<Product>Authentication_SDK</Product>
<Description />
Expand Down Expand Up @@ -36,6 +36,7 @@

<ItemGroup>
<PackageReference Include="jose-jwt" Version="4.1.0" />
<PackageReference Include="Microsoft.IdentityModel.Tokens" Version="8.14.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="NLog" Version="5.0.0" />
<PackageReference Include="Portable.BouncyCastle" Version="1.9.0" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -236,5 +236,32 @@ private static X509Certificate2Collection FetchCertificateCollectionFromP12File(
//return all certs in p12
return certificates;
}

public static void AddPublicKeyToCache(string publickey, string runEnvironment, string kid)
{
// Construct cache key similar to PHP logic
string cacheKey = $"{Constants.PUBLIC_KEY_CACHE_IDENTIFIER}_{runEnvironment}_{kid}";

ObjectCache cache = MemoryCache.Default;

var policy = new CacheItemPolicy();
// Optionally, set expiration or change monitors if needed

lock (mutex)
{
cache.Set(cacheKey, publickey, policy);
}
}
public static string GetPublicKeyFromCache(string runEnvironment, string keyId)
{
string cacheKey = $"{Constants.PUBLIC_KEY_CACHE_IDENTIFIER}_{runEnvironment}_{keyId}";
ObjectCache cache = MemoryCache.Default;

if (cache.Contains(cacheKey))
{
return cache.Get(cacheKey) as string;
}
throw new Exception($"Public key not found in cache for [RunEnvironment: {runEnvironment}, KeyId: {keyId}]");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -51,5 +51,7 @@ public static class Constants
public static readonly string MLE_CACHE_IDENTIFIER_FOR_CONFIG_CERT = "mleCertFromMerchantConfig";

public static readonly string MLE_CACHE_IDENTIFIER_FOR_P12_CERT = "mleCertFromP12";

public static readonly string PUBLIC_KEY_CACHE_IDENTIFIER = "FlexV2PublicKeys";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
using System;
using System.Collections.Generic;
using System.Security.Cryptography;
using Jose;
using Microsoft.IdentityModel.Tokens;
using Newtonsoft.Json;
using AuthenticationSdk.util.jwtExceptions;

namespace AuthenticationSdk.util
{
public static class JWTUtility
{


/// <summary>
/// Parses a JWT token to verify its structure and decodes its payload, without performing signature validation.
/// This is useful for inspecting the token's claims before verifying its authenticity.
/// </summary>
/// <param name="jwtToken">The JWT token string to parse.</param>
/// <returns>The JSON payload of the token as a string if the token is structurally valid.</returns>
/// <exception cref="ArgumentException">Thrown if the token is null, empty, malformed, or not a valid JWT structure.</exception>
public static string Parse(string jwtToken)
{
if (string.IsNullOrWhiteSpace(jwtToken))
{
throw new InvalidJwtException("JWT token cannot be null, empty, or whitespace.");
}

try
{
// The jose-jwt library's Payload method handles splitting the token and Base64Url decoding the payload part.
// It will throw an exception if the token does not have three parts or if the payload is not valid Base64Url.
string payloadJson = JWT.Payload(jwtToken);

// The JWT specification requires the payload (Claims Set) to be a JSON object.
// We'll verify this to ensure the token is fully compliant.
try
{
JsonConvert.DeserializeObject(payloadJson);
}
catch (JsonException jsonEx)
{
throw new JsonException("Invalid JWT: The payload is not a valid JSON object.", jsonEx);
}

// If all checks pass, return the decoded payload.
return payloadJson;
}
catch (JsonException)
{
// Rethrow JSON exceptions as they are.
throw;
}
catch (Exception ex)
{
// Catch exceptions from JWT.Payload() (e.g., malformed token)
throw new InvalidJwtException("The provided JWT is malformed.", ex);
}
}

public static IDictionary<string, object> GetJwtHeaders(string jwtToken)
{
return JWT.Headers(jwtToken);
}

/// <summary>
/// Verifies a JWT token against a public key provided as a JWK string.
/// </summary>
/// <param name="jwtValue">The JWT token to verify.</param>
/// <param name="publicKey">The public key in JWK JSON format.</param>
/// <returns>Returns true if the token is successfully verified.</returns>
/// <exception cref="Exception">
/// Throws an exception if verification fails due to an invalid signature,
/// a malformed token, a missing algorithm header, or other errors.
/// </exception>
/// <exception cref="JwtSignatureValidationException">
/// Thrown if verification fails due to an invalid signature, malformed token, missing algorithm header, or other errors.
/// </exception>
/// <exception cref="ArgumentException">
/// Thrown if the JWT header is missing the 'alg' parameter or the algorithm is not supported.
/// </exception>
/// <exception cref="InvalidJwkException">
/// Thrown if the JWK is invalid, not an RSA key, or missing required fields.
/// </exception>
public static bool VerifyJwt(string jwtValue, string publicKey)
{
try
{
// Step 1: Convert the JWK string into RSA parameters.
RSAParameters rsaParameters = ConvertJwkToRsaParameters(publicKey);

// Step 2: Create an RSACryptoServiceProvider and import the public key.
var rsa = new RSACryptoServiceProvider();
rsa.ImportParameters(rsaParameters);

// Step 3: Dynamically determine the algorithm from the JWT's header.
var headers = JWT.Headers(jwtValue);
if (!headers.TryGetValue("alg", out var alg))
{
throw new ArgumentException("JWT header is missing the 'alg' parameter.");
}

string algStr = alg as string;
var supportedRsaAlgorithms = new[] { "RS256", "RS384", "RS512" };
if (Array.IndexOf(supportedRsaAlgorithms, algStr) < 0)
{
throw new ArgumentException($"The algorithm in the JWT token is not RSA. Only {string.Join(", ", supportedRsaAlgorithms)} are supported.");
}

// Parse the string algorithm into the JwsAlgorithm enum.
var jwsAlgorithm = (JwsAlgorithm)Enum.Parse(typeof(JwsAlgorithm), algStr);

// Step 4: Decode and verify the token.
// The JWT.Decode method will perform signature validation and throw
// a Jose.IntegrityException if the signature is invalid.
JWT.Decode(jwtValue, rsa, jwsAlgorithm);

// Step 5: If JWT.Decode completes without throwing an exception, verification is successful.
return true;
}
catch (JoseException ex)
{
// This will catch signature validation errors (IntegrityException)
// or other JWT-specific issues from the jose-jwt library.
// Re-throwing as a general exception to signal verification failure.
throw new JwtSignatureValidationException("JWT verification failed. See inner exception for details.", ex);
}
catch (ArgumentException)
{
throw;
}
catch (InvalidJwkException)
{
throw;
}
catch (Exception ex)
{
// This catches other potential errors, such as from JWK conversion or invalid algorithm parsing.
throw new JwtSignatureValidationException("An unexpected error occurred during JWT verification.", ex);
}
}

/// <summary>
/// Converts a JSON Web Key (JWK) string into RSAParameters.
/// This method is designed for RSA public keys.
/// </summary>
/// <param name="jwkJson">The JWK in JSON string format.</param>
/// <returns>An RSAParameters object containing the public key.</returns>
/// <exception cref="InvalidJwkException">
/// Thrown if the JWK is invalid, not an RSA key, or missing required fields.
/// </exception>
private static RSAParameters ConvertJwkToRsaParameters(string jwkJson)
{
Dictionary<string, string> jwk;
try
{
jwk = JsonConvert.DeserializeObject<Dictionary<string, string>>(jwkJson);
}
catch (JsonException ex)
{
throw new InvalidJwkException("Malformed JWK: Not valid JSON format", ex);
}

if (jwk == null || !jwk.ContainsKey("kty") || jwk["kty"] != "RSA" || !jwk.ContainsKey("n") || !jwk.ContainsKey("e"))
{
throw new InvalidJwkException("Invalid JWK: Must be an RSA key with 'kty', 'n', and 'e' values.");
}

// Use the standard library for Base64Url decoding
byte[] modulus = Base64UrlEncoder.DecodeBytes(jwk["n"]);
byte[] exponent = Base64UrlEncoder.DecodeBytes(jwk["e"]);

return new RSAParameters
{
Modulus = modulus,
Exponent = exponent
};
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace AuthenticationSdk.util.jwtExceptions
{
public class InvalidJwkException : Exception
{
public InvalidJwkException(string message) : base(message) { }
public InvalidJwkException(string message, Exception cause) : base(message, cause) { }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace AuthenticationSdk.util.jwtExceptions
{
public class InvalidJwtException : Exception
{
public InvalidJwtException(string message) : base(message) { }
public InvalidJwtException(string message, Exception cause) : base(message, cause) { }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace AuthenticationSdk.util.jwtExceptions
{
public class JwtSignatureValidationException : Exception
{
public JwtSignatureValidationException(string message) : base(message) { }
public JwtSignatureValidationException(string message, Exception cause) : base(message, cause) { }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ public void BankAccountValidationRequestTest()
// TODO uncomment below to test the method and replace null with proper value
//AccountValidationsRequest accountValidationsRequest = null;
//var response = instance.BankAccountValidationRequest(accountValidationsRequest);
//Assert.IsInstanceOf<InlineResponse20013> (response, "response is InlineResponse20013");
//Assert.IsInstanceOf<InlineResponse20014> (response, "response is InlineResponse20014");
}

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ public void GetBatchReportTest()
// TODO uncomment below to test the method and replace null with proper value
//string batchId = null;
//var response = instance.GetBatchReport(batchId);
//Assert.IsInstanceOf<InlineResponse20012> (response, "response is InlineResponse20012");
//Assert.IsInstanceOf<InlineResponse20013> (response, "response is InlineResponse20013");
}

/// <summary>
Expand All @@ -85,7 +85,7 @@ public void GetBatchStatusTest()
// TODO uncomment below to test the method and replace null with proper value
//string batchId = null;
//var response = instance.GetBatchStatus(batchId);
//Assert.IsInstanceOf<InlineResponse20011> (response, "response is InlineResponse20011");
//Assert.IsInstanceOf<InlineResponse20012> (response, "response is InlineResponse20012");
}

/// <summary>
Expand All @@ -100,7 +100,7 @@ public void GetBatchesListTest()
//string fromDate = null;
//string toDate = null;
//var response = instance.GetBatchesList(offset, limit, fromDate, toDate);
//Assert.IsInstanceOf<InlineResponse20010> (response, "response is InlineResponse20010");
//Assert.IsInstanceOf<InlineResponse20011> (response, "response is InlineResponse20011");
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ public void FindProductsToSubscribeTest()
// TODO uncomment below to test the method and replace null with proper value
//string organizationId = null;
//var response = instance.FindProductsToSubscribe(organizationId);
//Assert.IsInstanceOf<List<InlineResponse2004>> (response, "response is List<InlineResponse2004>");
//Assert.IsInstanceOf<List<InlineResponse2005>> (response, "response is List<InlineResponse2005>");
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ public void ActionDecisionManagerCaseTest()
//string id = null;
//CaseManagementActionsRequest caseManagementActionsRequest = null;
//var response = instance.ActionDecisionManagerCase(id, caseManagementActionsRequest);
//Assert.IsInstanceOf<InlineResponse2001> (response, "response is InlineResponse2001");
//Assert.IsInstanceOf<InlineResponse2002> (response, "response is InlineResponse2002");
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ public void PostDeAssociateV3TerminalTest()
// TODO uncomment below to test the method and replace null with proper value
//List<DeviceDeAssociateV3Request> deviceDeAssociateV3Request = null;
//var response = instance.PostDeAssociateV3Terminal(deviceDeAssociateV3Request);
//Assert.IsInstanceOf<List<InlineResponse2008>> (response, "response is List<InlineResponse2008>");
//Assert.IsInstanceOf<List<InlineResponse2009>> (response, "response is List<InlineResponse2009>");
}

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ public void PostSearchQueryTest()
// TODO uncomment below to test the method and replace null with proper value
//PostDeviceSearchRequest postDeviceSearchRequest = null;
//var response = instance.PostSearchQuery(postDeviceSearchRequest);
//Assert.IsInstanceOf<InlineResponse2007> (response, "response is InlineResponse2007");
//Assert.IsInstanceOf<InlineResponse2008> (response, "response is InlineResponse2008");
}

/// <summary>
Expand All @@ -85,7 +85,7 @@ public void PostSearchQueryV3Test()
// TODO uncomment below to test the method and replace null with proper value
//PostDeviceSearchRequestV3 postDeviceSearchRequestV3 = null;
//var response = instance.PostSearchQueryV3(postDeviceSearchRequestV3);
//Assert.IsInstanceOf<InlineResponse2009> (response, "response is InlineResponse2009");
//Assert.IsInstanceOf<InlineResponse20010> (response, "response is InlineResponse20010");
}

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ public void GetWebhookSubscriptionsByOrgTest()
//string productId = null;
//string eventType = null;
//var response = instance.GetWebhookSubscriptionsByOrg(organizationId, productId, eventType);
//Assert.IsInstanceOf<List<InlineResponse2005>> (response, "response is List<InlineResponse2005>");
//Assert.IsInstanceOf<List<InlineResponse2006>> (response, "response is List<InlineResponse2006>");
}

/// <summary>
Expand All @@ -124,7 +124,7 @@ public void NotificationSubscriptionsV2WebhooksWebhookIdPatchTest()
//string webhookId = null;
//UpdateWebhook updateWebhook = null;
//var response = instance.NotificationSubscriptionsV2WebhooksWebhookIdPatch(webhookId, updateWebhook);
//Assert.IsInstanceOf<InlineResponse2006> (response, "response is InlineResponse2006");
//Assert.IsInstanceOf<InlineResponse2007> (response, "response is InlineResponse2007");
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ public void GetRegistrationTest()
// TODO uncomment below to test the method and replace null with proper value
//string registrationId = null;
//var response = instance.GetRegistration(registrationId);
//Assert.IsInstanceOf<InlineResponse2003> (response, "response is InlineResponse2003");
//Assert.IsInstanceOf<InlineResponse2004> (response, "response is InlineResponse2004");
}

/// <summary>
Expand Down
Loading