Skip to content

Commit 1fb894c

Browse files
authored
Port IgnoreAuthenticationContextInResponse compatibility option to 2.x
- Fixes Sustainsys#1045
2 parents b9cfe58 + 163afef commit 1fb894c

File tree

6 files changed

+164
-8
lines changed

6 files changed

+164
-8
lines changed

Sustainsys.Saml2/Configuration/Compatibility.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,14 @@ public Compatibility(CompatibilityElement configElement)
5656
/// </summary>
5757
public bool StrictOwinAuthenticationMode { get; set; }
5858

59+
/// <summary>
60+
/// Do not read the AuthnContext element in Saml2Response.
61+
/// If you do not need these values to be present as claims in the generated
62+
/// identity, using this option can prevent XML format errors (IDX13102)
63+
/// e.g. when value cannot parse as absolute URI
64+
/// </summary>
65+
public bool IgnoreAuthenticationContextInResponse { get; set; }
66+
5967
/// <summary>
6068
/// Ignore the check for the missing InResponseTo attribute in the Saml response.
6169
/// This is different to setting the allowUnsolicitedAuthnResponse as it will only

Sustainsys.Saml2/Configuration/SPOptions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ public Saml2PSecurityTokenHandler Saml2PSecurityTokenHandler
105105
if(value == null)
106106
{
107107
// Set the saved value, but don't trust it - still use a local var for the return.
108-
saml2PSecurityTokenHandler = value = new Saml2PSecurityTokenHandler();
108+
saml2PSecurityTokenHandler = value = new Saml2PSecurityTokenHandler(this);
109109
}
110110

111111
return value;

Sustainsys.Saml2/SAML2P/Saml2PSecurityTokenHandler.cs

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,15 @@ namespace Sustainsys.Saml2.Saml2P
1717
/// could be handled at transport level.
1818
/// </summary>
1919
public class Saml2PSecurityTokenHandler : Saml2SecurityTokenHandler
20-
{
21-
public Saml2PSecurityTokenHandler()
20+
{
21+
public Saml2PSecurityTokenHandler(): this(null)
2222
{
23-
Serializer = new Saml2PSerializer();
23+
// backward compatibility = null spOptions
24+
}
25+
26+
public Saml2PSecurityTokenHandler(SPOptions spOptions)
27+
{
28+
Serializer = new Saml2PSerializer(spOptions);
2429
}
2530

2631
// Overridden to fix the fact that the base class version uses NotBefore as the token replay expiry time

Sustainsys.Saml2/SAML2P/Saml2PSerializer.cs

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using Microsoft.IdentityModel.Tokens.Saml2;
22
using Microsoft.IdentityModel.Xml;
3+
using Sustainsys.Saml2.Configuration;
34
using Sustainsys.Saml2.Internal;
45
using System;
56
using System.Collections.Generic;
@@ -45,6 +46,13 @@ public Saml2EncryptedAssertion(Saml2NameIdentifier issuer) :
4546
// - ignore authentication context if configured to do so
4647
class Saml2PSerializer : Saml2Serializer
4748
{
49+
private SPOptions spOptions;
50+
51+
public Saml2PSerializer(SPOptions spOptions)
52+
{
53+
this.spOptions = spOptions;
54+
}
55+
4856
public ICollection<X509Certificate2> DecryptionCertificates { get; set; }
4957

5058
/// <summary>
@@ -284,7 +292,19 @@ public override void WriteAssertion(XmlWriter writer, Saml2Assertion assertion)
284292

285293
writer.WriteEndElement();
286294
}
287-
295+
296+
protected override Saml2AuthenticationContext ReadAuthenticationContext(XmlDictionaryReader reader)
297+
{
298+
if (spOptions?.Compatibility?.IgnoreAuthenticationContextInResponse ?? false)
299+
{
300+
reader.Skip();
301+
//hack to get around the lack of a sane constructor
302+
return (Saml2AuthenticationContext)System.Runtime.Serialization.FormatterServices.GetUninitializedObject(typeof(Saml2AuthenticationContext));
303+
}
304+
305+
return base.ReadAuthenticationContext(reader);
306+
}
307+
288308
internal static Exception LogReadException(string message)
289309
{
290310
return LogExceptionMessage(new Saml2SecurityTokenReadException(message));

Tests/Tests.Shared/Configuration/SPOptionsTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -624,7 +624,7 @@ public void SPOptions_Saml2PSecurityTokenHandler_Setter()
624624
{
625625
var subject = StubFactory.CreateSPOptions();
626626

627-
var handler = new Saml2PSecurityTokenHandler();
627+
var handler = new Saml2PSecurityTokenHandler(subject);
628628

629629
subject.Saml2PSecurityTokenHandler = handler;
630630

Tests/Tests.Shared/Saml2P/Saml2ResponseTests.cs

Lines changed: 125 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -456,6 +456,129 @@ public void Saml2Response_GetClaims_CorrectSignedResponseMessage_WithAuthnContex
456456
authMethodClaim.Value.Should().Be("urn:somespecialvalue");
457457
}
458458

459+
[TestMethod]
460+
public void Saml2Response_GetClaims_BadAuthnContext_IgnoredWhenConfigured()
461+
{
462+
var response =
463+
@"<?xml version=""1.0"" encoding=""UTF-8""?>
464+
<saml2p:Response xmlns:saml2p=""urn:oasis:names:tc:SAML:2.0:protocol""
465+
xmlns:saml2=""urn:oasis:names:tc:SAML:2.0:assertion""
466+
ID = """ + MethodBase.GetCurrentMethod().Name + @""" Version=""2.0"" IssueInstant=""2013-01-01T00:00:00Z"">
467+
<saml2:Issuer>https://idp.example.com</saml2:Issuer>
468+
<saml2p:Status>
469+
<saml2p:StatusCode Value=""urn:oasis:names:tc:SAML:2.0:status:Success"" />
470+
</saml2p:Status>
471+
<saml2:Assertion xmlns:saml2=""urn:oasis:names:tc:SAML:2.0:assertion""
472+
Version=""2.0"" ID=""" + MethodBase.GetCurrentMethod().Name + @"_Assertion1""
473+
IssueInstant=""2013-09-25T00:00:00Z"">
474+
<saml2:Issuer>https://idp.example.com</saml2:Issuer>
475+
<saml2:Subject>
476+
<saml2:NameID>AuthenticatedSomeone</saml2:NameID>
477+
<saml2:SubjectConfirmation Method=""urn:oasis:names:tc:SAML:2.0:cm:bearer"" />
478+
</saml2:Subject>
479+
<saml2:Conditions NotOnOrAfter=""2100-01-01T00:00:00Z"" />
480+
<saml2:AuthnStatement AuthnInstant=""2013-09-25T00:00:00Z"" SessionIndex=""17"" >
481+
<saml2:AuthnContext>
482+
<saml2:AuthnContextClassRef>badvalue</saml2:AuthnContextClassRef>
483+
</saml2:AuthnContext>
484+
</saml2:AuthnStatement>
485+
</saml2:Assertion>
486+
</saml2p:Response>";
487+
488+
var signedResponse = SignedXmlHelper.SignXml(response);
489+
490+
var options = StubFactory.CreateOptions();
491+
options.SPOptions.Compatibility.IgnoreAuthenticationContextInResponse = true;
492+
var result = Saml2Response.Read(signedResponse).GetClaims(options);
493+
494+
var authMethodClaim = result.Single().Claims.SingleOrDefault(c => c.Type == ClaimTypes.AuthenticationMethod);
495+
authMethodClaim.Should().BeNull("the authentication method claim should not be generated");
496+
497+
var nameidClaim = result.Single().Claims.SingleOrDefault(c => c.Type == ClaimTypes.NameIdentifier);
498+
nameidClaim.Should().NotBeNull("the subject nameid claim should be generated");
499+
nameidClaim.Value.Should().Be("AuthenticatedSomeone");
500+
}
501+
502+
[TestMethod]
503+
public void Saml2Response_GetClaims_HandlerWithNullOptions_AuthnContextGeneratesClaims()
504+
{
505+
var response =
506+
@"<?xml version=""1.0"" encoding=""UTF-8""?>
507+
<saml2p:Response xmlns:saml2p=""urn:oasis:names:tc:SAML:2.0:protocol""
508+
xmlns:saml2=""urn:oasis:names:tc:SAML:2.0:assertion""
509+
ID = """ + MethodBase.GetCurrentMethod().Name + @""" Version=""2.0"" IssueInstant=""2013-01-01T00:00:00Z"">
510+
<saml2:Issuer>https://idp.example.com</saml2:Issuer>
511+
<saml2p:Status>
512+
<saml2p:StatusCode Value=""urn:oasis:names:tc:SAML:2.0:status:Success"" />
513+
</saml2p:Status>
514+
<saml2:Assertion xmlns:saml2=""urn:oasis:names:tc:SAML:2.0:assertion""
515+
Version=""2.0"" ID=""" + MethodBase.GetCurrentMethod().Name + @"_Assertion1""
516+
IssueInstant=""2013-09-25T00:00:00Z"">
517+
<saml2:Issuer>https://idp.example.com</saml2:Issuer>
518+
<saml2:Subject>
519+
<saml2:NameID>SomeOne</saml2:NameID>
520+
<saml2:SubjectConfirmation Method=""urn:oasis:names:tc:SAML:2.0:cm:bearer"" />
521+
</saml2:Subject>
522+
<saml2:Conditions NotOnOrAfter=""2100-01-01T00:00:00Z"" />
523+
<saml2:AuthnStatement AuthnInstant=""2013-09-25T00:00:00Z"" SessionIndex=""17"" >
524+
<saml2:AuthnContext>
525+
<saml2:AuthnContextClassRef>urn:somespecialvalue</saml2:AuthnContextClassRef>
526+
</saml2:AuthnContext>
527+
</saml2:AuthnStatement>
528+
</saml2:Assertion>
529+
</saml2p:Response>";
530+
531+
var signedResponse = SignedXmlHelper.SignXml(response);
532+
533+
var options = Options.FromConfiguration;
534+
options.SPOptions.Saml2PSecurityTokenHandler = new Saml2PSecurityTokenHandler();
535+
var result = Saml2Response.Read(signedResponse).GetClaims(options);
536+
537+
var authMethodClaim = result.Single().Claims.SingleOrDefault(c => c.Type == ClaimTypes.AuthenticationMethod);
538+
authMethodClaim.Should().NotBeNull("the authentication method claim should be generated");
539+
authMethodClaim.Value.Should().Be("urn:somespecialvalue");
540+
}
541+
542+
[TestMethod]
543+
public void Saml2Response_GetClaims_OptionsWithNullCompatibility_AuthnContextGeneratesClaims()
544+
{
545+
var response =
546+
@"<?xml version=""1.0"" encoding=""UTF-8""?>
547+
<saml2p:Response xmlns:saml2p=""urn:oasis:names:tc:SAML:2.0:protocol""
548+
xmlns:saml2=""urn:oasis:names:tc:SAML:2.0:assertion""
549+
ID = """ + MethodBase.GetCurrentMethod().Name + @""" Version=""2.0"" IssueInstant=""2013-01-01T00:00:00Z"">
550+
<saml2:Issuer>https://idp.example.com</saml2:Issuer>
551+
<saml2p:Status>
552+
<saml2p:StatusCode Value=""urn:oasis:names:tc:SAML:2.0:status:Success"" />
553+
</saml2p:Status>
554+
<saml2:Assertion xmlns:saml2=""urn:oasis:names:tc:SAML:2.0:assertion""
555+
Version=""2.0"" ID=""" + MethodBase.GetCurrentMethod().Name + @"_Assertion1""
556+
IssueInstant=""2013-09-25T00:00:00Z"">
557+
<saml2:Issuer>https://idp.example.com</saml2:Issuer>
558+
<saml2:Subject>
559+
<saml2:NameID>SomeOne</saml2:NameID>
560+
<saml2:SubjectConfirmation Method=""urn:oasis:names:tc:SAML:2.0:cm:bearer"" />
561+
</saml2:Subject>
562+
<saml2:Conditions NotOnOrAfter=""2100-01-01T00:00:00Z"" />
563+
<saml2:AuthnStatement AuthnInstant=""2013-09-25T00:00:00Z"" SessionIndex=""17"" >
564+
<saml2:AuthnContext>
565+
<saml2:AuthnContextClassRef>urn:somespecialvalue</saml2:AuthnContextClassRef>
566+
</saml2:AuthnContext>
567+
</saml2:AuthnStatement>
568+
</saml2:Assertion>
569+
</saml2p:Response>";
570+
571+
var signedResponse = SignedXmlHelper.SignXml(response);
572+
573+
var options = StubFactory.CreateOptions();
574+
options.SPOptions.Compatibility = null;
575+
var result = Saml2Response.Read(signedResponse).GetClaims(options);
576+
577+
var authMethodClaim = result.Single().Claims.SingleOrDefault(c => c.Type == ClaimTypes.AuthenticationMethod);
578+
authMethodClaim.Should().NotBeNull("the authentication method claim should be generated");
579+
authMethodClaim.Value.Should().Be("urn:somespecialvalue");
580+
}
581+
459582
[TestMethod]
460583
public void Saml2Response_GetClaims_SessionIndexButNoNameId()
461584
{
@@ -1008,7 +1131,7 @@ public void Saml2Response_GetClaims_CorrectEncryptedSingleAssertion_OAEP()
10081131
}
10091132

10101133
[TestMethod]
1011-
public void Saml2Response_GetClaims_CorrectEncryptedSingleAssertion_UsingWIF()
1134+
public void Saml2Response_GetClaims_CorrectEncryptedSingleAssertion_UsingMSIdentityModel()
10121135
{
10131136
var response =
10141137
@"<saml2p:Response xmlns:saml2p=""urn:oasis:names:tc:SAML:2.0:protocol""
@@ -1027,7 +1150,7 @@ public void Saml2Response_GetClaims_CorrectEncryptedSingleAssertion_UsingWIF()
10271150
assertion.Conditions = new Saml2Conditions { NotOnOrAfter = new DateTime(2100, 1, 1) };
10281151

10291152
var token = new Saml2SecurityToken(assertion);
1030-
var handler = new Saml2PSecurityTokenHandler();
1153+
var handler = new Saml2SecurityTokenHandler();
10311154

10321155
var signingKey = new X509SecurityKey(SignedXmlHelper.TestCert);
10331156
var signingCreds = new SigningCredentials(signingKey,

0 commit comments

Comments
 (0)