1

I have separated the SAML authentication into an ExternalAuthenticationService layer (called from the controller) for easier unit testing:

Controller

[Route("samlConsume")]
public async Task<IActionResult> AssertionConsumerAsync()
{
    var samlLoginInfo = await externalAuthenticationService.AuthenticateAsync(HttpContext);
    // ...
}

Service

public async Task<LoginInfo> AuthenticateAsync(HttpContext context)
{
    var req = context.Request.ToGenericHttpRequest();
    var binding = new Saml2PostBinding();
    var saml2AuthnResponse = new Saml2AuthnResponse(config);

    binding.ReadSamlResponse(req, saml2AuthnResponse);
    if (saml2AuthnResponse.Status != Saml2StatusCodes.Success)
    {
        throw new AuthenticationException($"SAML Response status: {saml2AuthnResponse.Status}");
    }

    binding.Unbind(req, saml2AuthnResponse);
    var sessionPrincipal = await saml2AuthnResponse.CreateSession(
       context, claimsTransform: (claimsPrincipal) => ClaimsTransform.Transform(claimsPrincipal)
    );
    
    // do stuff with the sessionPrincipal and return a LoginInfo
}

This is all pretty much boilerplate stuff from the TestWebAppCore sample.

Unit testing the Service is proving to be problematic. I have a unit test that creates a request, fakes certificate, creates a Mock<HttpContext>, and tries to execute the AuthenticateAsync method:

Unit Test

namespace unit_tests.Service
{
    public class ExternalAuthenticationServiceTests
    {
        private readonly string Destination = "http://testdestination.com";
        private readonly IExternalAuthenticationService service;
        public ExternalAuthenticationServiceTests()
        {
            var testCertificate = GenerateCertificate("Test Certificate");
            var config = new Saml2Configuration()
            {
                AudienceRestricted = false,
                CertificateValidationMode = X509CertificateValidationMode.None,
                Issuer = "http://wonderwoman.com",
                RevocationMode = X509RevocationMode.NoCheck,
                SignatureAlgorithm = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256",
                SigningCertificate = testCertificate,
                SingleSignOnDestination = new Uri(Destination),
                ValidateArtifact = false,
            };
            service = new ExternalAuthenticationService(config);
        }

        [Fact]
        public async void AuthenticateAsync_sets_the_proper_values_in_SamlLoginInfo()
        {
            var contextMock = new Mock<HttpContext>();
            contextMock
                .SetupProperty(m => m.Request.Method, "POST")
                .SetupProperty(m => m.Request.QueryString, QueryString.Empty)
                .SetupProperty(m => m.Request.Query, QueryCollection.Empty)
                .SetupProperty(m => m.Request.Form, FakeSamlResponse())
                .Setup(m => m.Request.Headers).Returns(new HeaderDictionary());

            var result = await service.AuthenticateAsync(contextMock.Object);

            // assert some stuff 
        }


        private X509Certificate2 GenerateCertificate(string subjectName)
        {
            // Generate a new RSA key pair
            using var rsa = RSA.Create(2048);
            // Create a certificate request
            CertificateRequest request = new($"CN={subjectName}", rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);

            // Set certificate validity dates
            DateTimeOffset now = DateTimeOffset.UtcNow;
            request.CertificateExtensions.Add(new X509BasicConstraintsExtension(false, false, 0, false));
            request.CertificateExtensions.Add(new X509KeyUsageExtension(X509KeyUsageFlags.DigitalSignature | X509KeyUsageFlags.KeyEncipherment, false));
            request.CertificateExtensions.Add(new X509EnhancedKeyUsageExtension(new OidCollection { new Oid("1.3.6.1.5.5.7.3.1") }, false));
            request.CertificateExtensions.Add(new X509SubjectKeyIdentifierExtension(request.PublicKey, false));

            // Create a self-signed certificate
            X509Certificate2 certificate = request.CreateSelfSigned(now, now.AddYears(1));

            // Return the generated certificate
            return certificate;
        }

        private static FormCollection FakeSamlResponse()
        {
            var samlResponse = Convert.ToBase64String(Encoding.UTF8.GetBytes(SamlResponseXml));
            var fields = new Dictionary<string, StringValues>
            {
                ["SAMLResponse"] = samlResponse
            };
            var formData = new FormCollection(fields);
            return formData;
        }

        private static string SamlResponseXml =>
            @"<samlp:Response xmlns:samlp=""urn:oasis:names:tc:SAML:2.0:protocol"" ID=""_some-id_"" Version=""2.0"" IssueInstant=""2023-07-01T12:00:00Z"">
                <saml:Issuer xmlns:saml=""urn:oasis:names:tc:SAML:2.0:assertion"">https://example.com/idp</saml:Issuer>
                <samlp:Status>
                  <samlp:StatusCode Value=""urn:oasis:names:tc:SAML:2.0:status:Success"" />
                </samlp:Status>
                <saml:Assertion xmlns:saml=""urn:oasis:names:tc:SAML:2.0:assertion"" ID=""_some-assertion-id_"" Version=""2.0"" IssueInstant=""2023-07-01T12:00:00Z"">
                  <saml:Issuer>https://example.com/idp</saml:Issuer>
                  <saml:Subject>
                    <saml:NameID Format=""urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified"">john.doe@example.com</saml:NameID>
                    <saml:SubjectConfirmation Method=""urn:oasis:names:tc:SAML:2.0:cm:bearer"">
                      <saml:SubjectConfirmationData NotOnOrAfter=""3000-07-01T12:00:00Z"" Recipient=""https://example.com/sp"" />
                    </saml:SubjectConfirmation>
                  </saml:Subject>
                  <saml:Conditions NotBefore=""2020-06-27T00:00:00Z"" NotOnOrAfter=""3000-06-28T00:00:00Z"">
                    <saml:AudienceRestriction>
                      <saml:Audience>https://example.com/sp</saml:Audience>
                    </saml:AudienceRestriction>
                  </saml:Conditions>
                  <saml:AuthnStatement AuthnInstant=""2023-06-27T12:00:00Z"" SessionIndex=""_some-session-index_"">
                    <saml:AuthnContext>
                      <saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:Password</saml:AuthnContextClassRef>
                    </saml:AuthnContext>
                  </saml:AuthnStatement>
                </saml:Assertion>
              </samlp:Response>";
    }
}

…but I am getting an error:

Message:  ITfoxtec.Identity.Saml2.Cryptography.InvalidSignatureException : Signature is invalid.

Stack Trace:  Saml2Request.ValidateXmlSignature(SignatureValidation documentValidationResult)

How can I configure this so that the signing certificate I'm generating matches the SAML response?

Scott Baker
  • 10,013
  • 17
  • 56
  • 102

1 Answers1

0

Is your SAML authn response signed? I don't see the signing code. Anyway, it is important not to change the XML after it is signed, eg. no new linebreaks or spaces.

Scott Baker
  • 10,013
  • 17
  • 56
  • 102
Anders Revsgaard
  • 3,636
  • 1
  • 9
  • 25
  • 1
    No, the authn response isn't signed to my knowledge, and for this test it doesn't have to be - I just need everything to work together - whether signed or not. How should I do this? – Scott Baker Jun 30 '23 at 14:26
  • I do not think it is possible without signing the request. – Anders Revsgaard Jul 05 '23 at 06:24
  • 1
    is it possible to somehow generate a signed request? Right now I'm just plugging in some XML but I don't know how I would make a signed request. – Scott Baker Jul 05 '23 at 16:11
  • Yes you can emulate an IdP, this is a IdP sample: https://github.com/ITfoxtec/ITfoxtec.Identity.Saml2/tree/master/test/TestIdPCore – Anders Revsgaard Jul 06 '23 at 08:24