From 639fc9f59490d60d0390425ac82ccc9b5a6245f6 Mon Sep 17 00:00:00 2001 From: Ahson Khan Date: Sat, 14 Sep 2024 11:12:53 -0700 Subject: [PATCH] Add support for passing in the x509 certificate and its corresponding private key directly to ClientCertificateCredential, rather than reading from a pem file. (#5989) * Add support for passing in the x509 certificate and its corresponding private key directly to , rather than reading from a pem file. * Move the x509 and pkey objects * Add basic test. * Fix doc comment due to merge. * Fix merge, add back the bool * Pass in bool in other locations * Fix finding pem cert content from memory and add tests with send chain true. * Use d2i_PrivateKey_bio instead of PEM_read_bio_PrivateKey since the private key isn't in pem format. * Fix doc comments to match the type name rather than copy/paste typo from client secret cred. * Make options optional and add invalid content tests. * Disable cspell in some places within tests. * Make exception message consistent between platforms when reading a file. --- sdk/identity/azure-identity/CHANGELOG.md | 1 + .../client_certificate_credential.hpp | 32 +- .../src/client_certificate_credential.cpp | 303 ++++++--- .../ut/client_certificate_credential_test.cpp | 585 ++++++++++++++---- 4 files changed, 721 insertions(+), 200 deletions(-) diff --git a/sdk/identity/azure-identity/CHANGELOG.md b/sdk/identity/azure-identity/CHANGELOG.md index afbc61f42..dd58b8da8 100644 --- a/sdk/identity/azure-identity/CHANGELOG.md +++ b/sdk/identity/azure-identity/CHANGELOG.md @@ -5,6 +5,7 @@ ### Features Added - Added support for providing an object ID to `ManagedIdentityCredential`. +- Added support for passing in the x509 certificate and its corresponding private key directly to `ClientCertificateCredential`, rather than reading from a pem file. - Added support for sending an x5c parameter in `ClientCertificateCredential`. ### Breaking Changes diff --git a/sdk/identity/azure-identity/inc/azure/identity/client_certificate_credential.hpp b/sdk/identity/azure-identity/inc/azure/identity/client_certificate_credential.hpp index 54d680964..e7ac78027 100644 --- a/sdk/identity/azure-identity/inc/azure/identity/client_certificate_credential.hpp +++ b/sdk/identity/azure-identity/inc/azure/identity/client_certificate_credential.hpp @@ -96,9 +96,19 @@ namespace Azure { namespace Identity { bool sendCertificateChain, Core::Credentials::TokenCredentialOptions const& options); + explicit ClientCertificateCredential( + std::string tenantId, + std::string const& clientId, + std::vector clientCertificate, + std::vector privateKey, + std::string const& authorityHost, + std::vector additionallyAllowedTenants, + bool sendCertificateChain, + Core::Credentials::TokenCredentialOptions const& options); + public: /** - * @brief Constructs a Client Secret Credential. + * @brief Constructs a Client Certificate Credential. * * @param tenantId Tenant ID. * @param clientId Client ID. @@ -114,7 +124,25 @@ namespace Azure { namespace Identity { = Core::Credentials::TokenCredentialOptions()); /** - * @brief Constructs a Client Secret Credential. + * @brief Constructs a Client Certificate Credential. + * + * @param tenantId Tenant ID. + * @param clientId Client ID. + * @param clientCertificate The x509 certificate which is used for signing, in base64 string + * format, including the begin and end headers. + * @param privateKey The binary representation of the corresponding RSA private key of the + * certificate. + * @param options Options for token retrieval. + */ + explicit ClientCertificateCredential( + std::string tenantId, + std::string const& clientId, + std::vector clientCertificate, + std::vector privateKey, + ClientCertificateCredentialOptions const& options = {}); + + /** + * @brief Constructs a Client Certificate Credential. * * @param tenantId Tenant ID. * @param clientId Client ID. diff --git a/sdk/identity/azure-identity/src/client_certificate_credential.cpp b/sdk/identity/azure-identity/src/client_certificate_credential.cpp index 19d94bb05..985c3fdc4 100644 --- a/sdk/identity/azure-identity/src/client_certificate_credential.cpp +++ b/sdk/identity/azure-identity/src/client_certificate_credential.cpp @@ -77,11 +77,21 @@ template std::vector ToUInt8Vector(T const& in) return outVec; } -std::string FindPemCertificateContent(std::string const& path) +std::string FindPemCertificateContent( + std::string const& path, + std::vector clientCertificate) { - auto pemContent{FileBodyStream(path).ReadToEnd()}; - std::string pem{pemContent.begin(), pemContent.end()}; - pemContent = {}; + std::string pem{}; + if (clientCertificate.empty()) + { + auto pemContent{FileBodyStream(path).ReadToEnd()}; + pem = std::string{pemContent.begin(), pemContent.end()}; + pemContent = {}; + } + else + { + pem = std::string{clientCertificate.begin(), clientCertificate.end()}; + } const std::string beginHeader = std::string("-----BEGIN CERTIFICATE-----"); auto headerStart = pem.find(beginHeader); @@ -113,6 +123,50 @@ using CertificateThumbprint = std::vector; using UniquePrivateKey = Azure::Identity::_detail::UniquePrivateKey; using PrivateKey = decltype(std::declval().get()); +std::string GetJwtToken( + CertificateThumbprint mdVec, + bool sendCertificateChain, + std::string const& clientCertificatePath, + std::vector clientCertificate = {}) +{ + std::string thumbprintHexStr; + std::string thumbprintBase64Str; + + // Get thumbprint as hex string: + { + std::ostringstream thumbprintStream; + for (const auto md : mdVec) + { + thumbprintStream << std::uppercase << std::hex << std::setfill('0') << std::setw(2) + << static_cast(md); + } + thumbprintHexStr = thumbprintStream.str(); + } + + // Get thumbprint as Base64: + thumbprintBase64Str = Base64Url::Base64UrlEncode(ToUInt8Vector(mdVec)); + + std::string x5cHeaderParam{}; + if (sendCertificateChain) + { + // Since there is only one base64 encoded cert string, it can be written as a JSON string rather + // than a JSON array of strings. + x5cHeaderParam = ",\"x5c\":\""; + std::string certContent = FindPemCertificateContent(clientCertificatePath, clientCertificate); + x5cHeaderParam += certContent; + x5cHeaderParam += "\""; + } + + // Form a JWT token: + const auto tokenHeader = std::string("{\"x5t\":\"") + thumbprintBase64Str + "\",\"kid\":\"" + + thumbprintHexStr + "\",\"alg\":\"RS256\",\"typ\":\"JWT\"" + x5cHeaderParam + "}"; + + const auto tokenHeaderVec + = std::vector(tokenHeader.begin(), tokenHeader.end()); + + return Base64Url::Base64UrlEncode(ToUInt8Vector(tokenHeaderVec)); +} + #if defined(AZ_PLATFORM_WINDOWS) && (!defined(WINAPI_PARTITION_DESKTOP) || WINAPI_PARTITION_DESKTOP) enum PrivateKeyType { @@ -143,6 +197,18 @@ CertificateThumbprint GetThumbprint(PCCERT_CONTEXT cert) return thumbprint; } +wil::unique_cert_context ImportPemCertificate(std::vector clientCertificate) +{ + std::string certStr(clientCertificate.begin(), clientCertificate.end()); + auto certBuffer = PemToBinary(certStr.c_str(), static_cast(certStr.size())); + auto cert = CertCreateCertificateContext( + X509_ASN_ENCODING | PKCS_7_ASN_ENCODING, + certBuffer.data(), + static_cast(certBuffer.size())); + THROW_LAST_ERROR_IF_NULL(cert); + return wil::unique_cert_context{cert}; +} + wil::unique_cert_context ImportPemCertificate(std::string const& pem) { auto headerStart = pem.find("-----BEGIN CERTIFICATE-----"); @@ -261,6 +327,18 @@ UniquePrivateKey ImportPemPrivateKey(std::string const& pem) throw AuthenticationException("Invalid private key."); } +std::tuple ReadPemCertificate( + std::vector clientCertificate, + std::vector privateKey) +{ + auto certContext = ImportPemCertificate(clientCertificate); + + // We only support the RSA private key type. + return std::make_tuple( + GetThumbprint(certContext.get()), + ImportRsaPrivateKey(privateKey.data(), static_cast(privateKey.size()))); +} + std::tuple ReadPemCertificate(std::string const& path) { auto pemContent{FileBodyStream(path).ReadToEnd()}; @@ -327,13 +405,76 @@ template <> struct UniqueHandleHelper template using UniqueHandle = Azure::Core::_internal::UniqueHandle; +std::tuple GetThumbprintAndKey( + UniqueHandle x509, + UniquePrivateKey pkey) +{ + CertificateThumbprint thumbprint(EVP_MAX_MD_SIZE); + // Get certificate thumbprint: + unsigned int mdLen = 0; + const auto digestResult = X509_digest(x509.get(), EVP_sha1(), thumbprint.data(), &mdLen); + + if (!digestResult) + { + throw AuthenticationException("Failed to get certificate thumbprint."); + } + + // Drop unused buffer space: + const auto mdLenSz = static_cast(mdLen); + if (thumbprint.size() > mdLenSz) + { + thumbprint.resize(mdLenSz); + } + + return std::make_tuple(thumbprint, std::move(pkey)); +} + +std::tuple ReadPemCertificate( + std::vector clientCertificate, + std::vector privateKey) +{ + // Create a BIO from the private key vector data in memory. + UniqueHandle bioKey(BIO_new_mem_buf(privateKey.data(), static_cast(privateKey.size()))); + if (!bioKey) + { + throw AuthenticationException("Failed to create BIO for the binary private key."); + } + + UniquePrivateKey pkey{d2i_PrivateKey_bio(bioKey.get(), nullptr)}; + if (!pkey) + { + throw AuthenticationException("Failed to read the binary private key for the certificate."); + } + + // Create a BIO from the client certificate vector data in memory. + UniqueHandle bioCert( + BIO_new_mem_buf(clientCertificate.data(), static_cast(clientCertificate.size()))); + if (!bioCert) + { + throw AuthenticationException("Failed to create BIO for the client certificate."); + } + + UniqueHandle x509{PEM_read_bio_X509(bioCert.get(), nullptr, nullptr, nullptr)}; + if (!x509) + { + std::ignore = BIO_seek(bioCert.get(), 0); + x509.reset(PEM_read_bio_X509(bioCert.get(), nullptr, nullptr, nullptr)); + if (!x509) + { + throw AuthenticationException("Failed to read X509 section."); + } + } + + return GetThumbprintAndKey(std::move(x509), std::move(pkey)); +} + std::tuple ReadPemCertificate(const std::string& path) { // Open certificate file, then get private key and X509: UniqueHandle bio(BIO_new_file(path.c_str(), "r")); if (!bio) { - throw AuthenticationException("Failed to open certificate file."); + throw AuthenticationException("Failed to open file for reading. File name: '" + path + "'"); } UniquePrivateKey pkey{PEM_read_bio_PrivateKey(bio.get(), nullptr, nullptr, nullptr)}; @@ -353,24 +494,7 @@ std::tuple ReadPemCertificate(const std } } - CertificateThumbprint thumbprint(EVP_MAX_MD_SIZE); - // Get certificate thumbprint: - unsigned int mdLen = 0; - const auto digestResult = X509_digest(x509.get(), EVP_sha1(), thumbprint.data(), &mdLen); - - if (!digestResult) - { - throw AuthenticationException("Failed to get certificate thumbprint."); - } - - // Drop unused buffer space: - const auto mdLenSz = static_cast(mdLen); - if (thumbprint.size() > mdLenSz) - { - thumbprint.resize(mdLenSz); - } - - return std::make_tuple(thumbprint, std::move(pkey)); + return GetThumbprintAndKey(std::move(x509), std::move(pkey)); } std::vector SignPkcs1Sha256(PrivateKey key, const uint8_t* data, size_t size) @@ -430,76 +554,77 @@ ClientCertificateCredential::ClientCertificateCredential( m_tokenPayloadStaticPart( "\",\"iss\":\"" + clientId + "\",\"sub\":\"" + clientId + "\",\"jti\":\"") { - std::string thumbprintHexStr; - std::string thumbprintBase64Str; - + CertificateThumbprint mdVec; + try { - CertificateThumbprint mdVec; - try + if (clientCertificatePath.empty()) { - if (clientCertificatePath.empty()) - { - throw AuthenticationException("Certificate file path is empty."); - } - - using Azure::Core::_internal::StringExtensions; - std::string const PemExtension = ".pem"; - auto const certFileExtensionStart = clientCertificatePath.find_last_of('.'); - auto const certFileExtension = certFileExtensionStart != std::string::npos - ? clientCertificatePath.substr(certFileExtensionStart) - : std::string{}; - - if (!StringExtensions::LocaleInvariantCaseInsensitiveEqual(certFileExtension, PemExtension)) - { - throw AuthenticationException( - "Certificate format" - + (certFileExtension.empty() ? " " : " ('" + certFileExtension + "') ") - + "is not supported. Please convert your certificate to '" + PemExtension + "'."); - } - - std::tie(mdVec, m_pkey) = ReadPemCertificate(clientCertificatePath); + throw AuthenticationException("Certificate file path is empty."); } - catch (std::exception const& e) + + using Azure::Core::_internal::StringExtensions; + std::string const PemExtension = ".pem"; + auto const certFileExtensionStart = clientCertificatePath.find_last_of('.'); + auto const certFileExtension = certFileExtensionStart != std::string::npos + ? clientCertificatePath.substr(certFileExtensionStart) + : std::string{}; + + if (!StringExtensions::LocaleInvariantCaseInsensitiveEqual(certFileExtension, PemExtension)) { - // WIL does not throw AuthenticationException. throw AuthenticationException( - std::string("Identity: ClientCertificateCredential: ") + e.what()); + "Certificate format" + + (certFileExtension.empty() ? " " : " ('" + certFileExtension + "') ") + + "is not supported. Please convert your certificate to '" + PemExtension + "'."); } - // Get thumbprint as hex string: - { - std::ostringstream thumbprintStream; - for (const auto md : mdVec) - { - thumbprintStream << std::uppercase << std::hex << std::setfill('0') << std::setw(2) - << static_cast(md); - } - thumbprintHexStr = thumbprintStream.str(); - } - - // Get thumbprint as Base64: - thumbprintBase64Str = Base64Url::Base64UrlEncode(ToUInt8Vector(mdVec)); + std::tie(mdVec, m_pkey) = ReadPemCertificate(clientCertificatePath); } - - std::string x5cHeaderParam{}; - if (sendCertificateChain) + catch (std::exception const& e) { - // Since there is only one base64 encoded cert string, it can be written as a JSON string rather - // than a JSON array of strings. - x5cHeaderParam = ",\"x5c\":\""; - std::string certContent = FindPemCertificateContent(clientCertificatePath); - x5cHeaderParam += certContent; - x5cHeaderParam += "\""; + // WIL does not throw AuthenticationException. + throw AuthenticationException( + std::string("Identity: ClientCertificateCredential: ") + e.what()); } - // Form a JWT token: - const auto tokenHeader = std::string("{\"x5t\":\"") + thumbprintBase64Str + "\",\"kid\":\"" - + thumbprintHexStr + "\",\"alg\":\"RS256\",\"typ\":\"JWT\"" + x5cHeaderParam + "}"; + m_tokenHeaderEncoded = GetJwtToken(mdVec, sendCertificateChain, clientCertificatePath); +} - const auto tokenHeaderVec - = std::vector(tokenHeader.begin(), tokenHeader.end()); +ClientCertificateCredential::ClientCertificateCredential( + std::string tenantId, + std::string const& clientId, + std::vector clientCertificate, + std::vector privateKey, + std::string const& authorityHost, + std::vector additionallyAllowedTenants, + bool sendCertificateChain, + Core::Credentials::TokenCredentialOptions const& options) + : TokenCredential("ClientCertificateCredential"), + m_clientCredentialCore(tenantId, authorityHost, additionallyAllowedTenants), + m_tokenCredentialImpl(std::make_unique(options)), + m_requestBody( + std::string( + "grant_type=client_credentials" + "&client_assertion_type=" + "urn%3Aietf%3Aparams%3Aoauth%3Aclient-assertion-type%3Ajwt-bearer" // cspell:disable-line + "&client_id=") + + Url::Encode(clientId)), + m_tokenPayloadStaticPart( + "\",\"iss\":\"" + clientId + "\",\"sub\":\"" + clientId + "\",\"jti\":\"") +{ - m_tokenHeaderEncoded = Base64Url::Base64UrlEncode(ToUInt8Vector(tokenHeaderVec)); + CertificateThumbprint mdVec; + try + { + std::tie(mdVec, m_pkey) = ReadPemCertificate(clientCertificate, privateKey); + } + catch (std::exception const& e) + { + // WIL does not throw AuthenticationException. + throw AuthenticationException( + std::string("Identity: ClientCertificateCredential: ") + e.what()); + } + + m_tokenHeaderEncoded = GetJwtToken(mdVec, sendCertificateChain, {}, clientCertificate); } ClientCertificateCredential::ClientCertificateCredential( @@ -534,6 +659,24 @@ ClientCertificateCredential::ClientCertificateCredential( { } +ClientCertificateCredential::ClientCertificateCredential( + std::string tenantId, + std::string const& clientId, + std::vector clientCertificate, + std::vector privateKey, + ClientCertificateCredentialOptions const& options) + : ClientCertificateCredential( + tenantId, + clientId, + clientCertificate, + privateKey, + options.AuthorityHost, + options.AdditionallyAllowedTenants, + options.SendCertificateChain, + options) +{ +} + ClientCertificateCredential::~ClientCertificateCredential() = default; AccessToken ClientCertificateCredential::GetToken( diff --git a/sdk/identity/azure-identity/test/ut/client_certificate_credential_test.cpp b/sdk/identity/azure-identity/test/ut/client_certificate_credential_test.cpp index 3dc93dc52..ac79b7241 100644 --- a/sdk/identity/azure-identity/test/ut/client_certificate_credential_test.cpp +++ b/sdk/identity/azure-identity/test/ut/client_certificate_credential_test.cpp @@ -36,17 +36,175 @@ struct TempCertFile final static const char* const Path; ~TempCertFile(); TempCertFile(CertFormat algorithm = RsaPkcs); + TempCertFile(std::string content); }; std::vector SplitString(const std::string& s, char separator); std::string ToString(std::vector const& vec); + +// cspell:disable +std::string ExampleValidCertString + = "-----BEGIN CERTIFICATE-----\n" + "MIIDODCCAiCgAwIBAgIQNqa9U3MBxqBF7ksWk+XRkzANBgkqhkiG9w0BAQsFADAe\n" + "MRwwGgYDVQQDDBNhenVyZS1pZGVudGl0eS10ZXN0MCAXDTIyMDQyMjE1MDYwNloY\n" + "DzIyMjIwMTAxMDcwMDAwWjAeMRwwGgYDVQQDDBNhenVyZS1pZGVudGl0eS10ZXN0\n" + "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAz3ZuKbpDu7oBMfMF65qO\n" + "FSBKInKe8N0LBCRgNmzMfZxzL8LoBueLdeEKX6gUGEFi3i9R5qXA3or1Q/teWV3h\n" + "iwj1WQR4aGPGVhom34QAM6kND/QmtZMnY7weLiXBJxf0WLUL+p+jsJnTtcCdtiTX\n" + "EZTLWapp2/0NCJ9n41xG3ZfOfxmZWMzEEXcnyNMq4kkQXGFdpINM3lwsX5grwd62\n" + "+iNSqaFBR5ZHh7ZHg8JtFR1BLeB8/IIXAdNLSOXktnx9qz5CDUCnOvtEFAtiiAkA\n" + "vhsybGA28EDmqOVYZPNB+S0bjPTXc7/n1N5S55LWAoF4C/QF+C/0fWeD87bmqP6m\n" + "0QIDAQABo3AwbjAOBgNVHQ8BAf8EBAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwIG\n" + "CCsGAQUFBwMBMB4GA1UdEQQXMBWCE2F6dXJlLWlkZW50aXR5LXRlc3QwHQYDVR0O\n" + "BBYEFCoJ5tInmafyNuR0tGxZOz522jlWMA0GCSqGSIb3DQEBCwUAA4IBAQBzLXpw\n" + "Xmrg1sQTmzMnS24mREKxj9B3YILmgsdBMrHkH07QUROee7IbQ8gfBKeln0dEcfYi\n" + "Jyh42jn+fmg9AR17RP80wPthD2eKOt4WYNkNM3H8U4JEo+0ML0jZyswynpR48h/E\n" + "m96sm/NUeKUViD5iVTb1uHL4j8mQAN1IbXcunXvrrek1CzFVn5Rpah0Tn+6cYVKd\n" + "Jg531i53udzusgZtV1NPZ82tzYkPQG1vxB//D9vd0LzmcfCvT50MKhz0r/c5yJYk\n" + "i9q94DBuzMhe+O9j+Ob2pVQt5akVFJVtIVSfBZzRBAd66u9JeADlT4sxwS4QAUHi\n" + "RrCsEpJsnJXkx/6O\n" + "-----END CERTIFICATE-----\n"; + +std::string ExampleValidPrivateKeyString + = "-----BEGIN RSA PRIVATE KEY-----\n" + "MIIEpAIBAAKCAQEAz3ZuKbpDu7oBMfMF65qOFSBKInKe8N0LBCRgNmzMfZxzL8Lo\n" + "BueLdeEKX6gUGEFi3i9R5qXA3or1Q/teWV3hiwj1WQR4aGPGVhom34QAM6kND/Qm\n" + "tZMnY7weLiXBJxf0WLUL+p+jsJnTtcCdtiTXEZTLWapp2/0NCJ9n41xG3ZfOfxmZ\n" + "WMzEEXcnyNMq4kkQXGFdpINM3lwsX5grwd62+iNSqaFBR5ZHh7ZHg8JtFR1BLeB8\n" + "/IIXAdNLSOXktnx9qz5CDUCnOvtEFAtiiAkAvhsybGA28EDmqOVYZPNB+S0bjPTX\n" + "c7/n1N5S55LWAoF4C/QF+C/0fWeD87bmqP6m0QIDAQABAoIBAQDEGSK6KIk7me7l\n" + "QtyWvemNSI8qjoN0EswF50hWSXLlTIuIWsgtNpIZI1VF477SyoNklv/ob0amVFzP\n" + "HHwrJtU5MYeP0+zoZ18jJecWoVP7gNCLAvHP8b9qw3cXkbJIfJkHfGJNTLZSCKUY\n" + "CHBKqfnscWPhZnZXbZLzUpHFVATcEJ14vqFj4RNoLqNoNQT5NoGxdPtxb0q0PEMB\n" + "h4PrkCcK0KSfkgfU8rkBWrhkef8Eqh/d3BR+WAv/r+SO6lumUHtH+6xCkA8mxlc5\n" + "AZSichglWJj5+12v8Ca4sLPhWSHx8395tJCYoMSXfx8E65ykoPh/KAYJ4O5WS3QW\n" + "FhzBiYQNAoGBAOPJqFu7M3oL3y7lBWtLB38irjcrzr+1rneLGtJcHSjx0vmrcC+k\n" + "zVFggBpKJmAAxHt6omIDFw1/VN4ZVus5LWBY9N7Z0YOIgY6fJ3ISwVS391neUz0c\n" + "NVSruGVuN8vAUYWFlft2eLNZ8jBAwDRWykZi+ywwdOaFh3STIxSvy+mHAoGBAOko\n" + "VeL9kUIl85Fuhh0gWQyFRwnlsLyJXTpRHxu8M2VuHvMDQ4X0jLV8ia832xMlwbVS\n" + "qBEnT+jZ5vVu37XMp1veuUveEx7su/qH7x6OiQJvIP9Ll+9MGdui1PKoZCTE1prD\n" + "jQTSi8FM5BU+1RrHWgZYmptUS743k1EXUIJ37SLnAoGBAOBWGpk9JNVuG7/zjgK9\n" + "QgTUAwATBOuJ4umY9jF2xsEsaLu7PCGwDQW4JHG/1Ut3dgqmHIaqxGlmng6ephvD\n" + "lAzvjzprCwyfw/jSheay0fS9ub2oWBI3Vc6t0E0U356rKZ52kd+2Lel1DDC5lJH3\n" + "Z/8qPHSoxHjDyUPmJQaanBjBAoGAWa5iGsVdsgvW/AF/JITku6QoBu6KZHqRmXTK\n" + "emiRfFo3HVIMDuJZnRUiAHuDkIHdWFlKvA5a9j2aUJ0s/0iQtw2cSEpLIIH+bAcN\n" + "Oruoh38nOgthjXHAIHMpZYzPuDTeNvkwrMIvb1KcCG/6mCpFvlsmXMi3uZq212IY\n" + "XZazZ9ECgYA3vGkRvjDklE014wFbLGw2NFLPeNxTfdagZmoDag8qMygAKg6Cr3Uc\n" + "TNCJSA5zqbY+AH26SdSU4TTiQ2AaVPgM6PFKHnQDYJ3bWdp9dUUo5pUOkxP1hpbI\n" + "qxxMaq+sv5e9c56EJtctxNnAK27JsoadD+b+NjysZgMeKUdBIzSrHQ==\n" + "-----END RSA PRIVATE KEY-----\n"; +// cspell:enable + +std::vector ExampleValidPrivateKey{ + 48, 130, 4, 164, 2, 1, 0, 2, 130, 1, 1, 0, 207, 118, 110, 41, 186, 67, 187, + 186, 1, 49, 243, 5, 235, 154, 142, 21, 32, 74, 34, 114, 158, 240, 221, 11, 4, 36, + 96, 54, 108, 204, 125, 156, 115, 47, 194, 232, 6, 231, 139, 117, 225, 10, 95, 168, 20, + 24, 65, 98, 222, 47, 81, 230, 165, 192, 222, 138, 245, 67, 251, 94, 89, 93, 225, 139, + 8, 245, 89, 4, 120, 104, 99, 198, 86, 26, 38, 223, 132, 0, 51, 169, 13, 15, 244, + 38, 181, 147, 39, 99, 188, 30, 46, 37, 193, 39, 23, 244, 88, 181, 11, 250, 159, 163, + 176, 153, 211, 181, 192, 157, 182, 36, 215, 17, 148, 203, 89, 170, 105, 219, 253, 13, 8, + 159, 103, 227, 92, 70, 221, 151, 206, 127, 25, 153, 88, 204, 196, 17, 119, 39, 200, 211, + 42, 226, 73, 16, 92, 97, 93, 164, 131, 76, 222, 92, 44, 95, 152, 43, 193, 222, 182, + 250, 35, 82, 169, 161, 65, 71, 150, 71, 135, 182, 71, 131, 194, 109, 21, 29, 65, 45, + 224, 124, 252, 130, 23, 1, 211, 75, 72, 229, 228, 182, 124, 125, 171, 62, 66, 13, 64, + 167, 58, 251, 68, 20, 11, 98, 136, 9, 0, 190, 27, 50, 108, 96, 54, 240, 64, 230, + 168, 229, 88, 100, 243, 65, 249, 45, 27, 140, 244, 215, 115, 191, 231, 212, 222, 82, 231, + 146, 214, 2, 129, 120, 11, 244, 5, 248, 47, 244, 125, 103, 131, 243, 182, 230, 168, 254, + 166, 209, 2, 3, 1, 0, 1, 2, 130, 1, 1, 0, 196, 25, 34, 186, 40, 137, 59, + 153, 238, 229, 66, 220, 150, 189, 233, 141, 72, 143, 42, 142, 131, 116, 18, 204, 5, 231, + 72, 86, 73, 114, 229, 76, 139, 136, 90, 200, 45, 54, 146, 25, 35, 85, 69, 227, 190, + 210, 202, 131, 100, 150, 255, 232, 111, 70, 166, 84, 92, 207, 28, 124, 43, 38, 213, 57, + 49, 135, 143, 211, 236, 232, 103, 95, 35, 37, 231, 22, 161, 83, 251, 128, 208, 139, 2, + 241, 207, 241, 191, 106, 195, 119, 23, 145, 178, 72, 124, 153, 7, 124, 98, 77, 76, 182, + 82, 8, 165, 24, 8, 112, 74, 169, 249, 236, 113, 99, 225, 102, 118, 87, 109, 146, 243, + 82, 145, 197, 84, 4, 220, 16, 157, 120, 190, 161, 99, 225, 19, 104, 46, 163, 104, 53, + 4, 249, 54, 129, 177, 116, 251, 113, 111, 74, 180, 60, 67, 1, 135, 131, 235, 144, 39, + 10, 208, 164, 159, 146, 7, 212, 242, 185, 1, 90, 184, 100, 121, 255, 4, 170, 31, 221, + 220, 20, 126, 88, 11, 255, 175, 228, 142, 234, 91, 166, 80, 123, 71, 251, 172, 66, 144, + 15, 38, 198, 87, 57, 1, 148, 162, 114, 24, 37, 88, 152, 249, 251, 93, 175, 240, 38, + 184, 176, 179, 225, 89, 33, 241, 243, 127, 121, 180, 144, 152, 160, 196, 151, 127, 31, 4, + 235, 156, 164, 160, 248, 127, 40, 6, 9, 224, 238, 86, 75, 116, 22, 22, 28, 193, 137, + 132, 13, 2, 129, 129, 0, 227, 201, 168, 91, 187, 51, 122, 11, 223, 46, 229, 5, 107, + 75, 7, 127, 34, 174, 55, 43, 206, 191, 181, 174, 119, 139, 26, 210, 92, 29, 40, 241, + 210, 249, 171, 112, 47, 164, 205, 81, 96, 128, 26, 74, 38, 96, 0, 196, 123, 122, 162, + 98, 3, 23, 13, 127, 84, 222, 25, 86, 235, 57, 45, 96, 88, 244, 222, 217, 209, 131, + 136, 129, 142, 159, 39, 114, 18, 193, 84, 183, 247, 89, 222, 83, 61, 28, 53, 84, 171, + 184, 101, 110, 55, 203, 192, 81, 133, 133, 149, 251, 118, 120, 179, 89, 242, 48, 64, 192, + 52, 86, 202, 70, 98, 251, 44, 48, 116, 230, 133, 135, 116, 147, 35, 20, 175, 203, 233, + 135, 2, 129, 129, 0, 233, 40, 85, 226, 253, 145, 66, 37, 243, 145, 110, 134, 29, 32, + 89, 12, 133, 71, 9, 229, 176, 188, 137, 93, 58, 81, 31, 27, 188, 51, 101, 110, 30, + 243, 3, 67, 133, 244, 140, 181, 124, 137, 175, 55, 219, 19, 37, 193, 181, 82, 168, 17, + 39, 79, 232, 217, 230, 245, 110, 223, 181, 204, 167, 91, 222, 185, 75, 222, 19, 30, 236, + 187, 250, 135, 239, 30, 142, 137, 2, 111, 32, 255, 75, 151, 239, 76, 25, 219, 162, 212, + 242, 168, 100, 36, 196, 214, 154, 195, 141, 4, 210, 139, 193, 76, 228, 21, 62, 213, 26, + 199, 90, 6, 88, 154, 155, 84, 75, 190, 55, 147, 81, 23, 80, 130, 119, 237, 34, 231, + 2, 129, 129, 0, 224, 86, 26, 153, 61, 36, 213, 110, 27, 191, 243, 142, 2, 189, 66, + 4, 212, 3, 0, 19, 4, 235, 137, 226, 233, 152, 246, 49, 118, 198, 193, 44, 104, 187, + 187, 60, 33, 176, 13, 5, 184, 36, 113, 191, 213, 75, 119, 118, 10, 166, 28, 134, 170, + 196, 105, 102, 158, 14, 158, 166, 27, 195, 148, 12, 239, 143, 58, 107, 11, 12, 159, 195, + 248, 210, 133, 230, 178, 209, 244, 189, 185, 189, 168, 88, 18, 55, 85, 206, 173, 208, 77, + 20, 223, 158, 171, 41, 158, 118, 145, 223, 182, 45, 233, 117, 12, 48, 185, 148, 145, 247, + 103, 255, 42, 60, 116, 168, 196, 120, 195, 201, 67, 230, 37, 6, 154, 156, 24, 193, 2, + 129, 128, 89, 174, 98, 26, 197, 93, 178, 11, 214, 252, 1, 127, 36, 132, 228, 187, 164, + 40, 6, 238, 138, 100, 122, 145, 153, 116, 202, 122, 104, 145, 124, 90, 55, 29, 82, 12, + 14, 226, 89, 157, 21, 34, 0, 123, 131, 144, 129, 221, 88, 89, 74, 188, 14, 90, 246, + 61, 154, 80, 157, 44, 255, 72, 144, 183, 13, 156, 72, 74, 75, 32, 129, 254, 108, 7, + 13, 58, 187, 168, 135, 127, 39, 58, 11, 97, 141, 113, 192, 32, 115, 41, 101, 140, 207, + 184, 52, 222, 54, 249, 48, 172, 194, 47, 111, 82, 156, 8, 111, 250, 152, 42, 69, 190, + 91, 38, 92, 200, 183, 185, 154, 182, 215, 98, 24, 93, 150, 179, 103, 209, 2, 129, 128, + 55, 188, 105, 17, 190, 48, 228, 148, 77, 53, 227, 1, 91, 44, 108, 54, 52, 82, 207, + 120, 220, 83, 125, 214, 160, 102, 106, 3, 106, 15, 42, 51, 40, 0, 42, 14, 130, 175, + 117, 28, 76, 208, 137, 72, 14, 115, 169, 182, 62, 0, 125, 186, 73, 212, 148, 225, 52, + 226, 67, 96, 26, 84, 248, 12, 232, 241, 74, 30, 116, 3, 96, 157, 219, 89, 218, 125, + 117, 69, 40, 230, 149, 14, 147, 19, 245, 134, 150, 200, 171, 28, 76, 106, 175, 172, 191, + 151, 189, 115, 158, 132, 38, 215, 45, 196, 217, 192, 43, 110, 201, 178, 134, 157, 15, 230, + 254, 54, 60, 172, 102, 3, 30, 41, 71, 65, 35, 52, 171, 29, 171}; } // namespace class GetCredentialName : public ::testing::TestWithParam { TempCertFile m_certFile{GetParam()}; }; +class GetTokenFromCertInMemory : public ::testing::TestWithParam { +public: + bool GetSendCertChain() { return GetParam(); } + + std::string GetHeader() + { + // cspell:disable + std::string x5t = "\"V0pIIQwSzNn6vfSTPv-1f7Vt_Pw\""; + std::string kid = "\"574A48210C12CCD9FABDF4933EFFB57FB56DFCFC\""; + std::string x5c + = "\"MIIDODCCAiCgAwIBAgIQNqa9U3MBxqBF7ksWk+" + "XRkzANBgkqhkiG9w0BAQsFADAeMRwwGgYDVQQDDBNhenVyZS1pZGVudGl0eS10ZXN0MCAXDTIyMDQyMjE1MDYw" + "NloYDzIyMjIwMTAxMDcwMDAwWjAeMRwwGgYDVQQDDBNhenVyZS1pZGVudGl0eS10ZXN0MIIBIjANBgkqhkiG9w" + "0BAQEFAAOCAQ8AMIIBCgKCAQEAz3ZuKbpDu7oBMfMF65qOFSBKInKe8N0LBCRgNmzMfZxzL8LoBueLdeEKX6gU" + "GEFi3i9R5qXA3or1Q/teWV3hiwj1WQR4aGPGVhom34QAM6kND/" + "QmtZMnY7weLiXBJxf0WLUL+p+jsJnTtcCdtiTXEZTLWapp2/" + "0NCJ9n41xG3ZfOfxmZWMzEEXcnyNMq4kkQXGFdpINM3lwsX5grwd62+iNSqaFBR5ZHh7ZHg8JtFR1BLeB8/" + "IIXAdNLSOXktnx9qz5CDUCnOvtEFAtiiAkAvhsybGA28EDmqOVYZPNB+S0bjPTXc7/n1N5S55LWAoF4C/QF+C/" + "0fWeD87bmqP6m0QIDAQABo3AwbjAOBgNVHQ8BAf8EBAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwIGCCsGAQUFB" + "wMBMB4GA1UdEQQXMBWCE2F6dXJlLWlkZW50aXR5LXRlc3QwHQYDVR0OBBYEFCoJ5tInmafyNuR0tGxZOz522jl" + "WMA0GCSqGSIb3DQEBCwUAA4IBAQBzLXpwXmrg1sQTmzMnS24mREKxj9B3YILmgsdBMrHkH07QUROee7IbQ8gfB" + "Keln0dEcfYiJyh42jn+fmg9AR17RP80wPthD2eKOt4WYNkNM3H8U4JEo+0ML0jZyswynpR48h/Em96sm/" + "NUeKUViD5iVTb1uHL4j8mQAN1IbXcunXvrrek1CzFVn5Rpah0Tn+" + "6cYVKdJg531i53udzusgZtV1NPZ82tzYkPQG1vxB//D9vd0LzmcfCvT50MKhz0r/" + "c5yJYki9q94DBuzMhe+O9j+Ob2pVQt5akVFJVtIVSfBZzRBAd66u9JeADlT4sxwS4QAUHiRrCsEpJsnJXkx/" + "6O\""; + // cspell:enable + + if (GetSendCertChain()) + { + return "{\"x5t\":" + x5t + ",\"kid\":" + kid + + ",\"alg\":\"RS256\",\"typ\":\"JWT\"," + "\"x5c\":" + + x5c + "}"; + } + return "{\"x5t\":" + x5t + ",\"kid\":" + kid + ",\"alg\":\"RS256\",\"typ\":\"JWT\"}"; + } +}; + class GetToken : public ::testing::TestWithParam> { public: TestType GetTestType() { return std::get<0>(GetParam()); } @@ -129,6 +287,7 @@ public: "6cYVKdJg531i53udzusgZtV1NPZ82tzYkPQG1vxB//D9vd0LzmcfCvT50MKhz0r/" "c5yJYki9q94DBuzMhe+O9j+Ob2pVQt5akVFJVtIVSfBZzRBAd66u9JeADlT4sxwS4QAUHiRrCsEpJsnJXkx/" "6O\""; + // cspell:enable if (GetSendCertChain()) { @@ -180,6 +339,23 @@ TEST_P(GetCredentialName, ) TEST(ClientCertificateCredential, UnsupportedExtension) { + try + { + ClientCertificateCredential const cred( + "01234567-89ab-cdef-fedc-ba8976543210", + "fedcba98-7654-3210-0123-456789abcdef", + "doesnotexist.pem"); + + EXPECT_TRUE(!"ClientCertificateCredential with missing file is supposed to throw."); + } + catch (Azure::Core::Credentials::AuthenticationException const& ex) + { + EXPECT_EQ( + ex.what(), + std::string("Identity: ClientCertificateCredential: " + "Failed to open file for reading. File name: 'doesnotexist.pem'")); + } + try { ClientCertificateCredential const cred( @@ -218,10 +394,12 @@ TEST(ClientCertificateCredential, UnsupportedExtension) try { + // cspell:disable ClientCertificateCredential const cred( "01234567-89ab-cdef-fedc-ba8976543210", "fedcba98-7654-3210-0123-456789abcdef", "noextension"); + // cspell:enable EXPECT_TRUE(!"ClientCertificateCredential without an extension is supposed to throw."); } @@ -249,6 +427,141 @@ TEST(ClientCertificateCredential, UnsupportedExtension) } } +TEST(ClientCertificateCredential, InvalidContentInFile) +{ + { + TempCertFile certFile{""}; + EXPECT_THROW( + ClientCertificateCredential const cred( + "01234567-89ab-cdef-fedc-ba8976543210", + "fedcba98-7654-3210-0123-456789abcdef", + TempCertFile::Path), + Azure::Core::Credentials::AuthenticationException); + } + { + TempCertFile certFile{ExampleValidPrivateKeyString}; + EXPECT_THROW( + ClientCertificateCredential const cred( + "01234567-89ab-cdef-fedc-ba8976543210", + "fedcba98-7654-3210-0123-456789abcdef", + TempCertFile::Path), + Azure::Core::Credentials::AuthenticationException); + } + { + TempCertFile certFile{ExampleValidCertString}; + EXPECT_THROW( + ClientCertificateCredential const cred( + "01234567-89ab-cdef-fedc-ba8976543210", + "fedcba98-7654-3210-0123-456789abcdef", + TempCertFile::Path), + Azure::Core::Credentials::AuthenticationException); + } + + // cspell:disable + std::string invalidContents[] + = {"a", + "-----BEGIN CERTIFICATE-----\na", + "-----BEGIN RSA PRIVATE KEY-----\na", + "-----BEGIN RSA PRIVATE KEY-----\na-----BEGIN CERTIFICATE-----\na", + "-----BEGIN RSA PRIVATE " + "KEY-----\nqxxMaq+sv5e9c56EJtctxNnAK27JsoadD+b+NjysZgMeKUdBIzSrHQ==\n-----END RSA " + "PRIVATE KEY-----\n-----BEGIN " + "CERTIFICATE-----\nMIIDODCCAiCgAwIBAgIQNqa9U3MBxqBF7ksWk+XRkzANBgkqhkiG9w0BAQsFADAe\n----" + "-END CERTIFICATE-----"}; + // cspell:enable + + for (std::string invalidContent : invalidContents) + { + { + TempCertFile certFile{invalidContent}; + EXPECT_THROW( + ClientCertificateCredential const cred( + "01234567-89ab-cdef-fedc-ba8976543210", + "fedcba98-7654-3210-0123-456789abcdef", + TempCertFile::Path), + Azure::Core::Credentials::AuthenticationException); + } + { + TempCertFile certFile{ExampleValidPrivateKeyString + invalidContent}; + EXPECT_THROW( + ClientCertificateCredential const cred( + "01234567-89ab-cdef-fedc-ba8976543210", + "fedcba98-7654-3210-0123-456789abcdef", + TempCertFile::Path), + Azure::Core::Credentials::AuthenticationException); + } + { + TempCertFile certFile{ExampleValidCertString + invalidContent}; + EXPECT_THROW( + ClientCertificateCredential const cred( + "01234567-89ab-cdef-fedc-ba8976543210", + "fedcba98-7654-3210-0123-456789abcdef", + TempCertFile::Path), + Azure::Core::Credentials::AuthenticationException); + } + } +} + +TEST(ClientCertificateCredential, InvalidContentInMemory) +{ + EXPECT_THROW( + ClientCertificateCredential const cred( + "01234567-89ab-cdef-fedc-ba8976543210", + "fedcba98-7654-3210-0123-456789abcdef", + {}, + {}, + {}), + Azure::Core::Credentials::AuthenticationException); + + // cspell:disable + std::string invalidContents[] + = {"a", + "-----BEGIN CERTIFICATE-----\na", + "-----BEGIN RSA PRIVATE KEY-----\na", + "-----BEGIN RSA PRIVATE KEY-----\na-----BEGIN CERTIFICATE-----\na", + ExampleValidPrivateKeyString, + "-----BEGIN RSA PRIVATE " + "KEY-----\nqxxMaq+sv5e9c56EJtctxNnAK27JsoadD+b+NjysZgMeKUdBIzSrHQ==\n-----END RSA " + "PRIVATE KEY-----\n-----BEGIN " + "CERTIFICATE-----\nMIIDODCCAiCgAwIBAgIQNqa9U3MBxqBF7ksWk+XRkzANBgkqhkiG9w0BAQsFADAe\n----" + "-END CERTIFICATE-----"}; + // cspell:enable + + std::vector validCert(ExampleValidCertString.begin(), ExampleValidCertString.end()); + + for (std::string invalidContent : invalidContents) + { + std::vector invalid(invalidContent.begin(), invalidContent.end()); + + EXPECT_THROW( + ClientCertificateCredential const cred( + "01234567-89ab-cdef-fedc-ba8976543210", + "fedcba98-7654-3210-0123-456789abcdef", + validCert, + invalid, + {}), + Azure::Core::Credentials::AuthenticationException); + + EXPECT_THROW( + ClientCertificateCredential const cred( + "01234567-89ab-cdef-fedc-ba8976543210", + "fedcba98-7654-3210-0123-456789abcdef", + invalid, + ExampleValidPrivateKey, + {}), + Azure::Core::Credentials::AuthenticationException); + + EXPECT_THROW( + ClientCertificateCredential const cred( + "01234567-89ab-cdef-fedc-ba8976543210", + "fedcba98-7654-3210-0123-456789abcdef", + invalid, + invalid, + {}), + Azure::Core::Credentials::AuthenticationException); + } +} + TEST(ClientCertificateCredential, GetOptionsFromEnvironment) { { @@ -269,6 +582,146 @@ TEST(ClientCertificateCredential, GetOptionsFromEnvironment) } } +TEST_P(GetTokenFromCertInMemory, ) +{ + auto const actual = CredentialTestHelper::SimulateTokenRequest( + [this](auto transport) { + ClientCertificateCredentialOptions options; + options.Transport.Transport = transport; + options.SendCertificateChain = GetSendCertChain(); + + std::vector cert(ExampleValidCertString.begin(), ExampleValidCertString.end()); + + return std::make_unique( + "01234567-89ab-cdef-fedc-ba8976543210", + "fedcba98-7654-3210-0123-456789abcdef", + cert, + ExampleValidPrivateKey, + options); + }, + {{{"https://azure.com/.default"}}, {{}}}, + std::vector{ + "{\"expires_in\":3600, \"access_token\":\"ACCESSTOKEN1\"}", + "{\"expires_in\":7200, \"access_token\":\"ACCESSTOKEN2\"}"}); + + EXPECT_EQ(actual.Requests.size(), 2U); + EXPECT_EQ(actual.Responses.size(), 2U); + + auto const& request0 = actual.Requests.at(0); + auto const& request1 = actual.Requests.at(1); + + auto const& response0 = actual.Responses.at(0); + auto const& response1 = actual.Responses.at(1); + + EXPECT_EQ(request0.HttpMethod, HttpMethod::Post); + EXPECT_EQ(request1.HttpMethod, HttpMethod::Post); + + EXPECT_EQ( + request0.AbsoluteUrl, + "https://login.microsoftonline.com/01234567-89ab-cdef-fedc-ba8976543210/oauth2/v2.0/token"); + + EXPECT_EQ( + request1.AbsoluteUrl, + "https://login.microsoftonline.com/01234567-89ab-cdef-fedc-ba8976543210/oauth2/v2.0/token"); + + { + // cspell:disable + std::string expectedStr1 + = "grant_type=client_credentials" + "&client_assertion_type=urn%3Aietf%3Aparams%3Aoauth%3Aclient-assertion-type%3Ajwt-" + "bearer" + "&client_id=fedcba98-7654-3210-0123-456789abcdef" + "&scope=https%3A%2F%2Fazure.com%2F.default" + "&client_assertion="; + + std::string expectedStr2 + = "grant_type=client_credentials" + "&client_assertion_type=urn%3Aietf%3Aparams%3Aoauth%3Aclient-assertion-type%3Ajwt-bearer" + "&client_id=fedcba98-7654-3210-0123-456789abcdef" + "&client_assertion="; // cspell:enable + + auto expectedBodyStart0 = expectedStr1; + auto expectedBodyStart1 = expectedStr2; + + EXPECT_GT(request0.Body.size(), expectedBodyStart0.size()); + EXPECT_GT(request1.Body.size(), expectedBodyStart1.size()); + + EXPECT_EQ(request0.Body.substr(0, expectedBodyStart0.size()), expectedBodyStart0); + EXPECT_EQ(request1.Body.substr(0, expectedBodyStart1.size()), expectedBodyStart1); + + EXPECT_NE(request0.Headers.find("Content-Length"), request0.Headers.end()); + EXPECT_GT( + std::stoi(request0.Headers.at("Content-Length")), + static_cast(expectedBodyStart0.size())); + + EXPECT_NE(request1.Headers.find("Content-Length"), request1.Headers.end()); + EXPECT_GT( + std::stoi(request1.Headers.at("Content-Length")), + static_cast(expectedBodyStart1.size())); + + { + using Azure::Core::_internal::Base64Url; + + const auto assertion0 = request0.Body.substr(expectedBodyStart0.size()); + const auto assertion1 = request1.Body.substr(expectedBodyStart1.size()); + + const auto assertion0Parts = SplitString(assertion0, '.'); + const auto assertion1Parts = SplitString(assertion1, '.'); + + EXPECT_EQ(assertion0Parts.size(), 3U); + EXPECT_EQ(assertion1Parts.size(), 3U); + + const auto header0Vec = Base64Url::Base64UrlDecode(assertion0Parts[0]); + const auto header1Vec = Base64Url::Base64UrlDecode(assertion1Parts[0]); + + const auto payload0Vec = Base64Url::Base64UrlDecode(assertion0Parts[1]); + const auto payload1Vec = Base64Url::Base64UrlDecode(assertion1Parts[1]); + + const auto signature0 = assertion0Parts[2]; + const auto signature1 = assertion1Parts[2]; + + const auto header0 = ToString(header0Vec); + const auto header1 = ToString(header1Vec); + + const auto payload0 = ToString(payload0Vec); + const auto payload1 = ToString(payload1Vec); + + std::string ExpectedHeader = GetHeader(); + + EXPECT_EQ(header0, ExpectedHeader); + EXPECT_EQ(header1, ExpectedHeader); + + std::string ExpectedPayloadStart + = "{\"aud\":\"https://login.microsoftonline.com/01234567-89ab-cdef-fedc-ba8976543210/" + "oauth2/v2.0/token\"," + "\"iss\":\"fedcba98-7654-3210-0123-456789abcdef\"," + "\"sub\":\"fedcba98-7654-3210-0123-456789abcdef\",\"jti\":\""; + + EXPECT_EQ(payload0.substr(0, ExpectedPayloadStart.size()), ExpectedPayloadStart); + EXPECT_EQ(payload1.substr(0, ExpectedPayloadStart.size()), ExpectedPayloadStart); + + EXPECT_EQ(Base64Url::Base64UrlDecode(signature0).size(), 256); + EXPECT_EQ(Base64Url::Base64UrlDecode(signature1).size(), 256); + } + } + + EXPECT_NE(request0.Headers.find("Content-Type"), request0.Headers.end()); + EXPECT_EQ(request0.Headers.at("Content-Type"), "application/x-www-form-urlencoded"); + + EXPECT_NE(request1.Headers.find("Content-Type"), request1.Headers.end()); + EXPECT_EQ(request1.Headers.at("Content-Type"), "application/x-www-form-urlencoded"); + + EXPECT_EQ(response0.AccessToken.Token, "ACCESSTOKEN1"); + EXPECT_EQ(response1.AccessToken.Token, "ACCESSTOKEN2"); + + using namespace std::chrono_literals; + EXPECT_GE(response0.AccessToken.ExpiresOn, response0.EarliestExpiration + 3600s); + EXPECT_LE(response0.AccessToken.ExpiresOn, response0.LatestExpiration + 3600s); + + EXPECT_GE(response1.AccessToken.ExpiresOn, response1.EarliestExpiration + 7200s); + EXPECT_LE(response1.AccessToken.ExpiresOn, response1.LatestExpiration + 7200s); +} + TEST_P(GetToken, ) { auto const actual = CredentialTestHelper::SimulateTokenRequest( @@ -389,6 +842,11 @@ INSTANTIATE_TEST_SUITE_P( GetCredentialName, testing::Values(RsaPkcs, RsaRaw)); +INSTANTIATE_TEST_SUITE_P( + ClientCertificateCredential, + GetTokenFromCertInMemory, + testing::Values(true, false)); + INSTANTIATE_TEST_SUITE_P( ClientCertificateCredential, GetToken, @@ -451,127 +909,18 @@ TempCertFile::TempCertFile(CertFormat format) "\n" "issuer=CN = azure-identity-test\n" "\n" - "-----BEGIN CERTIFICATE-----\n" - "MIIDODCCAiCgAwIBAgIQNqa9U3MBxqBF7ksWk+XRkzANBgkqhkiG9w0BAQsFADAe\n" - "MRwwGgYDVQQDDBNhenVyZS1pZGVudGl0eS10ZXN0MCAXDTIyMDQyMjE1MDYwNloY\n" - "DzIyMjIwMTAxMDcwMDAwWjAeMRwwGgYDVQQDDBNhenVyZS1pZGVudGl0eS10ZXN0\n" - "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAz3ZuKbpDu7oBMfMF65qO\n" - "FSBKInKe8N0LBCRgNmzMfZxzL8LoBueLdeEKX6gUGEFi3i9R5qXA3or1Q/teWV3h\n" - "iwj1WQR4aGPGVhom34QAM6kND/QmtZMnY7weLiXBJxf0WLUL+p+jsJnTtcCdtiTX\n" - "EZTLWapp2/0NCJ9n41xG3ZfOfxmZWMzEEXcnyNMq4kkQXGFdpINM3lwsX5grwd62\n" - "+iNSqaFBR5ZHh7ZHg8JtFR1BLeB8/IIXAdNLSOXktnx9qz5CDUCnOvtEFAtiiAkA\n" - "vhsybGA28EDmqOVYZPNB+S0bjPTXc7/n1N5S55LWAoF4C/QF+C/0fWeD87bmqP6m\n" - "0QIDAQABo3AwbjAOBgNVHQ8BAf8EBAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwIG\n" - "CCsGAQUFBwMBMB4GA1UdEQQXMBWCE2F6dXJlLWlkZW50aXR5LXRlc3QwHQYDVR0O\n" - "BBYEFCoJ5tInmafyNuR0tGxZOz522jlWMA0GCSqGSIb3DQEBCwUAA4IBAQBzLXpw\n" - "Xmrg1sQTmzMnS24mREKxj9B3YILmgsdBMrHkH07QUROee7IbQ8gfBKeln0dEcfYi\n" - "Jyh42jn+fmg9AR17RP80wPthD2eKOt4WYNkNM3H8U4JEo+0ML0jZyswynpR48h/E\n" - "m96sm/NUeKUViD5iVTb1uHL4j8mQAN1IbXcunXvrrek1CzFVn5Rpah0Tn+6cYVKd\n" - "Jg531i53udzusgZtV1NPZ82tzYkPQG1vxB//D9vd0LzmcfCvT50MKhz0r/c5yJYk\n" - "i9q94DBuzMhe+O9j+Ob2pVQt5akVFJVtIVSfBZzRBAd66u9JeADlT4sxwS4QAUHi\n" - "RrCsEpJsnJXkx/6O\n" - "-----END CERTIFICATE-----\n"; + << ExampleValidCertString; // cspell:enable else if (format == RsaRaw) - cert << // cspell:disable - "-----BEGIN RSA PRIVATE KEY-----\n" - "MIIEpAIBAAKCAQEAz3ZuKbpDu7oBMfMF65qOFSBKInKe8N0LBCRgNmzMfZxzL8Lo\n" - "BueLdeEKX6gUGEFi3i9R5qXA3or1Q/teWV3hiwj1WQR4aGPGVhom34QAM6kND/Qm\n" - "tZMnY7weLiXBJxf0WLUL+p+jsJnTtcCdtiTXEZTLWapp2/0NCJ9n41xG3ZfOfxmZ\n" - "WMzEEXcnyNMq4kkQXGFdpINM3lwsX5grwd62+iNSqaFBR5ZHh7ZHg8JtFR1BLeB8\n" - "/IIXAdNLSOXktnx9qz5CDUCnOvtEFAtiiAkAvhsybGA28EDmqOVYZPNB+S0bjPTX\n" - "c7/n1N5S55LWAoF4C/QF+C/0fWeD87bmqP6m0QIDAQABAoIBAQDEGSK6KIk7me7l\n" - "QtyWvemNSI8qjoN0EswF50hWSXLlTIuIWsgtNpIZI1VF477SyoNklv/ob0amVFzP\n" - "HHwrJtU5MYeP0+zoZ18jJecWoVP7gNCLAvHP8b9qw3cXkbJIfJkHfGJNTLZSCKUY\n" - "CHBKqfnscWPhZnZXbZLzUpHFVATcEJ14vqFj4RNoLqNoNQT5NoGxdPtxb0q0PEMB\n" - "h4PrkCcK0KSfkgfU8rkBWrhkef8Eqh/d3BR+WAv/r+SO6lumUHtH+6xCkA8mxlc5\n" - "AZSichglWJj5+12v8Ca4sLPhWSHx8395tJCYoMSXfx8E65ykoPh/KAYJ4O5WS3QW\n" - "FhzBiYQNAoGBAOPJqFu7M3oL3y7lBWtLB38irjcrzr+1rneLGtJcHSjx0vmrcC+k\n" - "zVFggBpKJmAAxHt6omIDFw1/VN4ZVus5LWBY9N7Z0YOIgY6fJ3ISwVS391neUz0c\n" - "NVSruGVuN8vAUYWFlft2eLNZ8jBAwDRWykZi+ywwdOaFh3STIxSvy+mHAoGBAOko\n" - "VeL9kUIl85Fuhh0gWQyFRwnlsLyJXTpRHxu8M2VuHvMDQ4X0jLV8ia832xMlwbVS\n" - "qBEnT+jZ5vVu37XMp1veuUveEx7su/qH7x6OiQJvIP9Ll+9MGdui1PKoZCTE1prD\n" - "jQTSi8FM5BU+1RrHWgZYmptUS743k1EXUIJ37SLnAoGBAOBWGpk9JNVuG7/zjgK9\n" - "QgTUAwATBOuJ4umY9jF2xsEsaLu7PCGwDQW4JHG/1Ut3dgqmHIaqxGlmng6ephvD\n" - "lAzvjzprCwyfw/jSheay0fS9ub2oWBI3Vc6t0E0U356rKZ52kd+2Lel1DDC5lJH3\n" - "Z/8qPHSoxHjDyUPmJQaanBjBAoGAWa5iGsVdsgvW/AF/JITku6QoBu6KZHqRmXTK\n" - "emiRfFo3HVIMDuJZnRUiAHuDkIHdWFlKvA5a9j2aUJ0s/0iQtw2cSEpLIIH+bAcN\n" - "Oruoh38nOgthjXHAIHMpZYzPuDTeNvkwrMIvb1KcCG/6mCpFvlsmXMi3uZq212IY\n" - "XZazZ9ECgYA3vGkRvjDklE014wFbLGw2NFLPeNxTfdagZmoDag8qMygAKg6Cr3Uc\n" - "TNCJSA5zqbY+AH26SdSU4TTiQ2AaVPgM6PFKHnQDYJ3bWdp9dUUo5pUOkxP1hpbI\n" - "qxxMaq+sv5e9c56EJtctxNnAK27JsoadD+b+NjysZgMeKUdBIzSrHQ==\n" - "-----END RSA PRIVATE KEY-----\n" - "-----BEGIN CERTIFICATE-----\n" - "MIIDODCCAiCgAwIBAgIQNqa9U3MBxqBF7ksWk+XRkzANBgkqhkiG9w0BAQsFADAe\n" - "MRwwGgYDVQQDDBNhenVyZS1pZGVudGl0eS10ZXN0MCAXDTIyMDQyMjE1MDYwNloY\n" - "DzIyMjIwMTAxMDcwMDAwWjAeMRwwGgYDVQQDDBNhenVyZS1pZGVudGl0eS10ZXN0\n" - "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAz3ZuKbpDu7oBMfMF65qO\n" - "FSBKInKe8N0LBCRgNmzMfZxzL8LoBueLdeEKX6gUGEFi3i9R5qXA3or1Q/teWV3h\n" - "iwj1WQR4aGPGVhom34QAM6kND/QmtZMnY7weLiXBJxf0WLUL+p+jsJnTtcCdtiTX\n" - "EZTLWapp2/0NCJ9n41xG3ZfOfxmZWMzEEXcnyNMq4kkQXGFdpINM3lwsX5grwd62\n" - "+iNSqaFBR5ZHh7ZHg8JtFR1BLeB8/IIXAdNLSOXktnx9qz5CDUCnOvtEFAtiiAkA\n" - "vhsybGA28EDmqOVYZPNB+S0bjPTXc7/n1N5S55LWAoF4C/QF+C/0fWeD87bmqP6m\n" - "0QIDAQABo3AwbjAOBgNVHQ8BAf8EBAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwIG\n" - "CCsGAQUFBwMBMB4GA1UdEQQXMBWCE2F6dXJlLWlkZW50aXR5LXRlc3QwHQYDVR0O\n" - "BBYEFCoJ5tInmafyNuR0tGxZOz522jlWMA0GCSqGSIb3DQEBCwUAA4IBAQBzLXpw\n" - "Xmrg1sQTmzMnS24mREKxj9B3YILmgsdBMrHkH07QUROee7IbQ8gfBKeln0dEcfYi\n" - "Jyh42jn+fmg9AR17RP80wPthD2eKOt4WYNkNM3H8U4JEo+0ML0jZyswynpR48h/E\n" - "m96sm/NUeKUViD5iVTb1uHL4j8mQAN1IbXcunXvrrek1CzFVn5Rpah0Tn+6cYVKd\n" - "Jg531i53udzusgZtV1NPZ82tzYkPQG1vxB//D9vd0LzmcfCvT50MKhz0r/c5yJYk\n" - "i9q94DBuzMhe+O9j+Ob2pVQt5akVFJVtIVSfBZzRBAd66u9JeADlT4sxwS4QAUHi\n" - "RrCsEpJsnJXkx/6O\n" - "-----END CERTIFICATE-----"; - // cspell:enable + cert << ExampleValidPrivateKeyString << ExampleValidCertString; else if (format == RsaRawReverse) - cert << // cspell:disable - "-----BEGIN CERTIFICATE-----\n" - "MIIDODCCAiCgAwIBAgIQNqa9U3MBxqBF7ksWk+XRkzANBgkqhkiG9w0BAQsFADAe\n" - "MRwwGgYDVQQDDBNhenVyZS1pZGVudGl0eS10ZXN0MCAXDTIyMDQyMjE1MDYwNloY\n" - "DzIyMjIwMTAxMDcwMDAwWjAeMRwwGgYDVQQDDBNhenVyZS1pZGVudGl0eS10ZXN0\n" - "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAz3ZuKbpDu7oBMfMF65qO\n" - "FSBKInKe8N0LBCRgNmzMfZxzL8LoBueLdeEKX6gUGEFi3i9R5qXA3or1Q/teWV3h\n" - "iwj1WQR4aGPGVhom34QAM6kND/QmtZMnY7weLiXBJxf0WLUL+p+jsJnTtcCdtiTX\n" - "EZTLWapp2/0NCJ9n41xG3ZfOfxmZWMzEEXcnyNMq4kkQXGFdpINM3lwsX5grwd62\n" - "+iNSqaFBR5ZHh7ZHg8JtFR1BLeB8/IIXAdNLSOXktnx9qz5CDUCnOvtEFAtiiAkA\n" - "vhsybGA28EDmqOVYZPNB+S0bjPTXc7/n1N5S55LWAoF4C/QF+C/0fWeD87bmqP6m\n" - "0QIDAQABo3AwbjAOBgNVHQ8BAf8EBAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwIG\n" - "CCsGAQUFBwMBMB4GA1UdEQQXMBWCE2F6dXJlLWlkZW50aXR5LXRlc3QwHQYDVR0O\n" - "BBYEFCoJ5tInmafyNuR0tGxZOz522jlWMA0GCSqGSIb3DQEBCwUAA4IBAQBzLXpw\n" - "Xmrg1sQTmzMnS24mREKxj9B3YILmgsdBMrHkH07QUROee7IbQ8gfBKeln0dEcfYi\n" - "Jyh42jn+fmg9AR17RP80wPthD2eKOt4WYNkNM3H8U4JEo+0ML0jZyswynpR48h/E\n" - "m96sm/NUeKUViD5iVTb1uHL4j8mQAN1IbXcunXvrrek1CzFVn5Rpah0Tn+6cYVKd\n" - "Jg531i53udzusgZtV1NPZ82tzYkPQG1vxB//D9vd0LzmcfCvT50MKhz0r/c5yJYk\n" - "i9q94DBuzMhe+O9j+Ob2pVQt5akVFJVtIVSfBZzRBAd66u9JeADlT4sxwS4QAUHi\n" - "RrCsEpJsnJXkx/6O\n" - "-----END CERTIFICATE-----\n" - "-----BEGIN RSA PRIVATE KEY-----\n" - "MIIEpAIBAAKCAQEAz3ZuKbpDu7oBMfMF65qOFSBKInKe8N0LBCRgNmzMfZxzL8Lo\n" - "BueLdeEKX6gUGEFi3i9R5qXA3or1Q/teWV3hiwj1WQR4aGPGVhom34QAM6kND/Qm\n" - "tZMnY7weLiXBJxf0WLUL+p+jsJnTtcCdtiTXEZTLWapp2/0NCJ9n41xG3ZfOfxmZ\n" - "WMzEEXcnyNMq4kkQXGFdpINM3lwsX5grwd62+iNSqaFBR5ZHh7ZHg8JtFR1BLeB8\n" - "/IIXAdNLSOXktnx9qz5CDUCnOvtEFAtiiAkAvhsybGA28EDmqOVYZPNB+S0bjPTX\n" - "c7/n1N5S55LWAoF4C/QF+C/0fWeD87bmqP6m0QIDAQABAoIBAQDEGSK6KIk7me7l\n" - "QtyWvemNSI8qjoN0EswF50hWSXLlTIuIWsgtNpIZI1VF477SyoNklv/ob0amVFzP\n" - "HHwrJtU5MYeP0+zoZ18jJecWoVP7gNCLAvHP8b9qw3cXkbJIfJkHfGJNTLZSCKUY\n" - "CHBKqfnscWPhZnZXbZLzUpHFVATcEJ14vqFj4RNoLqNoNQT5NoGxdPtxb0q0PEMB\n" - "h4PrkCcK0KSfkgfU8rkBWrhkef8Eqh/d3BR+WAv/r+SO6lumUHtH+6xCkA8mxlc5\n" - "AZSichglWJj5+12v8Ca4sLPhWSHx8395tJCYoMSXfx8E65ykoPh/KAYJ4O5WS3QW\n" - "FhzBiYQNAoGBAOPJqFu7M3oL3y7lBWtLB38irjcrzr+1rneLGtJcHSjx0vmrcC+k\n" - "zVFggBpKJmAAxHt6omIDFw1/VN4ZVus5LWBY9N7Z0YOIgY6fJ3ISwVS391neUz0c\n" - "NVSruGVuN8vAUYWFlft2eLNZ8jBAwDRWykZi+ywwdOaFh3STIxSvy+mHAoGBAOko\n" - "VeL9kUIl85Fuhh0gWQyFRwnlsLyJXTpRHxu8M2VuHvMDQ4X0jLV8ia832xMlwbVS\n" - "qBEnT+jZ5vVu37XMp1veuUveEx7su/qH7x6OiQJvIP9Ll+9MGdui1PKoZCTE1prD\n" - "jQTSi8FM5BU+1RrHWgZYmptUS743k1EXUIJ37SLnAoGBAOBWGpk9JNVuG7/zjgK9\n" - "QgTUAwATBOuJ4umY9jF2xsEsaLu7PCGwDQW4JHG/1Ut3dgqmHIaqxGlmng6ephvD\n" - "lAzvjzprCwyfw/jSheay0fS9ub2oWBI3Vc6t0E0U356rKZ52kd+2Lel1DDC5lJH3\n" - "Z/8qPHSoxHjDyUPmJQaanBjBAoGAWa5iGsVdsgvW/AF/JITku6QoBu6KZHqRmXTK\n" - "emiRfFo3HVIMDuJZnRUiAHuDkIHdWFlKvA5a9j2aUJ0s/0iQtw2cSEpLIIH+bAcN\n" - "Oruoh38nOgthjXHAIHMpZYzPuDTeNvkwrMIvb1KcCG/6mCpFvlsmXMi3uZq212IY\n" - "XZazZ9ECgYA3vGkRvjDklE014wFbLGw2NFLPeNxTfdagZmoDag8qMygAKg6Cr3Uc\n" - "TNCJSA5zqbY+AH26SdSU4TTiQ2AaVPgM6PFKHnQDYJ3bWdp9dUUo5pUOkxP1hpbI\n" - "qxxMaq+sv5e9c56EJtctxNnAK27JsoadD+b+NjysZgMeKUdBIzSrHQ==\n" - "-----END RSA PRIVATE KEY-----"; - // cspell:enable + cert << ExampleValidCertString << ExampleValidPrivateKeyString; +} + +TempCertFile::TempCertFile(std::string content) +{ + std::ofstream cert(Path, std::ios_base::out | std::ios_base::trunc); + cert << content; } std::vector SplitString(const std::string& s, char separator)