Issue creating SamlResponse when following your example Idp code - within the LoginResponse method

Viewed 136

I have created an IDP using the code contained within https://github.com/ITfoxtec/ITfoxtec.Identity.Saml2/blob/master/test/TestIdPCore/Controllers/AuthController.cs

This is throwing an error when I attempt to bind the authNResponse using the following code:

var responsebinding = new Saml2PostBinding();
responsebinding.Bind(saml2AuthnResponse).XmlDocument.OuterXml;

This is the same code as within the PostContent method, but I've opted to use this code direct as I just needed the SamlResponse.

The error is:

Microsoft.IdentityModel.Tokens.Saml2.Saml2SecurityTokenWriteException: 'IDX13129: The SAML2:AttributeStatement must contain at least one SAML2:Attribute.'

With the following abridged stack trace:

   at Microsoft.IdentityModel.Tokens.Saml2.Saml2Serializer.WriteAttributeStatement(XmlWriter writer, Saml2AttributeStatement statement)
   at Microsoft.IdentityModel.Tokens.Saml2.Saml2Serializer.WriteStatement(XmlWriter writer, Saml2Statement statement)
   at Microsoft.IdentityModel.Tokens.Saml2.Saml2Serializer.WriteAssertion(XmlWriter writer, Saml2Assertion assertion)
   at Microsoft.IdentityModel.Tokens.Saml2.Saml2SecurityTokenHandler.WriteToken(XmlWriter writer, SecurityToken securityToken)
   at ITfoxtec.Identity.Saml2.Tokens.Saml2ResponseSecurityTokenHandler.WriteToken(SecurityToken token)
   at ITfoxtec.Identity.Saml2.Saml2AuthnResponse.ToXml()

I have used your example code almost exactly, so is there an issue within it, or am I missing something?

Many thanks

3 Answers

Maybe you are missing the part of adding claims to the token and creating the token?

saml2AuthnResponse.SessionIndex = sessionIndex;

var claimsIdentity = new ClaimsIdentity(claims);
saml2AuthnResponse.NameId = new Saml2NameIdentifier(claimsIdentity.Claims.Where(c => c.Type == ClaimTypes.NameIdentifier).Select(c => c.Value).Single(), NameIdentifierFormats.Persistent);
saml2AuthnResponse.ClaimsIdentity = claimsIdentity;

var token = saml2AuthnResponse.CreateSecurityToken(relyingParty.Issuer, subjectConfirmationLifetime: 5, issuedTokenLifetime: 60);

https://github.com/ITfoxtec/ITfoxtec.Identity.Saml2/blob/master/test/TestIdPCore/Controllers/AuthController.cs#L110

I have found that you need both the ClaimTypes.NameIdentifier and ClaimTypes.Email claims in order for the token to be generated successfully.

Here's a deeper analysis and two possible solutions/workarounds.

The situation: creating a ITfoxtec.Identity.Saml2.Saml2AuthnResponse for a ClaimsIdentity that has only one claim: the nameidentifier. The relevant code snippet (not complete, just the part that is relevant, but the ITFoxttec samples have the full code)

var response = new Saml2AuthnResponse(config);
response.ClaimsIdentity = new ClaimsIdentity(new[] { new Claim(ClaimTypes.NameIdentifier, "someone@somewhere.com") });
response.NameId = new Saml2NameIdentifier(....etc...);
var token = response.CreateSecurityToken(appliesToAddress);
//so far all is well, but the problem has been sneakily introduced!
//which is why the next line will give the error: Microsoft.IdentityModel.Tokens.Saml2.Saml2SecurityTokenWriteException: 'IDX13129: The SAML2:AttributeStatement must contain at least one SAML2:Attribute

return binding.Bind(response).ToActionResult();

Explanation:

ITfoxtec code: The nameidentifier claim is is removed from the claims when the token is created. This makes senses as it is supposed to be in the NameId property. This remaining claims are set as Subject in the SecurityTokenDescriptor that is fed to the Saml2SecurityTokenHandler which is Microsoft code.

var tokenDescriptor = new SecurityTokenDescriptor();
tokenDescriptor.Subject = new ClaimsIdentity(claims.Where(c => c.Type != ClaimTypes.NameIdentifier));

The claims in this tokendescriptor then end up as Attributes in the AttributeStatement in the generated Saml2SecurityToken (via a Saml2SecurityTokenHandler.CreateToken(tokendescriptor) call).

Unfortunately, if the nameidentifier was the only claim you had, then you end up with an AttributeStatement that has no Attributes. And subsequently run into the problem when the binding.Bind(response) deep down the bowels does its XML thing.. Unless you are supposed to always have an AttributeStatement it looks to me like a bug / edge case in the Microsoft.IdentityModel.Tokens.Saml library.

There are two solutions to solve it:

  1. Prevent ending up with no claims: Simply add another claim to the identity, doesn't have to be email, can be anything:

    response.ClaimsIdentity.AddClaim(new Claim("x", "y"))

  2. After the CreateSecurityToken call but before the call to Bind, check if the AttributeStatement is empty and if so remove it. A quick and dirty example for that:

    var x = (Saml2AttributeStatement)token.Assertion.Statements.FirstOrDefault(a => a.GetType() == typeof(Saml2AttributeStatement)); if (x?.Attributes.Count == 0) { token.Assertion.Statements.Remove(x); }

Personally, I prefer option 1, as it is generally safer to use and less code. Plus I'm sure there can always be 'something' to further attribute the identity with...

Related