first commit

This commit is contained in:
Zeeshaun
2026-03-11 11:18:25 -05:00
commit f3cb730f3b
472 changed files with 53651 additions and 0 deletions
@@ -0,0 +1,16 @@
namespace OnlineBankingAuth.Configuration
{
public class SecuritySettings
{
public string PrivateCertAzureThumbprint { get; set; }
public string PublicCertAzureThumbprint { get; set; }
public string TokenSecret { get; set; }
public string SAMLSuccessRedirectRoute { get; set; }
public string SAMLFailureRedirectRoute { get; set; }
public string DigitalInsightPostTokenURI { get; set; }
public string DigitalInsightConsumerKey { get; set; }
public string DigitalInsightConsumerSecret { get; set; }
public string DigitalInsightFIID { get; set; }
}
}
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<ErrorOnDuplicatePublishOutputFiles>false</ErrorOnDuplicatePublishOutputFiles>
<ImplicitUsings>enable</ImplicitUsings>
<TargetFramework>net8.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.3" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
<PackageReference Include="Portable.BouncyCastle" Version="1.8.4" />
<PackageReference Include="SAML2" Version="3.0.0.6" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.14.0" />
<PackageReference Include="CoreWCF.Primitives" Version="1.8.0" />
<PackageReference Include="CoreWCF.ConfigurationManager" Version="1.8.0" />
<PackageReference Include="CoreWCF.Http" Version="1.8.0" />
<PackageReference Include="CoreWCF.WebHttp" Version="1.8.0" />
<PackageReference Include="CoreWCF.NetTcp" Version="1.8.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Landmark.OLA.Models\Landmark.OLA.Models.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,74 @@
using Landmark.OLA.Models.OnlineBanking;
using System.Security.Authentication;
using System.Xml.Linq;
namespace OnlineBankingAuth.Parsers
{
public class AuthTokenParser : IAuthTokenParser
{
public AuthTokenResponse ParseAuthTokenResponse(XElement xElement)
{
try
{
var accessToken = (string)xElement.Element("access_token");
if (String.IsNullOrEmpty(accessToken))
{
return ParseErroneousAuthTokenResponse(xElement);
}
var authToken = new AuthToken
{
DiFiid = (string)xElement.Element("di_fiid"),
DiFicustomer = (string)xElement.Element("di_ficustomer"),
DiMemberNumber = (string)xElement.Element("di_member_number"),
AccessToken = (string)xElement.Element("access_token"),
ExpiresIn = (string)xElement.Element("expires_in"),
RefreshToken = (string)xElement.Element("refresh_token"),
RefreshTokenExpiresIn = (string)xElement.Element("refresh_token_expires_in")
};
var authTokenResponse = new AuthTokenResponse
{
Token = authToken,
Status = null
};
return authTokenResponse;
}
catch (Exception e)
{
throw new AuthenticationException($"Could not parse response from IdP vendor. {e.Message}, {e.StackTrace}");
}
}
private AuthTokenResponse ParseErroneousAuthTokenResponse(XElement xElement)
{
var messageElement = (XElement)xElement.FirstNode;
var errorElement = (XElement)xElement.FirstNode.NextNode;
var errorType = (XElement)errorElement.FirstNode;
var errorCode = (XElement)errorElement.FirstNode.NextNode;
var errorMessage = (XElement)errorElement.FirstNode.NextNode.NextNode;
var error = new AuthTokenError
{
ErrorType = errorType.Value,
ErrorCode = errorCode.Value,
ErrorMessage = errorMessage.Value
};
var authTokenStatus = new AuthTokenStatus
{
StatusMessage = messageElement.Value,
ErrorInfo = error
};
return new AuthTokenResponse
{
Status = authTokenStatus
};
}
}
}
@@ -0,0 +1,10 @@
using Landmark.OLA.Models.OnlineBanking;
using System.Xml.Linq;
namespace OnlineBankingAuth.Parsers
{
public interface IAuthTokenParser
{
AuthTokenResponse ParseAuthTokenResponse(XElement xElement);
}
}
@@ -0,0 +1,12 @@
using Landmark.OLA.Models.OnlineBanking;
using System.Xml;
namespace OnlineBankingAuth.Parsers
{
public interface ISAMLParser<T>
{
DISAMLAssertion ParseAssertion(XmlElement xmlElement);
SAMLConditions ParseConditions(XmlElement xmlElement);
AuthenticationStatement ParseAuthnStatement(XmlElement xmlElement);
}
}
@@ -0,0 +1,8 @@
namespace OnlineBankingAuth.Parsers
{
public class SAMLMessaging
{
public string SAMLResponse { get; set; }
}
}
@@ -0,0 +1,70 @@
using Landmark.OLA.Models.OnlineBanking;
using System.Xml;
namespace OnlineBankingAuth.Parsers
{
public class SAMLParser : ISAMLParser<DISAMLAssertion>
{
public string SAML_NAMESPACE = "saml:";
public DISAMLAssertion ParseAssertion(XmlElement xmlElement)
{
try
{
var assertion = new DISAMLAssertion
{
NameID = xmlElement.GetElementsByTagName(SAML_NAMESPACE + "NameID")[0].InnerText,
Recipient = xmlElement.GetElementsByTagName(SAML_NAMESPACE + "SubjectConfirmationData")[0].Attributes[1].InnerText,
MemberID = xmlElement.GetElementsByTagName(SAML_NAMESPACE + "AttributeValue")[0].InnerText,
FI_ID = xmlElement.GetElementsByTagName(SAML_NAMESPACE + "AttributeValue")[1].InnerText,
AuthnStatement = ParseAuthnStatement(xmlElement),
Conditions = ParseConditions(xmlElement),
};
return assertion;
}
catch
{
throw new FormatException("SAML Response is in an unexpected layout.");
}
}
public AuthenticationStatement ParseAuthnStatement(XmlElement xmlElement)
{
try
{
var authnStatement = new AuthenticationStatement
{
SessionIndex = xmlElement.GetElementsByTagName(SAML_NAMESPACE + "AuthnStatement")[0].Attributes[1].InnerText,
AuthContextClassRef = xmlElement.GetElementsByTagName(SAML_NAMESPACE + "AuthnContextClassRef")[0].InnerText
};
return authnStatement;
}
catch
{
throw new FormatException("AuthnStatement is in an unexpected layout.");
}
}
public SAMLConditions ParseConditions(XmlElement xmlElement)
{
try
{
var conditions = new SAMLConditions
{
Audience = xmlElement.GetElementsByTagName(SAML_NAMESPACE + "Audience")[0].InnerText,
NotBefore = DateTime.Parse(xmlElement.GetElementsByTagName(SAML_NAMESPACE + "Conditions")[0].Attributes[0].InnerText),
NotAfter = DateTime.Parse(xmlElement.GetElementsByTagName(SAML_NAMESPACE + "Conditions")[0].Attributes[1].InnerText),
};
return conditions;
}
catch
{
throw new FormatException("SAML Conditions is in an unexpected layout");
}
}
}
}
@@ -0,0 +1,84 @@
using System.Text;
using System.Xml.Linq;
using Microsoft.Extensions.Options;
using Landmark.OLA.Models.OnlineBanking;
using OnlineBankingAuth.Configuration;
using OnlineBankingAuth.Parsers;
namespace OnlineBankingAuth.Services
{
public class DigitalInsightService : IDigitalInsightService
{
private readonly SecuritySettings _settings;
private readonly IAuthTokenParser _parser;
public DigitalInsightService(
IAuthTokenParser parser,
IOptions<SecuritySettings> settings)
{
_parser = parser;
_settings = settings.Value;
}
public OnlineBankingFormData PopulateOnlineBankingDataFromConfig(OnlineBankingFormData data)
{
data.Authorization = "Basic " +
Convert.ToBase64String(
Encoding.UTF8.GetBytes(
_settings.DigitalInsightConsumerKey + ":" + _settings.DigitalInsightConsumerSecret
)
);
data.DI_FIID = _settings.DigitalInsightFIID;
data.DI_TID = (Guid.NewGuid()).ToString();
return data;
}
private IEnumerable<KeyValuePair<string, string>> BuildKeyValuePair(OnlineBankingFormData data)
{
return new Dictionary<string, string>
{
{"grant_type", data.GrantType },
{"username", data.Username },
{"password", data.Password }
};
}
public async Task<AuthTokenResponse> LoginUser(OnlineBankingFormData data)
{
try
{
using (var hc = new HttpClient())
{
var formData = BuildKeyValuePair(data);
using (var content = new FormUrlEncodedContent(formData))
{
content.Headers.Clear();
content.Headers.Add("Content-Type", "application/x-www-form-urlencoded");
HttpContent httpContent = new StringContent(await content.ReadAsStringAsync(), Encoding.UTF8, "application/x-www-form-urlencoded");
var httpRequest = new HttpRequestMessage(HttpMethod.Post, _settings.DigitalInsightPostTokenURI);
httpRequest.Content = httpContent;
httpRequest.Headers.Add("Authorization", data.Authorization);
httpRequest.Headers.Add("di_fiid", data.DI_FIID);
httpRequest.Headers.Add("di_tid", data.DI_TID);
var response = hc.SendAsync(httpRequest);
var xdoc = XDocument.Parse(await response.Result.Content.ReadAsStringAsync());
var xel = XElement.Parse(xdoc.ToString());
var authTokenResponse = _parser.ParseAuthTokenResponse(xel);
return authTokenResponse;
}
}
}
catch (Exception)
{
throw;
}
}
}
}
@@ -0,0 +1,143 @@
using System.Security.Cryptography;
using System.Security.Authentication;
using System.Xml;
using System.Text;
using System.Security.Cryptography.X509Certificates;
using Microsoft.Extensions.Options;
using OnlineBankingAuth.Configuration;
using OnlineBankingAuth.Services;
using OnlineBankingAuth.Parsers;
using Landmark.OLA.Models.OnlineBanking;
namespace OnlineBankingAuth.Communication
{
public class DigitalInsightVendorClient : IDigitalInsightVendorClient
{
private readonly ISAMLParser<DISAMLAssertion> _samlParser;
private readonly SecuritySettings _settings;
private readonly IDigitalInsightService _serivce;
public DigitalInsightVendorClient(
ISAMLParser<DISAMLAssertion> samlParser,
IOptions<SecuritySettings> settings,
IDigitalInsightService service)
{
_samlParser = samlParser;
_settings = settings.Value;
_serivce = service;
}
public bool ValidateDIPublicKey(XmlElement x509)
{
var vendorTrustedCert = GetPublicKeyFromStore();
var innerText = x509.InnerText;
byte[] payloadCertBytes = Encoding.Default.GetBytes(innerText);
var payloadCert = new X509Certificate2(payloadCertBytes);
return vendorTrustedCert.GetPublicKeyString().Equals(payloadCert.GetPublicKeyString());
// @TODO: Assert other equalities besides the "publicKeyString"?
}
public DISAMLAssertion GetSAMLPayload(XmlDocument xmlDocument)
{
var lmcuPrivateCertificate = GetPrivateKeyFromStore();
// The "EncryptedAsserion" block is the only acceptable block we can use to construct SAML2.Saml20EncryptedAssertion
var encryptedXml = (XmlElement)xmlDocument.GetElementsByTagName("saml:EncryptedAssertion")[0];
var encryptedXmlDoc = new XmlDocument();
encryptedXmlDoc.LoadXml(encryptedXml.OuterXml);
// SAML2 library does the decrypting work
var privateKeyAlgorithm = (RSA)lmcuPrivateCertificate.GetRSAPrivateKey();
var encryptedAssertion = new SAML2.Saml20EncryptedAssertion(privateKeyAlgorithm, encryptedXmlDoc);
encryptedAssertion.Decrypt();
// The decrypted document should be signed by DI. The Vendor Public Key is used to assert that this is the case.
var vendorPublicKey = GetPublicKeyFromStore().GetRSAPublicKey();
var checkSignature = SAML2.Utils.XmlSignatureUtils.CheckSignature(encryptedAssertion.Assertion, vendorPublicKey);
if (!checkSignature)
{
throw new AuthenticationException("Signature does not match therefore the decrypted payload cannot be accepted.");
}
var decryptedAssertion = _samlParser.ParseAssertion(encryptedAssertion.Assertion.DocumentElement);
return decryptedAssertion;
}
public bool ValidateTimeStamp(DISAMLAssertion assertion)
{
return 0 <= DateTime.Compare(assertion.Conditions.NotAfter, DateTime.Now);
}
public string GetSuccessRedirectUrl()
{
return _settings.SAMLSuccessRedirectRoute;
}
public string GetFailureRedirectUrl(string message)
{
var plainTextBytes = System.Text.Encoding.UTF8.GetBytes(message);
var encodedMessage = System.Convert.ToBase64String(plainTextBytes);
return _settings.SAMLFailureRedirectRoute + "&msg=" + encodedMessage;
}
private X509Certificate2 GetPrivateKeyFromStore()
{
// FOR TESTING LOCAL #1345. Note: the second argument is the password for the secure cert file
//return new X509Certificate2("Controllers" + Path.DirectorySeparatorChar + "dev.pfx", "Nvisia", X509KeyStorageFlags.Exportable);
///*
var certStore = new X509Store(StoreName.My, StoreLocation.CurrentUser);
certStore.Open(OpenFlags.ReadOnly);
var certCollection = certStore.Certificates.Find(X509FindType.FindByThumbprint, _settings.PrivateCertAzureThumbprint, false);
X509Certificate2 lmcuPrivateCertificate;
if (certCollection.Count == 0)
{
throw new AuthenticationException("Could not GET private key from PrivateCertAzureThumbprint");
}
// Should only have 1 cert with that thumbprint
lmcuPrivateCertificate = certCollection[0];
return lmcuPrivateCertificate;
//*/
}
private X509Certificate2 GetPublicKeyFromStore()
{
// FOR TESTING LOCAL #1345
//return new X509Certificate2("Controllers" + Path.DirectorySeparatorChar + "pfd-sign.qal1signingcert2018");
///*
var certStore = new X509Store(StoreName.My, StoreLocation.CurrentUser);
certStore.Open(OpenFlags.ReadOnly);
var certCollection = certStore.Certificates.Find(X509FindType.FindByThumbprint, _settings.PublicCertAzureThumbprint, false);
X509Certificate2 vendorTrustedCert;
if (certCollection.Count == 0)
{
throw new AuthenticationException("Could not GET public key from PublicCertAzureThumbprint");
}
// Should only have 1 cert with that thumbprint
vendorTrustedCert = certCollection[0];
return vendorTrustedCert;
//*/
}
public async Task<AuthTokenResponse> AuthenticateOnlineBankingAccount(OnlineBankingFormData data)
{
var onlineBankingData = _serivce.PopulateOnlineBankingDataFromConfig(data);
return await _serivce.LoginUser(onlineBankingData);
}
}
}
@@ -0,0 +1,10 @@
using Landmark.OLA.Models.OnlineBanking;
namespace OnlineBankingAuth.Services
{
public interface IDigitalInsightService
{
Task<AuthTokenResponse> LoginUser(OnlineBankingFormData data);
OnlineBankingFormData PopulateOnlineBankingDataFromConfig(OnlineBankingFormData data);
}
}
@@ -0,0 +1,16 @@
using System.Xml;
using Landmark.OLA.Models.OnlineBanking;
namespace OnlineBankingAuth.Services
{
public interface IDigitalInsightVendorClient
{
bool ValidateDIPublicKey(XmlElement x509);
DISAMLAssertion GetSAMLPayload(XmlDocument xmlDocument);
bool ValidateTimeStamp(DISAMLAssertion assertion);
string GetSuccessRedirectUrl();
string GetFailureRedirectUrl(string message);
Task<AuthTokenResponse> AuthenticateOnlineBankingAccount(OnlineBankingFormData data);
};
}
@@ -0,0 +1,82 @@
using System.Security.Cryptography;
using System.Text;
namespace Landmark.OLA.Application.Common.Security
{
public static class CookieCryptor
{
private static byte[] RandomBytes(int len)
{
var b = new byte[len];
RandomNumberGenerator.Fill(b);
return b;
}
private static byte[] DeriveKey(string pass, byte[] salt, int keyBytes = 32, int iterations = 100_000)
=> new Rfc2898DeriveBytes(pass, salt, iterations, HashAlgorithmName.SHA256).GetBytes(keyBytes);
public static string EncryptJwt(string jwt, string key)
{
var salt = RandomBytes(32);
using var aes = Aes.Create();
aes.KeySize = 256;
aes.BlockSize = 128;
aes.Mode = CipherMode.CBC;
aes.Padding = PaddingMode.PKCS7;
aes.GenerateIV();
aes.Key = DeriveKey(key, salt, 32);
using var ms = new MemoryStream();
ms.Write(salt, 0, salt.Length);
ms.Write(aes.IV, 0, aes.IV.Length);
using (var crypto = new CryptoStream(ms, aes.CreateEncryptor(), CryptoStreamMode.Write))
using (var writer = new StreamWriter(crypto, Encoding.UTF8))
{
writer.Write(jwt);
writer.Flush();
crypto.FlushFinalBlock();
}
return Convert.ToBase64String(ms.ToArray());
}
public static string DecryptJwt(string cipherText, string passPhrase)
{
var all = Convert.FromBase64String(cipherText);
var saltLen = 32;
var ivLen = 16;
if (all.Length < saltLen + ivLen)
throw new CryptographicException("Ciphertext too short.");
var salt = new byte[saltLen];
Buffer.BlockCopy(all, 0, salt, 0, saltLen);
var iv = new byte[ivLen];
Buffer.BlockCopy(all, saltLen, iv, 0, ivLen);
var cipher = new byte[all.Length - saltLen - ivLen];
Buffer.BlockCopy(all, saltLen + ivLen, cipher, 0, cipher.Length);
using var aes = Aes.Create();
aes.KeySize = 256;
aes.BlockSize = 128;
aes.Mode = CipherMode.CBC;
aes.Padding = PaddingMode.PKCS7;
aes.Key = DeriveKey(passPhrase, salt, 32);
aes.IV = iv;
using var ms = new MemoryStream(cipher);
using var crypto = new CryptoStream(ms, aes.CreateDecryptor(), CryptoStreamMode.Read);
using var reader = new StreamReader(crypto, Encoding.UTF8);
return reader.ReadToEnd();
}
}
}
@@ -0,0 +1,16 @@
using Landmark.OLA.Models.Core;
using Landmark.OLA.Models.OnlineBanking;
using Microsoft.AspNetCore.Http;
namespace OnlineBankingAuth.Tokens
{
public interface ITokenService
{
string CreateOnlineBankingJWT(OnlineBankingToken payload);
string RenewOnlineBankingJWT(HttpContext context);
string RenewAuthorization(HttpContext context);
OnlineBankingToken GetAuthenticatedDataFromCookie(HttpContext context);
OnlineBankingToken GetAuthenticatedUserDataFromJwt(string jwt);
string CreateAuthMetaToken(MemberMeta meta);
}
}
@@ -0,0 +1,168 @@
using System.Globalization;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Authentication;
using System.Security.Claims;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
using Newtonsoft.Json;
using Landmark.OLA.Application.Common.Security;
using Landmark.OLA.Models.Core;
using Landmark.OLA.Models.OnlineBanking;
using OnlineBankingAuth.Configuration;
namespace OnlineBankingAuth.Tokens
{
public class TokenService : ITokenService
{
private readonly SecuritySettings _settings;
public TokenService(
IOptions<SecuritySettings> settings)
{
_settings = settings.Value;
}
public string CreateOnlineBankingJWT(OnlineBankingToken payload)
{
var handler = new JwtSecurityTokenHandler { MapInboundClaims = false };
var claims = new List<Claim>
{
new(Constants.MemberID, payload.MemberID ?? string.Empty),
new(Constants.IndividualID, payload.IndividualID ?? string.Empty),
new(Constants.FI_ID, payload.FI_ID ?? string.Empty),
new(Constants.InProgressLoanId, payload.LoanId ?? string.Empty),
new(Constants.IsTwoFactor, payload.IsTwoFactor.ToString())
};
var jwt = new JwtSecurityToken(
claims: claims,
expires: DateTime.UtcNow.AddMinutes(15)
);
var token = handler.WriteToken(jwt);
var encrypted = CookieCryptor.EncryptJwt(token, _settings.TokenSecret);
return Constants.TokenPrefix + encrypted;
}
public string RenewOnlineBankingJWT(HttpContext context)
{
var cookies = context.Request.Cookies;
string cookieValue = null;
if (cookies[Constants.TokenPayload] != null)
{
var jwtCookie = cookies[Constants.TokenPayload][Constants.TokenPrefix.Length..];
var jwt = CookieCryptor.DecryptJwt(jwtCookie, _settings.TokenSecret);
if (!IsTokenReadable(jwt))
{
throw new AuthenticationException("Could not parse token.");
}
var prevToken = ReadJwtSecurityToken(jwt);
cookieValue = Constants.TokenPrefix + GenerateEncodedToken(prevToken);
}
return cookieValue;
}
public string RenewAuthorization(HttpContext context)
{
var cookies = context.Request.Cookies;
string cookieValue = null;
if (cookies[Constants.TokenAuthorization] != null)
{
cookieValue = cookies[Constants.TokenAuthorization];
}
return cookieValue;
}
public OnlineBankingToken GetAuthenticatedDataFromCookie(HttpContext context)
{
var cookies = context.Request.Cookies;
if (!string.IsNullOrEmpty(cookies[Constants.TokenPayload]))
{
return GetAuthenticatedUserDataFromJwt(cookies[Constants.TokenPayload][Constants.TokenPrefix.Length..]);
}
return null;
}
private string GenerateEncodedToken(JwtSecurityToken jwtToken)
{
var tokenHandler = new JwtSecurityTokenHandler { };
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(jwtToken.Claims),
Expires = DateTime.UtcNow.AddMinutes(15)
};
var token = tokenHandler.CreateEncodedJwt(tokenDescriptor);
var encryptedToken = CookieCryptor.EncryptJwt(token, _settings.TokenSecret);
return encryptedToken;
}
private static bool TryGet(JwtSecurityToken jwt, string key, out string? value)
{
var claim = jwt.Claims.FirstOrDefault(c => c.Type == key)
?? jwt.Claims.FirstOrDefault(c => string.Equals(c.Type, key, StringComparison.OrdinalIgnoreCase));
if (claim is not null) { value = claim.Value; return true; }
foreach (var kv in jwt.Payload)
if (string.Equals(kv.Key, key, StringComparison.OrdinalIgnoreCase))
{
value = Convert.ToString(kv.Value, CultureInfo.InvariantCulture);
return true;
}
value = null;
return false;
}
public OnlineBankingToken GetAuthenticatedUserDataFromJwt(string encryptedJwt)
{
var jwt = CookieCryptor.DecryptJwt(encryptedJwt, _settings.TokenSecret);
var token = ReadJwtSecurityToken(jwt);
var data = new OnlineBankingToken();
if (!TryGet(token, Constants.MemberID, out var memberId)) return null;
if (!TryGet(token, Constants.FI_ID, out var fiId)) return null;
data.MemberID = memberId!;
data.FI_ID = fiId!;
if (TryGet(token, Constants.IsTwoFactor, out var twoFa))
data.IsTwoFactor = bool.TryParse(twoFa, out var b) && b;
if (TryGet(token, Constants.IndividualID, out var indId))
data.IndividualID = indId;
return data;
}
private static JwtSecurityToken ReadJwtSecurityToken(string jwt)
{
return new JwtSecurityTokenHandler().ReadJwtToken(jwt);
}
private static bool IsTokenReadable(string jwt)
{
return new JwtSecurityTokenHandler().CanReadToken(jwt);
}
public string CreateAuthMetaToken(MemberMeta meta)
{
var json = JsonConvert.SerializeObject(meta);
var plainTextBytes = System.Text.Encoding.UTF8.GetBytes(json);
return Convert.ToBase64String(plainTextBytes);
}
}
}