diff --git a/src/Sustainsys.Saml2.AspNetCore/ServiceResolver.cs b/src/Sustainsys.Saml2.AspNetCore/ServiceResolver.cs
index 726c5bd23..d532afa7e 100644
--- a/src/Sustainsys.Saml2.AspNetCore/ServiceResolver.cs
+++ b/src/Sustainsys.Saml2.AspNetCore/ServiceResolver.cs
@@ -14,9 +14,10 @@ namespace Sustainsys.Saml2.AspNetCore;
///
/// The Sustainsys.Saml2 library uses multiple loosely coupled services internally. The
/// default implementation is to not register these in the main dependency injection
-/// system to avoid clutter. All services are resolved using the service resolver.
-/// To override services, override the factory method here. The resolver context
-/// always contains the HttpContext, which can be used to resolve services from DI.
+/// system to avoid clutter and to allow per scheme registrations. All services are
+/// resolved using the service resolver. To override services, override the factory
+/// method here. The resolver context always contains the HttpContext, which can be
+/// used to resolve services from DI.
///
public class ServiceResolver
{
@@ -68,12 +69,14 @@ public class ResolverContext(
public Func GetSamlXmlReader { get; set; }
= _ => new SamlXmlReader();
+ // TODO: Can this be a static instance?
///
/// Factory for
///
public Func GetSamlXmlWriter { get; set; }
= _ => new SamlXmlWriter();
+ // TODO: Can this be a static instance?
///
/// Factory for collection of front channel bindings.
///
@@ -118,4 +121,4 @@ public class BindingResolverContext(
.SingleOrDefault(b => b.Identifier == ctx.Binding)
?? throw new NotImplementedException($"Unknown binding {ctx.Binding} requested");
};
-}
+}
\ No newline at end of file
diff --git a/src/Sustainsys.Saml2/Bindings/HttpRedirectBinding.cs b/src/Sustainsys.Saml2/Bindings/HttpRedirectBinding.cs
index 8ee908655..08e2faa90 100644
--- a/src/Sustainsys.Saml2/Bindings/HttpRedirectBinding.cs
+++ b/src/Sustainsys.Saml2/Bindings/HttpRedirectBinding.cs
@@ -1,12 +1,18 @@
using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.WebUtilities;
using System.IO.Compression;
+using System.Linq;
+using System.Text;
+using System.Text.Unicode;
+using System.Xml;
+using static Microsoft.AspNetCore.WebUtilities.QueryStringEnumerable;
namespace Sustainsys.Saml2.Bindings;
///
/// Redirect binding implementation
///
-public interface IHttpRedirectBinding
+public interface IHttpRedirectBinding : IFrontChannelBinding
{
///
/// Unbind from a URL.
@@ -14,8 +20,8 @@ public interface IHttpRedirectBinding
/// Url to unbind from
/// Func that returns Identity provider from an entity id
/// Unbound message
- Saml2Message UnBindAsync(
- string url,
+ Task UnBindAsync(
+ string url,
Func> getSaml2Entity);
}
@@ -33,38 +39,104 @@ public HttpRedirectBinding() : base(Constants.BindingUris.HttpRedirect) { }
public override bool CanUnbind(HttpRequest httpRequest)
=> false; // Because we haven't implemented redirect unbind yet.
- ///
- /// Unbinds a Saml2 mesasge from a URL
- ///
- /// URL with Saml message
- /// Func that returns Identity provider from an entity id
- /// Unbount message
- public Saml2Message UnBindAsync(
+ ///
+ public virtual Task UnBindAsync(
string url,
Func> getSaml2Entity)
- => throw new NotImplementedException();
-
+ {
+ var uri = new Uri(url);
+ var query = new QueryStringEnumerable(uri.Query);
+
+ string? messageName = null;
+ string? message = null;
+ string? relayState = null;
+
+ foreach (var param in query)
+ {
+ // Simplicity is more important than performance here.
+ var encodedName = param.EncodedName.ToString();
+
+ // The standard is specific about the naming and they should never be
+ // encoded. So let's find encoded only.
+ if (encodedName == Constants.SamlResponse
+ || encodedName == Constants.SamlRequest)
+ {
+ if (messageName != null)
+ {
+ throw new InvalidOperationException($"Duplicate message parameters found: {messageName}, {encodedName}");
+ }
+
+ messageName = encodedName;
+ message = param.DecodeValue().ToString();
+ }
+
+ if (encodedName == Constants.RelayState)
+ {
+ if(relayState != null)
+ {
+ throw new InvalidOperationException("Duplicate RelayState parameters found");
+ }
+ relayState = param.DecodeValue().ToString();
+ }
+ }
+
+ if (messageName == null || message == null)
+ {
+ throw new InvalidOperationException("SAMLResponse or SAMLRequest parameter not found");
+ }
+
+ var xd = new XmlDocument();
+ xd.LoadXml(Inflate(message));
+
+ return Task.FromResult(new Saml2Message()
+ {
+ // We're not supporting destinations containing a query string.
+ Destination = uri.Scheme + "://" + uri.Host + uri.AbsolutePath,
+ Name = messageName,
+ RelayState = relayState,
+ Xml = xd.DocumentElement!
+ });
+ }
+
///
public override Task UnbindAsync(
HttpRequest httpRequest,
Func> getSaml2Entity) => throw new NotImplementedException();
///
- protected override async Task DoBindAsync(HttpResponse httpResponse, Saml2Message message)
+ protected override Task DoBindAsync(HttpResponse httpResponse, Saml2Message message)
{
var xmlString = message.Xml.OuterXml;
+ var encoded = Deflate(xmlString);
+
+ var location = $"{message.Destination}?{message.Name}={encoded}&RelayState={message.RelayState}";
+
+ httpResponse.Redirect(location);
+
+ return Task.CompletedTask;
+ }
+
+ private static string Deflate(string source)
+ {
using var compressed = new MemoryStream();
using (var deflateStream = new DeflateStream(compressed, CompressionLevel.Optimal))
{
using var writer = new StreamWriter(deflateStream);
- await writer.WriteAsync(xmlString);
+ writer.Write(source);
}
- var encoded = Uri.EscapeDataString(Convert.ToBase64String(compressed.ToArray()));
+ return Uri.EscapeDataString(Convert.ToBase64String(compressed.ToArray()));
+ }
+
+ private static string Inflate(string source)
+ {
+ var compressedBytes = Convert.FromBase64String(Uri.UnescapeDataString(source));
- var location = $"{message.Destination}?{message.Name}={encoded}&RelayState={message.RelayState}";
+ using var compressed = new MemoryStream(compressedBytes);
+ using var deflateStream = new DeflateStream(compressed, CompressionMode.Decompress);
+ using var reader = new StreamReader(deflateStream);
- httpResponse.Redirect(location);
+ return reader.ReadToEnd();
}
}
diff --git a/src/Sustainsys.Saml2/Common/Extensions.cs b/src/Sustainsys.Saml2/Common/Extensions.cs
index b62546a0c..55fafd651 100644
--- a/src/Sustainsys.Saml2/Common/Extensions.cs
+++ b/src/Sustainsys.Saml2/Common/Extensions.cs
@@ -5,4 +5,8 @@
///
public class Extensions
{
+ ///
+ /// Collection of conentent nodes read by extension-aware readers.
+ ///
+ public List Contents { get; set; } = new List();
}
diff --git a/src/Sustainsys.Saml2/Constants.cs b/src/Sustainsys.Saml2/Constants.cs
index ac2388b5f..b0f4f6865 100644
--- a/src/Sustainsys.Saml2/Constants.cs
+++ b/src/Sustainsys.Saml2/Constants.cs
@@ -215,6 +215,22 @@ public static class Elements
/// PDPDescriptor XML element name.
///
public const string PDPDescriptor = nameof(PDPDescriptor);
+
+ ///
+ /// AuthnRequest XML element name.
+ ///
+ public const string AuthnRequest = nameof(AuthnRequest);
+
+
+ ///
+ /// Subject XML element name
+ ///
+ public const string Subject = nameof(Subject);
+
+ ///
+ /// NameID element name
+ ///
+ public const string NameID = nameof(NameID);
}
///
@@ -304,5 +320,36 @@ public static class AttributeNames
/// Destination attribute name.
///
public const string Destination = nameof(Destination);
+
+ ///
+ /// AssertionConsumerServiceURL attribute name.
+ ///
+ public const string AssertionConsumerServiceURL = nameof(AssertionConsumerServiceURL);
+
+
+ ///
+ /// ForceAuthn attribute name
+ ///
+ public const string ForceAuthn = nameof(ForceAuthn);
+
+ ///
+ /// IsPassive attribute name
+ ///
+ public const string IsPassive = nameof(IsPassive);
+
+ ///
+ /// AssertionConsumerServiceIndex attribute name
+ ///
+ public const string AssertionConsumerServiceIndex = nameof(AssertionConsumerServiceIndex);
+
+ ///
+ /// ProtocolBinding attribute name
+ ///
+ public const string ProtocolBinding = nameof(ProtocolBinding);
+
+ ///
+ /// Consent attribute name
+ ///
+ public const string Consent = nameof(Consent);
}
}
diff --git a/src/Sustainsys.Saml2/Saml/Subject.cs b/src/Sustainsys.Saml2/Saml/Subject.cs
new file mode 100644
index 000000000..955b8a741
--- /dev/null
+++ b/src/Sustainsys.Saml2/Saml/Subject.cs
@@ -0,0 +1,20 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Sustainsys.Saml2.Saml;
+
+///
+/// A Saml2 Subject, see core 2.4.1.
+///
+public class Subject
+{
+ ///
+ /// NameId
+ ///
+ public NameId? NameId { get; set; }
+
+ // TODO: Support full Subject, including SubjectConfirmation
+}
diff --git a/src/Sustainsys.Saml2/Samlp/AuthnRequest.cs b/src/Sustainsys.Saml2/Samlp/AuthnRequest.cs
index e1de00b6b..76ed69b35 100644
--- a/src/Sustainsys.Saml2/Samlp/AuthnRequest.cs
+++ b/src/Sustainsys.Saml2/Samlp/AuthnRequest.cs
@@ -1,4 +1,6 @@
-namespace Sustainsys.Saml2.Samlp;
+using Sustainsys.Saml2.Saml;
+
+namespace Sustainsys.Saml2.Samlp;
///
/// Authentication request
@@ -9,4 +11,30 @@ public class AuthnRequest : RequestAbstractType
/// The assertion consumer service Url where the response should be posted back to.
///
public string? AssertionConsumerServiceUrl { get; set; }
+
+ ///
+ /// Identifying Uri for protocol binding
+ ///
+ public string? ProtocolBinding { get; set; }
+
+ ///
+ /// Requested Subject
+ ///
+ public Subject? Subject { get; set; }
+
+ ///
+ /// Indicates that the identity provider should force (re)authentication and not
+ /// rely on an existing session to do single sign on.
+ ///
+ public bool ForceAuthn { get; set; } = false;
+
+ ///
+ /// Indicates that the identity provider should not show any UI nor require interaction.
+ ///
+ public bool IsPassive { get; set; } = false;
+
+ ///
+ /// Index of assertion consumer service where the SP wants the response.
+ ///
+ public int? AssertionConsumerServiceIndex { get; set; }
}
diff --git a/src/Sustainsys.Saml2/Samlp/RequestAbstractType.cs b/src/Sustainsys.Saml2/Samlp/RequestAbstractType.cs
index ff74ef1dd..0ceed0946 100644
--- a/src/Sustainsys.Saml2/Samlp/RequestAbstractType.cs
+++ b/src/Sustainsys.Saml2/Samlp/RequestAbstractType.cs
@@ -1,5 +1,6 @@
using Sustainsys.Saml2.Xml;
using Sustainsys.Saml2.Saml;
+using Sustainsys.Saml2.Common;
namespace Sustainsys.Saml2.Samlp;
@@ -27,4 +28,24 @@ public class RequestAbstractType
/// Identifies the entity that generated the request message.
///
public NameId? Issuer { get; set; }
+
+ ///
+ /// Destination Url that the messages is/was sent to.
+ ///
+ public string? Destination { get; set; }
+
+ ///
+ /// URI reference for consent.
+ ///
+ public string? Consent { get; set; }
+
+ ///
+ /// Extensions
+ ///
+ public Extensions? Extensions { get; set; }
+
+ ///
+ /// Trust level, based on signature validation
+ ///
+ public TrustLevel TrustLevel { get; set; }
}
diff --git a/src/Sustainsys.Saml2/Serialization/ISamlXmlReader.cs b/src/Sustainsys.Saml2/Serialization/ISamlXmlReader.cs
index b251ecf1c..82df938ac 100644
--- a/src/Sustainsys.Saml2/Serialization/ISamlXmlReader.cs
+++ b/src/Sustainsys.Saml2/Serialization/ISamlXmlReader.cs
@@ -45,6 +45,7 @@ public interface ISamlXmlReader
/// Read an
///
/// Xml Traverser to read from
+ /// Callback that can inspect and alter errors before throwing
///
- AuthnRequest ReadAuthnRequest(XmlTraverser source);
+ AuthnRequest ReadAuthnRequest(XmlTraverser source, Action>? errorInspector = null);
}
diff --git a/src/Sustainsys.Saml2/Serialization/SamlXmlReader.AuthnRequest.cs b/src/Sustainsys.Saml2/Serialization/SamlXmlReader.AuthnRequest.cs
index c90f0a492..76115a0ad 100644
--- a/src/Sustainsys.Saml2/Serialization/SamlXmlReader.AuthnRequest.cs
+++ b/src/Sustainsys.Saml2/Serialization/SamlXmlReader.AuthnRequest.cs
@@ -1,12 +1,78 @@
using Sustainsys.Saml2.Samlp;
using Sustainsys.Saml2.Xml;
+using static Sustainsys.Saml2.Constants;
namespace Sustainsys.Saml2.Serialization;
public partial class SamlXmlReader
{
+ ///
+ /// Create an empty AuthnRequest instance
+ ///
+ /// AuthnRequest
+ protected virtual AuthnRequest CreateAuthnRequest() => new();
+
+ //TODO: Convert other reads to follow this pattern with a callback for errors
+
///
- public virtual AuthnRequest ReadAuthnRequest(XmlTraverser source)
+ public virtual AuthnRequest ReadAuthnRequest(
+ XmlTraverser source,
+ Action>? errorInspector = null)
+ {
+ var authnRequest = ReadAuthnRequest(source);
+
+ source.ThrowOnErrors();
+
+ return authnRequest;
+ }
+
+ ///
+ /// Read an
+ ///
+ /// Xml Traverser to read from
+ /// The AuthnRequest read
+ protected virtual AuthnRequest ReadAuthnRequest(XmlTraverser source)
+ {
+ var authnRequest = CreateAuthnRequest();
+
+ if (source.EnsureName(Namespaces.SamlpUri, Elements.AuthnRequest))
+ {
+ ReadAttributes(source, authnRequest);
+ ReadElements(source.GetChildren(), authnRequest);
+ }
+
+ source.MoveNext(true);
+
+ return authnRequest;
+ }
+
+ ///
+ /// Reads the child elements of an AuthnRequest.
+ ///
+ /// Xml traverser to read from
+ /// AuthnRequest to pupulate
+ protected virtual void ReadElements(XmlTraverser source, AuthnRequest authnRequest)
{
- throw new NotImplementedException();
+ ReadElements(source, (RequestAbstractType)authnRequest);
+
+ if (source.HasName(Namespaces.SamlUri, Elements.Subject))
+ {
+ authnRequest.Subject = ReadSubject(source);
+ }
+ }
+
+ ///
+ /// Reads attributes of an AuthnRequest
+ ///
+ /// Xml Traverser to read from
+ /// The AuthnRequest to populate
+ protected virtual void ReadAttributes(XmlTraverser source, AuthnRequest authnRequest)
+ {
+ ReadAttributes(source, (RequestAbstractType)authnRequest);
+
+ authnRequest.ForceAuthn = source.GetBoolAttribute(AttributeNames.ForceAuthn) ?? authnRequest.ForceAuthn;
+ authnRequest.IsPassive = source.GetBoolAttribute(AttributeNames.IsPassive) ?? authnRequest.IsPassive;
+ authnRequest.AssertionConsumerServiceIndex = source.GetIntAttribute(AttributeNames.AssertionConsumerServiceIndex);
+ authnRequest.AssertionConsumerServiceUrl = source.GetAttribute(AttributeNames.AssertionConsumerServiceURL);
+ authnRequest.ProtocolBinding = source.GetAttribute(AttributeNames.ProtocolBinding);
}
}
diff --git a/src/Sustainsys.Saml2/Serialization/SamlXmlReader.EndPoint.cs b/src/Sustainsys.Saml2/Serialization/SamlXmlReader.EndPoint.cs
index 8fa3cbe3e..e8b61fb9b 100644
--- a/src/Sustainsys.Saml2/Serialization/SamlXmlReader.EndPoint.cs
+++ b/src/Sustainsys.Saml2/Serialization/SamlXmlReader.EndPoint.cs
@@ -5,6 +5,18 @@
namespace Sustainsys.Saml2.Serialization;
partial class SamlXmlReader
{
+ ///
+ /// Factory for Endpoint.
+ ///
+ /// Created Endpoint
+ protected virtual Endpoint CreateEndpoint() => new();
+
+ ///
+ /// Factory for indexed endpoint
+ ///
+ /// Created IndexedEndpoint
+ protected virtual IndexedEndpoint CreateIndexedEndpoint() => new();
+
///
/// Reads an endpoint.
///
@@ -12,7 +24,7 @@ partial class SamlXmlReader
/// Endpoint read
protected virtual Endpoint ReadEndpoint(XmlTraverser source)
{
- var result = new Endpoint();
+ var result = CreateEndpoint();
ReadAttributes(source, result);
return result;
@@ -36,11 +48,9 @@ protected virtual void ReadAttributes(XmlTraverser source, Endpoint endpoint)
/// IndexedEndpoint
protected virtual IndexedEndpoint ReadIndexedEndpoint(XmlTraverser source)
{
- var result = new IndexedEndpoint()
- {
- Index = source.GetRequiredIntAttribute(AttributeNames.index),
- IsDefault = source.GetBoolAttribute(AttributeNames.isDefault) ?? false
- };
+ var result = CreateIndexedEndpoint();
+ result.Index = source.GetRequiredIntAttribute(AttributeNames.index);
+ result.IsDefault = source.GetBoolAttribute(AttributeNames.isDefault) ?? false;
ReadAttributes(source, result);
diff --git a/src/Sustainsys.Saml2/Serialization/SamlXmlReader.EntityDescriptor.cs b/src/Sustainsys.Saml2/Serialization/SamlXmlReader.EntityDescriptor.cs
index 30a4b84ef..78ec1fe85 100644
--- a/src/Sustainsys.Saml2/Serialization/SamlXmlReader.EntityDescriptor.cs
+++ b/src/Sustainsys.Saml2/Serialization/SamlXmlReader.EntityDescriptor.cs
@@ -76,16 +76,16 @@ protected virtual void ReadElements(XmlTraverser source, EntityDescriptor entity
{
switch (source.CurrentNode?.LocalName)
{
- case Constants.Elements.RoleDescriptor:
+ case Elements.RoleDescriptor:
entityDescriptor.RoleDescriptors.Add(ReadRoleDescriptor(source));
break;
- case Constants.Elements.IDPSSODescriptor:
+ case Elements.IDPSSODescriptor:
entityDescriptor.RoleDescriptors.Add(ReadIDPSSODescriptor(source));
break;
- case Constants.Elements.SPSSODescriptor:
- case Constants.Elements.AuthnAuthorityDescriptor:
- case Constants.Elements.AttributeAuthorityDescriptor:
- case Constants.Elements.PDPDescriptor:
+ case Elements.SPSSODescriptor:
+ case Elements.AuthnAuthorityDescriptor:
+ case Elements.AttributeAuthorityDescriptor:
+ case Elements.PDPDescriptor:
source.IgnoreChildren();
break;
default:
diff --git a/src/Sustainsys.Saml2/Serialization/SamlXmlReader.IDPSSODescriptor.cs b/src/Sustainsys.Saml2/Serialization/SamlXmlReader.IDPSSODescriptor.cs
index 3f6bce306..7ce26fdab 100644
--- a/src/Sustainsys.Saml2/Serialization/SamlXmlReader.IDPSSODescriptor.cs
+++ b/src/Sustainsys.Saml2/Serialization/SamlXmlReader.IDPSSODescriptor.cs
@@ -30,19 +30,19 @@ protected virtual IDPSSODescriptor ReadIDPSSODescriptor(XmlTraverser source)
///
/// Read attributes of IDPSSODescriptor.
///
- /// Source
+ /// Xml traverser to read from
/// Result
protected virtual void ReadAttributes(XmlTraverser source, IDPSSODescriptor result)
{
- result.WantAuthnRequestsSigned = source.GetBoolAttribute(AttributeNames.WantAuthnRequestsSigned) ?? false;
-
ReadAttributes(source, (SSODescriptor)result);
+
+ result.WantAuthnRequestsSigned = source.GetBoolAttribute(AttributeNames.WantAuthnRequestsSigned) ?? false;
}
///
/// Read child elements of IDPSSODescriptor
///
- ///
+ /// Xml traverser to read from
///
protected virtual void ReadElements(XmlTraverser source, IDPSSODescriptor result)
{
diff --git a/src/Sustainsys.Saml2/Serialization/SamlXmlReader.RequestAbstractType.cs b/src/Sustainsys.Saml2/Serialization/SamlXmlReader.RequestAbstractType.cs
new file mode 100644
index 000000000..c7e88eab2
--- /dev/null
+++ b/src/Sustainsys.Saml2/Serialization/SamlXmlReader.RequestAbstractType.cs
@@ -0,0 +1,58 @@
+using Microsoft.AspNetCore.Antiforgery;
+using Sustainsys.Saml2.Samlp;
+using Sustainsys.Saml2.Xml;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using static Sustainsys.Saml2.Constants;
+
+namespace Sustainsys.Saml2.Serialization;
+public partial class SamlXmlReader
+{
+ ///
+ /// Read attributes of
+ ///
+ /// Xml travers
+ /// RequestAbstractType
+ protected virtual void ReadAttributes(XmlTraverser source, RequestAbstractType request)
+ {
+ request.Id = source.GetRequiredAttribute(AttributeNames.ID);
+ request.IssueInstant = source.GetRequiredDateTimeAttribute(AttributeNames.IssueInstant);
+ request.Version = source.GetRequiredAttribute(AttributeNames.Version);
+ request.Destination = source.GetAttribute(AttributeNames.Destination);
+ request.Consent = source.GetAttribute(AttributeNames.Consent);
+ }
+
+ ///
+ /// Reads the child elements of a RequestAbstractType
+ ///
+ ///
+ ///
+ protected virtual void ReadElements(XmlTraverser source, RequestAbstractType request)
+ {
+ source.MoveNext(true);
+
+ if (source.HasName(Namespaces.SamlUri, Elements.Issuer))
+ {
+ request.Issuer = ReadNameId(source);
+ source.MoveNext(true);
+ }
+
+ (var trustedSigningKeys, var allowedHashAlgorithms) =
+ GetSignatureValidationParametersFromIssuer(source, request.Issuer);
+
+ if (source.ReadAndValidateOptionalSignature(trustedSigningKeys, allowedHashAlgorithms, out var trustLevel))
+ {
+ request.TrustLevel = trustLevel;
+ source.MoveNext();
+ }
+
+ if (source.HasName(Namespaces.SamlpUri, Elements.Extensions))
+ {
+ request.Extensions = ReadExtensions(source);
+ source.MoveNext(true);
+ }
+ }
+}
diff --git a/src/Sustainsys.Saml2/Serialization/SamlXmlReader.RoleDescriptor.cs b/src/Sustainsys.Saml2/Serialization/SamlXmlReader.RoleDescriptor.cs
index 3d8f3c838..79ad72a0b 100644
--- a/src/Sustainsys.Saml2/Serialization/SamlXmlReader.RoleDescriptor.cs
+++ b/src/Sustainsys.Saml2/Serialization/SamlXmlReader.RoleDescriptor.cs
@@ -47,7 +47,7 @@ protected virtual void ReadAttributes(XmlTraverser source, RoleDescriptor result
/// Source data
/// Target to set properties on
/// More elements available?
- protected virtual bool ReadElements(XmlTraverser source, RoleDescriptor result)
+ protected virtual void ReadElements(XmlTraverser source, RoleDescriptor result)
{
source.MoveNext(true);
@@ -88,8 +88,6 @@ protected virtual bool ReadElements(XmlTraverser source, RoleDescriptor result)
source.MoveNext(true);
}
-
- return true;
}
}
diff --git a/src/Sustainsys.Saml2/Serialization/SamlXmlReader.SSODescriptor.cs b/src/Sustainsys.Saml2/Serialization/SamlXmlReader.SSODescriptor.cs
index 6e01eaa5a..ef6333ee7 100644
--- a/src/Sustainsys.Saml2/Serialization/SamlXmlReader.SSODescriptor.cs
+++ b/src/Sustainsys.Saml2/Serialization/SamlXmlReader.SSODescriptor.cs
@@ -20,13 +20,9 @@ protected virtual void ReadAttributes(XmlTraverser source, SSODescriptor result)
///
/// Source data
/// Target to set properties on
- /// More elements available?
- protected virtual bool ReadElements(XmlTraverser source, SSODescriptor result)
+ protected virtual void ReadElements(XmlTraverser source, SSODescriptor result)
{
- if(!ReadElements(source, (RoleDescriptor)result))
- {
- return false;
- }
+ ReadElements(source, (RoleDescriptor)result);
while (source.HasName(Namespaces.MetadataUri, Elements.ArtifactResolutionService))
{
@@ -50,7 +46,5 @@ protected virtual bool ReadElements(XmlTraverser source, SSODescriptor result)
source.MoveNext(true);
}
-
- return true;
}
}
\ No newline at end of file
diff --git a/src/Sustainsys.Saml2/Serialization/SamlXmlReader.StatusResponseType.cs b/src/Sustainsys.Saml2/Serialization/SamlXmlReader.StatusResponseType.cs
index b1c421e73..952f55c74 100644
--- a/src/Sustainsys.Saml2/Serialization/SamlXmlReader.StatusResponseType.cs
+++ b/src/Sustainsys.Saml2/Serialization/SamlXmlReader.StatusResponseType.cs
@@ -1,4 +1,5 @@
-using Sustainsys.Saml2.Samlp;
+using Sustainsys.Saml2.Saml;
+using Sustainsys.Saml2.Samlp;
using Sustainsys.Saml2.Xml;
using System.Security.Cryptography.Xml;
using static Sustainsys.Saml2.Constants;
@@ -29,9 +30,6 @@ protected virtual void ReadElements(XmlTraverser source, StatusResponseType resp
{
source.MoveNext();
- var trustedSigningKeys = TrustedSigningKeys;
- var allowedHashAlgorithms = AllowedHashAlgorithms;
-
if (source.HasName(Namespaces.SamlUri, Elements.Issuer))
{
response.Issuer = ReadNameId(source);
@@ -39,23 +37,8 @@ protected virtual void ReadElements(XmlTraverser source, StatusResponseType resp
source.MoveNext();
}
- if (source.HasName(SignedXml.XmlDsigNamespaceUrl, Elements.Signature))
- {
- if (response.Issuer == null)
- {
- source.Errors.Add(new(ErrorReason.MissingElement, Elements.Issuer, source.CurrentNode,
- "A signature was found, but there was no Issuer specified. See profile spec 4.1.4.2, 4.4.4.2"));
- }
- else
- {
- if (EntityResolver != null)
- {
- var entity = EntityResolver(response.Issuer.Value);
- trustedSigningKeys = entity.TrustedSigningKeys;
- allowedHashAlgorithms = entity.AllowedHashAlgorithms ?? AllowedHashAlgorithms;
- }
- }
- }
+ (var trustedSigningKeys, var allowedHashAlgorithms) =
+ GetSignatureValidationParametersFromIssuer(source, response.Issuer);
if (source.ReadAndValidateOptionalSignature(trustedSigningKeys, allowedHashAlgorithms, out var trustLevel))
{
diff --git a/src/Sustainsys.Saml2/Serialization/SamlXmlReader.Subject.cs b/src/Sustainsys.Saml2/Serialization/SamlXmlReader.Subject.cs
new file mode 100644
index 000000000..96d85b21a
--- /dev/null
+++ b/src/Sustainsys.Saml2/Serialization/SamlXmlReader.Subject.cs
@@ -0,0 +1,61 @@
+using Sustainsys.Saml2.Metadata;
+using Sustainsys.Saml2.Saml;
+using Sustainsys.Saml2.Xml;
+using System.Diagnostics;
+using static Sustainsys.Saml2.Constants;
+
+namespace Sustainsys.Saml2.Serialization;
+partial class SamlXmlReader
+{
+ ///
+ /// Factory for Subject
+ ///
+ protected virtual Subject CreateSubject() => new();
+
+ ///
+ /// Reads a Subject.
+ ///
+ /// Source data
+ /// Subject read
+ protected virtual Subject ReadSubject(XmlTraverser source)
+ {
+ var result = CreateSubject();
+
+ ReadAttributes(source, result);
+
+ ReadElements(source.GetChildren(), result);
+
+ return result;
+ }
+
+ ///
+ /// Extension point to add reading of attributes for Subject
+ ///
+ /// Source
+ /// Subject
+ protected virtual void ReadAttributes(XmlTraverser source, Subject subject)
+ {
+ }
+
+ ///
+ /// Reads elements of a subject.
+ ///
+ /// Source Xml Reader
+ /// Subject to populate
+ protected virtual void ReadElements(XmlTraverser source, Subject subject)
+ {
+ source.MoveNext(true);
+
+ if (source.HasName(Namespaces.SamlUri, Elements.NameID))
+ {
+ subject.NameId = ReadNameId(source);
+ source.MoveNext(true);
+ }
+ else
+ {
+ // TODO: Support BaseID and EncryptedID
+ }
+
+ // TODO: Support SubjectConfirmation
+ }
+}
diff --git a/src/Sustainsys.Saml2/Serialization/SamlXmlReader.cs b/src/Sustainsys.Saml2/Serialization/SamlXmlReader.cs
index 17b5777c7..ad0e8a60d 100644
--- a/src/Sustainsys.Saml2/Serialization/SamlXmlReader.cs
+++ b/src/Sustainsys.Saml2/Serialization/SamlXmlReader.cs
@@ -1,11 +1,14 @@
using Sustainsys.Saml2.Common;
+using Sustainsys.Saml2.Saml;
using Sustainsys.Saml2.Xml;
using System;
using System.Collections.Generic;
+using System.Collections.ObjectModel;
using System.Linq;
using System.Security.Cryptography.Xml;
using System.Text;
using System.Threading.Tasks;
+using static Sustainsys.Saml2.Constants;
namespace Sustainsys.Saml2.Serialization;
@@ -16,13 +19,16 @@ public partial class SamlXmlReader : ISamlXmlReader
{
///
public virtual IEnumerable? AllowedHashAlgorithms { get; set; } =
- new[]
+ defaultAllowedHashAlgorithms;
+
+ private static IEnumerable defaultAllowedHashAlgorithms =
+ new ReadOnlyCollection(new[]
{
SignedXml.XmlDsigRSASHA256Url,
SignedXml.XmlDsigRSASHA384Url,
SignedXml.XmlDsigRSASHA512Url,
SignedXml.XmlDsigDSAUrl
- };
+ });
///
public virtual IEnumerable? TrustedSigningKeys { get; set; }
@@ -36,4 +42,41 @@ public partial class SamlXmlReader : ISamlXmlReader
///
protected virtual void ThrowOnErrors(XmlTraverser source)
=> source.ThrowOnErrors();
+
+ ///
+ /// Helper method to get the signing keys and allowed signature algorithms for
+ /// an issuer.
+ ///
+ /// Xml Travers source - used to report errors
+ /// The issuer to find parameters for
+ ///
+ /// Trusted signig keys and allowedHashAlgorithms. Hash algorithms uses the
+ /// if there is no specific configuration on the Issuer.
+ ///
+ protected (IEnumerable? trustedSigningKeys, IEnumerable? allowedHashAlgorithms)
+ GetSignatureValidationParametersFromIssuer(XmlTraverser source, NameId? issuer)
+ {
+ var trustedSigningKeys = TrustedSigningKeys;
+ var allowedHashAlgorithms = AllowedHashAlgorithms;
+ if (source.HasName(SignedXml.XmlDsigNamespaceUrl, Elements.Signature))
+ {
+ if (issuer == null)
+ {
+ source.Errors.Add(new(ErrorReason.MissingElement, Elements.Issuer, source.CurrentNode,
+ "A signature was found, but there was no Issuer specified. See profile spec 4.1.4.1, 4.1.4.2, 4.4.4.2"));
+ }
+ else
+ {
+ if (EntityResolver != null)
+ {
+ var entity = EntityResolver(issuer.Value);
+ trustedSigningKeys = entity.TrustedSigningKeys;
+ allowedHashAlgorithms = entity.AllowedHashAlgorithms ?? AllowedHashAlgorithms;
+ }
+ }
+ }
+
+ return (trustedSigningKeys, allowedHashAlgorithms);
+ }
+
}
diff --git a/src/Sustainsys.Saml2/Serialization/SamlXmlWriter.AuthnRequest.cs b/src/Sustainsys.Saml2/Serialization/SamlXmlWriter.AuthnRequest.cs
index e2df99998..062b8bb8c 100644
--- a/src/Sustainsys.Saml2/Serialization/SamlXmlWriter.AuthnRequest.cs
+++ b/src/Sustainsys.Saml2/Serialization/SamlXmlWriter.AuthnRequest.cs
@@ -23,8 +23,8 @@ public virtual XmlDocument Write(AuthnRequest authnRequest)
/// AuthnRequest
protected virtual void Append(XmlNode node, AuthnRequest authnRequest)
{
- var xe = Append(node, authnRequest, "AuthnRequest");
- xe.SetAttributeIfValue("AssertionConsumerServiceURL", authnRequest.AssertionConsumerServiceUrl);
+ var xe = Append(node, authnRequest, Elements.AuthnRequest);
+ xe.SetAttributeIfValue(AttributeNames.AssertionConsumerServiceURL, authnRequest.AssertionConsumerServiceUrl);
AppendIfValue(xe, authnRequest.Issuer, Namespaces.Saml, Elements.Issuer);
}
}
diff --git a/src/Sustainsys.Saml2/Xml/XmlTraverser.cs b/src/Sustainsys.Saml2/Xml/XmlTraverser.cs
index 9934b4755..040b40227 100644
--- a/src/Sustainsys.Saml2/Xml/XmlTraverser.cs
+++ b/src/Sustainsys.Saml2/Xml/XmlTraverser.cs
@@ -66,9 +66,9 @@ private XmlTraverser(XmlTraverser parent, List errors)
Errors = errors;
}
- private void AddError(ErrorReason reason, string message)
+ private void AddError(ErrorReason reason, string message, string? localName = null)
{
- Errors.Add(new(reason, CurrentNode!.LocalName, CurrentNode, message));
+ Errors.Add(new(reason, localName ?? CurrentNode!.LocalName, CurrentNode, message));
}
//TODO: Add callback function as parameter that allows ignoring - easier way to wire up with events.
@@ -344,7 +344,8 @@ public string GetRequiredAttribute(string localName)
{
AddError(
ErrorReason.MissingAttribute,
- $"Required attribute {localName} not found on {CurrentNode.Name}.");
+ $"Required attribute {localName} not found on {CurrentNode.Name}.",
+ localName);
}
return value!;
@@ -425,7 +426,7 @@ public DateTime GetRequiredDateTimeAttribute(string localName)
=> TryGetAttribute(localName, s => Enum.Parse(s, ignoreCase));
///
- /// Get a required attribute as int. On missing attribute or parse errors the Error
+ /// Get a required int attribute. On missing attribute or parse errors the error
/// is reported to the errors collection.
///
/// Local name of the attribute
@@ -433,6 +434,15 @@ public DateTime GetRequiredDateTimeAttribute(string localName)
public int GetRequiredIntAttribute(string localName)
=> GetRequiredAttribute(localName, int.Parse);
+ ///
+ /// Get an optional int attribute. On parse errors the error is reported to the
+ /// errors collection
+ ///
+ /// the local name of the attribute
+ /// Int, if available
+ public int? GetIntAttribute(string localName)
+ => TryGetAttribute(localName, int.Parse);
+
private TTarget GetRequiredAttribute(string localName, Func converter)
where TTarget : struct
{
diff --git a/src/Tests/Sustainsys.Saml2.AspNetCore.Tests/Saml2HandlerTests.cs b/src/Tests/Sustainsys.Saml2.AspNetCore.Tests/Saml2HandlerTests.cs
index 9465b30b3..e417f9987 100644
--- a/src/Tests/Sustainsys.Saml2.AspNetCore.Tests/Saml2HandlerTests.cs
+++ b/src/Tests/Sustainsys.Saml2.AspNetCore.Tests/Saml2HandlerTests.cs
@@ -34,7 +34,8 @@ public class Saml2HandlerTests
var scheme = new AuthenticationScheme("Saml2", "Saml2", typeof(Saml2Handler));
var httpContext = Substitute.For();
- httpContext.Request.Returns(Substitute.For());
+ httpContext.Response.HttpContext.Returns(httpContext);
+
httpContext.Request.Scheme = "https";
httpContext.Request.Host = new HostString("sp.example.com", 8888);
httpContext.Request.Path = "/path";
@@ -116,29 +117,30 @@ public async Task ChallengeSetsRedirect()
(var subject, var httpContext) = await CreateSubject(options);
var props = new AuthenticationProperties();
+
+ httpContext.Response.Redirect(Arg.Do(validateLocation));
+ bool validated = false;
+
await subject.ChallengeAsync(props);
- //bool validated = false;
-
void validateLocation(string location)
{
- location.Should().StartWith("https://idp.example.com/sso?SamlRequest=");
+ location.Should().StartWith("https://idp.example.com/sso?SAMLRequest=");
- var message = new HttpRedirectBinding().UnBindAsync(location, _ => throw new NotImplementedException());
+ var message = new HttpRedirectBinding().UnBindAsync(location, _ => throw new NotImplementedException()).Result;
var deserializedAuthnRequest = new SamlXmlReader()
.ReadAuthnRequest(message.Xml.GetXmlTraverser());
deserializedAuthnRequest.Should().BeEquivalentTo(authnRequest);
- // validated = true;
+ validated = true;
}
- httpContext.Response.Received().Redirect(Arg.Do(validateLocation));
+ httpContext.Response.Received().Redirect(Arg.Any());
- // TODO: Check that validation was called.
- //validated.Should().BeTrue();
+ validated.Should().BeTrue("The validation should have been called.");
}
diff --git a/src/Tests/Sustainsys.Saml2.Tests/Bindings/HttpRedirectBindingTests.cs b/src/Tests/Sustainsys.Saml2.Tests/Bindings/HttpRedirectBindingTests.cs
new file mode 100644
index 000000000..a8224b22c
--- /dev/null
+++ b/src/Tests/Sustainsys.Saml2.Tests/Bindings/HttpRedirectBindingTests.cs
@@ -0,0 +1,127 @@
+using Microsoft.AspNetCore.Http;
+using NSubstitute;
+using Sustainsys.Saml2.Bindings;
+using System;
+using System.ComponentModel;
+using System.IO;
+using System.IO.Compression;
+using System.Threading.Tasks;
+using System.Xml;
+
+namespace Sustainsys.Saml2.Tests.Bindings;
+
+public class HttpRedirectBindingTests
+{
+ private const string Xml = " ";
+
+ [Fact]
+ public async Task Bind()
+ {
+ var xd = new XmlDocument();
+ xd.LoadXml(Xml);
+
+ var message = new Saml2Message
+ {
+ Name = "SamlRequest",
+ Xml = xd.DocumentElement!,
+ Destination = "https://example.com/destination",
+ RelayState = "someRelayState"
+ };
+
+ var subject = new HttpRedirectBinding();
+
+ var httpResponse = Substitute.For();
+
+ bool validated = false;
+
+ httpResponse.Redirect(Arg.Do(validateUrl));
+
+ await subject.BindAsync(httpResponse, message);
+
+ void validateUrl(string url)
+ {
+ url.Should().StartWith(message.Destination);
+
+ Uri uri = new Uri(url);
+
+ var query = uri.Query.TrimStart('?').Split("&");
+
+ var expectedParam = $"{message!.Name}=";
+
+ query[0].Should().StartWith(expectedParam);
+
+ var value = query[0][expectedParam.Length..];
+
+ using var inflated = new MemoryStream(Convert.FromBase64String(Uri.UnescapeDataString(value)));
+ using var deflateStream = new DeflateStream(inflated, CompressionMode.Decompress);
+ using var reader = new StreamReader(deflateStream);
+
+ reader.ReadToEnd().Should().Be(Xml);
+
+ query[1].Should().Be("RelayState=someRelayState");
+
+ validated = true;
+ }
+
+ httpResponse.Received().Redirect(Arg.Any());
+
+ validated.Should().BeTrue();
+ }
+ private static string GetEncodedXml()
+ {
+ using var compressed = new MemoryStream();
+ using (var deflateStream = new DeflateStream(compressed, CompressionLevel.Optimal))
+ {
+ using var writer = new StreamWriter(deflateStream);
+ writer.Write(Xml);
+ }
+
+ return Uri.EscapeDataString(Convert.ToBase64String(compressed.ToArray()));
+ }
+
+ [Theory]
+ [InlineData(Constants.SamlRequest, null)]
+ [InlineData(Constants.SamlResponse, null)]
+ [InlineData("Invalid", "SAMLResponse or SAMLRequest parameter not found")]
+ [InlineData("SAMLRequest=x&SAMLResponse", "Duplicate message parameters*")]
+ [InlineData("SAMLRequest=x&SAMLRequest", "Duplicate message parameters found*")]
+ [InlineData("SAMLResponse=x&SAMLRequest", "Duplicate message parameters found: SAMLResponse, SAMLRequest")]
+ [InlineData("RelayState=x&SAMLRequest", "Duplicate RelayState parameters found")]
+ public async Task UnBind_String(string messageName, string? expectedException)
+ {
+ var encoded = GetEncodedXml();
+
+ var url = $"https://idp.example.com/sso?{messageName}={encoded}&RelayState=xyz123";
+
+ var subject = new HttpRedirectBinding();
+
+ var xd = new XmlDocument();
+ xd.LoadXml(Xml);
+
+ var expected = new Saml2Message
+ {
+ Destination = "https://idp.example.com/sso",
+ Name = messageName,
+ RelayState = "xyz123",
+ Xml = xd.DocumentElement!
+ };
+
+ bool caughtException = false;
+ Saml2Message actual = default!;
+ try
+ {
+ actual = await subject.UnBindAsync(url, x => Task.FromResult(new Saml2Entity()));
+ }
+ catch (Exception ex)
+ {
+ caughtException = true;
+ ex.Message.Should().Match(expectedException);
+ }
+
+ if (!caughtException)
+ {
+ expectedException.Should().BeNull();
+ actual.Should().BeEquivalentTo(expected);
+ }
+ }
+}
diff --git a/src/Tests/Sustainsys.Saml2.Tests/Bindings/RedirectBindingTests.cs b/src/Tests/Sustainsys.Saml2.Tests/Bindings/RedirectBindingTests.cs
deleted file mode 100644
index 07bc80a84..000000000
--- a/src/Tests/Sustainsys.Saml2.Tests/Bindings/RedirectBindingTests.cs
+++ /dev/null
@@ -1,61 +0,0 @@
-using Microsoft.AspNetCore.Http;
-using NSubstitute;
-using Sustainsys.Saml2.Bindings;
-using System;
-using System.IO;
-using System.IO.Compression;
-using System.Threading.Tasks;
-using System.Xml;
-
-namespace Sustainsys.Saml2.Tests.Bindings;
-
-public class RedirectBindingTests
-{
- private const string Xml = " ";
-
- [Fact]
- public async Task Bind()
- {
- var xd = new XmlDocument();
- xd.LoadXml(Xml);
-
- var message = new Saml2Message
- {
- Name = "SamlRequest",
- Xml = xd.DocumentElement!,
- Destination = "https://example.com/destination",
- RelayState = "someRelayState"
- };
-
- var subject = new HttpRedirectBinding();
-
- var httpResponse = Substitute.For();
-
- await subject.BindAsync(httpResponse, message);
-
- void validateUrl(string url)
- {
- url.Should().StartWith(message.Destination);
-
- Uri uri = new Uri(url);
-
- var query = uri.Query.Split("&");
-
- var expectedParam = $"{message!.Name}=";
-
- query[0].StartsWith(expectedParam).Should().BeTrue();
-
- var value = query[0][expectedParam.Length..];
-
- using var inflated = new MemoryStream(Convert.FromBase64String(Uri.UnescapeDataString(value)));
- using var deflateStream = new DeflateStream(inflated, CompressionMode.Decompress);
- using var reader = new StreamReader(deflateStream);
-
- reader.ReadToEnd().Should().Be(Xml);
-
- query[1].Should().Be("RelayState=someRelayState");
- }
-
- httpResponse.Received().Redirect(Arg.Do(validateUrl));
- }
-}
diff --git a/src/Tests/Sustainsys.Saml2.Tests/Serialization/SamlXmlReaderTests.Assertion.cs b/src/Tests/Sustainsys.Saml2.Tests/Serialization/SamlXmlReaderTests.Assertion.cs
new file mode 100644
index 000000000..1989576e1
--- /dev/null
+++ b/src/Tests/Sustainsys.Saml2.Tests/Serialization/SamlXmlReaderTests.Assertion.cs
@@ -0,0 +1,10 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Sustainsys.Saml2.Tests.Serialization;
+public partial class SamlXmlReaderTests
+{
+}
diff --git a/src/Tests/Sustainsys.Saml2.Tests/Serialization/SamlXmlReaderTests.AuthnRequest.cs b/src/Tests/Sustainsys.Saml2.Tests/Serialization/SamlXmlReaderTests.AuthnRequest.cs
new file mode 100644
index 000000000..fb96b1a40
--- /dev/null
+++ b/src/Tests/Sustainsys.Saml2.Tests/Serialization/SamlXmlReaderTests.AuthnRequest.cs
@@ -0,0 +1,82 @@
+using Sustainsys.Saml2.Saml;
+using Sustainsys.Saml2.Samlp;
+using Sustainsys.Saml2.Serialization;
+using Sustainsys.Saml2.Tests.Helpers;
+using Sustainsys.Saml2.Xml;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Xml;
+
+namespace Sustainsys.Saml2.Tests.Serialization;
+partial class SamlXmlReaderTests
+{
+ [Fact]
+ public void ReadAuthnRequest_Mandatory()
+ {
+ var source = GetXmlTraverser();
+
+ var subject = new SamlXmlReader();
+
+ var actual = subject.ReadAuthnRequest(source);
+
+ var expected = new AuthnRequest
+ {
+ Id = "x123",
+ IssueInstant = new DateTime(2023, 11, 24, 22, 44, 14, DateTimeKind.Utc),
+ Version = "2.0"
+ };
+
+ actual.Should().BeEquivalentTo(expected);
+ }
+
+ // TODO: Test for missing mandatory
+
+ // Test that an AuthnRequest with optional content present can be read. Start with the most common,
+ // add more later.
+ [Fact]
+ public void ReadAuthnRequest_CanReadOptional()
+ {
+ var source = GetXmlTraverser();
+ ((XmlElement)source.CurrentNode!).Sign(
+ TestData.Certificate, source.CurrentNode![Constants.Elements.Issuer, Constants.Namespaces.SamlUri]!);
+
+ var subject = new SamlXmlReader();
+
+ var actual = subject.ReadAuthnRequest(source);
+
+ var expected = new AuthnRequest
+ {
+ Id = "x123",
+ IssueInstant = new DateTime(2023, 11, 24, 22, 44, 14, DateTimeKind.Utc),
+ Version = "2.0",
+ Destination = "https://idp.example.com/Sso",
+ Consent = "urn:oasis:names:tc:SAML:2.0:consent:obtained",
+ Issuer = "https://sp.example.com/Metadata",
+ Extensions = new(),
+ Subject = new()
+ {
+ NameId = "abc12345"
+ },
+ // TODO: Add NameIDPolicy
+ // TODO: Add Conditions
+ // TODO: Add RequestedAuthnContext
+ // TODO: Add Scoping
+ ForceAuthn = true,
+ IsPassive = true,
+ AssertionConsumerServiceUrl = "https://sp.example.com/Acs",
+ AssertionConsumerServiceIndex = 42,
+ ProtocolBinding = Constants.BindingUris.HttpPOST,
+ // TODO: Add AttributeConsumingServiceIndex
+ // TODO: Add ProviderName
+ };
+
+ actual.Should().BeEquivalentTo(expected);
+ }
+
+ // TODO: Test with AssertionConsumerServiceIndex - note mutually exclusive to AcsUrl + Binding
+
+ // TODO: Test error callback
+}
diff --git a/src/Tests/Sustainsys.Saml2.Tests/Serialization/SamlXmlReaderTests.SamlResponse.cs b/src/Tests/Sustainsys.Saml2.Tests/Serialization/SamlXmlReaderTests.SamlResponse.cs
index ad5f9a3eb..f9f2f9876 100644
--- a/src/Tests/Sustainsys.Saml2.Tests/Serialization/SamlXmlReaderTests.SamlResponse.cs
+++ b/src/Tests/Sustainsys.Saml2.Tests/Serialization/SamlXmlReaderTests.SamlResponse.cs
@@ -68,7 +68,7 @@ public void ReadSamlResponse_MissingMandatory(string removeXPath, ErrorReason ex
}
// Test that a response with all optional content present in the Response can be read. This doesn't
- // mean that we actual read everything, a lot of rarely used stuff is just ignored (for now)
+ // mean that we actually read everything, a lot of rarely used stuff is just ignored (for now)
[Fact]
public void ReadSamlResponse_CanReadCompleteResponse()
{
@@ -96,6 +96,7 @@ public void ReadSamlResponse_CanReadCompleteResponse()
}
},
Extensions = new()
+ // TODO: Trustlevel?
};
actual.Should().BeEquivalentTo(expected);
diff --git a/src/Tests/Sustainsys.Saml2.Tests/Serialization/SamlXmlReaderTests/ReadAuthnRequest_CanReadOptional.xml b/src/Tests/Sustainsys.Saml2.Tests/Serialization/SamlXmlReaderTests/ReadAuthnRequest_CanReadOptional.xml
new file mode 100644
index 000000000..dc99bb866
--- /dev/null
+++ b/src/Tests/Sustainsys.Saml2.Tests/Serialization/SamlXmlReaderTests/ReadAuthnRequest_CanReadOptional.xml
@@ -0,0 +1,14 @@
+
+ https://sp.example.com/Metadata
+
+
+ abc12345
+
+
\ No newline at end of file
diff --git a/src/Tests/Sustainsys.Saml2.Tests/Serialization/SamlXmlReaderTests/ReadAuthnRequest_Mandatory.xml b/src/Tests/Sustainsys.Saml2.Tests/Serialization/SamlXmlReaderTests/ReadAuthnRequest_Mandatory.xml
new file mode 100644
index 000000000..e5fc7ca0e
--- /dev/null
+++ b/src/Tests/Sustainsys.Saml2.Tests/Serialization/SamlXmlReaderTests/ReadAuthnRequest_Mandatory.xml
@@ -0,0 +1,2 @@
+
\ No newline at end of file
diff --git a/src/Tests/Sustainsys.Saml2.Tests/Xml/XmlTraverserTests.cs b/src/Tests/Sustainsys.Saml2.Tests/Xml/XmlTraverserTests.cs
index 1788a2c86..aabb1cae7 100644
--- a/src/Tests/Sustainsys.Saml2.Tests/Xml/XmlTraverserTests.cs
+++ b/src/Tests/Sustainsys.Saml2.Tests/Xml/XmlTraverserTests.cs
@@ -3,13 +3,16 @@
using System;
using System.Linq;
using System.Xml;
+using Microsoft.AspNetCore.Authentication.Cookies;
+using Sustainsys.Saml2.Saml;
+using NSubstitute;
namespace Sustainsys.Saml2.Tests.Xml;
public class XmlTraverserTests
{
- private const string xml = "
abc ";
+ private const string xml = "
abc ";
private readonly XmlDocument signedXmlDocument;
@@ -53,26 +56,12 @@ public XmlTraverserTests()
[Theory]
[InlineData("x", "1")]
[InlineData("y", null)]
- [InlineData("z", "5")]
+ [InlineData("five", "5")]
public void GetAttribute(string localName, string expectedValue)
{
GetXmlTraverser().GetAttribute(localName).Should().Be(expectedValue);
}
- private enum GetEnumAttributeEnum
- {
- Xyz = 42
- }
-
- [Fact]
- public void GetEnumAttribute()
-
- {
- GetXmlTraverser().GetEnumAttribute("invalidTimeSpan", true).Should().Be(GetEnumAttributeEnum.Xyz);
-
- GetXmlTraverser().GetEnumAttribute("validTimeSpan", true).Should().Be(null);
- }
-
[Theory]
[InlineData("urn:r", "root")]
[InlineData("urn:X", "root", ErrorReason.UnexpectedNamespace)]
@@ -167,6 +156,37 @@ public void HandlesMixedSupression()
.Which.Errors.Count().Should().Be(2);
}
+ private enum GetEnumAttributeEnum
+ {
+ Def = 42
+ }
+
+ [Fact]
+ public void GetEnumAttribute()
+
+ {
+ GetXmlTraverser().GetEnumAttribute("def", true).Should().Be(GetEnumAttributeEnum.Def);
+ }
+
+ [Fact]
+ public void GetEnumAttribute_Missing()
+ {
+ GetXmlTraverser().GetEnumAttribute("notexisting", true)
+ .Should().BeNull();
+ }
+
+ [Fact]
+ public void GetEnumAttribute_ParseError()
+ {
+ var subject = GetXmlTraverser();
+
+ var actual = subject.GetEnumAttribute("xyz", true);
+
+ actual.Should().BeNull();
+
+ ValidateParseError(subject);
+ }
+
[Fact]
public void GetTimeSpanAttribute()
{
@@ -178,13 +198,22 @@ public void GetTimeSpanAttribute_ParseError()
{
var subject = GetXmlTraverser();
- var actual = subject.GetTimeSpanAttribute("invalidTimeSpan");
+ var actual = subject.GetTimeSpanAttribute("xyz");
- actual.HasValue.Should().BeFalse();
+ actual.Should().BeNull();
+ ValidateParseError(subject);
+ }
+
+ private static void ValidateParseError(XmlTraverser subject)
+ {
subject.Errors.Should().HaveCount(1);
- subject.Errors.Single().Reason.Should().Be(ErrorReason.ConversionFailed);
- subject.Errors.Single().StringValue.Should().Be("XYZ");
+ var error = subject.Errors.Single();
+
+ error.Reason.Should().Be(ErrorReason.ConversionFailed);
+ error.StringValue.Should().Be("XYZ");
+ error.LocalName.Should().Be("xyz");
+ error.Node.Should().BeSameAs(subject.CurrentNode);
}
[Fact]
@@ -193,6 +222,27 @@ public void GetRequiredAbsoluteUriAttribute()
GetXmlTraverser().GetRequiredAbsoluteUriAttribute("uri").Should().Be("urn:uri");
}
+ [Fact]
+ public void GetRequiredAbsoluteUriAttribute_Missing()
+ {
+ var subject = GetXmlTraverser();
+
+ subject.GetRequiredAbsoluteUriAttribute("notExisting").Should().BeNull();
+
+ ValidateMissing(subject);
+ }
+
+ private static void ValidateMissing(XmlTraverser subject)
+ {
+ subject.Errors.Count().Should().Be(1);
+ var error = subject.Errors.Single();
+
+ error.Reason.Should().Be(ErrorReason.MissingAttribute);
+ error.StringValue.Should().BeNull();
+ error.LocalName.Should().Be("notExisting");
+ error.Node.Should().BeSameAs(subject.CurrentNode);
+ }
+
[Fact]
public void GetRequiredAbsoluteUriAttribute_NotAbsoluteUri()
{
@@ -209,6 +259,82 @@ public void GetRequiredAbsoluteUriAttribute_NotAbsoluteUri()
error.StringValue.Should().Be("1");
}
+ [Fact]
+ public void GetBoolAttribute()
+ {
+ GetXmlTraverser().GetBoolAttribute("bool").Should().BeTrue();
+ }
+
+ [Fact]
+ public void GetBoolAttribute_Missing()
+ {
+ GetXmlTraverser().GetBoolAttribute("notExisting").Should().BeNull();
+ }
+
+ [Fact]
+ public void GetBoolAttribute_ParseError()
+ {
+ var subject = GetXmlTraverser();
+
+ var actual = subject.GetBoolAttribute("xyz");
+
+ actual.Should().BeNull();
+
+ ValidateParseError(subject);
+ }
+
+ [Fact]
+ public void GetIntAttribute()
+ {
+ GetXmlTraverser().GetIntAttribute("five").Should().Be(5);
+ }
+
+ [Fact]
+ public void GetIntAttribute_Missing()
+ {
+ GetXmlTraverser().GetIntAttribute("notExisting").Should().BeNull();
+ }
+
+ [Fact]
+ public void GetIntAttribute_ParseError()
+ {
+ var subject = GetXmlTraverser();
+
+ var actual = subject.GetIntAttribute("xyz");
+
+ actual.HasValue.Should().BeFalse();
+
+ ValidateParseError(subject);
+ }
+
+ [Fact]
+ public void GetRequiredIntAttribute()
+ {
+ GetXmlTraverser().GetRequiredIntAttribute("five").Should().Be(5);
+ }
+
+ [Fact]
+ public void GetRequiredIntAttribute_Missing()
+ {
+ var subject = GetXmlTraverser();
+
+ subject.GetRequiredIntAttribute("notExisting").Should().Be(0);
+
+ ValidateMissing(subject);
+ }
+
+ [Fact]
+ public void GetRequiredIntAttribute_ParseError()
+ {
+ var subject = GetXmlTraverser();
+
+ var actual = subject.GetRequiredIntAttribute("xyz");
+
+ actual.Should().Be(default);
+
+ ValidateParseError(subject);
+ }
+
[Fact]
public void TraverseChildren()
{
diff --git a/src/exclusion.dic b/src/exclusion.dic
index e746dd66f..ebc9af6f4 100644
--- a/src/exclusion.dic
+++ b/src/exclusion.dic
@@ -1,2 +1,3 @@
-Saml
-Sustainsys
+saml
+sustainsys
+authn