first commit
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+143
@@ -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);
|
||||
}
|
||||
}
|
||||
+16
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user