diff --git a/.gitignore b/.gitignore index 1f5322b..3a3fec5 100644 --- a/.gitignore +++ b/.gitignore @@ -193,3 +193,5 @@ FakesAssemblies/ *.dll Metadata-Test/ +/.vs +/src/.vs/SAML2/v16/Server/sqlite3 diff --git a/src/SAML2.Standard/Bindings/BindingUtility.cs b/src/SAML2.Standard/Bindings/BindingUtility.cs new file mode 100644 index 0000000..7303b05 --- /dev/null +++ b/src/SAML2.Standard/Bindings/BindingUtility.cs @@ -0,0 +1,53 @@ +using SAML2.Config; +using SAML2.Standard; +using System; + +namespace SAML2.Bindings +{ + /// + /// Utility functions for use in binding implementations. + /// + public class BindingUtility + { + /// + /// Validates the SAML20Federation configuration. + /// + /// True if validation passes, false otherwise + public static bool ValidateConfiguration(Saml2Configuration config) + { + if (config == null) + { + throw new ArgumentNullException("config", ErrorMessages.ConfigMissingSaml2Element); + } + + if (config.ServiceProvider == null) + { + throw new ArgumentOutOfRangeException("config", ErrorMessages.ConfigMissingServiceProviderElement); + } + + if (string.IsNullOrEmpty(config.ServiceProvider.Id)) + { + throw new ArgumentOutOfRangeException("config", ErrorMessages.ConfigMissingServiceProviderIdAttribute); + } + + if (config.ServiceProvider.SigningCertificate == null) + { + throw new ArgumentOutOfRangeException("config", ErrorMessages.ConfigMissingSigningCertificateElement); + } + + // This will throw if no certificate or multiple certificates are found + var certificate = config.ServiceProvider.SigningCertificate; + if (!certificate.HasPrivateKey) + { + throw new ArgumentOutOfRangeException("config", ErrorMessages.ConfigSigningCertificateMissingPrivateKey); + } + + if (config.IdentityProviders == null) + { + throw new ArgumentOutOfRangeException("config", ErrorMessages.ConfigMissingIdentityProvidersElement); + } + + return true; + } + } +} \ No newline at end of file diff --git a/src/SAML2.Standard/Bindings/HttpArtifactBindingBuilder.cs b/src/SAML2.Standard/Bindings/HttpArtifactBindingBuilder.cs new file mode 100644 index 0000000..22840f7 --- /dev/null +++ b/src/SAML2.Standard/Bindings/HttpArtifactBindingBuilder.cs @@ -0,0 +1,215 @@ +using System; +using System.IO; +using System.Xml; +using SAML2.Config; +using SAML2.Schema.Protocol; +using SAML2.Standard; +using SAML2.Utils; + +namespace SAML2.Bindings +{ + /// + /// Implementation of the artifact over HTTP SOAP binding. + /// + public class HttpArtifactBindingBuilder : HttpSoapBindingBuilder + { + private readonly Saml2Configuration config; + private readonly Action redirect; + private readonly Action sendResponseMessage; + + + + /// + /// Initializes a new instance of the class. + /// + /// The current http context. + /// Action to perform when redirecting. Parameter will be destination URL + /// Action to send messages to response stream + public HttpArtifactBindingBuilder(Saml2Configuration config, Action redirect, Action sendResponseMessage) + { + this.redirect = redirect ?? throw new ArgumentNullException("redirect"); + this.config = config ?? throw new ArgumentNullException("config"); + this.sendResponseMessage = sendResponseMessage ?? throw new ArgumentNullException("sendResponseMessage"); + } + + /// + /// Creates an artifact and redirects the user to the IdP + /// + /// The destination of the request. + /// The authentication request. + /// Relay state from client. May be null + public void RedirectFromLogin(IdentityProviderEndpoint destination, Saml20AuthnRequest request, string relayState, Action cacheInsert) + { + var index = (short)config.ServiceProvider.Endpoints.DefaultSignOnEndpoint.Index; + var doc = request.GetXml(); + XmlSignatureUtils.SignDocument(doc, request.Request.Id, config.ServiceProvider.SigningCertificate); + ArtifactRedirect(destination, index, doc, relayState, cacheInsert); + } + + + /// + /// Creates an artifact for the LogoutRequest and redirects the user to the IdP. + /// + /// The destination of the request. + /// The logout request. + /// The query string relay state value (relayState) to add to the communication + public void RedirectFromLogout(IdentityProviderEndpoint destination, Saml20LogoutRequest request, string relayState, Action cacheInsert) + { + var index = (short)config.ServiceProvider.Endpoints.DefaultLogoutEndpoint.Index; + var doc = request.GetXml(); + XmlSignatureUtils.SignDocument(doc, request.Request.Id, config.ServiceProvider.SigningCertificate); + ArtifactRedirect(destination, index, doc, relayState, cacheInsert); + } + + /// + /// Creates an artifact for the LogoutResponse and redirects the user to the IdP. + /// + /// The destination of the response. + /// The logout response. + /// The query string relay state value to add to the communication + + public void RedirectFromLogout(IdentityProviderEndpoint destination, Saml20LogoutResponse response, string relayState, Action cacheInsert) + { + var index = (short)config.ServiceProvider.Endpoints.DefaultLogoutEndpoint.Index; + var doc = response.GetXml(); + XmlSignatureUtils.SignDocument(doc, response.Response.ID, config.ServiceProvider.SigningCertificate); + + ArtifactRedirect(destination, index, doc, relayState, cacheInsert); + } + + /// + /// Resolves an artifact. + /// + /// A stream containing the artifact response from the IdP + /// artifact from request ("SAMLart") + public Stream ResolveArtifact(string artifact, string relayState, Saml2Configuration config) + { + var idpEndPoint = DetermineIdp(artifact); + if (idpEndPoint == null) + { + throw new InvalidOperationException(ErrorMessages.ArtifactResolveIdentityProviderUnknown); + } + + var endpointIndex = ArtifactUtil.GetEndpointIndex(artifact); + var endpointUrl = idpEndPoint.Metadata.GetIDPARSEndpoint(endpointIndex); + + Logger.DebugFormat(TraceMessages.ArtifactResolveForKnownIdentityProvider, artifact, idpEndPoint.Id, endpointUrl); + + var resolve = Saml20ArtifactResolve.GetDefault(config.ServiceProvider.Id); + resolve.Artifact = artifact; + + var doc = resolve.GetXml(); + if (doc.FirstChild is XmlDeclaration) + { + doc.RemoveChild(doc.FirstChild); + } + + XmlSignatureUtils.SignDocument(doc, resolve.Id, config.ServiceProvider.SigningCertificate); + + var artifactResolveString = doc.OuterXml; + + Logger.DebugFormat(TraceMessages.ArtifactResolved, artifactResolveString); + + return GetResponse(endpointUrl, artifactResolveString, idpEndPoint.ArtifactResolution, relayState); + } + + /// + /// Handles responses to an artifact resolve message. + /// + /// The artifact resolve message. + public void RespondToArtifactResolve(ArtifactResolve artifactResolve, XmlElement samlDoc) + { + var response = Saml20ArtifactResponse.GetDefault(config.ServiceProvider.Id); + response.StatusCode = Saml20Constants.StatusCodes.Success; + response.InResponseTo = artifactResolve.Id; + response.SamlElement = samlDoc; //samlDoc.DocumentElement; + + var responseDoc = response.GetXml(); + if (responseDoc.FirstChild is XmlDeclaration) + { + responseDoc.RemoveChild(responseDoc.FirstChild); + } + + XmlSignatureUtils.SignDocument(responseDoc, response.Id, config.ServiceProvider.SigningCertificate); + + Logger.DebugFormat(TraceMessages.ArtifactResolveResponseSent, artifactResolve.Artifact, responseDoc.OuterXml); + + sendResponseMessage(responseDoc.OuterXml); + } + + /// + /// Determines if the contents of 2 byte arrays are identical + /// + /// The first array + /// The second array + /// True of the byte arrays are equal, else false. + private static bool ByteArraysAreEqual(byte[] a, byte[] b) + { + for (int i = 0; i < a.Length; i++) + { + if (a[i] != b[i]) + { + return false; + } + } + + return true; + } + + /// + /// Handles all artifact creations and redirects. + /// + /// The destination. + /// Index of the local endpoint. + /// The signed SAML message. + /// The query string relay state value to add to the communication + private void ArtifactRedirect(IdentityProviderEndpoint destination, short localEndpointIndex, XmlDocument signedSamlMessage, string relayState, Action cacheInsert) + { + Logger.DebugFormat(TraceMessages.ArtifactRedirectReceived, signedSamlMessage.OuterXml); + + var sourceId = config.ServiceProvider.Id; + var sourceIdHash = ArtifactUtil.GenerateSourceIdHash(sourceId); + var messageHandle = ArtifactUtil.GenerateMessageHandle(); + + var artifact = ArtifactUtil.CreateArtifact(HttpArtifactBindingConstants.ArtifactTypeCode, localEndpointIndex, sourceIdHash, messageHandle); + cacheInsert(artifact, signedSamlMessage); + + var destinationUrl = destination.Url + (destination.Url.Contains("?") ? "&" : "?") + HttpArtifactBindingConstants.ArtifactQueryStringName + "=" + Uri.EscapeDataString(artifact); + if (!string.IsNullOrEmpty(relayState)) + { + destinationUrl += "&relayState=" + relayState; + } + + Logger.DebugFormat(TraceMessages.ArtifactCreated, artifact); + + redirect(destinationUrl); + } + + /// + /// Determines which IdP an artifact has been sent from. + /// + /// The artifact. + /// An IdP configuration element + private IdentityProvider DetermineIdp(string artifact) + { + short typeCodeValue = -1; + short endPointIndex = -1; + var sourceIdHash = new byte[20]; + var messageHandle = new byte[20]; + + if (ArtifactUtil.TryParseArtifact(artifact, ref typeCodeValue, ref endPointIndex, ref sourceIdHash, ref messageHandle)) + { + foreach (IdentityProvider ep in config.IdentityProviders) + { + var hash = ArtifactUtil.GenerateSourceIdHash(ep.Id); + if (ByteArraysAreEqual(sourceIdHash, hash)) + { + return ep; + } + } + } + + return null; + } + } +} diff --git a/src/SAML2.Standard/Bindings/HttpArtifactBindingConstants.cs b/src/SAML2.Standard/Bindings/HttpArtifactBindingConstants.cs new file mode 100644 index 0000000..2be4d53 --- /dev/null +++ b/src/SAML2.Standard/Bindings/HttpArtifactBindingConstants.cs @@ -0,0 +1,35 @@ +using System; + +namespace SAML2.Bindings +{ + /// + /// Constants pertaining to the artifact binding over HTTP SOAP. + /// + public class HttpArtifactBindingConstants + { + /// + /// Soap action + /// + public const string SoapAction = "http://www.oasis-open.org/committees/security"; + + /// + /// Default type code + /// + public const short ArtifactTypeCode = 0x0004; + + /// + /// Artifact query string name + /// + public const string ArtifactQueryStringName = "SAMLart"; + + /// + /// Name of artifact resolve + /// + public const string ArtifactResolve = "ArtifactResolve"; + + /// + /// Name of artifact response + /// + public const string ArtifactResponse = "ArtifactResponse"; + } +} diff --git a/src/SAML2.Standard/Bindings/HttpArtifactBindingParser.cs b/src/SAML2.Standard/Bindings/HttpArtifactBindingParser.cs new file mode 100644 index 0000000..77e2afb --- /dev/null +++ b/src/SAML2.Standard/Bindings/HttpArtifactBindingParser.cs @@ -0,0 +1,128 @@ +using System; +using System.IO; +using System.Xml; +using SAML2.Schema.Protocol; +using SAML2.Utils; + +namespace SAML2.Bindings +{ + /// + /// Parses the response messages related to the artifact binding. + /// + public class HttpArtifactBindingParser : HttpSoapBindingParser + { + /// + /// The artifact resolve. + /// + private ArtifactResolve _artifactResolve; + + /// + /// The artifact response. + /// + private ArtifactResponse _artifactResponse; + + /// + /// Initializes a new instance of the class. + /// + /// The input stream. + public HttpArtifactBindingParser(Stream inputStream) : base(inputStream) { } + + /// + /// Gets the artifact resolve message. + /// + /// The artifact resolve. + public ArtifactResolve ArtifactResolve + { + get + { + if (!IsArtifactResolve) + { + throw new InvalidOperationException("The SAML message is not an ArtifactResolve"); + } + + LoadArtifactResolve(); + + return _artifactResolve; + } + } + + /// + /// Gets the artifact response message. + /// + /// The artifact response. + public ArtifactResponse ArtifactResponse + { + get + { + if (!IsArtifactResponse) + { + throw new InvalidOperationException("The SAML message is not an ArtifactResponse"); + } + + LoadArtifactResponse(); + + return _artifactResponse; + } + } + + /// + /// Gets a value indicating whether this instance is artifact resolve. + /// + public bool IsArtifactResolve + { + get { return SamlMessage.LocalName == HttpArtifactBindingConstants.ArtifactResolve; } + } + + /// + /// Gets a value indicating whether this instance is artifact response. + /// + public bool IsArtifactResponse + { + get { return SamlMessage.LocalName == HttpArtifactBindingConstants.ArtifactResponse; } + } + + /// + /// Gets the issuer of the current message. + /// + /// The issuer. + public string Issuer + { + get + { + if (IsArtifactResolve) + { + return ArtifactResolve.Issuer.Value; + } + + if (IsArtifactResponse) + { + return ArtifactResponse.Issuer.Value; + } + + return string.Empty; + } + } + + /// + /// Loads the current message as an artifact resolve. + /// + private void LoadArtifactResolve() + { + if (_artifactResolve == null) + { + _artifactResolve = Serialization.Deserialize(new XmlNodeReader(SamlMessage)); + } + } + + /// + /// Loads the current message as an artifact response. + /// + private void LoadArtifactResponse() + { + if (_artifactResponse == null) + { + _artifactResponse = Serialization.Deserialize(new XmlNodeReader(SamlMessage)); + } + } + } +} \ No newline at end of file diff --git a/src/SAML2.Standard/Bindings/HttpPostBindingBuilder.cs b/src/SAML2.Standard/Bindings/HttpPostBindingBuilder.cs new file mode 100644 index 0000000..34095f9 --- /dev/null +++ b/src/SAML2.Standard/Bindings/HttpPostBindingBuilder.cs @@ -0,0 +1,128 @@ +using System; +using System.Text; +using SAML2.Config; + +namespace SAML2.Bindings +{ + /// + /// Implementation of the HTTP POST binding. + /// + public class HttpPostBindingBuilder + { + /// + /// The endpoint to send the message to. + /// + private readonly IdentityProviderEndpoint _destinationEndpoint; + + /// + /// Request backing field. + /// + private string _request; + + /// + /// Response backing field. + /// + private string _response; + + /// + /// Initializes a new instance of the class. + /// + /// The IdP endpoint that messages will be sent to. + public HttpPostBindingBuilder(IdentityProviderEndpoint endpoint) + { + _destinationEndpoint = endpoint; + Action = SamlActionType.SAMLRequest; + RelayState = string.Empty; + } + + /// + /// Gets or sets the action. + /// + /// The action. + public SamlActionType Action { get; set; } + + /// + /// Gets or sets the relay state + /// + /// The relay state. + public string RelayState { get; set; } + + /// + /// Gets or sets the request. + /// + /// The request. + public string Request + { + get { return _request; } + set + { + if (!string.IsNullOrEmpty(_response)) + { + throw new ArgumentException("Response property is already specified. Unable to set Request property."); + } + + _request = value; + } + } + + /// + /// Gets or sets the response. + /// + /// The response. + public string Response + { + get { return _response; } + set + { + if (!string.IsNullOrEmpty(_request)) + { + throw new ArgumentException("Request property is already specified. Unable to set Response property."); + } + + _response = value; + } + } + + /// + /// Gets the ASP.Net page that will serve html to user agent. + /// + /// The Page. + public string GetPage() + { + if (_request == null && _response == null) + { + throw new InvalidOperationException("A response or request message MUST be specified before generating the page."); + } + + var msg = _request ?? _response; + + var rc = new StringBuilder(800); + rc.Append(@" + + + + + SAML2.0 POST binding + + + +"); + + rc.AppendFormat("
", _destinationEndpoint.Url); + + if (!string.IsNullOrEmpty(RelayState)) + rc.AppendFormat(" ", RelayState); + + rc.AppendFormat(" ", Enum.GetName(typeof(SamlActionType), Action), Convert.ToBase64String(Encoding.UTF8.GetBytes(msg))); + + rc.Append(@" +
+
+ + +"); + return rc.ToString(); + } + + } +} diff --git a/src/SAML2.Standard/Bindings/HttpPostBindingParser.cs b/src/SAML2.Standard/Bindings/HttpPostBindingParser.cs new file mode 100644 index 0000000..9a70402 --- /dev/null +++ b/src/SAML2.Standard/Bindings/HttpPostBindingParser.cs @@ -0,0 +1,102 @@ +using System; +using System.Collections.Generic; +using System.Security.Cryptography.Xml; +using System.Text; +using System.Xml; +using SAML2.Schema.Metadata; +using SAML2.Utils; +using System.Collections.Specialized; + +namespace SAML2.Bindings +{ + /// + /// Parses the response messages related to the HTTP POST binding. + /// + public class HttpPostBindingParser + { + /// + /// Initializes a new instance of the class. + /// + /// The current HTTP context. + public HttpPostBindingParser(NameValueCollection requestParams) + { + var base64 = string.Empty; + + if (requestParams["SAMLRequest"] != null) + { + base64 = requestParams["SAMLRequest"]; + IsRequest = true; + } + + if (requestParams["SAMLResponse"] != null) + { + base64 = requestParams["SAMLResponse"]; + IsResponse = true; + } + + Message = Encoding.UTF8.GetString(Convert.FromBase64String(base64)); + + Document = new XmlDocument { PreserveWhitespace = true }; + Document.LoadXml(Message); + } + + /// + /// Gets the document. + /// + public XmlDocument Document { get; private set; } + + /// + /// Gets a value indicating whether this instance is request. + /// + public bool IsRequest { get; private set; } + + /// + /// Gets a value indicating whether this instance is response. + /// + public bool IsResponse { get; private set; } + + /// + /// Gets a value indicating whether the message is signed. + /// + public bool IsSigned + { + get { return XmlSignatureUtils.IsSigned(Document); } + } + + /// + /// Gets the message. + /// + public string Message { get; private set; } + + /// + /// Checks the signature. + /// + /// True of the signature is valid, else false. + public bool CheckSignature() + { + return XmlSignatureUtils.CheckSignature(Document); + } + + /// + /// Checks the signature of the message, using a specific set of keys + /// + /// The set of keys to check the signature against + /// True of the signature is valid, else false. + public bool CheckSignature(IEnumerable keys) + { + foreach (var keyDescriptor in keys) + { + foreach (KeyInfoClause clause in (KeyInfo)keyDescriptor.KeyInfo) + { + var key = XmlSignatureUtils.ExtractKey(clause); + if (key != null && XmlSignatureUtils.CheckSignature(Document, key)) + { + return true; + } + } + } + + return false; + } + } +} diff --git a/src/SAML2.Standard/Bindings/HttpRedirectBindingBuilder.cs b/src/SAML2.Standard/Bindings/HttpRedirectBindingBuilder.cs new file mode 100644 index 0000000..a43a96c --- /dev/null +++ b/src/SAML2.Standard/Bindings/HttpRedirectBindingBuilder.cs @@ -0,0 +1,220 @@ +using System; +using System.Security.Cryptography; +using System.Security.Cryptography.Xml; +using System.Text; +using CONSTS = SAML2.Bindings.HttpRedirectBindingConstants; + +namespace SAML2.Bindings +{ + /// + /// Handles the creation of redirect locations when using the HTTP redirect binding, which is outlined in [SAMLBind] + /// section 3.4. + /// + public class HttpRedirectBindingBuilder + { + /// + /// Request backing field. + /// + private string _request; + + /// + /// Response backing field. + /// + private string _response; + + /// + /// SigningKey backing field. + /// + private AsymmetricAlgorithm _signingKey; + + /// + /// Gets or sets the request. + /// + /// The request. + public string Request + { + get { return _request; } + set + { + if (!string.IsNullOrEmpty(_response)) + { + throw new ArgumentException("Response property is already specified. Unable to set Request property."); + } + + _request = value; + } + } + + /// + /// Gets or sets the response. + /// + /// The response. + public string Response + { + get { return _response; } + set + { + if (!string.IsNullOrEmpty(_request)) + { + throw new ArgumentException("Request property is already specified. Unable to set Response property."); + } + + _response = value; + } + } + + /// + /// Gets or sets the relay state of the message. + /// If the message being built is a response message, the relay state will be included unmodified. + /// If the message being built is a request message, the relay state will be encoded and compressed before being included. + /// + public string RelayState { get; set; } + + /// + /// Gets or sets the signing key. + /// + /// The signing key. + public AsymmetricAlgorithm SigningKey + { + get { return _signingKey; } + set + { + // Check if the key is of a supported type. [SAMLBind] sect. 3.4.4.1 specifies this. + if (!(value is RSACryptoServiceProvider || value is DSA || value == null)) + { + throw new ArgumentException("Signing key must be an instance of either RSACryptoServiceProvider or DSA."); + } + + _signingKey = value; + } + } + + /// + /// Returns the query part of the url that should be redirected to. + /// The resulting string should be pre-pended with either ? or & before use. + /// + /// The query string part of the redirect URL. + public string ToQuery() + { + var result = new StringBuilder(); + + AddMessageParameter(result); + AddRelayState(result); + AddSignature(result); + + return result.ToString(); + } + + /// + /// Uppercase the URL-encoded parts of the string. Needed because Ping does not seem to be able to handle lower-cased URL-encodings. + /// + /// The value. + /// The value with URL encodings uppercased. + private static string UpperCaseUrlEncode(string value) + { + var result = new StringBuilder(value); + for (var i = 0; i < result.Length; i++) + { + if (result[i] == '%') + { + result[++i] = char.ToUpper(result[i]); + result[++i] = char.ToUpper(result[i]); + } + } + + return result.ToString(); + } + + /// + /// If the RelayState property has been set, this method adds it to the query string. + /// + /// The result. + private void AddRelayState(StringBuilder result) + { + if (RelayState == null) + { + return; + } + + result.Append("&RelayState="); + + // Encode the relay state if we're building a request. Otherwise, append unmodified. + result.Append(_request != null ? Uri.EscapeDataString(Utils.Compression.DeflateEncode(RelayState)) : RelayState); + } + + /// + /// If an asymmetric key has been specified, sign the request. + /// + /// The result. + private void AddSignature(StringBuilder result) + { + if (_signingKey == null) + { + return; + } + + result.Append(string.Format("&{0}=", HttpRedirectBindingConstants.SigAlg)); + + if (_signingKey is RSA) + { + result.Append(UpperCaseUrlEncode(Uri.EscapeDataString(SignedXml.XmlDsigRSASHA1Url))); + } + else + { + result.Append(UpperCaseUrlEncode(Uri.EscapeDataString(SignedXml.XmlDsigDSAUrl))); + } + + // Calculate the signature of the URL as described in [SAMLBind] section 3.4.4.1. + var signature = SignData(Encoding.UTF8.GetBytes(result.ToString())); + + result.AppendFormat("&{0}=", HttpRedirectBindingConstants.Signature); + result.Append(Uri.EscapeDataString(Convert.ToBase64String(signature))); + } + + /// + /// Create the signature for the data. + /// + /// The data. + /// SignData based on passed data and SigningKey. + private byte[] SignData(byte[] data) + { + if (_signingKey is RSACryptoServiceProvider) + { + var rsa = (RSACryptoServiceProvider)_signingKey; + return rsa.SignData(data, new SHA1CryptoServiceProvider()); + } + else + { + var dsa = (DSACryptoServiceProvider)_signingKey; + return dsa.SignData(data); + } + } + + /// + /// Depending on which one is specified, this method adds the SAMLRequest or SAMLResponse parameter to the URL query. + /// + /// The result. + private void AddMessageParameter(StringBuilder result) + { + if (!(_response == null || _request == null)) + { + throw new Exception("Request or Response property MUST be set."); + } + + string value; + if (_request != null) + { + result.AppendFormat("{0}=", CONSTS.SamlRequest); + value = _request; + } + else + { + result.AppendFormat("{0}=", HttpRedirectBindingConstants.SamlResponse); + value = _response; + } + + var encoded = Utils.Compression.DeflateEncode(value); + result.Append(UpperCaseUrlEncode(Uri.EscapeDataString(encoded))); + } + } +} diff --git a/src/SAML2.Standard/Bindings/HttpRedirectBindingConstants.cs b/src/SAML2.Standard/Bindings/HttpRedirectBindingConstants.cs new file mode 100644 index 0000000..2d3f112 --- /dev/null +++ b/src/SAML2.Standard/Bindings/HttpRedirectBindingConstants.cs @@ -0,0 +1,34 @@ +namespace SAML2.Bindings +{ + /// + /// Contains constant string versions of the parameters in the HTTP Redirect Binding. + /// Introduced to avoid errors caused by stupid errors in capitalization when using the binding. + /// + public class HttpRedirectBindingConstants + { + /// + /// SAMLResponse name + /// + public const string SamlResponse = "SAMLResponse"; + + /// + /// SAMLRequest name + /// + public const string SamlRequest = "SAMLRequest"; + + /// + /// Signature Algorithm name + /// + public const string SigAlg = "SigAlg"; + + /// + /// Relay state name + /// + public const string RelayState = "RelayState"; + + /// + /// Signature name + /// + public const string Signature = "Signature"; + } +} \ No newline at end of file diff --git a/src/SAML2.Standard/Bindings/HttpRedirectBindingParser.cs b/src/SAML2.Standard/Bindings/HttpRedirectBindingParser.cs new file mode 100644 index 0000000..97fe098 --- /dev/null +++ b/src/SAML2.Standard/Bindings/HttpRedirectBindingParser.cs @@ -0,0 +1,261 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Cryptography; +using System.Security.Cryptography.Xml; +using System.Text; +using SAML2.Schema.Metadata; +using SAML2.Utils; + +namespace SAML2.Bindings +{ + /// + /// Parses and validates the query parameters of a HttpRedirectBinding. [SAMLBind] section 3.4. + /// + public class HttpRedirectBindingParser + { + /// + /// RelaystateDecoded backing field. + /// + private string _relaystateDecoded; + + /// + /// The signed part of the query is recreated in this string. + /// + private string _signedquery; + + /// + /// Initializes a new instance of the class. + /// + /// The URL that the user was redirected to by the IDP. It is essential for the survival of the signature, + /// that the URL is not modified in any way, e.g. by URL-decoding it. + public HttpRedirectBindingParser(Uri uri) + { + var paramDict = UriToDictionary(uri); + foreach (var param in paramDict) + { + SetParam(param.Key, Uri.UnescapeDataString(param.Value)); + } + + // If the message is signed, save the original, encoded parameters so that the signature can be verified. + if (IsSigned) + { + CreateSignatureSubject(paramDict); + } + + ReadMessageParameter(); + } + + /// + /// Gets a value indicating whether the parsed message contains a request message. + /// + public bool IsRequest + { + get { return !IsResponse; } + } + + /// + /// Gets a value indicating whether the parsed message contains a response message. + /// + public bool IsResponse { get; private set; } + + /// + /// Gets a value indicating whether the parsed message contains a signature. + /// + public bool IsSigned + { + get { return Signature != null; } + } + + /// + /// Gets the message that was contained in the query. Use the IsResponse or the IsRequest property + /// to determine the kind of message. + /// + public string Message { get; private set; } + + /// + /// Gets the relay state that was included with the query. The result will still be encoded according to the + /// rules given in section 3.4.4.1 of [SAMLBind], i.e. base64-encoded and DEFLATE-compressed. Use the property + /// RelayStateDecoded to get the decoded contents of the RelayState parameter. + /// + public string RelayState { get; private set; } + + /// + /// Gets a decoded and decompressed version of the RelayState parameter. + /// + public string RelayStateDecoded + { + get { return _relaystateDecoded ?? (_relaystateDecoded = Utils.Compression.DeflateDecompress(RelayState)); } + } + + /// + /// Gets the signature value + /// + public string Signature { get; private set; } + + /// + /// Gets the signature algorithm. + /// + /// The signature algorithm. + public string SignatureAlgorithm { get; private set; } + + /// + /// Validates the signature using the public part of the asymmetric key given as parameter. + /// + /// The key. + /// true if the signature is present and can be verified using the given key. + /// false if the signature is present, but can't be verified using the given key. + /// If the query is not signed, and therefore cannot have its signature verified. Use + /// the IsSigned property to check for this situation before calling this method. + public bool CheckSignature(AsymmetricAlgorithm key) + { + if (key == null) + { + throw new ArgumentNullException("key"); + } + + if (!(key is DSA || key is RSACryptoServiceProvider)) + { + throw new ArgumentException("The key must be an instance of either DSA or RSACryptoServiceProvider."); + } + + if (!IsSigned) + { + throw new InvalidOperationException("Query is not signed, so there is no signature to verify."); + } + + using (SHA1Managed sHA1Managed = new SHA1Managed()) + { + byte[] hash = sHA1Managed.ComputeHash(Encoding.UTF8.GetBytes(_signedquery)); + if (key is RSACryptoServiceProvider rsa) + { + return rsa.VerifyHash(hash, "SHA1", DecodeSignature()); + } + else + { + var dsa = (DSA)key; + return dsa.VerifySignature(hash, DecodeSignature()); + } + } + } + + /// + /// Check the signature of a HTTP-Redirect message using the list of keys. + /// + /// A list of KeyDescriptor elements. Probably extracted from the metadata describing the IDP that sent the message. + /// True, if one of the given keys was able to verify the signature. False in all other cases. + public bool VerifySignature(IEnumerable keys) + { + foreach (var keyDescriptor in keys) + { + foreach (KeyInfoClause clause in (KeyInfo)keyDescriptor.KeyInfo) + { + var key = XmlSignatureUtils.ExtractKey(clause); + if (key != null && CheckSignature(key)) + { + return true; + } + } + } + + return false; + } + + /// + /// Converts the URI to dictionary. + /// + /// The URI. + /// Dictionary of query parameters. + private static Dictionary UriToDictionary(Uri uri) + { + var parameters = uri.Query.Substring(1).Split('&'); + var result = new Dictionary(parameters.Length); + foreach (var parameter in parameters.Select(s => s.Split('='))) + { + result.Add(parameter[0], parameter[1]); + } + + return result; + } + + /// + /// Re-creates the list of parameters that are signed, in order to verify the signature. + /// + /// The query parameters. + private void CreateSignatureSubject(IDictionary queryParams) + { + var signedQuery = new StringBuilder(); + if (IsResponse) + { + signedQuery.AppendFormat("{0}={1}", HttpRedirectBindingConstants.SamlResponse, queryParams[HttpRedirectBindingConstants.SamlResponse]); + } + else + { + signedQuery.AppendFormat("{0}={1}", HttpRedirectBindingConstants.SamlRequest, queryParams[HttpRedirectBindingConstants.SamlRequest]); + } + + if (RelayState != null) + { + signedQuery.AppendFormat("&{0}={1}", HttpRedirectBindingConstants.RelayState, queryParams[HttpRedirectBindingConstants.RelayState]); + } + + if (Signature != null) + { + signedQuery.AppendFormat("&{0}={1}", HttpRedirectBindingConstants.SigAlg, queryParams[HttpRedirectBindingConstants.SigAlg]); + } + + _signedquery = signedQuery.ToString(); + } + + /// + /// Decodes the Signature parameter. + /// + /// The decoded signature. + private byte[] DecodeSignature() + { + if (!IsSigned) + { + throw new InvalidOperationException("Query does not contain a signature."); + } + + return Convert.FromBase64String(Signature); + } + + /// + /// Decodes the message parameter. + /// + private void ReadMessageParameter() + { + Message = Compression.DeflateDecompress(Message); + } + + /// + /// Sets the parameter. + /// + /// The key. + /// The value. + private void SetParam(string key, string value) + { + switch (key.ToLower()) + { + case "samlrequest": + IsResponse = false; + Message = value; + return; + case "samlresponse": + IsResponse = true; + Message = value; + return; + case "relaystate": + RelayState = value; + return; + case "sigalg": + SignatureAlgorithm = value; + return; + case "signature": + Signature = value; + return; + } + } + } +} diff --git a/src/SAML2.Standard/Bindings/HttpSOAPBindingBuilder.cs b/src/SAML2.Standard/Bindings/HttpSOAPBindingBuilder.cs new file mode 100644 index 0000000..58f85b4 --- /dev/null +++ b/src/SAML2.Standard/Bindings/HttpSOAPBindingBuilder.cs @@ -0,0 +1,160 @@ +using System; +using System.IO; +using System.Net; +using System.Net.Security; +using System.Reflection; +using System.Security.Cryptography.X509Certificates; +using System.ServiceModel; +using System.ServiceModel.Channels; +using System.Text; +using System.Xml; +using SAML2.Config; +using SAML2.Logging; + +namespace SAML2.Bindings +{ + /// + /// Implements the HTTP SOAP binding + /// + public class HttpSoapBindingBuilder + { + /// + /// Logger instance. + /// + protected static readonly IInternalLogger Logger = LoggerProvider.LoggerFor(MethodBase.GetCurrentMethod().DeclaringType); + + /// + /// Validates the server certificate. + /// + /// The sender. + /// The certificate. + /// The chain. + /// The SSL policy errors. + /// True if validation of the server certificate generates no policy errors + public static bool ValidateServerCertificate(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors) + { + return sslPolicyErrors == SslPolicyErrors.None; + } + + /// + /// Gets a response from the IdP based on a message. + /// + /// The IdP endpoint. + /// The message. + /// Basic authentication settings. + /// The Stream. + public Stream GetResponse(string endpoint, string message, HttpAuth auth, string relayState) + { + if (auth != null && auth.ClientCertificate != null && auth.Credentials != null) + { + throw new Saml20Exception(string.Format("Artifact resolution cannot specify both client certificate and basic credentials for endpoint {0}", endpoint)); + } + + var binding = CreateSslBinding(); + if (auth != null && auth.ClientCertificate != null) + { + // Client certificate auth + binding.Security.Transport.ClientCredentialType = HttpClientCredentialType.Certificate; + } + + var request = Message.CreateMessage(binding.MessageVersion, HttpArtifactBindingConstants.SoapAction, new SimpleBodyWriter(message)); + request.Headers.To = new Uri(endpoint); + + var property = new HttpRequestMessageProperty { Method = "POST" }; + property.Headers.Add(HttpRequestHeader.ContentType, "text/xml; charset=utf-8"); + + if (auth != null && auth.Credentials != null) + { + // Basic http auth over ssl + var basicAuthzHeader = "Basic " + Convert.ToBase64String(Encoding.UTF8.GetBytes(auth.Credentials.Username + ":" + auth.Credentials.Password)); + property.Headers.Add(HttpRequestHeader.Authorization, basicAuthzHeader); + } + + request.Properties.Add(HttpRequestMessageProperty.Name, property); + if (relayState != null) + { + request.Properties.Add("relayState", relayState); + } + + var epa = new EndpointAddress(endpoint); + + var factory = new ChannelFactory(binding, epa); + if (auth != null && auth.ClientCertificate != null) + { + // Client certificate + factory.Credentials.ClientCertificate.Certificate = auth.ClientCertificate; + } + + var reqChannel = factory.CreateChannel(); + + reqChannel.Open(); + var response = reqChannel.Request(request); + Console.WriteLine(response); + reqChannel.Close(); + + var doc = new XmlDocument { PreserveWhitespace = true }; + doc.Load(response.GetReaderAtBodyContents()); + var outerXml = doc.DocumentElement.OuterXml; + var memStream = new MemoryStream(Encoding.UTF8.GetBytes(outerXml)); + + return memStream; + } + + /// + /// Wraps a message in a SOAP envelope. + /// + /// The message. + /// The wrapped message. + public static string WrapInSoapEnvelope(string message) + { + var builder = new StringBuilder(); + + builder.AppendLine(SoapConstants.EnvelopeBegin); + builder.AppendLine(SoapConstants.BodyBegin); + builder.AppendLine(message); + builder.AppendLine(SoapConstants.BodyEnd); + builder.AppendLine(SoapConstants.EnvelopeEnd); + + return builder.ToString(); + } + + /// + /// Creates a WCF SSL binding. + /// + /// The WCF SSL binding. + private static BasicHttpBinding CreateSslBinding() + { + return new BasicHttpBinding(BasicHttpSecurityMode.Transport) { TextEncoding = Encoding.UTF8 }; + } + + /// + /// A simple body writer + /// + internal class SimpleBodyWriter : BodyWriter + { + /// + /// The message. + /// + private readonly string _message; + + /// + /// Initializes a new instance of the class. + /// + /// The message. + public SimpleBodyWriter(string message) + : base(false) + { + _message = message; + } + + /// + /// When implemented, provides an extensibility point when the body contents are written. + /// + /// The used to write out the message body. + protected override void OnWriteBodyContents(XmlDictionaryWriter writer) + { + writer.WriteRaw(_message); + } + } + } +} \ No newline at end of file diff --git a/src/SAML2.Standard/Bindings/HttpSOAPBindingParser.cs b/src/SAML2.Standard/Bindings/HttpSOAPBindingParser.cs new file mode 100644 index 0000000..dd05fb4 --- /dev/null +++ b/src/SAML2.Standard/Bindings/HttpSOAPBindingParser.cs @@ -0,0 +1,179 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Security.Cryptography; +using System.Security.Cryptography.Xml; +using System.Xml; +using SAML2.Schema.Metadata; +using SAML2.Schema.Protocol; +using SAML2.Utils; + +namespace SAML2.Bindings +{ + /// + /// Parses messages pertaining to the HTTP SOAP binding. + /// + public class HttpSoapBindingParser + { + /// + /// The current logout request + /// + private LogoutRequest _logoutRequest; + + /// + /// The current SAML message + /// + private XmlElement _samlMessage; + + /// + /// Initializes a new instance of the class. + /// + /// The HTTP input stream. + public HttpSoapBindingParser(Stream httpInputStream) + { + InputStream = httpInputStream; + } + + /// + /// Gets a value indicating whether the current message is a LogoutRequest. + /// true if the current message is a LogoutRequest; otherwise, false. + /// + public bool IsLogoutReqest + { + get { return SamlMessageName == LogoutRequest.ElementName; } + } + + /// + /// Gets the LogoutRequest message. + /// + /// The logout request. + public LogoutRequest LogoutRequest + { + get + { + if (!IsLogoutReqest) + { + throw new InvalidOperationException("The SAML message is not an LogoutRequest"); + } + + LoadLogoutRequest(); + + return _logoutRequest; + } + } + + /// + /// Gets the current SAML message. + /// + /// The SAML message. + public XmlElement SamlMessage + { + get + { + LoadSamlMessage(); + return _samlMessage; + } + } + + /// + /// Gets the name of the SAML message. + /// + /// The name of the SAML message. + public string SamlMessageName + { + get + { + return SamlMessage.LocalName; + } + } + + /// + /// Gets or sets the input stream. + /// + /// The input stream. + protected Stream InputStream { get; set; } + + /// + /// Gets or sets the SOAP envelope. + /// + /// The SOAP envelope. + protected string SoapEnvelope { get; set; } + + /// + /// Checks the SAML message signature. + /// + /// The keys to check the signature against. + /// True if the signature is valid, else false. + public bool CheckSamlMessageSignature(List keys) + { + foreach (var keyDescriptor in keys) + { + foreach (KeyInfoClause clause in (KeyInfo)keyDescriptor.KeyInfo) + { + var key = XmlSignatureUtils.ExtractKey(clause); + if (key != null && CheckSignature(key)) + { + return true; + } + } + } + + return false; + } + + /// + /// Gets the status of the current message. + /// + /// The . + public Status GetStatus() + { + var status = (XmlElement)SamlMessage.GetElementsByTagName(Status.ElementName, Saml20Constants.Protocol)[0]; + return status != null ? Serialization.Deserialize(new XmlNodeReader(status)) : null; + } + + /// + /// Loads the SAML message. + /// + protected void LoadSamlMessage() + { + if (_samlMessage != null) + { + return; + } + + var reader = new StreamReader(InputStream); + SoapEnvelope = reader.ReadToEnd(); + + var doc = new XmlDocument { PreserveWhitespace = true }; + doc.LoadXml(SoapEnvelope); + + var soapBody = (XmlElement)doc.GetElementsByTagName(SoapConstants.SoapBody, SoapConstants.SoapNamespace)[0]; + + _samlMessage = soapBody != null ? (XmlElement)soapBody.FirstChild : doc.DocumentElement; + } + + /// + /// Checks the signature. + /// + /// The key to check against. + /// True if the signature is valid, else false. + private bool CheckSignature(AsymmetricAlgorithm key) + { + var doc = new XmlDocument { PreserveWhitespace = true }; + doc.LoadXml(SamlMessage.OuterXml); + + return XmlSignatureUtils.CheckSignature(doc, key); + } + + /// + /// Loads the current message as a LogoutRequest. + /// + private void LoadLogoutRequest() + { + if (_logoutRequest == null) + { + _logoutRequest = Serialization.Deserialize(new XmlNodeReader(SamlMessage)); + } + } + } +} diff --git a/src/SAML2.Standard/Bindings/SOAPConstants.cs b/src/SAML2.Standard/Bindings/SOAPConstants.cs new file mode 100644 index 0000000..f65bbc3 --- /dev/null +++ b/src/SAML2.Standard/Bindings/SOAPConstants.cs @@ -0,0 +1,43 @@ +namespace SAML2.Bindings +{ + /// + /// Constants related to the HTTP SOAP binding + /// + public class SoapConstants + { + /// + /// Soap action name + /// + public const string SoapAction = "SOAPAction"; + + /// + /// Soap body name + /// + public const string SoapBody = "Body"; + + /// + /// Soap namespace + /// + public const string SoapNamespace = "http://schemas.xmlsoap.org/soap/envelope/"; + + /// + /// Soap envelope begin constant + /// + public const string EnvelopeBegin = ""; + + /// + /// Soap envelope end constant + /// + public const string EnvelopeEnd = ""; + + /// + /// soap body begin constant + /// + public const string BodyBegin = ""; + + /// + /// Soap body end constant + /// + public const string BodyEnd = ""; + } +} \ No newline at end of file diff --git a/src/SAML2.Standard/Config/Attribute.cs b/src/SAML2.Standard/Config/Attribute.cs new file mode 100644 index 0000000..f037fcf --- /dev/null +++ b/src/SAML2.Standard/Config/Attribute.cs @@ -0,0 +1,24 @@ +using System.Configuration; + +namespace SAML2.Config +{ + /// + /// Attribute configuration element. + /// + public class Attribute + { + + /// + /// Gets or sets a value indicating whether this attribute is required. + /// + /// true if this attribute is required; otherwise, false. + public bool IsRequired { get; set; } + + /// + /// Gets or sets the name. + /// + /// The name. + public string Name { get; set; } + + } +} diff --git a/src/SAML2.Standard/Config/AuthenticationContext.cs b/src/SAML2.Standard/Config/AuthenticationContext.cs new file mode 100644 index 0000000..7bf504e --- /dev/null +++ b/src/SAML2.Standard/Config/AuthenticationContext.cs @@ -0,0 +1,23 @@ +using System.Configuration; + +namespace SAML2.Config +{ + /// + /// Authentication Context configuration element. + /// + public class AuthenticationContext + { + /// + /// Gets or sets the context. + /// + /// The context. + public string Context { get; set; } + + /// + /// Gets or sets the reference type. + /// + /// The reference type. + public string ReferenceType { get; set; } + + } +} diff --git a/src/SAML2.Standard/Config/AuthenticationContextComparison.cs b/src/SAML2.Standard/Config/AuthenticationContextComparison.cs new file mode 100644 index 0000000..c2faa32 --- /dev/null +++ b/src/SAML2.Standard/Config/AuthenticationContextComparison.cs @@ -0,0 +1,28 @@ +namespace SAML2.Config +{ + /// + /// AuthenticationContext Comparison types. + /// + public enum AuthenticationContextComparison + { + /// + /// Exact comparison type. + /// + Exact, + + /// + /// Minimum comparison type. + /// + Minimum, + + /// + /// Maximum comparison type. + /// + Maximum, + + /// + /// Better comparison type. + /// + Better + } +} \ No newline at end of file diff --git a/src/SAML2.Standard/Config/AuthenticationContexts.cs b/src/SAML2.Standard/Config/AuthenticationContexts.cs new file mode 100644 index 0000000..680b97e --- /dev/null +++ b/src/SAML2.Standard/Config/AuthenticationContexts.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; +using System.Configuration; + +namespace SAML2.Config +{ + /// + /// Service Provider Endpoint configuration collection. + /// + public class AuthenticationContexts : List + { + public AuthenticationContexts() : base() { } + public AuthenticationContexts(IEnumerable collection) : base(collection) { } + /// + /// Gets the comparison. + /// + public AuthenticationContextComparison Comparison { get; set; } + } +} diff --git a/src/SAML2.Standard/Config/BindingType.cs b/src/SAML2.Standard/Config/BindingType.cs new file mode 100644 index 0000000..1e732d2 --- /dev/null +++ b/src/SAML2.Standard/Config/BindingType.cs @@ -0,0 +1,36 @@ +using System; + +namespace SAML2.Config +{ + /// + /// Binding types. + /// + [Flags] + public enum BindingType + { + /// + /// No binding set. + /// + NotSet = 0, + + /// + /// POST binding + /// + Post = 1, + + /// + /// Redirect binding + /// + Redirect = 2, + + /// + /// Artifact binding + /// + Artifact = 4, + + /// + /// SOAP binding + /// + Soap = 8 + } +} diff --git a/src/SAML2.Standard/Config/CommonDomainCookie.cs b/src/SAML2.Standard/Config/CommonDomainCookie.cs new file mode 100644 index 0000000..009edb4 --- /dev/null +++ b/src/SAML2.Standard/Config/CommonDomainCookie.cs @@ -0,0 +1,20 @@ +using System.Configuration; + +namespace SAML2.Config +{ + /// + /// Common Domain Cookie configuration element. + /// + public class CommonDomainCookie { + /// + /// Gets or sets a value indicating whether Common Domain Cookie is enabled. + /// + /// true if enabled; otherwise, false. + public bool Enabled { get; set; } + /// + /// Gets or sets the local reader endpoint. + /// + /// The local reader endpoint. + public string LocalReaderEndpoint { get; set; } + } +} diff --git a/src/SAML2.Standard/Config/Contact.cs b/src/SAML2.Standard/Config/Contact.cs new file mode 100644 index 0000000..c2bcabf --- /dev/null +++ b/src/SAML2.Standard/Config/Contact.cs @@ -0,0 +1,56 @@ +using System.Configuration; + +namespace SAML2.Config +{ + /// + /// Contact configuration element. + /// + public class Contact + { + + /// + /// Gets or sets the company. + /// + /// The company. + public string Company { get; set; } + + /// + /// Gets or sets the email. + /// + /// The email. + public string Email { get; set; } + + /// + /// Gets or sets the given name. + /// + /// The given name. + public string GivenName { get; set; } + + /// + /// Gets or sets the phone. + /// + /// The phone. + public string Phone { get; set; } + + /// + /// Gets or sets the name of the sur. + /// + /// The name of the sur. + public string SurName { get; set; } + + /// + /// Gets or sets the type. + /// + /// The type. + public ContactType Type { get; set; } + + /// + /// Gets the element key. + /// + public object ElementKey + { + get { return Type; } + } + + } +} diff --git a/src/SAML2.Standard/Config/ContactType.cs b/src/SAML2.Standard/Config/ContactType.cs new file mode 100644 index 0000000..fcfb5fb --- /dev/null +++ b/src/SAML2.Standard/Config/ContactType.cs @@ -0,0 +1,33 @@ +namespace SAML2.Config +{ + /// + /// Contact types. + /// + public enum ContactType + { + /// + /// Technical contact type. + /// + Technical, + + /// + /// Support contact type. + /// + Support, + + /// + /// Administrative contact type. + /// + Administrative, + + /// + /// Billing contact type. + /// + Billing, + + /// + /// Other contact type. + /// + Other + } +} diff --git a/src/SAML2.Standard/Config/EndpointType.cs b/src/SAML2.Standard/Config/EndpointType.cs new file mode 100644 index 0000000..d2cd488 --- /dev/null +++ b/src/SAML2.Standard/Config/EndpointType.cs @@ -0,0 +1,23 @@ +namespace SAML2.Config +{ + /// + /// Endpoint types. + /// + public enum EndpointType + { + /// + /// SignOn endpoint. + /// + SignOn, + + /// + /// Logout endpoint. + /// + Logout, + + /// + /// Metadata endpoint. + /// + Metadata + } +} diff --git a/src/SAML2.Standard/Config/HttpAuth.cs b/src/SAML2.Standard/Config/HttpAuth.cs new file mode 100644 index 0000000..6b91ba8 --- /dev/null +++ b/src/SAML2.Standard/Config/HttpAuth.cs @@ -0,0 +1,19 @@ +using System.Configuration; +using System.Security.Cryptography.X509Certificates; + +namespace SAML2.Config +{ + /// + /// Http Basic Authentication configuration element. + /// + public class HttpAuth + { + /// + /// Gets or sets the clientCertificate in web.config to enable client certificate authentication. + /// + public X509Certificate2 ClientCertificate { get; set; } /// + /// Gets or sets the credentials to use for artifact resolution. + /// + public HttpAuthCredentials Credentials { get; set; } + } +} diff --git a/src/SAML2.Standard/Config/HttpAuthCredentials.cs b/src/SAML2.Standard/Config/HttpAuthCredentials.cs new file mode 100644 index 0000000..4586d5e --- /dev/null +++ b/src/SAML2.Standard/Config/HttpAuthCredentials.cs @@ -0,0 +1,24 @@ +using System.Configuration; + +namespace SAML2.Config +{ + /// + /// Http Basic Authentication configuration element. + /// + public class HttpAuthCredentials + { + + /// + /// Gets or sets the username. + /// + /// The username. + public string Username { get; set; } + + /// + /// Gets or sets the password. + /// + /// The password. + public string Password { get; set; } + + } +} diff --git a/src/SAML2.Standard/Config/IConfigurationProvider.cs b/src/SAML2.Standard/Config/IConfigurationProvider.cs new file mode 100644 index 0000000..7a77c13 --- /dev/null +++ b/src/SAML2.Standard/Config/IConfigurationProvider.cs @@ -0,0 +1,7 @@ +namespace SAML2.Config +{ + public interface IConfigurationProvider + { + Saml2Configuration GetConfiguration(); + } +} diff --git a/src/SAML2.Standard/Config/IdentityProvider.cs b/src/SAML2.Standard/Config/IdentityProvider.cs new file mode 100644 index 0000000..f1b7e05 --- /dev/null +++ b/src/SAML2.Standard/Config/IdentityProvider.cs @@ -0,0 +1,127 @@ +using System.Collections.Generic; +using System.Configuration; + +namespace SAML2.Config +{ + /// + /// Identity Provider configuration element. + /// + public class IdentityProvider + { + /// + /// Gets or sets the metadata. + /// + /// The metadata. + public Saml20MetadataDocument Metadata { get; set; } + + /// + /// Gets or sets a value indicating whether this is default. + /// + /// true if default; otherwise, false. + /// + /// Use default in case common domain cookie is not set, and more than one endpoint is available. + /// + public bool Default{ get; set; } + + /// + /// Gets or sets a value indicating whether to force authentication on each AuthnRequest. + /// + /// true if force authentication; otherwise, false. + public bool ForceAuth { get; set; } + /// + /// Gets or sets the id. + /// + /// The id. + public string Id { get; set; } + /// + /// Gets or sets a value indicating whether this AuthnRequest should be passive. + /// + /// true if this instance is passive; otherwise, false. + public bool IsPassive { get; set; } + /// + /// Gets or sets the name. + /// + /// The name. + public string Name { get; set; } + + /// + /// Gets or sets a value indicating whether to omit assertion signature check. + /// + /// true if assertion signature check should be omitted; otherwise, false. + public bool OmitAssertionSignatureCheck { get; set; } + + /// + /// Gets or sets a value indicating whether to allow IdP Initiated SSO. This profile allows SAML responses without an SP-initiated request. + /// + /// true if IdP Initiated SSO should be allowed; otherwise, false> + public bool AllowIdPInitiatedSso { get; set; } + /// + /// Gets or sets a value indicating whether to allow replay attacks. This is more aggressive than the Idp Initiated SSO in that the InReponseTo attribute will be completely ignored. Do not use this unless your IdP is inadvertently sending InResponseTo values when it should not. + /// + /// true if replay attacks should be allowed; otherwise, false> + public bool AllowReplayAttacks { get; set; } + + /// + /// Gets or sets a value indicating whether quirks mode should be enabled. + /// + /// true if quirks mode should be enabled; otherwise, false. + public bool QuirksMode { get; set; } + + /// + /// Gets or sets a value for overriding option for the default UTF-8 encoding convention on SAML responses + /// + /// The response encoding. + public string ResponseEncoding { get; set; } + + + /// + /// Gets or sets the artifact resolution. + /// + /// The artifact resolution. + public HttpAuth ArtifactResolution { get; set; } + + /// + /// Gets or sets the attribute query configuration parameters. + /// + /// The attribute query. + public HttpAuth AttributeQuery { get; set; } + + /// + /// Gets or sets the certificate validations. + /// + /// The certificate validations. + public List CertificateValidationTypes { get; set; } + + + /// + /// Gets or sets the common domain cookie configuration settings. + /// + /// The common domain cookie. + public IDictionary CommonDomainCookie { get; set; } + + /// + /// Gets or sets the endpoints. + /// + /// The endpoints. + public IdentityProviderEndpoints Endpoints { get; set; } + + /// + /// Gets or sets the persistent pseudonym configuration settings. + /// + /// The persistent pseudonym. + public PersistentPseudonym PersistentPseudonym { get; set; } + + /// + /// Gets the element key. + /// + public object ElementKey + { + get { return Id; } + } + + public IdentityProvider() + { + Endpoints = new IdentityProviderEndpoints(); + } + } +} diff --git a/src/SAML2.Standard/Config/IdentityProviderEndpoint.cs b/src/SAML2.Standard/Config/IdentityProviderEndpoint.cs new file mode 100644 index 0000000..ce297b4 --- /dev/null +++ b/src/SAML2.Standard/Config/IdentityProviderEndpoint.cs @@ -0,0 +1,42 @@ +using System.Configuration; + +namespace SAML2.Config +{ + /// + /// Identity Provider Endpoint configuration element. + /// + public class IdentityProviderEndpoint + { + /// + /// Gets or sets the binding. + /// + /// The binding. + public BindingType Binding { get; set; } + + /// + /// Gets or sets the protocol binding to force. + /// + /// The force protocol binding. + public string ForceProtocolBinding { get; set; } + + /// + /// Gets or sets a value indicating a caller to access the xml representation of an assertion before it's + /// translated to a strongly typed instance + /// + /// The token accessor. + public string TokenAccessor { get; set; } + + /// + /// Gets or sets the type. + /// + /// The type. + public EndpointType Type { get; set; } + + /// + /// Gets or sets the URL. + /// + /// The URL. + public string Url { get; set; } + + } +} diff --git a/src/SAML2.Standard/Config/IdentityProviderEndpoints.cs b/src/SAML2.Standard/Config/IdentityProviderEndpoints.cs new file mode 100644 index 0000000..0f58150 --- /dev/null +++ b/src/SAML2.Standard/Config/IdentityProviderEndpoints.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using System.Configuration; +using System.Linq; + +namespace SAML2.Config +{ + /// + /// Identity Provider Endpoint configuration collection. + /// + public class IdentityProviderEndpoints : List + { + public IdentityProviderEndpoints() : base() { } + public IdentityProviderEndpoints(IEnumerable collection) : base(collection) { } + /// + /// Gets the log off endpoint. + /// + public IdentityProviderEndpoint DefaultLogoutEndpoint + { + get { return this.FirstOrDefault(x => x.Type == EndpointType.Logout); } + } + + /// + /// Gets the sign on endpoint. + /// + public IdentityProviderEndpoint DefaultSignOnEndpoint + { + get { return this.FirstOrDefault(x => x.Type == EndpointType.SignOn); } + } + + } +} diff --git a/src/SAML2.Standard/Config/IdentityProviders.cs b/src/SAML2.Standard/Config/IdentityProviders.cs new file mode 100644 index 0000000..ee650cb --- /dev/null +++ b/src/SAML2.Standard/Config/IdentityProviders.cs @@ -0,0 +1,123 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Security.Cryptography; +using System.Text; +using System.Xml; +using SAML2.Schema.Metadata; +using SAML2.Utils; + +namespace SAML2.Config +{ + /// + /// Identity Provider configuration collection. + /// + //[ConfigurationCollection(typeof(IdentityProviderElement), CollectionType = ConfigurationElementCollectionType.AddRemoveClearMap)] + public class IdentityProviders : List + { + /// + /// A list of the files that have currently been loaded. The filename is used as key, while last seen modification time is used as value. + /// + private Dictionary _fileInfo; + + /// + /// The locking object for assuring thread safe refresh. + /// + private object _lockSync = new object(); + + public IdentityProviders() : base() { Initialize(); } + + public IdentityProviders(IEnumerable collection) : base(collection) { Initialize(); } + /// + /// Initializes a new instance of the class. + /// + private void Initialize() + { + _fileInfo = new Dictionary(); + } + + + + /// + /// Gets or sets the encodings. + /// + public string Encodings { get; set; } + + + /// + /// Gets the selection URL to use for choosing identity providers if multiple are available and none are set as default. + /// + public string SelectionUrl { get; set; } + + public void AddByMetadataUrl(Uri url) + { + var request = System.Net.WebRequest.Create(url); + // It may be more efficient to pass the stream directly, but + // it's likely a bit safer to pull the data off the response + // stream and create a new memorystream with the data + using (var ms = new MemoryStream()) { + using (var response = request.GetResponse().GetResponseStream()) { + response.CopyTo(ms); + response.Close(); + } + ms.Seek(0, SeekOrigin.Begin); // Rewind memorystream back to the beginning + // We want to allow exceptions to bubble up in this case + var metadataDoc = new Saml20MetadataDocument(ms, GetEncodings()); + AdjustIdpListWithNewMetadata(metadataDoc); + } + } + + public void AddByMetadataDirectory(string path) + { + AddByMetadata(Directory.GetFiles(path)); + } + + public void AddByMetadata(params string[] files) + { + foreach (var file in files) { + TryAddByMetadata(file); // ignore errors + } + } + public bool TryAddByMetadata(string file) + { + try { + var metadataDoc = new Saml20MetadataDocument(file, GetEncodings()); + AdjustIdpListWithNewMetadata(metadataDoc); + return true; + } + catch (Exception) { + return false; + } + } + + private void AdjustIdpListWithNewMetadata(Saml20MetadataDocument metadataDoc) + { + var endp = this.FirstOrDefault(x => x.Id == metadataDoc.EntityId); + if (endp == null) { + // If the endpoint does not exist, create it. + endp = new IdentityProvider(); + Add(endp); + } + + endp.Id = endp.Name = metadataDoc.EntityId; + endp.Metadata = metadataDoc; + } + + + /// + /// Returns a list of the encodings that should be tried when a metadata file does not contain a valid signature + /// or cannot be loaded by the XmlDocument class. Either returns a list specified by the administrator in the configuration file + /// or a default list. + /// + /// The list of encodings. + internal IEnumerable GetEncodings() + { + var rc = string.IsNullOrEmpty(Encodings) + ? new [] { Encoding.UTF8, Encoding.GetEncoding("iso-8859-1") } + : Encodings.Split(' ').Select(Encoding.GetEncoding); + + return rc; + } + } +} diff --git a/src/SAML2.Standard/Config/Metadata.cs b/src/SAML2.Standard/Config/Metadata.cs new file mode 100644 index 0000000..0db1103 --- /dev/null +++ b/src/SAML2.Standard/Config/Metadata.cs @@ -0,0 +1,48 @@ +using SAML2.Schema.Metadata; +using System.Collections.Generic; +using System.Configuration; + +namespace SAML2.Config +{ + /// + /// Metadata configuration element. + /// + public class Metadata + { + + + /// + /// Gets or sets a value indicating whether to exclude artifact endpoints in metadata generation. + /// + /// true if exclude artifact endpoints; otherwise, false. + + public bool ExcludeArtifactEndpoints { get; set; } + + /// + /// Gets or sets the contacts. + /// + /// The contacts. + + public IEnumerable Contacts { get; set; } + + /// + /// Gets or sets the organization. + /// + /// The organization. + + public Organization Organization { get; set; } + + /// + /// Gets or sets the requested attributes. + /// + /// The requested attributes. + public IList RequestedAttributes { get; set; } + + public Metadata() + { + RequestedAttributes = new List(); + //Organization = new Organization(); // The Organization element appears to break metaadata (missing required lang attribute) + Contacts = new List(); + } + } +} diff --git a/src/SAML2.Standard/Config/NameIdFormat.cs b/src/SAML2.Standard/Config/NameIdFormat.cs new file mode 100644 index 0000000..7ac5671 --- /dev/null +++ b/src/SAML2.Standard/Config/NameIdFormat.cs @@ -0,0 +1,21 @@ +using System.Configuration; + +namespace SAML2.Config +{ + /// + /// NameIdFormat configuration element. + /// + public class NameIdFormat + { + /// + /// The regular expression string used to validate the Format attribute. + /// + private const string NameIdFormatsRegex = @"^(urn:oasis:names:tc:SAML:2\.0:nameid-format:persistent|urn:oasis:names:tc:SAML:2\.0:nameid-format:transient|urn:oasis:names:tc:SAML:1\.1:nameid-format:emailAddress|urn:oasis:names:tc:SAML:1\.1:nameid-format:unspecified|urn:oasis:names:tc:SAML:1\.1:nameid-format:X509SubjectName|urn:oasis:names:tc:SAML:1\.1:nameid-format:WindowsDomainQualifiedName|urn:oasis:names:tc:SAML:2\.0:nameid-format:kerberos|urn:oasis:names:tc:SAML:2\.0:nameid-format:entity)$"; + + /// + /// Gets or sets the format. + /// + /// The format. + public string Format { get; set; } + } +} diff --git a/src/SAML2.Standard/Config/NameIdFormats.cs b/src/SAML2.Standard/Config/NameIdFormats.cs new file mode 100644 index 0000000..5984209 --- /dev/null +++ b/src/SAML2.Standard/Config/NameIdFormats.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; +using System.Configuration; + +namespace SAML2.Config +{ + /// + /// Service Provider Endpoint configuration collection. + /// + public class NameIdFormats : List + { + public NameIdFormats() : base() { } + public NameIdFormats(IEnumerable collection) : base(collection) { } + + } +} diff --git a/src/SAML2.Standard/Config/Organization.cs b/src/SAML2.Standard/Config/Organization.cs new file mode 100644 index 0000000..4be65a1 --- /dev/null +++ b/src/SAML2.Standard/Config/Organization.cs @@ -0,0 +1,27 @@ +using System.Configuration; + +namespace SAML2.Config +{ + /// + /// Organization configuration element. + /// + public class Organization + { + /// + /// Gets or sets the display name. + /// + /// The display name. + public string DisplayName { get; set; } + /// + /// Gets or sets the name. + /// + /// The name. + + public string Name { get; set; } + /// + /// Gets or sets the URL. + /// + /// The URL. + public string Url { get; set; } + } +} \ No newline at end of file diff --git a/src/SAML2.Standard/Config/PersistentPseudonym.cs b/src/SAML2.Standard/Config/PersistentPseudonym.cs new file mode 100644 index 0000000..1a6d053 --- /dev/null +++ b/src/SAML2.Standard/Config/PersistentPseudonym.cs @@ -0,0 +1,41 @@ +using System; +using System.Configuration; +using SAML2.Identity; + +namespace SAML2.Config +{ + /// + /// Persistent Pseudonym configuration element. + /// + public class PersistentPseudonym + { + /// + /// Persistent Pseudonym mapper instance. + /// + private IPersistentPseudonymMapper _mapper; + + /// + /// Gets or sets the mapper. + /// + /// The mapper. + public string Mapper { get; set; } + + /// + /// Returns the runtime-class configured pseudonym mapper (if any is present) for a given IdP. + /// + /// The implementation. + public IPersistentPseudonymMapper GetMapper() + { + if (!string.IsNullOrEmpty(Mapper)) + { + _mapper = (IPersistentPseudonymMapper)Activator.CreateInstance(Type.GetType(Mapper), true); + } + else + { + _mapper = null; + } + + return _mapper; + } + } +} diff --git a/src/SAML2.Standard/Config/Saml2Configuration.cs b/src/SAML2.Standard/Config/Saml2Configuration.cs new file mode 100644 index 0000000..36b359e --- /dev/null +++ b/src/SAML2.Standard/Config/Saml2Configuration.cs @@ -0,0 +1,64 @@ +using System.Collections.Generic; +using System.Configuration; + +namespace SAML2.Config +{ + /// + /// SAML2 Configuration Section. + /// + public class Saml2Configuration + { + /// + /// Gets the section name. + /// + public static string Name { get { return "saml2"; } } + + /// + /// Gets or sets the allowed audience uris. + /// + /// The allowed audience uris. + public List AllowedAudienceUris { get; set; } + + /// + /// Gets or sets the assertion profile. + /// + /// The assertion profile configuration. + public string AssertionProfileValidator { get; set; } + /// + /// Gets or sets the common domain cookie configuration. + /// + /// The common domain cookie configuration. + public CommonDomainCookie CommonDomainCookie { get; set; } + + /// + /// Gets or sets the identity providers. + /// + /// The identity providers. + public IdentityProviders IdentityProviders { get; set; } + /// + /// Gets or sets the logging configuration. + /// + /// The logging configuration. + public string LoggingFactoryType { get; set; } + + /// + /// Gets or sets the metadata. + /// + /// The metadata. + public Metadata Metadata { get; set; } + + /// + /// Gets or sets the service provider. + /// + /// The service provider. + public ServiceProvider ServiceProvider { get; set; } + + public Saml2Configuration() + { + IdentityProviders = new IdentityProviders(); + AllowedAudienceUris = new List(); + Metadata = new Metadata(); + } + + } +} diff --git a/src/SAML2.Standard/Config/ServiceProvider.cs b/src/SAML2.Standard/Config/ServiceProvider.cs new file mode 100644 index 0000000..830b1fa --- /dev/null +++ b/src/SAML2.Standard/Config/ServiceProvider.cs @@ -0,0 +1,58 @@ +using System.Configuration; +using System.Security.Cryptography.X509Certificates; + +namespace SAML2.Config +{ + /// + /// ServiceProvider configuration element. + /// + public class ServiceProvider + { + + /// + /// Gets or sets the id. + /// + /// The id. + public string Id { get; set; } + + /// + /// Gets or sets the server. + /// + /// The server. + public string Server { get; set; } + + + /// + /// Gets or sets the authentication contexts. + /// + /// The authentication contexts. + public AuthenticationContexts AuthenticationContexts + { get; set; } + + /// + /// Gets or sets the endpoints. + /// + /// The endpoints. + public ServiceProviderEndpoints Endpoints { get; set; } + + /// + /// Gets or sets the name id formats. + /// + /// The name id formats. + public NameIdFormats NameIdFormats { get; set; } + + /// + /// Gets or sets the signing certificate. + /// + /// The signing certificate. + public X509Certificate2 SigningCertificate { get; set; } + + + public ServiceProvider() + { + NameIdFormats = new NameIdFormats(); + Endpoints = new ServiceProviderEndpoints(); + AuthenticationContexts = new AuthenticationContexts(); + } + } +} diff --git a/src/SAML2.Standard/Config/ServiceProviderEndpoint.cs b/src/SAML2.Standard/Config/ServiceProviderEndpoint.cs new file mode 100644 index 0000000..645fa9e --- /dev/null +++ b/src/SAML2.Standard/Config/ServiceProviderEndpoint.cs @@ -0,0 +1,50 @@ +using System.Configuration; + +namespace SAML2.Config +{ + /// + /// Service Provider Endpoint configuration element. + /// + public class ServiceProviderEndpoint + { + public ServiceProviderEndpoint() { } + public ServiceProviderEndpoint(EndpointType type, string localPath, string redirectUrl = null, BindingType bindingType = BindingType.NotSet) : this() + { + Type = type; + LocalPath = localPath; + RedirectUrl = redirectUrl; + Binding = bindingType; + } + + /// + /// Gets or sets the binding. + /// + /// The binding. + public BindingType Binding { get; set; } + + /// + /// Gets or sets the index. + /// + /// The index. + public int Index { get; set; } + + /// + /// Gets or sets the local path. + /// + /// The local path. + public string LocalPath { get; set; } + + /// + /// Gets or sets the redirect URL. + /// + /// The redirect URL. + public string RedirectUrl { get; set; } + + /// + /// Gets or sets the type. + /// + /// The type. + public EndpointType Type { get; set; } + + } +} diff --git a/src/SAML2.Standard/Config/ServiceProviderEndpoints.cs b/src/SAML2.Standard/Config/ServiceProviderEndpoints.cs new file mode 100644 index 0000000..8b70d68 --- /dev/null +++ b/src/SAML2.Standard/Config/ServiceProviderEndpoints.cs @@ -0,0 +1,30 @@ +using System.Collections.Generic; +using System.Configuration; +using System.Linq; + +namespace SAML2.Config +{ + /// + /// Service Provider Endpoint configuration collection. + /// + public class ServiceProviderEndpoints : List + { + public ServiceProviderEndpoints() : base() { } + public ServiceProviderEndpoints(IEnumerable collection) : base(collection) { } + /// + /// Gets the log off endpoint. + /// + public ServiceProviderEndpoint DefaultLogoutEndpoint + { + get { return this.FirstOrDefault(x => x.Type == EndpointType.Logout); } + } + + /// + /// Gets the sign on endpoint. + /// + public ServiceProviderEndpoint DefaultSignOnEndpoint + { + get { return this.FirstOrDefault(x => x.Type == EndpointType.SignOn); } + } + } +} diff --git a/src/SAML2.Standard/ErrorMessages.resx b/src/SAML2.Standard/ErrorMessages.resx new file mode 100644 index 0000000..9f5b052 --- /dev/null +++ b/src/SAML2.Standard/ErrorMessages.resx @@ -0,0 +1,252 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Artifact is not from a known identity provider + + + Could not verify SAML SOAP binding message signature + + + ArtifactResponse did not contain an assertion + + + Unsupported payload message in ArtifactResponse + + + Could not verify artifact response message signature + + + ArtifactResponse status code was invalid, expected {0} + + + Assertion expiration has been exceeded + + + Could not process assertion with an unknown identity provider + + + Assertion not found + + + Assertion with OneTimeUse condition detected more than once + + + Assertion signature is invalid + + + Attribute query can not be performed when user is not logged in with an identity provider + + + AttrQuery response with status code "{0}" received when "Success" was expected + + + Certificate with DN "{0}" and thumbprint "{1}" is not valid according to RFC3280 + + + Certificate {0} was not found + + + Found more than one certificate matching {0} + + + Common Domain Cookie identity provider not found in list of known identity providers: {0} + + + Configuration element <saml2> does not contain an <identityProviders> element + + + Configuration for <identityProviders> does not contain a "metadata" attribute + + + Configuration does not contain <saml2> element + + + Configuration element <saml2> does not contain a <serviceProvider> element + + + Configuration for <serviceProvider> does not contain an "id" attribute + + + Configuration for <serviceProvider> does not contain a <signingCertificate> element + + + Configuration for <serviceProvider> does not contain a SignOn endpoint + + + Specified <signingCertificate> does not have a private key + + + The endpoint binding must be one of POST, Redirect, or Artifact + + + Empty protocol message id is not allowed + + + Session ExpectedInResponseTo missing + + + An error occurred + + + The specified metadata directory "{0}" could not be located + + + The "sign"query string parameter could not be parsed + + + Possible replay attack detected, unexpected value {0} for InResponseTo, expected {1} + + + Request signature is invalid + + + Request is not signed + + + Received a response message that did not contain an InResponseTo attribute + + + Response signature is invalid + + + Response is not signed + + + Response with status code NoPassive received. A user cannot be signed in with the IsPassiveFlag set when the user does not have a session with the identity provider + + + Response with status code "{0}" received when "Success"was expected + + + SOAP message did not contain a supported SamlMessage element + + + User accessing resource "{0}" without authentication + + + Encoding "{0}" is not supported + + + Unknown identity provider "{0}" + + + RequestType "{0}" is not supported. + + \ No newline at end of file diff --git a/src/SAML2.Standard/ISaml20IdpTokenAccessor.cs b/src/SAML2.Standard/ISaml20IdpTokenAccessor.cs new file mode 100644 index 0000000..1c22da1 --- /dev/null +++ b/src/SAML2.Standard/ISaml20IdpTokenAccessor.cs @@ -0,0 +1,18 @@ +using System.Xml; + +namespace SAML2 +{ + /// + /// Implementers of this interface will be presented with the Xml form of the SAML 2.0 assertion issued by the IdP + /// before it is translated to a runtime type. + /// Implementers MUST NOT alter the xml element or its containing xml document as this may invalidate the xml signature + /// + public interface ISaml20IdpTokenAccessor + { + /// + /// Read the incoming xml representation of the assertion + /// + /// The cml representation of assertion. + void ReadToken(XmlElement assertion); + } +} diff --git a/src/SAML2.Standard/Identity/IPersistentPseudonymMapper.cs b/src/SAML2.Standard/Identity/IPersistentPseudonymMapper.cs new file mode 100644 index 0000000..4ae3472 --- /dev/null +++ b/src/SAML2.Standard/Identity/IPersistentPseudonymMapper.cs @@ -0,0 +1,19 @@ +using SAML2.Schema.Core; + +namespace SAML2.Identity +{ + /// + /// Implement this interface and register it in web.config on the relevant IdP endpoint to activate mapping of persistent pseudonyms + /// before creating the name of the current IPrincipal. The returned value will be set as the Name on the Identity of the current Principal. + /// If not registered, the Identity.Name value will be the value of the SAML Subject as returned by the IdP. + /// + public interface IPersistentPseudonymMapper + { + /// + /// Service-provider specific pseudonym mapping is implemented here + /// + /// The SAML Subject identity value returned by the IdP + /// The service-provider specific mapping of the input parameter + string MapIdentity(NameId samlSubject); + } +} diff --git a/src/SAML2.Standard/Identity/ISaml20Identity.cs b/src/SAML2.Standard/Identity/ISaml20Identity.cs new file mode 100644 index 0000000..cab44ba --- /dev/null +++ b/src/SAML2.Standard/Identity/ISaml20Identity.cs @@ -0,0 +1,34 @@ +using System.Collections.Generic; +using System.Security.Principal; +using SAML2.Schema.Core; + +namespace SAML2.Identity +{ + /// + /// The SAML 2.0 extension to the IIdentity interface. + /// + public interface ISaml20Identity : IEnumerable, IIdentity + { + /// + /// Gets the value of the persistent pseudonym issued by the IdP if the Service Provider connection + /// is set up with persistent pseudonyms. Otherwise, returns null. + /// + string PersistentPseudonym { get; } + + /// + /// Retrieve an SAML 20 attribute using its name. Note that this is the value contained in the 'Name' attribute, and + /// not the 'FriendlyName' attribute. + /// + /// The attribute name. + /// A list of . + /// If the identity instance does not have the requested attribute. + List this[string attributeName] { get; } + + /// + /// Check if the identity contains a certain attribute. + /// + /// The name of the attribute to look for. + /// true if the specified attribute name has attribute; otherwise, false. + bool HasAttribute(string attributeName); + } +} \ No newline at end of file diff --git a/src/SAML2.Standard/Identity/Saml20Identity.cs b/src/SAML2.Standard/Identity/Saml20Identity.cs new file mode 100644 index 0000000..1eaac6b --- /dev/null +++ b/src/SAML2.Standard/Identity/Saml20Identity.cs @@ -0,0 +1,151 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Security.Principal; +using SAML2.Config; +using SAML2.Schema.Core; + +namespace SAML2.Identity +{ + /// + /// + /// A specialized version of GenericIdentity that contains attributes from a SAML 2 assertion. + /// + /// + /// The AuthenticationType property of the Identity will be "urn:oasis:names:tc:SAML:2.0:assertion". + /// + /// + /// The order of the attributes is not maintained when converting from the SAML assertion to this class. + /// + /// + [Serializable] + public class Saml20Identity : GenericIdentity, ISaml20Identity + { + /// + /// The attributes. + /// + private readonly Dictionary> _attributes; + + /// + /// Initializes a new instance of the class. + /// + /// The name. + /// The attributes. + /// The persistent pseudonym. + public Saml20Identity(string name, ICollection attributes, string persistentPseudonym) + : base(name, Saml20Constants.Assertion) + { + PersistentPseudonym = persistentPseudonym; + + _attributes = new Dictionary>(); + foreach (var att in attributes) + { + if (!_attributes.ContainsKey(att.Name)) + { + _attributes.Add(att.Name, new List()); + } + + _attributes[att.Name].Add(att); + } + } + + /// + /// Gets the value of the persistent pseudonym issued by the IdP if the Service Provider connection + /// is set up with persistent pseudonyms. Otherwise, returns null. + /// + /// The persistent pseudonym. + public string PersistentPseudonym { get; private set; } + + /// + /// Retrieve an SAML 20 attribute using its name. Note that this is the value contained in the 'Name' attribute, and + /// not the 'FriendlyName' attribute. + /// + /// The attribute name. + /// List of . + /// If the identity instance does not have the requested attribute. + public List this[string attributeName] + { + get { return _attributes[attributeName]; } + } + + + /// + /// Returns an enumerator that iterates through a collection. + /// + /// + /// An object that can be used to iterate through the collection. + /// + public IEnumerator GetEnumerator() + { + return ((IEnumerable)this).GetEnumerator(); + } + + /// + /// Check if the identity contains a certain attribute. + /// + /// The name of the attribute to look for. + /// true if the specified attribute name has attribute; otherwise, false. + public bool HasAttribute(string attributeName) + { + return _attributes.ContainsKey(attributeName); + } + + /// + /// Returns an enumerator that iterates through the collection. + /// + /// + /// A that can be used to iterate through the collection. + /// + IEnumerator IEnumerable.GetEnumerator() + { + var allAttributes = new List(); + foreach (var name in _attributes.Keys) + { + allAttributes.AddRange(_attributes[name]); + } + + return allAttributes.GetEnumerator(); + } + + /// + /// This method converts the received SAML assertion into an . + /// + /// The assertion. + /// The point. + /// The . + internal static IPrincipal InitSaml20Identity(Saml20Assertion assertion, IdentityProvider point) + { + var isPersistentPseudonym = assertion.Subject.Format == Saml20Constants.NameIdentifierFormats.Persistent; + + // Protocol-level support for persistent pseudonyms: If a mapper has been configured, use it here before constructing the principal. + var subjectIdentifier = assertion.Subject.Value; + if (isPersistentPseudonym && point.PersistentPseudonym != null) + { + subjectIdentifier = point.PersistentPseudonym.GetMapper().MapIdentity(assertion.Subject); + } + + // Create identity + var identity = new Saml20Identity(subjectIdentifier, assertion.Attributes, isPersistentPseudonym ? assertion.Subject.Value : null); + + return new GenericPrincipal(identity, new string[] { }); + } + + /// + /// Adds the attribute from query. + /// + /// The name. + /// The value. + internal void AddAttributeFromQuery(string name, SamlAttribute value) + { + if (!_attributes.ContainsKey(name)) + { + _attributes.Add(name, new List()); + } + + if (!_attributes[name].Contains(value)) + { + _attributes[name].Add(value); + } + } + } +} diff --git a/src/SAML2.Standard/Logging/DebugLoggerFactory.cs b/src/SAML2.Standard/Logging/DebugLoggerFactory.cs new file mode 100644 index 0000000..9ca033e --- /dev/null +++ b/src/SAML2.Standard/Logging/DebugLoggerFactory.cs @@ -0,0 +1,152 @@ +using System; + +namespace SAML2.Logging +{ + public class DebugLoggerFactory : ILoggerFactory + { + private readonly IInternalLogger verboseLogger = new VerboseLogger(s => { + System.Diagnostics.Debug.WriteLine(s); + }); + + public IInternalLogger LoggerFor(Type type) + { + return LoggerFor((string)null); + } + + public IInternalLogger LoggerFor(string keyName) + { + return verboseLogger; + } + + private class VerboseLogger : IInternalLogger + { + private readonly Action write; + public VerboseLogger(Action write) + { + if (write == null) throw new ArgumentNullException("write"); + this.write = write; + } + public bool IsDebugEnabled + { + get + { + return true; + } + } + + public bool IsErrorEnabled + { + get + { + return true; + } + } + + public bool IsFatalEnabled + { + get + { + return true; + } + } + + public bool IsInfoEnabled + { + get + { + return true; + } + } + + public bool IsWarnEnabled + { + get + { + return true; + } + } + + public void Debug(object message) + { + Write("DEBUG", message); + } + + + private void WriteFormat(string verbosity, string format, params object[] args) + { + Write(verbosity, string.Format(format, args)); + } + private void Write(string verbosity, object message, Exception exception = null) + { + if (exception != null) + message = message.ToString() + ", EXCEPTION: " + exception.Message; + write(string.Format("{0}: {1}", verbosity, message)); + } + + public void Debug(object message, Exception exception) + { + Write("DEBUG", message, exception); + } + + public void DebugFormat(string format, params object[] args) + { + WriteFormat("DEBUG", format, args); + } + + public void Error(object message) + { + Write("ERROR", message); + } + + public void Error(object message, Exception exception) + { + Write("ERROR", message, exception); + } + + public void ErrorFormat(string format, params object[] args) + { + WriteFormat("ERROR", format, args); + } + + public void Fatal(object message) + { + Write("FATAL", message); + } + + public void Fatal(object message, Exception exception) + { + Write("FATAL", message, exception); + } + + public void Info(object message) + { + Write("INFO", message); + } + + public void Info(object message, Exception exception) + { + Write("INFO", message, exception); + } + + public void InfoFormat(string format, params object[] args) + { + WriteFormat("INFO", format, args); + } + + public void Warn(object message) + { + Write("WARN", message); + } + + public void Warn(object message, Exception exception) + { + Write("WARN", message, exception); + } + + public void WarnFormat(string format, params object[] args) + { + WriteFormat("WARN", format, args); + } + } + } +} diff --git a/src/SAML2.Standard/Logging/IInternalLogger.cs b/src/SAML2.Standard/Logging/IInternalLogger.cs new file mode 100644 index 0000000..bf80c86 --- /dev/null +++ b/src/SAML2.Standard/Logging/IInternalLogger.cs @@ -0,0 +1,128 @@ +using System; + +namespace SAML2.Logging +{ + /// + /// Interface for all internal logging implementations. + /// + public interface IInternalLogger + { + /// + /// Gets a value indicating whether this instance is debug enabled. + /// + bool IsDebugEnabled { get; } + + /// + /// Gets a value indicating whether this instance is error enabled. + /// + bool IsErrorEnabled { get; } + + /// + /// Gets a value indicating whether this instance is fatal enabled. + /// + bool IsFatalEnabled { get; } + + /// + /// Gets a value indicating whether this instance is info enabled. + /// + bool IsInfoEnabled { get; } + + /// + /// Gets a value indicating whether this instance is warn enabled. + /// + bool IsWarnEnabled { get; } + + /// + /// Logs specified debug message. + /// + /// The message. + void Debug(object message); + + /// + /// Logs specified debug message. + /// + /// The message. + /// The exception. + void Debug(object message, Exception exception); + + /// + /// Logs specified debug message. + /// + /// The format. + /// The args. + void DebugFormat(string format, params object[] args); + + /// + /// Logs specified error message. + /// + /// The message. + void Error(object message); + + /// + /// Logs specified error message. + /// + /// The message. + /// The exception. + void Error(object message, Exception exception); + + /// + /// Logs specified error message. + /// + /// The format. + /// The args. + void ErrorFormat(string format, params object[] args); + + /// + /// Logs specified fatal error message. + /// + /// The message. + void Fatal(object message); + + /// + /// Logs specified fatal error message. + /// + /// The message. + /// The exception. + void Fatal(object message, Exception exception); + + /// + /// Logs specified info message. + /// + /// The message. + void Info(object message); + + /// + /// Logs specified info message. + /// + /// The message. + /// The exception. + void Info(object message, Exception exception); + + /// + /// Logs specified info message. + /// + /// The format. + /// The args. + void InfoFormat(string format, params object[] args); + + /// + /// Logs specified warn message. + /// + /// The message. + void Warn(object message); + + /// + /// Logs specified warn message. + /// + /// The message. + /// The exception. + void Warn(object message, Exception exception); + + /// + /// Logs specified warn message. + /// + /// The format. + /// The args. + void WarnFormat(string format, params object[] args); + } +} diff --git a/src/SAML2.Standard/Logging/ILoggerFactory.cs b/src/SAML2.Standard/Logging/ILoggerFactory.cs new file mode 100644 index 0000000..ab05772 --- /dev/null +++ b/src/SAML2.Standard/Logging/ILoggerFactory.cs @@ -0,0 +1,22 @@ +namespace SAML2.Logging +{ + /// + /// Interface for all logger factory implementations. + /// + public interface ILoggerFactory + { + /// + /// Gets a logger for the specified name. + /// + /// Name of the key. + /// An implementation. + IInternalLogger LoggerFor(string keyName); + + /// + /// Gets a logger for specified type. + /// + /// The type. + /// An implementation. + IInternalLogger LoggerFor(System.Type type); + } +} diff --git a/src/SAML2.Standard/Logging/LoggerProvider.cs b/src/SAML2.Standard/Logging/LoggerProvider.cs new file mode 100644 index 0000000..d86d1b7 --- /dev/null +++ b/src/SAML2.Standard/Logging/LoggerProvider.cs @@ -0,0 +1,117 @@ +using System; +using SAML2.Config; + +namespace SAML2.Logging +{ + /// + /// Logger provider. + /// + public class LoggerProvider + { + /// + /// Logger provider static instance. + /// + private static LoggerProvider _instance; + + /// + /// The logger factory. + /// + private readonly ILoggerFactory _loggerFactory; + + public static Saml2Configuration Configuration { get; set; } + + static LoggerProvider() + { + SetLoggerFactory(new LazyLoggerFactory()); + } + private class LazyLoggerFactory : ILoggerFactory + { + private ILoggerFactory loggerFactory; + private ILoggerFactory LoggerFactory + { + get { return loggerFactory = (loggerFactory ?? LocateLoggerFactory()); } + } + private ILoggerFactory LocateLoggerFactory() + { + string loggerClass = Configuration.LoggingFactoryType; + return string.IsNullOrEmpty(loggerClass) ? new NoLoggingLoggerFactory() : GetLoggerFactory(loggerClass); + } + public IInternalLogger LoggerFor(string keyName) + { + return LoggerFactory.LoggerFor(keyName); + } + + public IInternalLogger LoggerFor(Type type) + { + return LoggerFactory.LoggerFor(type); + } + } + + /// + /// Initializes a new instance of the class. + /// + /// The logger factory. + private LoggerProvider(ILoggerFactory loggerFactory) + { + _loggerFactory = loggerFactory; + } + + /// + /// Gets a logger for the specified key. + /// + /// Name of the key. + /// An instance of . + public static IInternalLogger LoggerFor(string keyName) + { + return _instance._loggerFactory.LoggerFor(keyName); + } + + /// + /// Gets a logger for the specified type. + /// + /// The type. + /// An instance of . + public static IInternalLogger LoggerFor(Type type) + { + return _instance._loggerFactory.LoggerFor(type); + } + + /// + /// Sets the logger factory. + /// + /// The logger factory. + public static void SetLoggerFactory(ILoggerFactory loggerFactory) + { + _instance = new LoggerProvider(loggerFactory); + } + + /// + /// Gets the logger factory. + /// + /// The SAML2 logger class. + /// The implementation of . + private static ILoggerFactory GetLoggerFactory(string saml2LoggerClass) + { + ILoggerFactory loggerFactory; + var loggerFactoryType = Type.GetType(saml2LoggerClass); + try + { + loggerFactory = (ILoggerFactory)Activator.CreateInstance(loggerFactoryType); + } + catch (MissingMethodException ex) + { + throw new ApplicationException("Public constructor was not found for " + loggerFactoryType, ex); + } + catch (InvalidCastException ex) + { + throw new ApplicationException(loggerFactoryType + "Type does not implement " + typeof(ILoggerFactory), ex); + } + catch (Exception ex) + { + throw new ApplicationException("Unable to instantiate: " + loggerFactoryType, ex); + } + + return loggerFactory; + } + } +} diff --git a/src/SAML2.Standard/Logging/NoLoggingInternalLogger.cs b/src/SAML2.Standard/Logging/NoLoggingInternalLogger.cs new file mode 100644 index 0000000..00e47ca --- /dev/null +++ b/src/SAML2.Standard/Logging/NoLoggingInternalLogger.cs @@ -0,0 +1,171 @@ +using System; + +namespace SAML2.Logging +{ + /// + /// Internal logger implementation that provides no logging services. + /// + public class NoLoggingInternalLogger : IInternalLogger + { + /// + /// Gets a value indicating whether this instance is debug enabled. + /// + public bool IsDebugEnabled + { + get { return false; } + } + + /// + /// Gets a value indicating whether this instance is error enabled. + /// + public bool IsErrorEnabled + { + get { return false; } + } + + /// + /// Gets a value indicating whether this instance is fatal enabled. + /// + public bool IsFatalEnabled + { + get { return false; } + } + + /// + /// Gets a value indicating whether this instance is info enabled. + /// + public bool IsInfoEnabled + { + get { return false; } + } + + /// + /// Gets a value indicating whether this instance is warn enabled. + /// + public bool IsWarnEnabled + { + get { return false; } + } + + /// + /// Logs specified debug message. + /// + /// The message. + public void Debug(object message) + { + } + + /// + /// Logs specified debug message. + /// + /// The message. + /// The exception. + public void Debug(object message, Exception exception) + { + } + + /// + /// Logs specified debug message. + /// + /// The format. + /// The args. + public void DebugFormat(string format, params object[] args) + { + } + + /// + /// Logs specified error message. + /// + /// The message. + public void Error(object message) + { + } + + /// + /// Logs specified error message. + /// + /// The message. + /// The exception. + public void Error(object message, Exception exception) + { + } + + /// + /// Logs specified error message. + /// + /// The format. + /// The args. + public void ErrorFormat(string format, params object[] args) + { + } + + /// + /// Logs specified fatal error message. + /// + /// The message. + public void Fatal(object message) + { + } + + /// + /// Logs specified fatal error message. + /// + /// The message. + /// The exception. + public void Fatal(object message, Exception exception) + { + } + + /// + /// Logs specified info message. + /// + /// The message. + public void Info(object message) + { + } + + /// + /// Logs specified info message. + /// + /// The message. + /// The exception. + public void Info(object message, Exception exception) + { + } + + /// + /// Logs specified info message. + /// + /// The format. + /// The args. + public void InfoFormat(string format, params object[] args) + { + } + + /// + /// Logs specified warn message. + /// + /// The message. + public void Warn(object message) + { + } + + /// + /// Logs specified warn message. + /// + /// The message. + /// The exception. + public void Warn(object message, Exception exception) + { + } + + /// + /// Logs specified warn message. + /// + /// The format. + /// The args. + public void WarnFormat(string format, params object[] args) + { + } + } +} diff --git a/src/SAML2.Standard/Logging/NoLoggingLoggerFactory.cs b/src/SAML2.Standard/Logging/NoLoggingLoggerFactory.cs new file mode 100644 index 0000000..fd89d4e --- /dev/null +++ b/src/SAML2.Standard/Logging/NoLoggingLoggerFactory.cs @@ -0,0 +1,33 @@ +namespace SAML2.Logging +{ + /// + /// Logging factory used to create a . + /// + public class NoLoggingLoggerFactory : ILoggerFactory + { + /// + /// No Logging internal logger instance. + /// + private static readonly IInternalLogger NoLogging = new NoLoggingInternalLogger(); + + /// + /// Gets a logger for the specified name. + /// + /// Name of the key. + /// An implementation. + public IInternalLogger LoggerFor(string keyName) + { + return NoLogging; + } + + /// + /// Gets a logger for specified type. + /// + /// The type. + /// An implementation. + public IInternalLogger LoggerFor(System.Type type) + { + return NoLogging; + } + } +} diff --git a/src/SAML2.Standard/Protocol/CommonDomainCookie.cs b/src/SAML2.Standard/Protocol/CommonDomainCookie.cs new file mode 100644 index 0000000..11a855f --- /dev/null +++ b/src/SAML2.Standard/Protocol/CommonDomainCookie.cs @@ -0,0 +1,142 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace SAML2.Protocol +{ + /// + /// Implements access to the common domain cookie specified in the SAML20 identity provider discovery profile. + /// + public class CommonDomainCookie + { + /// + /// The name of the common domain cookie. + /// + public const string CommonDomainCookieName = "_samlIdp"; + + + /// + /// The KnownIdps backing field. + /// + private readonly List _knownIdps; + + /// + /// The SAML identity provider. + /// + private readonly string _samlIdp; + + /// + /// Indicates if this instance has been loaded. + /// + private bool _isLoaded; + + /// + /// Indicates if the cookie is set. + /// + private bool _isSet; + + /// + /// Initializes a new instance of the class. + /// + /// The SAML identity provider. + public CommonDomainCookie(string samlIdp) + { + this._samlIdp = samlIdp; + this._knownIdps = new List(); + } + + /// + /// Gets a value indicating whether the Common Domain Cookie was set (had valid values). + /// + /// true if the Common Domain Cookie is set; otherwise, false. + public bool IsSet + { + get + { + Load(); + + return _isSet; + } + } + + /// + /// Gets the list of known IDPs. + /// + /// The known IDPs. Caller should check that values are valid URIs before using them as such. + public List KnownIdps + { + get + { + EnsureSet(); + + return this._knownIdps; + } + } + + /// + /// Gets the preferred IDP. + /// + /// The preferred IDP. Caller should check that this value is a valid URI. + public string PreferredIDP + { + get + { + EnsureSet(); + + return this._knownIdps.Count > 0 ? this._knownIdps[this._knownIdps.Count - 1] : string.Empty; + } + } + + /// + /// Ensures the cookie is set. + /// + private void EnsureSet() + { + Load(); + if (!_isSet) + { + throw new Saml20Exception("The common domain cookie is not set. Please make sure to check the IsSet property before accessing the class' properties."); + } + } + + /// + /// Loads this instance. + /// + private void Load() + { + if (!string.IsNullOrEmpty(this._samlIdp)) + { + LoadFromString(); + } + } + + /// + /// Loads from string. + /// + private void LoadFromString() + { + if (!_isLoaded) + { + ParseCookie(this._samlIdp); + _isSet = true; + _isLoaded = true; + } + } + + /// + /// Parses the cookie. + /// + /// The raw value. + private void ParseCookie(string rawValue) + { + var value = Uri.UnescapeDataString(rawValue); + var idps = value.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); + foreach (var base64idp in idps) + { + var bytes = Convert.FromBase64String(base64idp); + var idp = Encoding.ASCII.GetString(bytes); + _knownIdps.Add(idp); + } + } + } +} diff --git a/src/SAML2.Standard/Protocol/Logout.cs b/src/SAML2.Standard/Protocol/Logout.cs new file mode 100644 index 0000000..e526c0e --- /dev/null +++ b/src/SAML2.Standard/Protocol/Logout.cs @@ -0,0 +1,103 @@ +using SAML2.Bindings; +using SAML2.Logging; +using SAML2.Schema.Protocol; +using SAML2.Utils; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using SAML2.Config; +using SAML2.Standard; + +namespace SAML2.Protocol +{ + public class Logout + { + private readonly Saml2Configuration config; + private readonly IInternalLogger logger; + + public Logout(IInternalLogger logger, SAML2.Config.Saml2Configuration config) + { + this.logger = logger ?? throw new ArgumentNullException("logger"); + this.config = config ?? throw new ArgumentNullException("config"); + } + public void ValidateLogoutRequest(string requestType, System.Collections.Specialized.NameValueCollection requestParams, Uri requestUrl) + { + logger.DebugFormat(TraceMessages.LogoutResponseReceived); + + var message = string.Empty; + LogoutResponse response = null; + switch (requestType) { + case "GET": + ValidateLogoutViaGet(requestUrl, out message, out response); + break; + case "POST": + ValidateLogoutViaPost(requestParams, out message, out response); + break; + default: + break; + } + + if (response == null) { + logger.ErrorFormat(ErrorMessages.UnsupportedRequestType, requestType); + throw new Saml20Exception(string.Format(ErrorMessages.UnsupportedRequestType, requestType)); + } + + logger.DebugFormat(TraceMessages.LogoutResponseParsed, message); + + if (response.Status.StatusCode.Value != Saml20Constants.StatusCodes.Success) { + logger.ErrorFormat(ErrorMessages.ResponseStatusNotSuccessful, response.Status.StatusCode.Value); + throw new Saml20Exception(string.Format(ErrorMessages.ResponseStatusNotSuccessful, response.Status.StatusCode.Value)); + } + } + + private void ValidateLogoutViaPost(System.Collections.Specialized.NameValueCollection requestParams, out string message, out LogoutResponse response) + { + var parser = new HttpPostBindingParser(requestParams); + logger.DebugFormat(TraceMessages.LogoutResponsePostBindingParse, parser.Message); + + response = Serialization.DeserializeFromXmlString(parser.Message); + + var idp = IdpSelectionUtil.RetrieveIDPConfiguration(response.Issuer.Value, config); + if (idp.Metadata == null) { + logger.ErrorFormat(ErrorMessages.UnknownIdentityProvider, idp.Id); + throw new Saml20Exception(string.Format(ErrorMessages.UnknownIdentityProvider, idp.Id)); + } + + if (!parser.IsSigned) { + logger.Error(ErrorMessages.ResponseSignatureMissing); + throw new Saml20Exception(ErrorMessages.ResponseSignatureMissing); + } + + // signature on final message in logout + if (!parser.CheckSignature(idp.Metadata.Keys)) { + logger.Error(ErrorMessages.ResponseSignatureInvalid); + throw new Saml20Exception(ErrorMessages.ResponseSignatureInvalid); + } + + message = parser.Message; + } + + private void ValidateLogoutViaGet(Uri requestUrl, out string message, out LogoutResponse response) + { + var parser = new HttpRedirectBindingParser(requestUrl); + response = Serialization.DeserializeFromXmlString(parser.Message); + + logger.DebugFormat(TraceMessages.LogoutResponseRedirectBindingParse, parser.Message, parser.SignatureAlgorithm, parser.Signature); + + var idp = IdpSelectionUtil.RetrieveIDPConfiguration(response.Issuer.Value, config); + if (idp.Metadata == null) { + logger.ErrorFormat(ErrorMessages.UnknownIdentityProvider, idp.Id); + throw new Saml20Exception(string.Format(ErrorMessages.UnknownIdentityProvider, idp.Id)); + } + + if (!parser.VerifySignature(idp.Metadata.Keys)) { + logger.Error(ErrorMessages.ResponseSignatureInvalid); + throw new Saml20Exception(ErrorMessages.ResponseSignatureInvalid); + } + + message = parser.Message; + } + } +} diff --git a/src/SAML2.Standard/Protocol/Utility.cs b/src/SAML2.Standard/Protocol/Utility.cs new file mode 100644 index 0000000..e21440d --- /dev/null +++ b/src/SAML2.Standard/Protocol/Utility.cs @@ -0,0 +1,430 @@ +using SAML2.Bindings; +using SAML2.Config; +using SAML2.Logging; +using SAML2.Schema.Core; +using SAML2.Schema.Metadata; +using SAML2.Schema.Protocol; +using SAML2.Specification; +using SAML2.Standard; +using SAML2.Utils; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; +using System.Security.Cryptography.Xml; +using System.Text; +using System.Xml; + +namespace SAML2.Protocol +{ + public class Utility + { + private static readonly IInternalLogger logger = LoggerProvider.LoggerFor(typeof(Utility)); + + /// + /// Expected responses if session support is not present + /// + private static readonly HashSet expectedResponses = new HashSet(); + + /// + /// Session key used to save the current message id with the purpose of preventing replay attacks + /// + private const string ExpectedInResponseToSessionKey = "ExpectedInResponseTo"; + + /// + /// Gets the trusted signers. + /// + /// The keys. + /// The identity provider. + /// List of trusted certificate signers. + public static IEnumerable GetTrustedSigners(ICollection keys, IdentityProvider identityProvider) + { + if (keys == null) { + throw new ArgumentNullException("keys"); + } + + foreach (var clause in keys.SelectMany(k => k.KeyInfo.Items.AsEnumerable().Cast())) { + // Check certificate specifications + if (clause is KeyInfoX509Data) { + var cert = XmlSignatureUtils.GetCertificateFromKeyInfo((KeyInfoX509Data)clause); + if (!CertificateSatisfiesSpecifications(identityProvider, cert)) { + continue; + } + } + + var key = XmlSignatureUtils.ExtractKey(clause); + yield return key; + } + } + + /// + /// Determines whether the certificate is satisfied by all specifications. + /// + /// The identity provider. + /// The cert. + /// true if certificate is satisfied by all specifications; otherwise, false. + private static bool CertificateSatisfiesSpecifications(IdentityProvider idp, X509Certificate2 cert) + { + return SpecificationFactory.GetCertificateSpecifications(idp).All(spec => spec.IsSatisfiedBy(cert)); + } + + /// + /// Retrieves the name of the issuer from an XmlElement containing an assertion. + /// + /// An XmlElement containing an assertion + /// The identifier of the Issuer + public static string GetIssuer(XmlElement assertion) + { + var result = string.Empty; + var list = assertion.GetElementsByTagName("Issuer", Saml20Constants.Assertion); + if (list.Count > 0) { + var issuer = (XmlElement)list[0]; + result = issuer.InnerText; + } + + return result; + } + + /// + /// Gets the assertion. + /// + /// The el. + /// if set to true [is encrypted]. + /// The assertion XML. + public static XmlElement GetAssertion(XmlElement el, out bool isEncrypted) + { + logger.Debug(TraceMessages.AssertionParse); + + var encryptedList = el.GetElementsByTagName(EncryptedAssertion.ElementName, Saml20Constants.Assertion); + if (encryptedList.Count == 1) { + isEncrypted = true; + var encryptedAssertion = (XmlElement)encryptedList[0]; + + logger.DebugFormat(TraceMessages.EncryptedAssertionFound, encryptedAssertion.OuterXml); + + return encryptedAssertion; + } + + var assertionList = el.GetElementsByTagName(Assertion.ElementName, Saml20Constants.Assertion); + if (assertionList.Count == 1) { + isEncrypted = false; + var assertion = (XmlElement)assertionList[0]; + + logger.DebugFormat(TraceMessages.AssertionFound, assertion.OuterXml); + + return assertion; + } + + logger.Warn(ErrorMessages.AssertionNotFound); + + isEncrypted = false; + return null; + } + + public static void AddExpectedResponseId(string id) + { + expectedResponses.Add(id); + } + + + /// + /// Is called before the assertion is made into a strongly typed representation + /// + /// The assertion element. + /// The endpoint. + /// + public static void PreHandleAssertion(XmlElement elem, IdentityProvider endpoint) + { + logger.DebugFormat(TraceMessages.AssertionPrehandlerCalled); + + if (endpoint != null && endpoint.Endpoints.DefaultLogoutEndpoint != null && !string.IsNullOrEmpty(endpoint.Endpoints.DefaultLogoutEndpoint.TokenAccessor)) { + var idpTokenAccessor = Activator.CreateInstance(Type.GetType(endpoint.Endpoints.DefaultLogoutEndpoint.TokenAccessor, false)) as ISaml20IdpTokenAccessor; + if (idpTokenAccessor != null) { + logger.DebugFormat("{0}.{1} called", idpTokenAccessor.GetType(), "ReadToken"); + idpTokenAccessor.ReadToken(elem); + logger.DebugFormat("{0}.{1} finished", idpTokenAccessor.GetType(), "ReadToken"); + } + } + } + + /// + /// Gets the decoded SAML response. + /// + /// This is base64 encoded SAML Response (usually SAMLResponse on query string) + /// The encoding. + /// The decoded SAML response XML. + public static XmlDocument GetDecodedSamlResponse(string samlResponse, Encoding encoding) + { + logger.Debug(TraceMessages.SamlResponseDecoding); + + + var doc = new XmlDocument { PreserveWhitespace = true }; + samlResponse = encoding.GetString(Convert.FromBase64String(samlResponse)); + doc.LoadXml(samlResponse); + + logger.DebugFormat(TraceMessages.SamlResponseDecoded, samlResponse); + + return doc; + } + + /// + /// Gets the decrypted assertion. + /// + /// The elem. + /// The decrypted . + public static Saml20EncryptedAssertion GetDecryptedAssertion(XmlElement elem, Saml2Configuration config) + { + logger.Debug(TraceMessages.EncryptedAssertionDecrypting); + + var decryptedAssertion = new Saml20EncryptedAssertion((RSA)config.ServiceProvider.SigningCertificate.PrivateKey); + decryptedAssertion.LoadXml(elem); + decryptedAssertion.Decrypt(); + + logger.DebugFormat(TraceMessages.EncryptedAssertionDecrypted, decryptedAssertion.Assertion.DocumentElement.OuterXml); + + return decryptedAssertion; + } + + /// + /// Gets the status element. + /// + /// The element. + /// The element. + public static Status GetStatusElement(XmlElement element) + { + var statElem = element.GetElementsByTagName(Status.ElementName, Saml20Constants.Protocol)[0]; + return Serialization.DeserializeFromXmlString(statElem.OuterXml); + } + + /// + /// Checks for replay attack. + /// + /// The context. + /// The element. + public static void CheckReplayAttack(XmlElement element, bool requireInResponseTo, IDictionary session) + { + logger.Debug(TraceMessages.ReplayAttackCheck); + + var inResponseToAttribute = element.Attributes["InResponseTo"]; + if (!requireInResponseTo && inResponseToAttribute == null) { + return; + } + if (inResponseToAttribute == null) { + throw new Saml20Exception(ErrorMessages.ResponseMissingInResponseToAttribute); + } + + var inResponseTo = inResponseToAttribute.Value; + if (string.IsNullOrEmpty(inResponseTo)) { + throw new Saml20Exception(ErrorMessages.ExpectedInResponseToEmpty); + } + + if (session != null) { + if (!session.ContainsKey(ExpectedInResponseToSessionKey)) { + throw new Saml20Exception(ErrorMessages.ExpectedInResponseToMissing); + } + var expectedInResponseTo = (string)session[ExpectedInResponseToSessionKey]; + + if (inResponseTo != expectedInResponseTo) { + logger.ErrorFormat(ErrorMessages.ReplayAttack, inResponseTo, expectedInResponseTo); + throw new Saml20Exception(string.Format(ErrorMessages.ReplayAttack, inResponseTo, expectedInResponseTo)); + } + } else { + if (!expectedResponses.Contains(inResponseTo)) { + throw new Saml20Exception(ErrorMessages.ExpectedInResponseToMissing); + } + expectedResponses.Remove(inResponseTo); + } + logger.Debug(TraceMessages.ReplaceAttackCheckCleared); + } + + public static void AddExpectedResponse(Saml20AuthnRequest request, IDictionary session) + { + // Save request message id to session + if (session != null) { + session.Add(ExpectedInResponseToSessionKey, request.Id); + } else { + expectedResponses.Add(request.Id); + } + } + + /// + /// Deserializes an assertion, verifies its signature and logs in the user if the assertion is valid. + /// + /// The context. + /// The elem. + public static Saml20Assertion HandleAssertion(XmlElement elem, Saml2Configuration config, Func getFromCache, Action setInCache) + { + logger.DebugFormat(TraceMessages.AssertionProcessing, elem.OuterXml); + + var issuer = GetIssuer(elem); + var endp = IdpSelectionUtil.RetrieveIDPConfiguration(issuer, config); + + PreHandleAssertion(elem, endp); + + if (endp == null || endp.Metadata == null) { + logger.Error(ErrorMessages.AssertionIdentityProviderUnknown); + throw new Saml20Exception(ErrorMessages.AssertionIdentityProviderUnknown); + } + + var quirksMode = endp.QuirksMode; + var assertion = new Saml20Assertion(elem, null, quirksMode, config); + + // Check signatures + if (!endp.OmitAssertionSignatureCheck) { + if (!assertion.CheckSignature(GetTrustedSigners(endp.Metadata.GetKeys(KeyTypes.Signing), endp))) { + logger.Error(ErrorMessages.AssertionSignatureInvalid); + throw new Saml20Exception(ErrorMessages.AssertionSignatureInvalid); + } + } + + // Check expiration + if (assertion.IsExpired) { + logger.Error(ErrorMessages.AssertionExpired); + throw new Saml20Exception(ErrorMessages.AssertionExpired); + } + + // Check one time use + if (assertion.IsOneTimeUse) { + if (getFromCache(assertion.Id) != null) { + logger.Error(ErrorMessages.AssertionOneTimeUseExceeded); + throw new Saml20Exception(ErrorMessages.AssertionOneTimeUseExceeded); + } + + setInCache(assertion.Id, string.Empty, assertion.NotOnOrAfter); + } + + logger.DebugFormat(TraceMessages.AssertionParsed, assertion.Id); + return assertion; + } + + /// + /// Decrypts an encrypted assertion, and sends the result to the HandleAssertion method. + /// + /// The context. + /// The elem. + public static Saml20Assertion HandleEncryptedAssertion(XmlElement elem, Saml2Configuration config, Func getFromCache, Action setInCache) + { + return HandleAssertion(GetDecryptedAssertion(elem, config).Assertion.DocumentElement, config, getFromCache, setInCache); + } + + /// + /// Handles the SOAP. + /// + /// The context. + /// The input stream. + public static void HandleSoap(HttpArtifactBindingBuilder builder, Stream inputStream, Saml2Configuration config, Action signonCallback, Func getFromCache, Action setInCache, IDictionary session) + { + var parser = new HttpArtifactBindingParser(inputStream); + logger.DebugFormat(TraceMessages.SOAPMessageParse, parser.SamlMessage.OuterXml); + + if (parser.IsArtifactResolve) { + logger.Debug(TraceMessages.ArtifactResolveReceived); + + var idp = IdpSelectionUtil.RetrieveIDPConfiguration(parser.Issuer, config); + if (!parser.CheckSamlMessageSignature(idp.Metadata.Keys)) { + logger.Error(ErrorMessages.ArtifactResolveSignatureInvalid); + throw new Saml20Exception(ErrorMessages.ArtifactResolveSignatureInvalid); + } + + builder.RespondToArtifactResolve(parser.ArtifactResolve, ((XmlDocument)getFromCache(parser.ArtifactResolve.Artifact)).DocumentElement); + } else if (parser.IsArtifactResponse) { + logger.Debug(TraceMessages.ArtifactResolveReceived); + + var idp = IdpSelectionUtil.RetrieveIDPConfiguration(parser.Issuer, config); + if (!parser.CheckSamlMessageSignature(idp.Metadata.Keys)) { + logger.Error(ErrorMessages.ArtifactResponseSignatureInvalid); + throw new Saml20Exception(ErrorMessages.ArtifactResponseSignatureInvalid); + } + + var status = parser.ArtifactResponse.Status; + if (status.StatusCode.Value != Saml20Constants.StatusCodes.Success) { + logger.ErrorFormat(ErrorMessages.ArtifactResponseStatusCodeInvalid, status.StatusCode.Value); + throw new Saml20Exception(string.Format(ErrorMessages.ArtifactResponseStatusCodeInvalid, status.StatusCode.Value)); + } + + if (parser.ArtifactResponse.Any.LocalName == Response.ElementName) { + Utility.CheckReplayAttack(parser.ArtifactResponse.Any, true, session); + + var responseStatus = Utility.GetStatusElement(parser.ArtifactResponse.Any); + if (responseStatus.StatusCode.Value != Saml20Constants.StatusCodes.Success) { + logger.ErrorFormat(ErrorMessages.ArtifactResponseStatusCodeInvalid, responseStatus.StatusCode.Value); + throw new Saml20Exception(string.Format(ErrorMessages.ArtifactResponseStatusCodeInvalid, responseStatus.StatusCode.Value)); + } + + var assertion = Utility.GetAssertion(parser.ArtifactResponse.Any, out bool isEncrypted); + if (assertion == null) { + logger.Error(ErrorMessages.ArtifactResponseMissingAssertion); + throw new Saml20Exception(ErrorMessages.ArtifactResponseMissingAssertion); + } + + var samlAssertion = isEncrypted + ? Utility.HandleEncryptedAssertion(assertion, config, getFromCache, setInCache) + : Utility.HandleAssertion(assertion, config, getFromCache, setInCache); + signonCallback(samlAssertion); + } else { + logger.ErrorFormat(ErrorMessages.ArtifactResponseMissingResponse); + throw new Saml20Exception(ErrorMessages.ArtifactResponseMissingResponse); + } + } else { + logger.ErrorFormat(ErrorMessages.SOAPMessageUnsupportedSamlMessage); + throw new Saml20Exception(ErrorMessages.SOAPMessageUnsupportedSamlMessage); + } + } + + + /// + /// Handle the authentication response from the IDP. + /// + /// The context. + public static Saml20Assertion HandleResponse(Saml2Configuration config, string samlResponse, IDictionary session, Func getFromCache, Action setInCache) + { + var defaultEncoding = Encoding.UTF8; + var doc = Utility.GetDecodedSamlResponse(samlResponse, defaultEncoding); + logger.DebugFormat(TraceMessages.SamlResponseReceived, doc.OuterXml); + + // Determine whether the assertion should be decrypted before being validated. + var assertion = Utility.GetAssertion(doc.DocumentElement, out bool isEncrypted); + if (isEncrypted) { + assertion = Utility.GetDecryptedAssertion(assertion, config).Assertion.DocumentElement; + } + + // Check if an encoding-override exists for the IdP endpoint in question + var issuer = Utility.GetIssuer(assertion); + var endpoint = IdpSelectionUtil.RetrieveIDPConfiguration(issuer, config); + if (!endpoint.AllowReplayAttacks) { + Utility.CheckReplayAttack(doc.DocumentElement, !endpoint.AllowIdPInitiatedSso, session); + } + var status = Utility.GetStatusElement(doc.DocumentElement); + if (status.StatusCode.Value != Saml20Constants.StatusCodes.Success) { + if (status.StatusCode.Value == Saml20Constants.StatusCodes.NoPassive) { + logger.Error(ErrorMessages.ResponseStatusIsNoPassive); + throw new Saml20Exception(ErrorMessages.ResponseStatusIsNoPassive); + } + + logger.ErrorFormat(ErrorMessages.ResponseStatusNotSuccessful, status); + throw new Saml20Exception(string.Format(ErrorMessages.ResponseStatusNotSuccessful, status)); + } + + if (!string.IsNullOrEmpty(endpoint.ResponseEncoding)) { + Encoding encodingOverride; + try { + encodingOverride = Encoding.GetEncoding(endpoint.ResponseEncoding); + } + catch (ArgumentException ex) { + logger.ErrorFormat(ErrorMessages.UnknownEncoding, endpoint.ResponseEncoding); + throw new ArgumentException(string.Format(ErrorMessages.UnknownEncoding, endpoint.ResponseEncoding), ex); + } + + if (encodingOverride.CodePage != defaultEncoding.CodePage) { + var doc1 = GetDecodedSamlResponse(samlResponse, encodingOverride); + assertion = GetAssertion(doc1.DocumentElement, out isEncrypted); + } + } + + return HandleAssertion(assertion, config, getFromCache, setInCache); + } + + } +} diff --git a/src/SAML2.Standard/Resources.resx b/src/SAML2.Standard/Resources.resx new file mode 100644 index 0000000..aa78a90 --- /dev/null +++ b/src/SAML2.Standard/Resources.resx @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Please choose the identity provider of your choice from the list below: + + + Choose Identity Provider + + \ No newline at end of file diff --git a/src/SAML2.Standard/SAML2.Standard.csproj b/src/SAML2.Standard/SAML2.Standard.csproj new file mode 100644 index 0000000..8d42718 --- /dev/null +++ b/src/SAML2.Standard/SAML2.Standard.csproj @@ -0,0 +1,49 @@ + + + + netstandard2.0 + + + + + + + + + + + + + ErrorMessages.resx + True + True + + + Resources.resx + True + True + + + TraceMessages.resx + True + True + + + + + + Designer + ErrorMessages.Designer.cs + PublicResXFileCodeGenerator + + + Resources.Designer.cs + PublicResXFileCodeGenerator + + + TraceMessages.Designer.cs + PublicResXFileCodeGenerator + + + + diff --git a/src/SAML2.Standard/Saml20ArtifactResolve.cs b/src/SAML2.Standard/Saml20ArtifactResolve.cs new file mode 100644 index 0000000..2324b51 --- /dev/null +++ b/src/SAML2.Standard/Saml20ArtifactResolve.cs @@ -0,0 +1,97 @@ +using System; +using System.Xml; +using SAML2.Config; +using SAML2.Schema.Core; +using SAML2.Schema.Protocol; +using SAML2.Utils; + +namespace SAML2 +{ + /// + /// Encapsulates the ArtifactResolve schema class. + /// + public class Saml20ArtifactResolve + { + /// + /// Artifact resolve. + /// + private readonly ArtifactResolve _artifactResolve; + + /// + /// Initializes a new instance of the class. + /// + public Saml20ArtifactResolve() + { + _artifactResolve = new ArtifactResolve + { + Version = Saml20Constants.Version, + Id = "id" + Guid.NewGuid().ToString("N"), + Issuer = new NameId(), + IssueInstant = DateTime.Now + }; + } + + /// + /// Gets or sets the artifact string. + /// + /// The artifact string. + public string Artifact + { + get { return _artifactResolve.Artifact; } + set { _artifactResolve.Artifact = value; } + } + + /// + /// Gets the ID of the SAML message. + /// + /// The ID. + public string Id + { + get { return _artifactResolve.Id; } + } + + /// + /// Gets or sets the issuer. + /// + /// The issuer. + public string Issuer + { + get { return _artifactResolve.Issuer.Value; } + set { _artifactResolve.Issuer.Value = value; } + } + + /// + /// Gets the underlying schema instance. + /// + /// The . + public ArtifactResolve Resolve + { + get + { + return _artifactResolve; + } + } + + /// + /// Gets a default instance of this class with proper values set. + /// + /// The default . + public static Saml20ArtifactResolve GetDefault(string serviceProviderId) + { + if (serviceProviderId == null) throw new ArgumentNullException("serviceProviderId"); + return new Saml20ArtifactResolve { Issuer = serviceProviderId }; + } + + /// + /// Returns the ArtifactResolve as an XML document. + /// + /// The XML document. + public XmlDocument GetXml() + { + var doc = new XmlDocument { PreserveWhitespace = true }; + doc.LoadXml(Serialization.SerializeToXmlString(_artifactResolve)); + + return doc; + } + } +} diff --git a/src/SAML2.Standard/Saml20ArtifactResponse.cs b/src/SAML2.Standard/Saml20ArtifactResponse.cs new file mode 100644 index 0000000..a4f0201 --- /dev/null +++ b/src/SAML2.Standard/Saml20ArtifactResponse.cs @@ -0,0 +1,109 @@ +using System; +using System.Xml; +using SAML2.Config; +using SAML2.Schema.Core; +using SAML2.Schema.Protocol; +using SAML2.Utils; + +namespace SAML2 +{ + /// + /// Encapsulates the ArtifactResponse schema class + /// + public class Saml20ArtifactResponse + { + /// + /// The artifact response. + /// + private readonly ArtifactResponse _artifactResponse; + + /// + /// Initializes a new instance of the class. + /// + public Saml20ArtifactResponse() + { + _artifactResponse = new ArtifactResponse + { + Version = Saml20Constants.Version, + ID = "id" + Guid.NewGuid().ToString("N"), + Issuer = new NameId(), + IssueInstant = DateTime.Now, + Status = new Status { StatusCode = new StatusCode() } + }; + } + + /// + /// Gets the ID. + /// + /// The ID. + public string Id + { + get { return _artifactResponse.ID; } + } + + /// + /// Gets or sets the issuer. + /// + /// The issuer. + public string Issuer + { + get { return _artifactResponse.Issuer.Value; } + set { _artifactResponse.Issuer.Value = value; } + } + + /// + /// Gets or sets InResponseTo. + /// + /// The in response to. + public string InResponseTo + { + get { return _artifactResponse.InResponseTo; } + set { _artifactResponse.InResponseTo = value; } + } + + /// + /// Gets or sets the SAML element. + /// + /// The SAML element. + public XmlElement SamlElement + { + get { return _artifactResponse.Any; } + set { _artifactResponse.Any = value; } + } + + /// + /// Gets or sets the status code. + /// + /// The status code. + public string StatusCode + { + get { return _artifactResponse.Status.StatusCode.Value; } + set { _artifactResponse.Status.StatusCode.Value = value; } + } + + /// + /// Gets a default instance of this class with proper values set. + /// + /// The default . + public static Saml20ArtifactResponse GetDefault(string serviceProviderId) + { + if (serviceProviderId == null) throw new ArgumentNullException("serviceProviderId"); + var result = new Saml20ArtifactResponse(); + result.Issuer = serviceProviderId; + + return result; + } + + /// + /// Returns the ArtifactResponse as an XML document. + /// + /// The XML document. + public XmlDocument GetXml() + { + var doc = new XmlDocument { PreserveWhitespace = true }; + doc.LoadXml(Serialization.SerializeToXmlString(_artifactResponse)); + + return doc; + } + } +} diff --git a/src/SAML2.Standard/Saml20Assertion.cs b/src/SAML2.Standard/Saml20Assertion.cs new file mode 100644 index 0000000..b23a04c --- /dev/null +++ b/src/SAML2.Standard/Saml20Assertion.cs @@ -0,0 +1,630 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; +using System.Security.Cryptography.Xml; +using System.Xml; +using SAML2.Config; +using SAML2.Schema.Core; +using SAML2.Schema.Protocol; +using SAML2.Utils; +using SAML2.Validation; + +namespace SAML2 +{ + /// + /// Encapsulates the functionality required of a DK-SAML 2.0 Assertion. + /// + public class Saml20Assertion + { + /// + /// Configuration element + /// + private readonly Saml2Configuration _config = null; + + /// + /// Auto validate assertions. + /// + private readonly bool _autoValidate = true; + + /// + /// The profile. + /// + private readonly string _profile; + + /// + /// Quirks mode switch. + /// + private readonly bool _quirksMode; + + /// + /// A strongly-typed version of the assertion. It is generated on-demand from the contents of the _samlAssertion + /// field. + /// + private Assertion _assertion; + + /// + /// An list of the unencrypted attributes in the assertion. This list is lazy initialized, i.e. it will only be retrieved + /// from the _samlAssertion field when it is requested through the Attributes property. + /// When the Sign method is called, the attributes in the list are embedded into the _samlAssertion + /// and this variable is nulled. + /// + private List _assertionAttributes; + + /// + /// The assertion validator. + /// + private ISaml20AssertionValidator _assertionValidator; + + /// + /// List of encrypted assertion attributes. + /// + private List _encryptedAssertionAttributes; + + + /// + /// Initializes a new instance of the class. + /// + /// The assertion. + /// If null, the signature of the given assertion is not verified. + /// if set to true quirks mode is enabled. + public Saml20Assertion(XmlElement assertion, IEnumerable trustedSigners, bool quirksMode, Saml2Configuration config) + { + _quirksMode = quirksMode; + _profile = null; + _config = config; + LoadXml(assertion, trustedSigners, config); + } + + /// + /// Initializes a new instance of the class. + /// + /// The assertion. + /// If null, the signature of the given assertion is not verified. + /// Determines the type of validation to perform on the token + /// if set to true quirks mode is enabled. + public Saml20Assertion(XmlElement assertion, IEnumerable trustedSigners, string profile, bool quirksMode) + { + _profile = profile; + _quirksMode = quirksMode; + LoadXml(assertion, trustedSigners, null); + } + + /// + /// Initializes a new instance of the class. + /// + /// The assertion. + /// If null, the signature of the given assertion is not verified. + /// Determines the type of validation to perform on the token + /// if set to true quirks mode is enabled. + /// Turn automatic validation on or off + public Saml20Assertion(XmlElement assertion, IEnumerable trustedSigners, string profile, bool quirksMode, bool autoValidate) + { + _profile = profile; + _quirksMode = quirksMode; + _autoValidate = autoValidate; + LoadXml(assertion, trustedSigners, null); + } + + /// + /// Value of current time, usually needed only for unit tests + /// + internal static DateTime? AlternateNow { private get; set; } + + private DateTime Now + { + get { return (AlternateNow ?? (DateTime?)DateTime.Now.ToUniversalTime()).Value; } + } + + /// + /// Gets a strongly-typed version of the SAML Assertion. It is lazily generated based on the contents of the + /// _samlAssertion field. + /// + public Assertion Assertion + { + get + { + if (_assertion == null) + { + if (XmlAssertion == null) + { + throw new InvalidOperationException("No assertion is loaded."); + } + + _assertion = Serialization.Deserialize(new XmlNodeReader(XmlAssertion)); + } + + return _assertion; + } + } + + /// + /// Gets or sets the unencrypted attributes of the assertion. + /// + public List Attributes + { + get + { + if (_assertionAttributes == null) + { + ExtractAttributes(); // Lazy initialization of the attributes list. + } + + return _assertionAttributes; + } + + set + { + // _assertionAttributes == null is reserved for signalling that the attribute is not initialized, so + // convert it to an empty list. + if (value == null) + { + value = new List(0); + } + + _assertionAttributes = value; + } + } + + /// + /// Gets the conditions element of the assertion. + /// + /// The conditions element. + public Conditions Conditions + { + get + { + return _assertion.Conditions; + } + } + + /// + /// Gets or sets the encrypted attributes of the assertion. + /// + /// The encrypted attributes. + public List EncryptedAttributes + { + get + { + if (_encryptedAssertionAttributes == null) + { + ExtractAttributes(); // Lazy initialization of the attributes list. + } + + return _encryptedAssertionAttributes; + } + + set + { + // _encryptedAssertionAttributes == null is reserved for signalling that the attribute is not initialized, so + // convert it to an empty list. + if (value == null) + { + value = new List(0); + } + + _encryptedAssertionAttributes = value; + } + } + + /// + /// Gets or sets the encrypted id. + /// + /// The encrypted id. + public string EncryptedId { get; set; } + + /// + /// Gets the ID attribute of the <Assertion> element. + /// + public string Id + { + get { return Assertion.Id; } + } + + /// + /// Gets a value indicating whether the expiration time has been exceeded. + /// + public bool IsExpired + { + get { return Now > NotOnOrAfter; } + } + + /// + /// Gets a value indicating whether the assertion has a OneTimeUse condition. + /// + /// + /// true if the assertion has a OneTimeUse condition; otherwise, false. + /// + public bool IsOneTimeUse + { + get { return Assertion.Conditions.Items.OfType().Any(); } + } + + /// + /// Gets the value of the <Issuer> element. + /// + public string Issuer + { + get { return Assertion.Issuer.Value; } + } + + /// + /// Gets the NotOnOrAfter property, if it is included in the assertion. + /// + public DateTime NotOnOrAfter + { + get + { + // Find the SubjectConfirmation element for the ValidTo attribute. [DKSAML] ch. 7.1.4. + foreach (var o in Assertion.Subject.Items) + { + if (o is SubjectConfirmation) + { + var subjectConfirmation = (SubjectConfirmation)o; + if (subjectConfirmation.SubjectConfirmationData.NotOnOrAfter.HasValue) + { + return subjectConfirmation.SubjectConfirmationData.NotOnOrAfter.Value; + } + } + } + + return DateTime.MaxValue; + } + } + + /// + /// Gets the SessionIndex of the AuthnStatement + /// + public string SessionIndex + { + get + { + var list = Assertion.GetAuthnStatements(); + return list.Count > 0 ? list[0].SessionIndex : string.Empty; + } + } + + /// + /// Gets or sets the asymmetric key that can verify the signature of the assertion. + /// + public AsymmetricAlgorithm SigningKey { get; set; } + + /// + /// Gets the subject. + /// + /// The subject. + public NameId Subject + { + get { return Assertion.Subject.Items.OfType().FirstOrDefault(); } + } + + /// + /// Gets the subject items. + /// + /// The subject items. + public object[] SubjectItems + { + get + { + return Assertion.Subject.Items; + } + } + + /// + /// Gets the assertion in XmlElement representation. + /// + /// The XML assertion. + public XmlElement XmlAssertion { get; private set; } + + /// + /// Gets the assertion validator. + /// + private ISaml20AssertionValidator GetAssertionValidator(Saml2Configuration config) + { + if (_assertionValidator == null) + { + if (config == null || config.AllowedAudienceUris == null) + { + if (string.IsNullOrEmpty(_profile)) + { + _assertionValidator = new Saml20AssertionValidator(null, _quirksMode); + } + else + { + _assertionValidator = (ISaml20AssertionValidator)Activator.CreateInstance(Type.GetType(_profile), null, _quirksMode); + } + } + else + { + if (string.IsNullOrEmpty(_profile)) + { + _assertionValidator = new Saml20AssertionValidator(config.AllowedAudienceUris, _quirksMode); + } + else + { + _assertionValidator = (ISaml20AssertionValidator)Activator.CreateInstance(Type.GetType(_profile), config.AllowedAudienceUris, _quirksMode); + } + } + } + + return _assertionValidator; + } + + /// + /// Check the signature of the XmlDocument using the list of keys. + /// If the signature key is found, the SigningKey property is set. + /// + /// A list of KeyDescriptor elements. Probably extracted from the metadata describing the IDP that sent the message. + /// True, if one of the given keys was able to verify the signature. False in all other cases. + public bool CheckSignature(IEnumerable keys) + { + if (keys == null) + { + throw new ArgumentNullException("keys"); + } + + return keys.Where(key => key != null).Any(CheckSignature); + } + + /// + /// Verifies the assertion's signature and its time to live. + /// + /// The trusted signers. + /// if the assertion's signature can not be verified or its time to live has been exceeded. + public void CheckValid(IEnumerable trustedSigners) + { + if (!CheckSignature(trustedSigners)) + { + throw new Saml20Exception("Signature could not be verified."); + } + + if (IsExpired) + { + throw new Saml20Exception("Assertion is no longer valid."); + } + } + + /// + /// Returns the KeyInfo element of the signature of the token. + /// + /// Null if the token is not signed. The KeyInfo element otherwise. + public KeyInfo GetSignatureKeys() + { + return !XmlSignatureUtils.IsSigned(XmlAssertion) ? null : XmlSignatureUtils.ExtractSignatureKeys(XmlAssertion); + } + + /// + /// Returns the SubjectConfirmationData from the assertion subject items + /// + /// SubjectConfirmationData object from subject items, null if none present + public SubjectConfirmationData GetSubjectConfirmationData() + { + return SubjectItems.OfType().Select(item => item.SubjectConfirmationData).FirstOrDefault(); + } + + /// + /// Gets the assertion as an XmlDocument. + /// + /// The Xml of the assertion. + public XmlElement GetXml() + { + return XmlAssertion; + } + + /// + /// Signs the assertion with the given certificate. + /// + /// The certificate to sign the assertion with. + public void Sign(X509Certificate2 cert, Saml2Configuration config) + { + CheckCertificateCanSign(cert); + + // Clear the strongly typed version of the assertion in preparation for a new source. + _assertion = null; + + // Merge the modified attributes to the assertion. + InsertAttributes(); + + // Remove existing signatures when resigning the assertion + var signatureParentNode = XmlAssertion; // FIX.DocumentElement; + XmlNode sigNode; + while ((sigNode = signatureParentNode.GetElementsByTagName(Schema.XmlDSig.Signature.ElementName, Saml20Constants.Xmldsig)[0]) != null) + { + signatureParentNode.RemoveChild(sigNode); + } + + var assertionDocument = new XmlDocument(); + assertionDocument.Load(new StringReader(Serialization.SerializeToXmlString(XmlAssertion))); + + AddSignature(assertionDocument, cert); + + LoadXml(assertionDocument.DocumentElement, new List(new[] { cert.PublicKey.Key }), config); + } + + /// + /// Writes the token to a writer. + /// + /// The writer. + public void WriteAssertion(XmlWriter writer) + { + XmlAssertion.WriteTo(writer); + } + + /// + /// Adds the signature. + /// + /// The assertion document. + /// The cert. + private static void AddSignature(XmlDocument assertionDocument, X509Certificate2 cert) + { + var signedXml = new SignedXml(assertionDocument); + signedXml.SignedInfo.CanonicalizationMethod = SignedXml.XmlDsigExcC14NTransformUrl; + signedXml.SigningKey = cert.PrivateKey; + + // Retrieve the value of the "ID" attribute on the root assertion element. + var list = assertionDocument.GetElementsByTagName(Assertion.ElementName, Saml20Constants.Assertion); + var el = (XmlElement)list[0]; + var reference = new Reference("#" + el.GetAttribute("ID")); + + reference.AddTransform(new XmlDsigEnvelopedSignatureTransform()); + reference.AddTransform(new XmlDsigExcC14NTransform()); + + signedXml.AddReference(reference); + + // Include the public key of the certificate in the assertion. + // signedXml.KeyInfo = new KeyInfo(); + // signedXml.KeyInfo.AddClause(new KeyInfoX509Data(cert, X509IncludeOption.WholeChain)); + signedXml.ComputeSignature(); + + // Append the computed signature. The signature must be placed as the sibling of the Issuer element. + var nodes = assertionDocument.DocumentElement.GetElementsByTagName("Issuer", Saml20Constants.Assertion); + if (nodes.Count != 1) + { + throw new Saml20Exception("Assertion MUST contain one element."); + } + + assertionDocument.DocumentElement.InsertAfter(assertionDocument.ImportNode(signedXml.GetXml(), true), nodes[0]); + } + + /// + /// Checks the certificate can sign. + /// + /// The cert. + private static void CheckCertificateCanSign(X509Certificate2 cert) + { + if (cert == null) + { + throw new ArgumentNullException("cert"); + } + + if (!cert.HasPrivateKey) + { + throw new Saml20Exception("The private key must be part of the certificate."); + } + } + + /// + /// Checks the signature. + /// + /// The key. + /// True, if the given key was able to verify the signature. False in all other cases. + private bool CheckSignature(AsymmetricAlgorithm key) + { + if (XmlSignatureUtils.CheckSignature(XmlAssertion, key)) + { + SigningKey = key; + return true; + } + + return false; + } + + /// + /// Extracts the list of attributes from the <AttributeStatement> of the assertion, and + /// stores it in _assertionAttributes. + /// + private void ExtractAttributes() + { + _assertionAttributes = new List(0); + _encryptedAssertionAttributes = new List(0); + + var list = XmlAssertion.GetElementsByTagName(AttributeStatement.ElementName, Saml20Constants.Assertion); + if (list.Count == 0) + { + return; + } + + // NOTE It would be nice to implement a better-performing solution where only the AttributeStatement is converted. + // NOTE Namespace issues in the xml-schema "type"-attribute prevents this, though. + var assertion = Serialization.Deserialize(new XmlNodeReader(XmlAssertion)); + + var attributeStatements = assertion.GetAttributeStatements(); + if (attributeStatements.Count == 0 || attributeStatements[0].Items == null) + { + return; + } + + var attributeStatement = attributeStatements[0]; + foreach (var item in attributeStatement.Items) + { + if (item is SamlAttribute) + { + _assertionAttributes.Add((SamlAttribute)item); + } + + if (item is EncryptedElement) + { + _encryptedAssertionAttributes.Add((EncryptedElement)item); + } + } + } + + /// + /// Merges the modified attributes into AttributeStatement of the assertion. + /// + private void InsertAttributes() + { + if (_assertionAttributes == null) + { + return; + } + + // Generate the new AttributeStatement + var attributeStatement = new AttributeStatement(); + var statements = new List(_encryptedAssertionAttributes.Count + _assertionAttributes.Count); + statements.AddRange(_assertionAttributes.ToArray()); + statements.AddRange(_encryptedAssertionAttributes.ToArray()); + attributeStatement.Items = statements.ToArray(); + + var list = XmlAssertion.GetElementsByTagName(AttributeStatement.ElementName, Saml20Constants.Assertion); + + if (list.Count > 0) + { + // Remove the old AttributeStatement. + XmlAssertion.RemoveChild(list[0]); + + // FIX _samlAssertion.DocumentElement.RemoveChild(list[0]); + } + + // Only insert a new AttributeStatement if there are attributes. + if (statements.Count > 0) + { + // Convert the new AttributeStatement to the Document Object Model and make a silent prayer that one day we will + // be able to make this transition in a more elegant way. + var attributeStatementDoc = Serialization.Serialize(attributeStatement); + var attr = XmlAssertion.OwnerDocument.ImportNode(attributeStatementDoc.DocumentElement, true); + + // Insert the new statement. + XmlAssertion.AppendChild(attr); + } + + _encryptedAssertionAttributes = null; + _assertionAttributes = null; + } + + /// + /// Loads an assertion from XML. + /// + /// The element. + /// The trusted signers. + private void LoadXml(XmlElement element, IEnumerable trustedSigners, Saml2Configuration config) + { + XmlAssertion = element; + if (trustedSigners != null) + { + if (!CheckSignature(trustedSigners)) + { + throw new Saml20Exception("Assertion signature could not be verified."); + } + } + + // Validate the saml20Assertion. + if (_autoValidate) + { + GetAssertionValidator(config).ValidateAssertion(Assertion); + } + } + } +} diff --git a/src/SAML2.Standard/Saml20AttributeQuery.cs b/src/SAML2.Standard/Saml20AttributeQuery.cs new file mode 100644 index 0000000..ad70fa0 --- /dev/null +++ b/src/SAML2.Standard/Saml20AttributeQuery.cs @@ -0,0 +1,250 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using SAML2.Config; +using SAML2.Logging; +using SAML2.Schema.Core; +using SAML2.Schema.Protocol; + +namespace SAML2 +{ + /// + /// Performs SAML2.0 attribute queries + /// + public class Saml20AttributeQuery + { + /// + /// Logger instance. + /// + protected static readonly IInternalLogger Logger = LoggerProvider.LoggerFor(MethodBase.GetCurrentMethod().DeclaringType); + + /// + /// List of attributes. + /// + private readonly List _attributes; + + /// + /// The attribute query. + /// + private readonly AttributeQuery _attrQuery; + + /// + /// Prevents a default instance of the class from being created. + /// + private Saml20AttributeQuery() + { + _attrQuery = new AttributeQuery + { + Version = Saml20Constants.Version, + Id = "id" + Guid.NewGuid().ToString("N"), + Issuer = new NameId(), + IssueInstant = DateTime.Now, + Subject = new Subject() + }; + _attributes = new List(); + } + + /// + /// Gets the ID of the attribute query. + /// + /// The ID. + public string Id + { + get { return _attrQuery.Id; } + } + + /// + /// Gets or sets the issuer of the attribute query. + /// + /// The issuer. + public string Issuer + { + get { return _attrQuery.Issuer.Value; } + set { _attrQuery.Issuer.Value = value; } + } + + /// + /// Gets a default instance of this class with meaningful default values set. + /// + /// The default . + public static Saml20AttributeQuery GetDefault(Saml2Configuration config) + { + return new Saml20AttributeQuery { Issuer = config.ServiceProvider.Id }; + } + + /// + /// Adds an attribute to be queried using basic name format. + /// + /// Name of the attribute. + public void AddAttribute(string attrName) + { + AddAttribute(attrName, Saml20NameFormat.Basic); + } + + /// + /// Adds an attribute by name using the specified name format. + /// + /// Name of the attribute. + /// The name format of the attribute. + public void AddAttribute(string attrName, Saml20NameFormat nameFormat) + { + if (_attributes.Any(at => at.Name == attrName && at.NameFormat == GetNameFormat(nameFormat))) + { + throw new InvalidOperationException(string.Format("An attribute with name \"{0}\" and name format \"{1}\" has already been added", attrName, Enum.GetName(typeof(Saml20NameFormat), nameFormat))); + } + + var attr = new SamlAttribute + { + Name = attrName, + NameFormat = GetNameFormat(nameFormat) + }; + + _attributes.Add(attr); + } + + // TODO: I believe this is being used by the bindings, so it's likely to come back in some form + ///// + ///// Performs the attribute query and adds the resulting attributes to Saml20Identity.Current. + ///// + ///// The http context. + //public void PerformQuery(HttpContext context) + //{ + // var config = Saml2Config.GetConfig(); + + // var endpointId = (string)context.Session[Saml20AbstractEndpointHandler.IdpLoginSessionKey]; + // if (string.IsNullOrEmpty(endpointId)) + // { + // Logger.Error(ErrorMessages.AttrQueryNoLogin); + // throw new InvalidOperationException(ErrorMessages.AttrQueryNoLogin); + // } + + // var ep = config.IdentityProviders.FirstOrDefault(x => x.Id == endpointId); + // if (ep == null) + // { + // throw new Saml20Exception(string.Format(ErrorMessages.UnknownIdentityProvider, endpointId)); + // } + + // PerformQuery(context, ep); + //} + + + ///// + ///// Performs the attribute query against the specified IdP endpoint and adds the resulting attributes to Saml20Identity.Current. + ///// + ///// The http context. + ///// The IdP to perform the query against. + //public void PerformQuery(HttpContext context, IdentityProviderElement endPoint) + //{ + // var nameIdFormat = (string)context.Session[Saml20AbstractEndpointHandler.IdpNameIdFormat]; + // if (string.IsNullOrEmpty(nameIdFormat)) + // { + // nameIdFormat = Saml20Constants.NameIdentifierFormats.Persistent; + // } + + // PerformQuery(context, endPoint, nameIdFormat); + //} + + ///// + ///// Performs the attribute query against the specified IdP endpoint and adds the resulting attributes to Saml20Identity.Current. + ///// + ///// The http context. + ///// The IdP to perform the query against. + ///// The name id format. + //public void PerformQuery(HttpContext context, IdentityProviderElement endPoint, string nameIdFormat) + //{ + // Logger.DebugFormat("{0}.{1} called", GetType(), "PerformQuery()"); + + // var builder = new HttpSoapBindingBuilder(context); + + // var name = new NameId + // { + // Value = Saml20Identity.Current.Name, + // Format = nameIdFormat + // }; + // _attrQuery.Subject.Items = new object[] { name }; + // _attrQuery.SamlAttribute = _attributes.ToArray(); + + // var query = new XmlDocument(); + // query.LoadXml(Serialization.SerializeToXmlString(_attrQuery)); + + // XmlSignatureUtils.SignDocument(query, Id); + // if (query.FirstChild is XmlDeclaration) + // { + // query.RemoveChild(query.FirstChild); + // } + + // Logger.DebugFormat(TraceMessages.AttrQuerySent, endPoint.Metadata.GetAttributeQueryEndpointLocation(), query.OuterXml); + + // Stream s; + // try + // { + // s = builder.GetResponse(endPoint.Metadata.GetAttributeQueryEndpointLocation(), query.OuterXml, endPoint.AttributeQuery); + // } + // catch (Exception e) + // { + // Logger.Error(e.Message, e); + // throw; + // } + + // var parser = new HttpSoapBindingParser(s); + // var status = parser.GetStatus(); + // if (status.StatusCode.Value != Saml20Constants.StatusCodes.Success) + // { + // Logger.ErrorFormat(ErrorMessages.AttrQueryStatusNotSuccessful, Serialization.SerializeToXmlString(status)); + // throw new Saml20Exception(status.StatusMessage); + // } + + // bool isEncrypted; + // var xmlAssertion = Saml20SignonHandler.GetAssertion(parser.SamlMessage, out isEncrypted); + // if (isEncrypted) + // { + // var ass = new Saml20EncryptedAssertion((RSA)Saml2Config.GetConfig().ServiceProvider.SigningCertificate.GetCertificate().PrivateKey); + // ass.LoadXml(xmlAssertion); + // ass.Decrypt(); + // xmlAssertion = ass.Assertion.DocumentElement; + // } + + // var assertion = new Saml20Assertion(xmlAssertion, null, Saml2Config.GetConfig().AssertionProfile.AssertionValidator, endPoint.QuirksMode); + // Logger.DebugFormat(TraceMessages.AttrQueryAssertionReceived, xmlAssertion == null ? string.Empty : xmlAssertion.OuterXml); + + // if (!assertion.CheckSignature(Saml20SignonHandler.GetTrustedSigners(endPoint.Metadata.Keys, endPoint))) + // { + // Logger.Error(ErrorMessages.AssertionSignatureInvalid); + // throw new Saml20Exception(ErrorMessages.AssertionSignatureInvalid); + // } + + // foreach (var attr in assertion.Attributes) + // { + // Saml20Identity.Current.AddAttributeFromQuery(attr.Name, attr); + // } + //} + + /// + /// Gets the name format's string representation. + /// + /// The name format. + /// The name format string. + private static string GetNameFormat(Saml20NameFormat nameFormat) + { + string result; + + switch (nameFormat) { + case Saml20NameFormat.Basic: + result = SamlAttribute.NameformatBasic; + break; + case Saml20NameFormat.Uri: + result = SamlAttribute.NameformatUri; + break; + default: + throw new ArgumentException(string.Format("Unsupported nameFormat: {0}", Enum.GetName(typeof(Saml20NameFormat), nameFormat)), "nameFormat"); + } + + return result; + } + + public class HttpContext + { + } + } +} \ No newline at end of file diff --git a/src/SAML2.Standard/Saml20AuthnRequest.cs b/src/SAML2.Standard/Saml20AuthnRequest.cs new file mode 100644 index 0000000..742ba8e --- /dev/null +++ b/src/SAML2.Standard/Saml20AuthnRequest.cs @@ -0,0 +1,273 @@ +using System; +using System.Collections.Generic; +using System.Xml; +using SAML2.Config; +using SAML2.Schema.Core; +using SAML2.Schema.Protocol; +using SAML2.Utils; + +namespace SAML2 +{ + /// + /// Encapsulates a SAML 2.0 authentication request + /// + public class Saml20AuthnRequest + { + /// + /// Initializes a new instance of the class. + /// + public Saml20AuthnRequest() + { + Request = new AuthnRequest + { + Version = Saml20Constants.Version, + Id = "id" + Guid.NewGuid().ToString("N"), + Issuer = new NameId(), + IssueInstant = DateTime.Now + }; + } + + #region Request properties + + /// + /// Gets or sets the assertion consumer service URL. + /// + /// The assertion consumer service URL. + public string AssertionConsumerServiceUrl + { + get { return Request.AssertionConsumerServiceUrl; } + set { Request.AssertionConsumerServiceUrl = value; } + } + + /// + /// Gets the underlying schema class object. + /// + /// The request. + public AuthnRequest Request { get; private set; } + + /// + /// Gets or sets the Destination attribute of the AuthnRequest. + /// + public string Destination + { + get { return Request.Destination; } + set { Request.Destination = value; } + } + + /// + /// Gets or sets the ForceAuthn attribute of the AuthnRequest. + /// + public bool? ForceAuthn + { + get { return Request.ForceAuthn; } + set { Request.ForceAuthn = value; } + } + + /// + /// Gets or sets the ID attribute of the AuthnRequest message. + /// + public string Id + { + get { return Request.Id; } + set { Request.Id = value; } + } + + /// + /// Gets or sets the 'IsPassive' attribute of the AuthnRequest. + /// + public bool? IsPassive + { + get { return Request.IsPassive; } + set { Request.IsPassive = value; } + } + + /// + /// Gets or sets the IssueInstant of the AuthnRequest. + /// + /// The issue instant. + public DateTime? IssueInstant + { + get { return Request.IssueInstant; } + set { Request.IssueInstant = value; } + } + + /// + /// Gets or sets the issuer value. + /// + /// The issuer value. + public string Issuer + { + get { return Request.Issuer.Value; } + set { Request.Issuer.Value = value; } + } + + /// + /// Gets or sets the issuer format. + /// + /// The issuer format. + public string IssuerFormat + { + get { return Request.Issuer.Format; } + set { Request.Issuer.Format = value; } + } + + /// + /// Gets or sets the name ID policy. + /// + /// The name ID policy. + public NameIdPolicy NameIdPolicy + { + get { return Request.NameIdPolicy; } + set { Request.NameIdPolicy = value; } + } + + /// + /// Gets or sets the ProtocolBinding on the request + /// + public string ProtocolBinding + { + get { return Request.ProtocolBinding; } + set { Request.ProtocolBinding = value; } + } + + /// + /// Gets or sets the requested authentication context. + /// + /// The requested authentication context. + public RequestedAuthnContext RequestedAuthnContext + { + get { return Request.RequestedAuthnContext; } + set { Request.RequestedAuthnContext = value; } + } + + #endregion + + public static Saml20AuthnRequest GetDefault() + { + return GetDefault(null); + } + /// + /// Returns an instance of the class with meaningful default values set. + /// + /// The default . + public static Saml20AuthnRequest GetDefault(Saml2Configuration config) + { + var result = new Saml20AuthnRequest { Issuer = config.ServiceProvider.Id }; + if (config.ServiceProvider.Endpoints.DefaultSignOnEndpoint.Binding != BindingType.NotSet) + { + var baseUrl = new Uri(config.ServiceProvider.Server); + result.AssertionConsumerServiceUrl = new Uri(baseUrl, config.ServiceProvider.Endpoints.DefaultSignOnEndpoint.LocalPath).ToString(); + } + + // Binding + switch (config.ServiceProvider.Endpoints.DefaultSignOnEndpoint.Binding) + { + case BindingType.Artifact: + result.Request.ProtocolBinding = Saml20Constants.ProtocolBindings.HttpArtifact; + break; + case BindingType.Post: + result.Request.ProtocolBinding = Saml20Constants.ProtocolBindings.HttpPost; + break; + case BindingType.Redirect: + result.Request.ProtocolBinding = Saml20Constants.ProtocolBindings.HttpRedirect; + break; + case BindingType.Soap: + result.Request.ProtocolBinding = Saml20Constants.ProtocolBindings.HttpSoap; + break; + } + + // NameIDPolicy + if (config.ServiceProvider.NameIdFormats.Count > 0) + { + result.NameIdPolicy = new NameIdPolicy + { + AllowCreate = false, + Format = config.ServiceProvider.NameIdFormats[0].Format + }; + + if (result.NameIdPolicy.Format != Saml20Constants.NameIdentifierFormats.Entity) + { + result.NameIdPolicy.SPNameQualifier = config.ServiceProvider.Id; + } + } + + // RequestedAuthnContext + if (config.ServiceProvider.AuthenticationContexts.Count > 0) + { + result.RequestedAuthnContext = new RequestedAuthnContext(); + switch (config.ServiceProvider.AuthenticationContexts.Comparison) + { + case AuthenticationContextComparison.Better: + result.RequestedAuthnContext.Comparison = AuthnContextComparisonType.Better; + result.RequestedAuthnContext.ComparisonSpecified = true; + break; + case AuthenticationContextComparison.Minimum: + result.RequestedAuthnContext.Comparison = AuthnContextComparisonType.Minimum; + result.RequestedAuthnContext.ComparisonSpecified = true; + break; + case AuthenticationContextComparison.Maximum: + result.RequestedAuthnContext.Comparison = AuthnContextComparisonType.Maximum; + result.RequestedAuthnContext.ComparisonSpecified = true; + break; + case AuthenticationContextComparison.Exact: + result.RequestedAuthnContext.Comparison = AuthnContextComparisonType.Exact; + result.RequestedAuthnContext.ComparisonSpecified = true; + break; + default: + result.RequestedAuthnContext.ComparisonSpecified = false; + break; + } + + result.RequestedAuthnContext.Items = new string[config.ServiceProvider.AuthenticationContexts.Count]; + result.RequestedAuthnContext.ItemsElementName = new Schema.Protocol.AuthnContextType[config.ServiceProvider.AuthenticationContexts.Count]; + + var count = 0; + foreach (var authenticationContext in config.ServiceProvider.AuthenticationContexts) + { + result.RequestedAuthnContext.Items[count] = authenticationContext.Context; + switch (authenticationContext.ReferenceType) + { + case "AuthnContextDeclRef": + result.RequestedAuthnContext.ItemsElementName[count] = Schema.Protocol.AuthnContextType.AuthnContextDeclRef; + break; + default: + result.RequestedAuthnContext.ItemsElementName[count] = Schema.Protocol.AuthnContextType.AuthnContextClassRef; + break; + } + + count++; + } + } + + // Restrictions + var audienceRestrictions = new List(1); + var audienceRestriction = new AudienceRestriction { Audience = new List(1) { config.ServiceProvider.Id } }; + audienceRestrictions.Add(audienceRestriction); + + result.SetConditions(audienceRestrictions); + + return result; + } + + /// + /// Returns the AuthnRequest as an XML document. + /// + /// The request XML. + public XmlDocument GetXml() + { + var doc = new XmlDocument { PreserveWhitespace = true }; + doc.LoadXml(Serialization.SerializeToXmlString(Request)); + + return doc; + } + + /// + /// Sets the conditions. + /// + /// The conditions. + private void SetConditions(List conditions) + { + Request.Conditions = new Conditions { Items = conditions }; + } + } +} diff --git a/src/SAML2.Standard/Saml20Constants.cs b/src/SAML2.Standard/Saml20Constants.cs new file mode 100644 index 0000000..22e98c9 --- /dev/null +++ b/src/SAML2.Standard/Saml20Constants.cs @@ -0,0 +1,284 @@ +namespace SAML2 +{ + /// + /// Constants related to SAML 2.0 + /// + public class Saml20Constants + { + /// + /// SAML Version + /// + public const string Version = "2.0"; + + /// + /// The XML namespace of the SAML 2.0 assertion schema. + /// + public const string Assertion = "urn:oasis:names:tc:SAML:2.0:assertion"; + + /// + /// The XML namespace of the SAML 2.0 protocol schema + /// + public const string Protocol = "urn:oasis:names:tc:SAML:2.0:protocol"; + + /// + /// The XML namespace of the SAML 2.0 metadata schema + /// + public const string Metadata = "urn:oasis:names:tc:SAML:2.0:metadata"; + + /// + /// The XML namespace of XmlDSig + /// + public const string Xmldsig = "http://www.w3.org/2000/09/xmldsig#"; + + /// + /// The XML namespace of XmlEnc + /// + public const string Xenc = "http://www.w3.org/2001/04/xmlenc#"; + + /// + /// The default value of the Format property for a NameID element + /// + public const string DefaultNameIdFormat = "urn:oasis:names:tc:SAML:1.0:nameid-format:unspecified"; + + /// + /// The mime type that must be used when publishing a metadata document. + /// + public const string MetadataMimetype = "application/samlmetadata+xml"; + + /// + /// All the namespaces defined and reserved by the SAML 2.0 standard + /// + public static readonly string[] SamlNamespaces = { Assertion, Protocol, Metadata }; + + /// + /// Formats of name identifier formats + /// + public static class NameIdentifierFormats + { + /// + /// urn for Unspecified name identifier format + /// + public const string Unspecified = "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified"; + + /// + /// urn for Email name identifier format + /// + public const string Email = "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"; + + /// + /// urn for X509SubjectName name identifier format + /// + public const string X509SubjectName = "urn:oasis:names:tc:SAML:1.1:nameid-format:X509SubjectName"; + + /// + /// urn for Windows name identifier format + /// + public const string Windows = "urn:oasis:names:tc:SAML:1.1:nameid-format:WindowsDomainQualifiedName"; + + /// + /// urn for Kerberos name identifier format + /// + public const string Kerberos = "urn:oasis:names:tc:SAML:2.0:nameid-format:kerberos"; + + /// + /// urn for Entity name identifier format + /// + public const string Entity = "urn:oasis:names:tc:SAML:2.0:nameid-format:entity"; + + /// + /// urn for Persistent name identifier format + /// + public const string Persistent = "urn:oasis:names:tc:SAML:2.0:nameid-format:persistent"; + + /// + /// urn for Transient name identifier format + /// + public const string Transient = "urn:oasis:names:tc:SAML:2.0:nameid-format:transient"; + } + + /// + /// Protocol bindings + /// + public static class ProtocolBindings + { + /// + /// HTTP Redirect protocol binding + /// + public const string HttpRedirect = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"; + + /// + /// HTTP Post protocol binding + /// + public const string HttpPost = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"; + + /// + /// HTTP Artifact protocol binding + /// + public const string HttpArtifact = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact"; + + /// + /// HTTP SOAP protocol binding + /// + public const string HttpSoap = "urn:oasis:names:tc:SAML:2.0:bindings:SOAP"; + } + + /// + /// Subject confirmation methods + /// + public static class SubjectConfirmationMethods + { + /// + /// Holder of key confirmation method + /// + public const string HolderOfKey = "urn:oasis:names:tc:SAML:2.0:cm:holder-of-key"; + } + + /// + /// Logout reasons + /// + public static class Reasons + { + /// + /// Specifies that the message is being sent because the principal wishes to terminate the indicated session. + /// + public const string User = "urn:oasis:names:tc:SAML:2.0:logout:user"; + + /// + /// Specifies that the message is being sent because an administrator wishes to terminate the indicated + /// session for that principal. + /// + public const string Admin = "urn:oasis:names:tc:SAML:2.0:logout:admin"; + } + + /// + /// Status codes + /// + public static class StatusCodes + { + /// + /// The request succeeded. + /// + public const string Success = "urn:oasis:names:tc:SAML:2.0:status:Success"; + + /// + /// The request could not be performed due to an error on the part of the requester. + /// + public const string Requester = "urn:oasis:names:tc:SAML:2.0:status:Requester"; + + /// + /// The request could not be performed due to an error on the part of the SAML responder or SAML authority. + /// + public const string Responder = "urn:oasis:names:tc:SAML:2.0:status:Responder"; + + /// + /// The SAML responder could not process the request because the version of the request message was incorrect. + /// + public const string VersionMismatch = "urn:oasis:names:tc:SAML:2.0:status:VersionMismatch"; + + /// + /// The responding provider was unable to successfully authenticate the principal. + /// + public const string AuthnFailed = "urn:oasis:names:tc:SAML:2.0:status:AuthnFailed"; + + /// + /// Unexpected or invalid content was encountered within a <saml:Attribute> or <saml:AttributeValue> element. + /// + public const string InvalidAttrNameOrValue = "urn:oasis:names:tc:SAML:2.0:status:InvalidAttrNameOrValue"; + + /// + /// The responding provider cannot or will not support the requested name identifier policy. + /// + public const string InvalidNameIdPolicy = "urn:oasis:names:tc:SAML:2.0:status:InvalidNameIDPolicy"; + + /// + /// The specified authentication context requirements cannot be met by the responder. + /// + public const string NoAuthnContext = "urn:oasis:names:tc:SAML:2.0:status:NoAuthnContext"; + + /// + /// Used by an intermediary to indicate that none of the supported identity provider <Loc> elements in an + /// <IDPList> can be resolved or that none of the supported identity providers are available. + /// + public const string NoAvailableIdp = "urn:oasis:names:tc:SAML:2.0:status:NoAvailableIDP"; + + /// + /// Indicates the responding provider cannot authenticate the principal passively, as has been requested. + /// + public const string NoPassive = "urn:oasis:names:tc:SAML:2.0:status:NoPassive"; + + /// + /// Used by an intermediary to indicate that none of the identity providers in an <IDPList> are + /// supported by the intermediary. + /// + public const string NoSupportedIdp = "urn:oasis:names:tc:SAML:2.0:status:NoSupportedIDP"; + + /// + /// Used by a session authority to indicate to a session participant that it was not able to propagate logout + /// to all other session participants. + /// + public const string PartialLogout = "urn:oasis:names:tc:SAML:2.0:status:PartialLogout"; + + /// + /// Indicates that a responding provider cannot authenticate the principal directly and is not permitted to + /// proxy the request further. + /// + public const string ProxyCountExceeded = "urn:oasis:names:tc:SAML:2.0:status:ProxyCountExceeded"; + + /// + /// The SAML responder or SAML authority is able to process the request but has chosen not to respond. + /// This status code MAY be used when there is concern about the security context of the request + /// message or the sequence of request messages received from a particular requester. + /// + public const string RequestDenied = "urn:oasis:names:tc:SAML:2.0:status:RequestDenied"; + + /// + /// The SAML responder or SAML authority does not support the request. + /// + public const string RequestUnsupported = "urn:oasis:names:tc:SAML:2.0:status:RequestUnsupported"; + + /// + /// The SAML responder cannot process any requests with the protocol version specified in the request. + /// + public const string RequestVersionDeprecated = "urn:oasis:names:tc:SAML:2.0:status:RequestVersionDeprecated"; + + /// + /// The SAML responder cannot process the request because the protocol version specified in the + /// request message is a major upgrade from the highest protocol version supported by the responder. + /// + public const string RequestVersionTooHigh = "urn:oasis:names:tc:SAML:2.0:status:RequestVersionTooHigh"; + + /// + /// The SAML responder cannot process the request because the protocol version specified in the + /// request message is too low. + /// + public const string RequestVersionTooLow = "urn:oasis:names:tc:SAML:2.0:status:RequestVersionTooLow"; + + /// + /// The resource value provided in the request message is invalid or unrecognized. + /// + public const string ResourceNotRecognized = "urn:oasis:names:tc:SAML:2.0:status:ResourceNotRecognized"; + + /// + /// The response message would contain more elements than the SAML responder is able to return. + /// + public const string TooManyResponses = "urn:oasis:names:tc:SAML:2.0:status:TooManyResponses"; + + /// + /// An entity that has no knowledge of a particular attribute profile has been presented with an attribute + /// drawn from that profile. + /// + public const string UnknownAttrProfile = "urn:oasis:names:tc:SAML:2.0:status:UnknownAttrProfile"; + + /// + /// The responding provider does not recognize the principal specified or implied by the request. + /// + public const string UnknownPrincipal = "urn:oasis:names:tc:SAML:2.0:status:UnknownPrincipal"; + + /// + /// The SAML responder cannot properly fulfill the request using the protocol binding specified in the + /// request. + /// + public const string UnsupportedBinding = "urn:oasis:names:tc:SAML:2.0:status:UnsupportedBinding"; + } + } +} diff --git a/src/SAML2.Standard/Saml20EncryptedAssertion.cs b/src/SAML2.Standard/Saml20EncryptedAssertion.cs new file mode 100644 index 0000000..c4973f7 --- /dev/null +++ b/src/SAML2.Standard/Saml20EncryptedAssertion.cs @@ -0,0 +1,387 @@ +using System; +using System.IO; +using System.Security.Cryptography; +using System.Security.Cryptography.Xml; +using System.Text; +using System.Xml; +using SAML2.Schema.Protocol; +using SAML2.Utils; + +namespace SAML2 +{ + /// + /// Handles the EncryptedAssertion element. + /// + public class Saml20EncryptedAssertion + { + /// + /// Whether to use OAEP (Optimal Asymmetric Encryption Padding) by default, if no EncryptionMethod is specified + /// on the <EncryptedKey> element. + /// + private const bool UseOaepDefault = false; + + /// + /// The EncryptedAssertion element containing an Assertion. + /// + private XmlDocument _encryptedAssertion; + + /// + /// The session key. + /// + private SymmetricAlgorithm _sessionKey; + + /// + /// Session key algorithm. + /// + private string _sessionKeyAlgorithm = EncryptedXml.XmlEncAES256Url; + + #region Constructors + + /// + /// Initializes a new instance of the class. + /// + public Saml20EncryptedAssertion() { } + + /// + /// Initializes a new instance of the class. + /// + /// The transport key is used for securing the symmetric key that has encrypted the assertion. + public Saml20EncryptedAssertion(RSA transportKey) : this() + { + TransportKey = transportKey; + } + + /// + /// Initializes a new instance of the class. + /// + /// The transport key is used for securing the symmetric key that has encrypted the assertion. + /// An XmlDocument containing an EncryptedAssertion element. + public Saml20EncryptedAssertion(RSA transportKey, XmlDocument encryptedAssertion) : this(transportKey) + { + LoadXml(encryptedAssertion.DocumentElement); + } + + #endregion + + #region Properties + + /// + /// Gets or sets the Assertion element that is embedded within the EncryptedAssertion element. + /// + public XmlDocument Assertion { get; set; } + + /// + /// Gets or sets the algorithm to use for the session key. The algorithm is specified using the identifiers given in the + /// Xml Encryption Specification. see also http://www.w3.org/TR/xmlenc-core/#sec-Algorithms + /// The class EncryptedXml contains public fields with the identifiers. If nothing is + /// specified, a 256 bit AES key is used. + /// + public string SessionKeyAlgorithm + { + get { return _sessionKeyAlgorithm; } + set + { + // Validate that the URI used to identify the algorithm of the session key is probably correct. Not a complete validation, but should catch most obvious mistakes. + if (!value.StartsWith(Saml20Constants.Xenc)) + { + throw new ArgumentException("The session key algorithm must be specified using the identifying URIs listed in the specification."); + } + + _sessionKeyAlgorithm = value; + } + } + + /// + /// Gets or sets the transport key is used for securing the symmetric key that has encrypted the assertion. + /// + public RSA TransportKey { get; set; } + + /// + /// Gets the key used for encrypting the Assertion. This key is embedded within a KeyInfo element + /// in the EncryptedAssertion element. The session key is encrypted with the TransportKey before + /// being embedded. + /// + private SymmetricAlgorithm SessionKey + { + get + { + if (_sessionKey == null) + { + _sessionKey = GetKeyInstance(_sessionKeyAlgorithm); + _sessionKey.GenerateKey(); + } + + return _sessionKey; + } + } + + #endregion + + /// + /// Decrypts the assertion using the key given as the method parameter. The resulting assertion + /// is available through the Assertion property. + /// + /// Thrown if it not possible to decrypt the assertion. + public void Decrypt() + { + if (TransportKey == null) + { + throw new InvalidOperationException("The \"TransportKey\" property must contain the asymmetric key to decrypt the assertion."); + } + + if (_encryptedAssertion == null) + { + throw new InvalidOperationException("Unable to find the element. Use a constructor or the LoadXml - method to set it."); + } + + var encryptedDataElement = GetElement(Schema.XEnc.EncryptedData.ElementName, Saml20Constants.Xenc, _encryptedAssertion.DocumentElement); + var encryptedData = new EncryptedData(); + encryptedData.LoadXml(encryptedDataElement); + + SymmetricAlgorithm sessionKey; + if (encryptedData.EncryptionMethod != null) + { + _sessionKeyAlgorithm = encryptedData.EncryptionMethod.KeyAlgorithm; + sessionKey = ExtractSessionKey(_encryptedAssertion, _sessionKeyAlgorithm); + } + else + { + sessionKey = ExtractSessionKey(_encryptedAssertion); + } + + /* + * NOTE: + * The EncryptedXml class can't handle an element without an underlying element, + * despite the standard dictating that this is ok. + * If this becomes a problem with other IDPs, consider adding a default EncryptionMethod instance manually before decrypting. + */ + var encryptedXml = new EncryptedXml(); + var plaintext = encryptedXml.DecryptData(encryptedData, sessionKey); + + Assertion = new XmlDocument { PreserveWhitespace = true }; + try + { + Assertion.Load(new StringReader(Encoding.UTF8.GetString(plaintext))); + } + catch (XmlException e) + { + Assertion = null; + throw new Saml20FormatException("Unable to parse the decrypted assertion.", e); + } + } + + /// + /// Encrypts the Assertion in the assertion property and creates an EncryptedAssertion element + /// that can be retrieved using the GetXml method. + /// + public void Encrypt() + { + if (TransportKey == null) + { + throw new InvalidOperationException("The \"TransportKey\" property is required to encrypt the assertion."); + } + + if (Assertion == null) + { + throw new InvalidOperationException("The \"Assertion\" property is required for this operation."); + } + + var encryptedData = new EncryptedData + { + Type = EncryptedXml.XmlEncElementUrl, + EncryptionMethod = new EncryptionMethod(_sessionKeyAlgorithm) + }; + + // Encrypt the assertion and add it to the encryptedData instance. + var encryptedXml = new EncryptedXml(); + var encryptedElement = encryptedXml.EncryptData(Assertion.DocumentElement, SessionKey, false); + encryptedData.CipherData.CipherValue = encryptedElement; + + // Add an encrypted version of the key used. + encryptedData.KeyInfo = new KeyInfo(); + + var encryptedKey = new EncryptedKey + { + EncryptionMethod = new EncryptionMethod(EncryptedXml.XmlEncRSA15Url), + CipherData = new CipherData(EncryptedXml.EncryptKey(SessionKey.Key, TransportKey, false)) + }; + encryptedData.KeyInfo.AddClause(new KeyInfoEncryptedKey(encryptedKey)); + + // Create an empty EncryptedAssertion to hook into. + var encryptedAssertion = new EncryptedAssertion { EncryptedData = new Schema.XEnc.EncryptedData() }; + + var result = new XmlDocument(); + result.LoadXml(Serialization.SerializeToXmlString(encryptedAssertion)); + + var encryptedDataElement = GetElement(Schema.XEnc.EncryptedData.ElementName, Saml20Constants.Xenc, result.DocumentElement); + EncryptedXml.ReplaceElement(encryptedDataElement, encryptedData, false); + + _encryptedAssertion = result; + } + + /// + /// Returns the XML representation of the encrypted assertion. + /// + /// The encrypted assertion XML. + public XmlDocument GetXml() + { + return _encryptedAssertion; + } + + /// + /// Initializes the instance with a new EncryptedAssertion element. + /// + /// The element. + public void LoadXml(XmlElement element) + { + CheckEncryptedAssertionElement(element); + + _encryptedAssertion = new XmlDocument(); + _encryptedAssertion.AppendChild(_encryptedAssertion.ImportNode(element, true)); + } + + /// + /// Writes the assertion to the XmlWriter. + /// + /// The writer. + public void WriteAssertion(XmlWriter writer) + { + _encryptedAssertion.WriteTo(writer); + } + + /// + /// Verifies that the given XmlElement is actually a SAML 2.0 EncryptedAssertion element. + /// + /// The element. + private static void CheckEncryptedAssertionElement(XmlElement element) + { + if (element == null) + { + throw new ArgumentNullException("element"); + } + + if (element.LocalName != EncryptedAssertion.ElementName) + { + throw new ArgumentException("The element must be of type \"EncryptedAssertion\"."); + } + + if (element.NamespaceURI != Saml20Constants.Assertion) + { + throw new ArgumentException("The element must be of type \"" + Saml20Constants.Assertion + "#EncryptedAssertion\"."); + } + } + + /// + /// Utility method for retrieving a single element from a document. + /// + /// The element. + /// The element namespace. + /// The doc. + /// The desired element. + private static XmlElement GetElement(string element, string elementNS, XmlElement doc) + { + var list = doc.GetElementsByTagName(element, elementNS); + return list.Count == 0 ? null : (XmlElement)list[0]; + } + + /// + /// Creates an instance of a symmetric key, based on the algorithm identifier found in the Xml Encryption standard. + /// see also http://www.w3.org/TR/xmlenc-core/#sec-Algorithms + /// + /// A string containing one of the algorithm identifiers found in the XML Encryption standard. The class + /// EncryptedXml contains the identifiers as fields. + /// The . + private static SymmetricAlgorithm GetKeyInstance(string algorithm) + { + SymmetricAlgorithm result; + switch (algorithm) + { + case EncryptedXml.XmlEncTripleDESUrl: + result = TripleDES.Create(); + break; + case EncryptedXml.XmlEncAES128Url: + result = new RijndaelManaged { KeySize = 128 }; + break; + case EncryptedXml.XmlEncAES192Url: + result = new RijndaelManaged { KeySize = 192 }; + break; + case EncryptedXml.XmlEncAES256Url: + default: + result = new RijndaelManaged { KeySize = 256 }; + break; + } + + return result; + } + + /// + /// An overloaded version of ExtractSessionKey that does not require a keyAlgorithm. + /// + /// The encrypted assertion doc. + /// The . + private SymmetricAlgorithm ExtractSessionKey(XmlDocument encryptedAssertionDoc) + { + return ExtractSessionKey(encryptedAssertionDoc, string.Empty); + } + + /// + /// Locates and deserializes the key used for encrypting the assertion. Searches the list of keys below the <EncryptedAssertion> element and + /// the <KeyInfo> element of the <EncryptedData> element. + /// + /// The encrypted assertion doc. + /// The XML Encryption standard identifier for the algorithm of the session key. + /// A SymmetricAlgorithm containing the key if it was successfully found. Null if the method was unable to locate the key. + private SymmetricAlgorithm ExtractSessionKey(XmlDocument encryptedAssertionDoc, string keyAlgorithm) + { + // Check if there are any elements immediately below the EncryptedAssertion element. + foreach (XmlNode node in encryptedAssertionDoc.DocumentElement.ChildNodes) + { + if (node.LocalName == Schema.XEnc.EncryptedKey.ElementName && node.NamespaceURI == Saml20Constants.Xenc) + { + return ToSymmetricKey((XmlElement)node, keyAlgorithm); + } + } + + // Check if the key is embedded in the element. + var encryptedData = GetElement(Schema.XEnc.EncryptedData.ElementName, Saml20Constants.Xenc, encryptedAssertionDoc.DocumentElement); + if (encryptedData != null) + { + var encryptedKeyElement = GetElement(Schema.XEnc.EncryptedKey.ElementName, Saml20Constants.Xenc, encryptedAssertionDoc.DocumentElement); + if (encryptedKeyElement != null) + { + return ToSymmetricKey(encryptedKeyElement, keyAlgorithm); + } + } + + throw new Saml20FormatException("Unable to locate assertion decryption key."); + } + + /// + /// Extracts the key from a <EncryptedKey> element. + /// + /// The encrypted key element. + /// The key algorithm. + /// The . + private SymmetricAlgorithm ToSymmetricKey(XmlElement encryptedKeyElement, string keyAlgorithm) + { + var encryptedKey = new EncryptedKey(); + encryptedKey.LoadXml(encryptedKeyElement); + + var useOaep = UseOaepDefault; + if (encryptedKey.EncryptionMethod != null) + { + useOaep = encryptedKey.EncryptionMethod.KeyAlgorithm == EncryptedXml.XmlEncRSAOAEPUrl; + } + + if (encryptedKey.CipherData.CipherValue != null) + { + var key = GetKeyInstance(keyAlgorithm); + key.Key = EncryptedXml.DecryptKey(encryptedKey.CipherData.CipherValue, TransportKey, useOaep); + + return key; + } + + throw new NotImplementedException("Unable to decode CipherData of type \"CipherReference\"."); + } + } +} diff --git a/src/SAML2.Standard/Saml20Exception.cs b/src/SAML2.Standard/Saml20Exception.cs new file mode 100644 index 0000000..6b35609 --- /dev/null +++ b/src/SAML2.Standard/Saml20Exception.cs @@ -0,0 +1,29 @@ +using System; + +namespace SAML2 +{ + /// + /// This exception is thrown to indicate an error in the SAML2 toolkit. It was introduced to make it easy to distinguish between + /// exceptions thrown deliberately by the toolkit, and exceptions that are thrown as the result of bugs. + /// + public class Saml20Exception : Exception + { + /// + /// Initializes a new instance of the class. + /// + public Saml20Exception() { } + + /// + /// Initializes a new instance of the class. + /// + /// The MSG. + public Saml20Exception(string msg) : base(msg) { } + + /// + /// Initializes a new instance of the class. + /// + /// A message describing the problem that caused the exception. + /// Another exception that may be related to the problem. + public Saml20Exception(string msg, Exception cause) : base(msg, cause) { } + } +} diff --git a/src/SAML2.Standard/Saml20FormatException.cs b/src/SAML2.Standard/Saml20FormatException.cs new file mode 100644 index 0000000..9706333 --- /dev/null +++ b/src/SAML2.Standard/Saml20FormatException.cs @@ -0,0 +1,28 @@ +using System; + +namespace SAML2 +{ + /// + /// Thrown when a token does not comply with the SAML 2.0 specification. + /// + public class Saml20FormatException : Exception + { + /// + /// Initializes a new instance of the class. + /// + public Saml20FormatException() { } + + /// + /// Initializes a new instance of the class. + /// + /// The MSG. + public Saml20FormatException(string msg) : base(msg) { } + + /// + /// Initializes a new instance of the class. + /// + /// A message describing the problem that caused the exception. + /// Another exception that may be related to the problem. + public Saml20FormatException(string msg, Exception cause) : base(msg, cause) { } + } +} diff --git a/src/SAML2.Standard/Saml20LogoutRequest.cs b/src/SAML2.Standard/Saml20LogoutRequest.cs new file mode 100644 index 0000000..3a36dcd --- /dev/null +++ b/src/SAML2.Standard/Saml20LogoutRequest.cs @@ -0,0 +1,135 @@ +using System; +using System.Xml; +using SAML2.Config; +using SAML2.Schema.Core; +using SAML2.Schema.Protocol; +using SAML2.Utils; + +namespace SAML2 +{ + /// + /// Encapsulates the LogoutRequest schema class + /// + public class Saml20LogoutRequest + { + /// + /// Initializes a new instance of the class. + /// + public Saml20LogoutRequest() + { + Request = new LogoutRequest + { + Version = Saml20Constants.Version, + Id = "id" + Guid.NewGuid().ToString("N"), + Issuer = new NameId(), + IssueInstant = DateTime.Now + }; + } + + #region Properties + + /// + /// Gets or sets NotOnOrAfter. + /// + /// The Not On Or After date. + public DateTime? NotOnOrAfter + { + get { return Request.NotOnOrAfter; } + set { Request.NotOnOrAfter = value; } + } + + /// + /// Gets or sets the destination. + /// + /// The destination. + public string Destination + { + get { return Request.Destination; } + set { Request.Destination = value; } + } + + /// + /// Gets the id of the logout request. + /// + public string Id + { + get { return Request.Id; } + } + + /// + /// Gets or sets the issuer value. + /// + /// The issuer value. + public string Issuer + { + get { return Request.Issuer.Value; } + set { Request.Issuer.Value = value; } + } + + /// + /// Gets or sets the reason for this logout request. + /// Defined values should be on uri form. + /// + /// The reason. + public string Reason + { + get { return Request.Reason; } + set { Request.Reason = value; } + } + + /// + /// Gets the underlying LogoutRequest schema class instance. + /// + /// The request. + public LogoutRequest Request { get; private set; } + + /// + /// Gets or sets the SessionIndex. + /// + /// The SessionIndex. + public string SessionIndex + { + get { return Request.SessionIndex[0]; } + set { Request.SessionIndex = new[] { value }; } + } + + /// + /// Gets or sets SubjectToLogOut. + /// + /// The Subject To LogOut. + public NameId SubjectToLogOut + { + get { return Request.Item as NameId; } + set { Request.Item = value; } + } + + #endregion + + /// + /// Returns an instance of the class with meaningful default values set. + /// + /// The . + public static Saml20LogoutRequest GetDefault(Saml2Configuration config) + { + var result = new Saml20LogoutRequest + { + SubjectToLogOut = new NameId(), + Issuer = config.ServiceProvider.Id + }; + + return result; + } + + /// + /// Returns the AuthnRequest as an XML document. + /// + /// The request XML. + public XmlDocument GetXml() + { + var doc = new XmlDocument { PreserveWhitespace = true }; + doc.LoadXml(Serialization.SerializeToXmlString(Request)); + + return doc; + } + } +} diff --git a/src/SAML2.Standard/Saml20LogoutResponse.cs b/src/SAML2.Standard/Saml20LogoutResponse.cs new file mode 100644 index 0000000..b15b6f8 --- /dev/null +++ b/src/SAML2.Standard/Saml20LogoutResponse.cs @@ -0,0 +1,95 @@ +using System; +using System.Xml; +using SAML2.Schema.Core; +using SAML2.Schema.Protocol; +using SAML2.Utils; + +namespace SAML2 +{ + /// + /// Encapsulates the LogoutResponse schema class + /// + public class Saml20LogoutResponse + { + /// + /// Initializes a new instance of the class. + /// + public Saml20LogoutResponse() + { + Response = new LogoutResponse + { + Version = Saml20Constants.Version, + ID = "id" + Guid.NewGuid().ToString("N"), + Issuer = new NameId(), + IssueInstant = DateTime.Now, + Status = new Status { StatusCode = new StatusCode() } + }; + } + + /// + /// Gets the ID. + /// + /// The ID. + public string Id + { + get { return Response.ID; } + } + + /// + /// Gets or sets the destination. + /// + /// The destination. + public string Destination + { + get { return Response.Destination; } + set { Response.Destination = value; } + } + + /// + /// Gets or sets the id of the LogoutRequest to which this LogoutResponse corresponds. + /// + /// The id of the LogoutRequest to which this LogoutResponse corresponds. + public string InResponseTo + { + get { return Response.InResponseTo; } + set { Response.InResponseTo = value; } + } + + /// + /// Gets or sets the issuer. + /// + /// The issuer. + public string Issuer + { + get { return Response.Issuer.Value; } + set { Response.Issuer.Value = value; } + } + + /// + /// Gets the underlying Response schema class instance. + /// + /// The response. + public LogoutResponse Response { get; private set; } + + /// + /// Gets or sets the status code. + /// + /// The status code. + public string StatusCode + { + get { return Response.Status.StatusCode.Value; } + set { Response.Status.StatusCode.Value = value; } + } + + /// + /// Gets LogoutResponse as an XmlDocument + /// + /// The XML document. + public XmlDocument GetXml() + { + var doc = new XmlDocument { PreserveWhitespace = true }; + doc.LoadXml(Serialization.SerializeToXmlString(Response)); + return doc; + } + } +} diff --git a/src/SAML2.Standard/Saml20MetadataDocument.cs b/src/SAML2.Standard/Saml20MetadataDocument.cs new file mode 100644 index 0000000..7b7d9c4 --- /dev/null +++ b/src/SAML2.Standard/Saml20MetadataDocument.cs @@ -0,0 +1,914 @@ +using System; +using System.Collections.Generic; +using System.Configuration; +using System.Linq; +using System.Security.Cryptography.X509Certificates; +using System.Security.Cryptography.Xml; +using System.Text; +using System.Xml; +using SAML2.Config; +using SAML2.Schema.Core; +using SAML2.Schema.Metadata; +using SAML2.Utils; +using System.Security.Cryptography; +using System.IO; + +namespace SAML2 +{ + /// + /// The class handles functionality related to the <EntityDescriptor> element. + /// If a received metadata document contains a <EntitiesDescriptor> element, it is necessary to use an + /// instance of this class for each <EntityDescriptor> contained. + /// + public class Saml20MetadataDocument + { + /// + /// AssertionConsumerServiceEndpoints backing field. + /// + private List _assertionConsumerServiceEndpoints; + + /// + /// Attribute query endpoints. + /// + private List _attributeQueryEndpoints; + + /// + /// ARSEndpoints backing field. + /// + private Dictionary _idpArsEndpoints; + + /// + /// SLOEndpoints backing field. + /// + private List _idpSloEndpoints; + + /// + /// The keys. + /// + private List _keys; + + /// + /// ARSEndpoints backing field. + /// + private Dictionary _spArsEndpoints; + + /// + /// SLOEndpoints backing field. + /// + private List _spSloEndpoints; + + /// + /// SSOEndpoints backing field. + /// + private List _ssoEndpoints; + + /// + /// Initializes a new instance of the class. + /// + public Saml20MetadataDocument() { } + + /// + /// Initializes a new instance of the class. + /// + /// The entity descriptor. + public Saml20MetadataDocument(XmlDocument entityDescriptor) + : this() + { + Initialize(entityDescriptor); + } + + private void Initialize(XmlDocument entityDescriptor) + { + if (XmlSignatureUtils.IsSigned(entityDescriptor)) { + if (!XmlSignatureUtils.CheckSignature(entityDescriptor)) { + throw new Saml20Exception("Metadata signature could not be verified."); + } + } + + ExtractKeyDescriptors(entityDescriptor); + Entity = Serialization.DeserializeFromXmlString(entityDescriptor.OuterXml); + } + + + /// + /// Initializes a new instance of the class. + /// + /// if set to true the metadata document will be signed. + public Saml20MetadataDocument(bool sign) + : this() + { + Sign = sign; + } + + /// + /// Initializes a new instance of the class. + /// + /// The config. + /// key information for the service provider certificate. + /// if set to true the metadata document will be signed. + public Saml20MetadataDocument(Saml2Configuration config, KeyInfo keyinfo, bool sign) + : this(sign) + { + ConvertToMetadata(config, keyinfo); + } + + /// + /// Parses the metadata files found in the directory specified in the configuration. + /// + /// The file. + /// The parsed . + public Saml20MetadataDocument(string file) : this(file, null) + { } + + public Saml20MetadataDocument(string file, IEnumerable encodings) + { + if (file == null) throw new ArgumentNullException("file"); + + try { + var doc = LoadFileAsXmlDocument(file, encodings ?? DefaultEncodings()); + InitializeDocument(doc); + } + catch (Exception e) { + // Probably not a metadata file. + Logging.LoggerProvider.LoggerFor(typeof(IdentityProviders)).Error("Problem parsing metadata file", e); + throw; + } + } + public Saml20MetadataDocument(Stream document, IEnumerable encodings) + { + if (document == null) throw new ArgumentNullException("file"); + + try { + var doc = LoadStreamAsXmlDocument(document, encodings ?? DefaultEncodings()); + InitializeDocument(doc); + } + catch (Exception e) { + // Probably not a metadata file. + Logging.LoggerProvider.LoggerFor(typeof(IdentityProviders)).Error("Problem parsing metadata file", e); + throw; + } + } + + private void InitializeDocument(XmlDocument doc) + { + if (doc == null) throw new InvalidOperationException("Metadata not a valid document"); + var isInitialized = false; + foreach (var child in doc.ChildNodes.Cast().Where(child => child.NamespaceURI == Saml20Constants.Metadata)) { + if (child.LocalName == EntityDescriptor.ElementName) { + Initialize(doc); + isInitialized = true; + } + + // TODO Decide how to handle several entities in one metadata file. + if (child.LocalName == EntitiesDescriptor.ElementName) { + throw new NotImplementedException(); + } + } + + // No entity descriptor found. + if (!isInitialized) throw new InvalidDataException(); + } + + private static IEnumerable DefaultEncodings() + { + return new [] { Encoding.UTF8, Encoding.GetEncoding("iso-8859-1") }; + } + + private static XmlDocument LoadStreamAsXmlDocument(Stream stream, IEnumerable encodings) + { + return LoadAsXmlDocument(encodings, + d => d.Load(stream), + (d, e) => { + var reader = new StreamReader(stream, e); + d.Load(reader); + }); + } + + private static XmlDocument LoadFileAsXmlDocument(string filename, IEnumerable encodings) + { + return LoadAsXmlDocument(encodings, + d => d.Load(filename), + (d,e) => { + var reader = new StreamReader(filename, e); + d.Load(reader); + }); + } + + /// + /// Loads a file into an XmlDocument. If the loading or the signature check fails, the method will retry using another encoding. + /// + /// The filename. + /// The XML document. + private static XmlDocument LoadAsXmlDocument(IEnumerable encodings, Action docLoad, Action quirksModeDocLoad) + { + var doc = new XmlDocument { PreserveWhitespace = true }; + + try { + // First attempt a standard load, where the XML document is expected to declare its encoding by itself. + docLoad(doc); + try { + if (XmlSignatureUtils.IsSigned(doc) && !XmlSignatureUtils.CheckSignature(doc)) { + // Bad, bad, bad... never use exceptions for control flow! Who wrote this? + // Throw an exception to get into quirksmode. + throw new InvalidOperationException("Invalid file signature"); + } + } + catch (CryptographicException) { + // Ignore cryptographic exception caused by Geneva server's inability to generate a + // .NET compliant xml signature + return ParseGenevaServerMetadata(doc); + } + + return doc; + } + catch (XmlException) { + // Enter quirksmode + foreach (var encoding in encodings) { + StreamReader reader = null; + try { + quirksModeDocLoad(doc, encoding); + if (XmlSignatureUtils.IsSigned(doc) && !XmlSignatureUtils.CheckSignature(doc)) { + continue; + } + } + catch (XmlException) { + continue; + } + finally { + if (reader != null) { + reader.Close(); + } + } + + return doc; + } + } + + return null; + } + + /// + /// Parses the geneva server metadata. + /// + /// The doc. + /// The XML document. + private static XmlDocument ParseGenevaServerMetadata(XmlDocument doc) + { + if (doc == null) { + throw new ArgumentNullException("doc"); + } + + if (doc.DocumentElement == null) { + throw new ArgumentException("DocumentElement cannot be null", "doc"); + } + + var other = new XmlDocument { PreserveWhitespace = true }; + other.LoadXml(doc.OuterXml); + + foreach (var node in other.DocumentElement.ChildNodes.Cast().Where(node => node.Name != IdpSsoDescriptor.ElementName).ToList()) { + other.DocumentElement.RemoveChild(node); + } + + return other; + } + + + /// + /// Gets the endpoints specified in the <AssertionConsumerService> element in the SpSsoDescriptor. + /// These endpoints are only applicable if we are reading metadata issued by a service provider. + /// + public List AssertionConsumerServiceEndpoints + { + get + { + if (_assertionConsumerServiceEndpoints == null) + { + ExtractEndpoints(); + } + + return _assertionConsumerServiceEndpoints; + } + } + + /// + /// Gets the entity. + /// + /// The entity. + public EntityDescriptor Entity { get; private set; } + + /// + /// Gets the ID of the entity described in the document. + /// + public string EntityId + { + get + { + if (Entity != null) + { + return Entity.EntityID; + } + + throw new InvalidOperationException("This instance does not contain a metadata document"); + } + } + + /// + /// Gets the IDP SLO endpoints. + /// + public List IDPSLOEndpoints + { + get + { + if (_idpSloEndpoints == null) + { + ExtractEndpoints(); + } + + return _idpSloEndpoints; + } + } + + /// + /// Gets the keys contained in the metadata document. + /// + public List Keys + { + get + { + if (_keys == null) + { + ExtractKeyDescriptors(); + } + + return _keys; + } + } + + /// + /// Gets or sets a value indicating whether the metadata should be signed when the ToXml() method is called. + /// + public bool Sign { get; set; } + + /// + /// Gets the SP SLO endpoints. + /// + public List SPSLOEndpoints + { + get + { + if (_spSloEndpoints == null) + { + ExtractEndpoints(); + } + + return _spSloEndpoints; + } + } + + /// + /// Gets the SSO endpoints. + /// + public List SSOEndpoints + { + get + { + if (_ssoEndpoints == null) + { + ExtractEndpoints(); + } + + return _ssoEndpoints; + } + } + + /// + /// Creates a default entity in the + /// + /// The default . + public EntityDescriptor CreateDefaultEntity() + { + if (Entity != null) + { + throw new InvalidOperationException("An entity is already created in this document."); + } + + Entity = GetDefaultEntityInstance(); + + return Entity; + } + + /// + /// Gets the IDP ArtifactResolutionService endpoint. + /// + /// The index. + /// The artifact resolution service endpoint. + public string GetIDPARSEndpoint(ushort index) + { + var ep = _idpArsEndpoints[index]; + return ep != null ? ep.Location : string.Empty; + } + + /// + /// Gets all AttributeQuery endpoints. + /// + /// The List of attribute query endpoints. + public List GetAttributeQueryEndpoints() + { + if (_attributeQueryEndpoints == null) + { + ExtractEndpoints(); + } + + return _attributeQueryEndpoints; + } + + /// + /// Gets the location of the first AttributeQuery endpoint. + /// + /// The attribute query endpoint location. + public string GetAttributeQueryEndpointLocation() + { + var endpoints = GetAttributeQueryEndpoints(); + if (endpoints.Count == 0) + { + throw new Saml20Exception("The identity provider does not support attribute queries."); + } + + return endpoints[0].Location; + } + + /// + /// Retrieves the keys marked with the usage given as parameter. + /// + /// The usage. + /// A list containing the keys. If no key is marked with the given usage, the method returns an empty list. + public List GetKeys(KeyTypes usage) + { + return Keys.FindAll(desc => desc.Use == usage); + } + + /// + /// Return a string containing the metadata XML based on the settings added to this instance. + /// The resulting XML will be signed, if the AsymmetricAlgorithm property has been set. + /// + /// The encoding. + /// Certificate to be used for signing (if appropriate) + /// The XML. + public string ToXml(Encoding encoding, X509Certificate2 certificate) + { + encoding = encoding ?? Encoding.UTF8; + var doc = new XmlDocument { PreserveWhitespace = true }; + doc.LoadXml(Serialization.SerializeToXmlString(Entity)); + + // Add the correct encoding to the head element. + if (doc.FirstChild is XmlDeclaration) + { + ((XmlDeclaration)doc.FirstChild).Encoding = encoding.WebName; + } + else + { + doc.PrependChild(doc.CreateXmlDeclaration("1.0", encoding.WebName, null)); + } + + if (Sign) + { + SignDocument(doc, certificate); + } + + return doc.OuterXml; + } + + /// + /// Gets the binding. + /// + /// The SAML binding. + /// The default value. + /// The binding. + private static string GetBinding(BindingType samlBinding, string defaultValue) + { + switch (samlBinding) + { + case BindingType.Artifact: + return Saml20Constants.ProtocolBindings.HttpArtifact; + case BindingType.Post: + return Saml20Constants.ProtocolBindings.HttpPost; + case BindingType.Redirect: + return Saml20Constants.ProtocolBindings.HttpRedirect; + case BindingType.Soap: + return Saml20Constants.ProtocolBindings.HttpSoap; + case BindingType.NotSet: + return defaultValue; + default: + throw new InvalidOperationException(string.Format("Unsupported SAML binding {0}", Enum.GetName(typeof(BindingType), samlBinding))); + } + } + + /// + /// Gets the default entity instance. + /// + /// The default . + private static EntityDescriptor GetDefaultEntityInstance() + { + var result = new EntityDescriptor + { + ID = "id" + Guid.NewGuid().ToString("N") + }; + + return result; + } + + /// + /// Signs the document. + /// + /// The doc. + private static void SignDocument(XmlDocument doc, X509Certificate2 certificate) + { + if (!certificate.HasPrivateKey) + { + throw new InvalidOperationException("Private key access to the signing certificate is required."); + } + + var signedXml = new SignedXml(doc); + signedXml.SignedInfo.CanonicalizationMethod = SignedXml.XmlDsigExcC14NTransformUrl; + signedXml.SigningKey = certificate.PrivateKey; + + // Retrieve the value of the "ID" attribute on the root assertion element. + var reference = new Reference("#" + doc.DocumentElement.GetAttribute("ID")); + + reference.AddTransform(new XmlDsigEnvelopedSignatureTransform()); + reference.AddTransform(new XmlDsigExcC14NTransform()); + + signedXml.AddReference(reference); + + // Include the public key of the certificate in the assertion. + signedXml.KeyInfo = new KeyInfo(); + // This was WholeChain, requiring us to trust our certs. WIF WSFed does not have this requirement, + // so the option was relaxed to EndCertOnly. This might need to be configurable + signedXml.KeyInfo.AddClause(new KeyInfoX509Data(certificate, X509IncludeOption.EndCertOnly)); + + signedXml.ComputeSignature(); + + // Append the computed signature. The signature must be placed as the sibling of the Issuer element. + doc.DocumentElement.InsertBefore(doc.ImportNode(signedXml.GetXml(), true), doc.DocumentElement.FirstChild); + } + + /// + /// Takes the configuration class and converts it to a SAML2.0 metadata document. + /// + /// The config. + /// The keyInfo. + private void ConvertToMetadata(Saml2Configuration config, KeyInfo keyInfo) + { + var entity = CreateDefaultEntity(); + entity.EntityID = config.ServiceProvider.Id; + entity.ValidUntil = DateTime.Now.AddDays(7); + + var serviceProviderDescriptor = new SpSsoDescriptor + { + ProtocolSupportEnumeration = new[] { Saml20Constants.Protocol }, + AuthnRequestsSigned = XmlConvert.ToString(true), + WantAssertionsSigned = XmlConvert.ToString(true) + }; + + if (config.ServiceProvider.NameIdFormats.Count > 0) + { + serviceProviderDescriptor.NameIdFormat = new string[config.ServiceProvider.NameIdFormats.Count]; + var count = 0; + foreach (var elem in config.ServiceProvider.NameIdFormats) + { + serviceProviderDescriptor.NameIdFormat[count++] = elem.Format; + } + } + + var baseUrl = new Uri(config.ServiceProvider.Server); + var logoutServiceEndpoints = new List(); + var signonServiceEndpoints = new List(); + + var artifactResolutionEndpoints = new List(2); + + // Include endpoints. + foreach (var endpoint in config.ServiceProvider.Endpoints) + { + if (endpoint.Type == EndpointType.SignOn) + { + var loginEndpoint = new IndexedEndpoint + { + Index = endpoint.Index, + IsDefault = true, + Location = new Uri(baseUrl, endpoint.LocalPath).ToString(), + Binding = GetBinding(endpoint.Binding, Saml20Constants.ProtocolBindings.HttpPost) + }; + signonServiceEndpoints.Add(loginEndpoint); + + var artifactSignonEndpoint = new IndexedEndpoint + { + Binding = Saml20Constants.ProtocolBindings.HttpSoap, + Index = loginEndpoint.Index, + Location = loginEndpoint.Location + }; + artifactResolutionEndpoints.Add(artifactSignonEndpoint); + + continue; + } + + if (endpoint.Type == EndpointType.Logout) + { + var logoutEndpoint = new Endpoint + { + Location = new Uri(baseUrl, endpoint.LocalPath).ToString() + }; + logoutEndpoint.ResponseLocation = logoutEndpoint.Location; + logoutEndpoint.Binding = GetBinding(endpoint.Binding, Saml20Constants.ProtocolBindings.HttpPost); + logoutServiceEndpoints.Add(logoutEndpoint); + + // TODO: Look at this... + logoutEndpoint = new Endpoint + { + Location = new Uri(baseUrl, endpoint.LocalPath).ToString() + }; + logoutEndpoint.ResponseLocation = logoutEndpoint.Location; + logoutEndpoint.Binding = GetBinding(endpoint.Binding, Saml20Constants.ProtocolBindings.HttpRedirect); + logoutServiceEndpoints.Add(logoutEndpoint); + + var artifactLogoutEndpoint = new IndexedEndpoint + { + Binding = Saml20Constants.ProtocolBindings.HttpSoap, + Index = endpoint.Index, + Location = logoutEndpoint.Location + }; + artifactResolutionEndpoints.Add(artifactLogoutEndpoint); + + continue; + } + } + + serviceProviderDescriptor.SingleLogoutService = logoutServiceEndpoints.ToArray(); + serviceProviderDescriptor.AssertionConsumerService = signonServiceEndpoints.ToArray(); + + // Attribute consuming service. + if (config.Metadata.RequestedAttributes.Count > 0) + { + var attConsumingService = new AttributeConsumingService(); + serviceProviderDescriptor.AttributeConsumingService = new[] { attConsumingService }; + attConsumingService.Index = signonServiceEndpoints[0].Index; + attConsumingService.IsDefault = true; + attConsumingService.ServiceName = new[] { new LocalizedName("SP", "en") }; + + attConsumingService.RequestedAttribute = new RequestedAttribute[config.Metadata.RequestedAttributes.Count]; + + for (var i = 0; i < config.Metadata.RequestedAttributes.Count; i++) + { + attConsumingService.RequestedAttribute[i] = new RequestedAttribute + { + Name = config.Metadata.RequestedAttributes[i].Name + }; + if (config.Metadata.RequestedAttributes[i].IsRequired) + { + attConsumingService.RequestedAttribute[i].IsRequired = true; + } + + attConsumingService.RequestedAttribute[i].NameFormat = SamlAttribute.NameformatBasic; + } + } + else + { + serviceProviderDescriptor.AttributeConsumingService = new AttributeConsumingService[0]; + } + + if (config.Metadata == null || !config.Metadata.ExcludeArtifactEndpoints) + { + serviceProviderDescriptor.ArtifactResolutionService = artifactResolutionEndpoints.ToArray(); + } + + entity.Items = new object[] { serviceProviderDescriptor }; + + // Keyinfo + var keySigning = new KeyDescriptor(); + var keyEncryption = new KeyDescriptor(); + serviceProviderDescriptor.KeyDescriptor = new[] { keySigning, keyEncryption }; + + keySigning.Use = KeyTypes.Signing; + keySigning.UseSpecified = true; + + keyEncryption.Use = KeyTypes.Encryption; + keyEncryption.UseSpecified = true; + + // Ugly conversion between the .Net framework classes and our classes ... avert your eyes!! + keySigning.KeyInfo = Serialization.DeserializeFromXmlString(keyInfo.GetXml().OuterXml); + keyEncryption.KeyInfo = keySigning.KeyInfo; + + // apply the element + if (config.Metadata.Organization != null) + { + entity.Organization = new Schema.Metadata.Organization + { + OrganizationName = new[] { new LocalizedName { Value = config.Metadata.Organization.Name } }, + OrganizationDisplayName = new[] { new LocalizedName { Value = config.Metadata.Organization.DisplayName } }, + OrganizationURL = new[] { new LocalizedURI { Value = config.Metadata.Organization.Url } } + }; + } + + if (config.Metadata.Contacts != null && config.Metadata.Contacts.Any()) + { + entity.ContactPerson = config.Metadata.Contacts.Select(x => new Schema.Metadata.Contact + { + ContactType = + (Schema.Metadata.ContactType) + ((int)x.Type), + Company = x.Company, + GivenName = x.GivenName, + SurName = x.SurName, + EmailAddress = new[] { x.Email }, + TelephoneNumber = new[] { x.Phone } + }).ToArray(); + } + } + + /// + /// Extracts the endpoints. + /// + private void ExtractEndpoints() + { + if (Entity != null) + { + _ssoEndpoints = new List(); + _idpSloEndpoints = new List(); + _idpArsEndpoints = new Dictionary(); + _spSloEndpoints = new List(); + _spArsEndpoints = new Dictionary(); + _assertionConsumerServiceEndpoints = new List(); + _attributeQueryEndpoints = new List(); + + foreach (var item in Entity.Items) + { + if (item is IdpSsoDescriptor) + { + var descriptor = (IdpSsoDescriptor)item; + foreach (var endpoint in descriptor.SingleSignOnService) + { + BindingType binding; + switch (endpoint.Binding) + { + case Saml20Constants.ProtocolBindings.HttpPost: + binding = BindingType.Post; + break; + case Saml20Constants.ProtocolBindings.HttpRedirect: + binding = BindingType.Redirect; + break; + case Saml20Constants.ProtocolBindings.HttpArtifact: + binding = BindingType.Artifact; + break; + case Saml20Constants.ProtocolBindings.HttpSoap: + binding = BindingType.Artifact; + break; + case "urn:mace:shibboleth:1.0:profiles:AuthnRequest": + // This is a SAML 1.1 binding, it is silly, we shall ignore it + continue; + default: + throw new InvalidOperationException("Binding not supported: " + endpoint.Binding); + } + + _ssoEndpoints.Add(new IdentityProviderEndpoint { Url = endpoint.Location, Binding = binding }); + } + + if (descriptor.SingleLogoutService != null) + { + foreach (var endpoint in descriptor.SingleLogoutService) + { + BindingType binding; + switch (endpoint.Binding) + { + case Saml20Constants.ProtocolBindings.HttpPost: + binding = BindingType.Post; + break; + case Saml20Constants.ProtocolBindings.HttpRedirect: + binding = BindingType.Redirect; + break; + case Saml20Constants.ProtocolBindings.HttpArtifact: + binding = BindingType.Artifact; + break; + case Saml20Constants.ProtocolBindings.HttpSoap: + binding = BindingType.Artifact; + break; + default: + throw new InvalidOperationException("Binding not supported: " + endpoint.Binding); + } + + _idpSloEndpoints.Add(new IdentityProviderEndpoint { Url = endpoint.Location, Binding = binding }); + } + } + + if (descriptor.ArtifactResolutionService != null) + { + foreach (var ie in descriptor.ArtifactResolutionService) + { + _idpArsEndpoints.Add(ie.Index, ie); + } + } + } + + if (item is SpSsoDescriptor) + { + var descriptor = (SpSsoDescriptor)item; + foreach (var endpoint in descriptor.AssertionConsumerService) + { + BindingType binding; + switch (endpoint.Binding) + { + case Saml20Constants.ProtocolBindings.HttpPost: + binding = BindingType.Post; + break; + case Saml20Constants.ProtocolBindings.HttpRedirect: + binding = BindingType.Redirect; + break; + case Saml20Constants.ProtocolBindings.HttpArtifact: + binding = BindingType.Artifact; + break; + case Saml20Constants.ProtocolBindings.HttpSoap: + binding = BindingType.Artifact; + break; + default: + throw new InvalidOperationException("Binding not supported: " + endpoint.Binding); + } + + _assertionConsumerServiceEndpoints.Add(new IdentityProviderEndpoint { Url = endpoint.Location, Binding = binding }); + } + + if (descriptor.SingleLogoutService != null) + { + foreach (var endpoint in descriptor.SingleLogoutService) + { + BindingType binding; + switch (endpoint.Binding) + { + case Saml20Constants.ProtocolBindings.HttpPost: + binding = BindingType.Post; + break; + case Saml20Constants.ProtocolBindings.HttpRedirect: + binding = BindingType.Redirect; + break; + case Saml20Constants.ProtocolBindings.HttpArtifact: + binding = BindingType.Artifact; + break; + case Saml20Constants.ProtocolBindings.HttpSoap: + binding = BindingType.Artifact; + break; + default: + throw new InvalidOperationException("Binding not supported: " + endpoint.Binding); + } + + _spSloEndpoints.Add(new IdentityProviderEndpoint { Url = endpoint.Location, Binding = binding }); + } + } + + if (descriptor.ArtifactResolutionService != null) + { + foreach (var ie in descriptor.ArtifactResolutionService) + { + _spArsEndpoints.Add(ie.Index, ie); + } + } + } + + if (item is AttributeAuthorityDescriptor) + { + var aad = (AttributeAuthorityDescriptor)item; + _attributeQueryEndpoints.AddRange(aad.AttributeService); + } + } + } + } + + /// + /// Extract KeyDescriptors from the metadata document represented by this instance. + /// + private void ExtractKeyDescriptors() + { + if (_keys != null || Entity == null) + { + return; + } + + _keys = new List(); + foreach (var keyDescriptor in Entity.Items.OfType().SelectMany(rd => rd.KeyDescriptor)) + { + _keys.Add(keyDescriptor); + } + } + + /// + /// Retrieves the key descriptors contained in the document + /// + /// The doc. + private void ExtractKeyDescriptors(XmlDocument doc) + { + var list = doc.GetElementsByTagName(KeyDescriptor.ElementName, Saml20Constants.Metadata); + _keys = new List(list.Count); + + foreach (XmlNode node in list) + { + _keys.Add(Serialization.DeserializeFromXmlString(node.OuterXml)); + } + } + } +} diff --git a/src/SAML2.Standard/Saml20NameFormat.cs b/src/SAML2.Standard/Saml20NameFormat.cs new file mode 100644 index 0000000..aeb593b --- /dev/null +++ b/src/SAML2.Standard/Saml20NameFormat.cs @@ -0,0 +1,18 @@ +namespace SAML2 +{ + /// + /// Name formats for queried attributes + /// + public enum Saml20NameFormat + { + /// + /// Basic name format + /// + Basic, + + /// + /// Uri name format + /// + Uri + } +} diff --git a/src/SAML2.Standard/SamlActionType.cs b/src/SAML2.Standard/SamlActionType.cs new file mode 100644 index 0000000..f4e2849 --- /dev/null +++ b/src/SAML2.Standard/SamlActionType.cs @@ -0,0 +1,18 @@ +namespace SAML2 +{ + /// + /// SAML Action types. + /// + public enum SamlActionType + { + /// + /// Request action type. + /// + SAMLRequest, + + /// + /// Response action type. + /// + SAMLResponse + } +} diff --git a/src/SAML2.Standard/Schema/Core/Action.cs b/src/SAML2.Standard/Schema/Core/Action.cs new file mode 100644 index 0000000..a299644 --- /dev/null +++ b/src/SAML2.Standard/Schema/Core/Action.cs @@ -0,0 +1,42 @@ +using System; +using System.Xml.Serialization; + +namespace SAML2.Schema.Core +{ + /// + /// The <Action> element specifies an action on the specified resource for which permission is sought. Its + /// string-data content provides the label for an action sought to be performed on the specified resource, + /// + [Serializable] + [XmlType(Namespace = Saml20Constants.Assertion)] + [XmlRoot(ElementName, Namespace = Saml20Constants.Assertion, IsNullable = false)] + public class Action + { + /// + /// The XML Element name of this class + /// + public const string ElementName = "Action"; + + /// + /// Gets or sets the value. + /// + /// The value. + [XmlText] + public string Value { get; set; } + + #region Attributes + + /// + /// Gets or sets the namespace. + /// A URI reference representing the namespace in which the name of the specified action is to be + /// interpreted. If this element is absent, the namespace + /// urn:oasis:names:tc:SAML:1.0:action:rwedc-negation specified in Section 8.1.2 is in + /// effect. + /// + /// The namespace. + [XmlAttribute("Namespace", DataType = "anyURI")] + public string Namespace { get; set; } + + #endregion + } +} diff --git a/src/SAML2.Standard/Schema/Core/Advice.cs b/src/SAML2.Standard/Schema/Core/Advice.cs new file mode 100644 index 0000000..b3d3560 --- /dev/null +++ b/src/SAML2.Standard/Schema/Core/Advice.cs @@ -0,0 +1,51 @@ +using System; +using System.Xml.Serialization; +using SAML2.Schema.Protocol; + +namespace SAML2.Schema.Core +{ + /// + /// The <Advice> element contains any additional information that the SAML authority wishes to provide. + /// This information MAY be ignored by applications without affecting either the semantics or the validity of + /// the assertion. + /// + /// + /// Advice is optional, and there are only implicit demands on the reference types. + /// We do not use it (yet) and let it pass without validation. + /// + [Serializable] + [XmlType(Namespace = Saml20Constants.Assertion)] + [XmlRoot(ElementName, Namespace = Saml20Constants.Assertion, IsNullable = false)] + public class Advice + { + /// + /// The XML Element name of this class + /// + public const string ElementName = "Advice"; + + #region Elements + + /// + /// Gets or sets the items. + /// Items may be of types: Assertion, AssertionIDRef, AssertionURIRef and EncryptedAssertion + /// + /// The items. + [XmlAnyElement] + [XmlElement(Assertion.ElementName, typeof(Assertion), Order = 1)] + [XmlElement("AssertionIDRef", typeof(string), DataType = "NCName", Order = 1)] + [XmlElement("AssertionURIRef", typeof(string), DataType = "anyURI", Order = 1)] + [XmlElement("EncryptedAssertion", typeof(EncryptedElement), Order = 1)] + [XmlChoiceIdentifier("ItemsElementName")] + public object[] Items { get; set; } + + /// + /// Gets or sets the name of the items element. + /// + /// The name of the items element. + [XmlElement("ItemsElementName")] + [XmlIgnore] + public AdviceType[] ItemsElementName { get; set; } + + #endregion + } +} diff --git a/src/SAML2.Standard/Schema/Core/AdviceType.cs b/src/SAML2.Standard/Schema/Core/AdviceType.cs new file mode 100644 index 0000000..0e9c330 --- /dev/null +++ b/src/SAML2.Standard/Schema/Core/AdviceType.cs @@ -0,0 +1,43 @@ +using System; +using System.Xml.Serialization; + +namespace SAML2.Schema.Core +{ + /// + /// AdviceType enumeration. + /// + [Serializable] + [XmlType(Namespace = Saml20Constants.Assertion, IncludeInSchema = false)] + public enum AdviceType + { + /// + /// Item of type any + /// + [XmlEnum("##any:")] + Item, + + /// + /// Item of type Assertion + /// + [XmlEnum("Assertion")] + Assertion, + + /// + /// Item of type AssertionIDRef + /// + [XmlEnum("AssertionIDRef")] + AssertionIDRef, + + /// + /// Item of type AssertionURIRef + /// + [XmlEnum("AssertionURIRef")] + AssertionURIRef, + + /// + /// Item of type EncryptedAssertion + /// + [XmlEnum("EncryptedAssertion")] + EncryptedAssertion, + } +} \ No newline at end of file diff --git a/src/SAML2.Standard/Schema/Core/Assertion.cs b/src/SAML2.Standard/Schema/Core/Assertion.cs new file mode 100644 index 0000000..cecb8f7 --- /dev/null +++ b/src/SAML2.Standard/Schema/Core/Assertion.cs @@ -0,0 +1,161 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Xml.Serialization; +using SAML2.Schema.XmlDSig; +using SAML2.Utils; + +namespace SAML2.Schema.Core +{ + /// + /// The <Assertion> element is of the AssertionType complex type. This type specifies the basic + /// information that is common to all assertions, + /// + [Serializable] + [XmlType(Namespace = Saml20Constants.Assertion)] + [XmlRoot(ElementName, Namespace = Saml20Constants.Assertion, IsNullable = false)] + public class Assertion + { + /// + /// The XML Element name of this class + /// + public const string ElementName = "Assertion"; + + /// + /// Initializes a new instance of the class. + /// + public Assertion() + { + Version = Saml20Constants.Version; + } + + /// + /// Gets or sets the issue instant. + /// The time instant of issue in UTC + /// + /// The issue instant. + [XmlIgnore] + public DateTime? IssueInstant { get; set; } + + #region Attributes + + /// + /// Gets or sets the ID. + /// The identifier for this assertion. It is of type xs:ID, and MUST follow the requirements specified in + /// Section 1.3.4 for identifier uniqueness. + /// + /// The ID. + [XmlAttribute("ID", DataType = "ID")] + public string Id { get; set; } + + /// + /// Gets or sets a string representation of the issue instant. + /// + /// The issue instant string. + [XmlAttribute("IssueInstant")] + public string IssueInstantString + { + get { return IssueInstant.HasValue ? Saml20Utils.ToUtcString(IssueInstant.Value) : null; } + set { IssueInstant = string.IsNullOrEmpty(value) ? (DateTime?)null : Saml20Utils.FromUtcString(value); } + } + + /// + /// Gets or sets the version. + /// The version of this assertion. The identifier for the version of SAML defined in this specification is "2.0". + /// + /// The version. + [XmlAttribute] + public string Version { get; set; } + + #endregion + + #region Elements + + /// + /// Gets or sets the advice. + /// Additional information related to the assertion that assists processing in certain situations but which + /// MAY be ignored by applications that do not understand the advice or do not wish to make use of it. + /// + /// The advice. + [XmlElement("Advice", Order = 5)] + public Advice Advice { get; set; } + + /// + /// Gets or sets the conditions. + /// Conditions that MUST be evaluated when assessing the validity of and/or when using the assertion. + /// + /// The conditions. + [XmlElement("Conditions", Order = 4)] + public Conditions Conditions { get; set; } + + /// + /// Gets or sets the issuer. + /// The SAML authority that is making the claim(s) in the assertion. The issuer SHOULD be unambiguous + /// to the intended relying parties. + /// This specification defines no particular relationship between the entity represented by this element + /// and the signer of the assertion (if any). Any such requirements imposed + /// + /// The issuer. + [XmlElement("Issuer", Order = 1)] + public NameId Issuer { get; set; } + + /// + /// Gets or sets the Statements (AttributeStatement, AuthnStatement and AuthzDecisionStatement types) + /// + /// The items. + [XmlElement("AttributeStatement", typeof(AttributeStatement), Order = 6)] + [XmlElement("AuthnStatement", typeof(AuthnStatement), Order = 6)] + [XmlElement("AuthzDecisionStatement", typeof(AuthzDecisionStatement), Order = 6)] + [XmlElement("Statement", typeof(StatementAbstract), Order = 6)] + public StatementAbstract[] Items { get; set; } + + /// + /// Gets or sets the signature. + /// An XML Signature that protects the integrity of and authenticates the issuer of the assertion + /// + /// The signature. + [XmlElement("Signature", Order = 2, Namespace = Saml20Constants.Xmldsig)] + public Signature Signature { get; set; } + + /// + /// Gets or sets the subject. + /// The subject of the statement(s) in the assertion + /// + /// The subject. + [XmlElement("Subject", Order = 3)] + public Subject Subject { get; set; } + + #endregion + + /// + /// Get the AttributeStatement elements of the Assertion. + /// + /// A list containing the AttributeStatement instances found in the assertion. An empty list if none could be found. + public List GetAttributeStatements() + { + return GetStatements(); + } + + /// + /// Get the AuthnStatement elements of the Assertion. + /// + /// A list containing the AuthnStatement instances found in the assertion. An empty list if none could be found. + public List GetAuthnStatements() + { + return GetStatements(); + } + + /// + /// Utility method for extracting statements of a particular type from the list of items. + /// + /// The statement type. + /// The statements of the specified type. + private List GetStatements() where T : StatementAbstract + { + var result = new List(1); + result.AddRange(Items.OfType()); + + return result; + } + } +} diff --git a/src/SAML2.Standard/Schema/Core/AttributeStatement.cs b/src/SAML2.Standard/Schema/Core/AttributeStatement.cs new file mode 100644 index 0000000..eb901ac --- /dev/null +++ b/src/SAML2.Standard/Schema/Core/AttributeStatement.cs @@ -0,0 +1,35 @@ +using System; +using System.Xml.Serialization; +using SAML2.Schema.Protocol; + +namespace SAML2.Schema.Core +{ + /// + /// The <AttributeStatement> element describes a statement by the SAML authority asserting that the + /// assertion subject is associated with the specified attributes. Assertions containing + /// <AttributeStatement> elements MUST contain a <Subject> element. + /// + [Serializable] + [XmlType(Namespace = Saml20Constants.Assertion)] + [XmlRoot(ElementName, Namespace = Saml20Constants.Assertion, IsNullable = false)] + public class AttributeStatement : StatementAbstract + { + /// + /// The XML Element name of this class + /// + public new const string ElementName = "AttributeStatement"; + + #region Elements + + /// + /// Gets or sets the items. + /// Items may be of type Attribute and EncryptedAttribute + /// + /// The items. + [XmlElement(SamlAttribute.ElementName, typeof(SamlAttribute), Order = 1)] + [XmlElement("EncryptedAttribute", typeof(EncryptedElement), Order = 1)] + public object[] Items { get; set; } + + #endregion + } +} \ No newline at end of file diff --git a/src/SAML2.Standard/Schema/Core/AudienceRestriction.cs b/src/SAML2.Standard/Schema/Core/AudienceRestriction.cs new file mode 100644 index 0000000..cd8446f --- /dev/null +++ b/src/SAML2.Standard/Schema/Core/AudienceRestriction.cs @@ -0,0 +1,40 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Xml.Serialization; + +namespace SAML2.Schema.Core +{ + /// + /// The <AudienceRestriction> element specifies that the assertion is addressed to one or more + /// specific audiences identified by <Audience> elements. Although a SAML relying party that is outside the + /// audiences specified is capable of drawing conclusions from an assertion, the SAML asserting party + /// explicitly makes no representation as to accuracy or trustworthiness to such a party. It contains the + /// following element: + /// + [Serializable] + [DebuggerStepThrough] + [XmlType(Namespace = Saml20Constants.Assertion)] + [XmlRoot(ElementName, Namespace = Saml20Constants.Assertion, IsNullable = false)] + public class AudienceRestriction : ConditionAbstract + { + /// + /// The XML Element name of this class + /// + public new const string ElementName = "AudienceRestriction"; + + #region Elements + + /// + /// Gets or sets the audience. + /// A URI reference that identifies an intended audience. The URI reference MAY identify a document + /// that describes the terms and conditions of audience membership. It MAY also contain the unique + /// identifier URI from a SAML name identifier that describes a system entity + /// + /// The audience. + [XmlElement("Audience", DataType = "anyURI", Order = 1)] + public List Audience { get; set; } + + #endregion + } +} diff --git a/src/SAML2.Standard/Schema/Core/AuthnContext.cs b/src/SAML2.Standard/Schema/Core/AuthnContext.cs new file mode 100644 index 0000000..5bc9d5e --- /dev/null +++ b/src/SAML2.Standard/Schema/Core/AuthnContext.cs @@ -0,0 +1,54 @@ +using System; +using System.Xml.Serialization; + +namespace SAML2.Schema.Core +{ + /// + /// The <AuthnContext> element specifies the context of an authentication event. The element can contain + /// an authentication context class reference, an authentication context declaration or declaration reference, + /// or both. + /// + [Serializable] + [XmlType(Namespace = Saml20Constants.Assertion)] + [XmlRoot(ElementName, Namespace = Saml20Constants.Assertion, IsNullable = false)] + public class AuthnContext + { + /// + /// The XML Element name of this class + /// + public const string ElementName = "AuthnContext"; + + #region Elements + + /// + /// Gets or sets the authenticating authority. + /// Zero or more unique identifiers of authentication authorities that were involved in the authentication of + /// the principal (not including the assertion issuer, who is presumed to have been involved without being + /// explicitly named here). + /// + /// The authenticating authority. + [XmlElement("AuthenticatingAuthority", DataType = "anyURI", Order = 2)] + public string[] AuthenticatingAuthority { get; set; } + + /// + /// Gets or sets the items. + /// Items may be of types: AuthnContextClassRef, AuthnContextDecl and AuthnContextDeclRef + /// + /// The items. + [XmlElement("AuthnContextClassRef", typeof(string), DataType = "anyURI", Order = 1)] + [XmlElement("AuthnContextDecl", typeof(object), Order = 1)] + [XmlElement("AuthnContextDeclRef", typeof(string), DataType = "anyURI", Order = 1)] + [XmlChoiceIdentifier("ItemsElementName")] + public object[] Items { get; set; } + + /// + /// Gets or sets the name of the items element. + /// + /// The name of the items element. + [XmlElement("ItemsElementName")] + [XmlIgnore] + public AuthnContextType[] ItemsElementName { get; set; } + + #endregion + } +} diff --git a/src/SAML2.Standard/Schema/Core/AuthnContextType.cs b/src/SAML2.Standard/Schema/Core/AuthnContextType.cs new file mode 100644 index 0000000..066a90e --- /dev/null +++ b/src/SAML2.Standard/Schema/Core/AuthnContextType.cs @@ -0,0 +1,31 @@ +using System; +using System.Xml.Serialization; + +namespace SAML2.Schema.Core +{ + /// + /// Authentication context type enumeration. + /// + [Serializable] + [XmlTypeAttribute(Namespace = Saml20Constants.Assertion, IncludeInSchema = false)] + public enum AuthnContextType + { + /// + /// Item of type AuthnContextClassRef + /// + [XmlEnum("AuthnContextClassRef")] + AuthnContextClassRef, + + /// + /// Item of type AuthnContextDecl + /// + [XmlEnum("AuthnContextDecl")] + AuthnContextDecl, + + /// + /// Item of type AuthnContextDeclRef + /// + [XmlEnum("AuthnContextDeclRef")] + AuthnContextDeclRef, + } +} \ No newline at end of file diff --git a/src/SAML2.Standard/Schema/Core/AuthnStatement.cs b/src/SAML2.Standard/Schema/Core/AuthnStatement.cs new file mode 100644 index 0000000..27ce194 --- /dev/null +++ b/src/SAML2.Standard/Schema/Core/AuthnStatement.cs @@ -0,0 +1,100 @@ +using System; +using System.Xml.Serialization; +using SAML2.Utils; + +namespace SAML2.Schema.Core +{ + /// + /// The <AuthnStatement> element describes a statement by the SAML authority asserting that the + /// assertion subject was authenticated by a particular means at a particular time. Assertions containing + /// <AuthnStatement> elements MUST contain a <Subject> element. + /// + [Serializable] + [XmlType(Namespace = Saml20Constants.Assertion)] + [XmlRoot(ElementName, Namespace = Saml20Constants.Assertion, IsNullable = false)] + public class AuthnStatement : StatementAbstract + { + /// + /// The XML Element name of this class + /// + public new const string ElementName = "AuthnStatement"; + + /// + /// Gets or sets the authentication instant. + /// Specifies the time at which the authentication took place. The time value is encoded in UTC + /// + /// The authentication instant. + [XmlIgnore] + public DateTime? AuthnInstant { get; set; } + + /// + /// Gets or sets the session not on or after. + /// Specifies a time instant at which the session between the principal identified by the subject and the + /// SAML authority issuing this statement MUST be considered ended. The time value is encoded in + /// UTC, as described in Section 1.3.3. There is no required relationship between this attribute and a + /// NotOnOrAfter condition attribute that may be present in the assertion. + /// + /// The session not on or after. + [XmlIgnore] + public DateTime? SessionNotOnOrAfter { get; set; } + + #region Attributes + + /// + /// Gets or sets the authentication instant string. + /// + /// The authentication instant string. + [XmlAttribute("AuthnInstant")] + public string AuthnInstantString + { + get { return AuthnInstant.HasValue ? Saml20Utils.ToUtcString(AuthnInstant.Value) : null; } + set { AuthnInstant = Saml20Utils.FromUtcString(value); } + } + + /// + /// Gets or sets the index of the session. + /// Specifies the index of a particular session between the principal identified by the subject and the + /// authenticating authority. + /// + /// The index of the session. + [XmlAttribute("SessionIndex")] + public string SessionIndex { get; set; } + + /// + /// Gets or sets the session not on or after string. + /// + /// The session not on or after string. + [XmlAttribute("SessionNotOnOrAfter")] + public string SessionNotOnOrAfterString + { + get { return SessionNotOnOrAfter.HasValue ? Saml20Utils.ToUtcString(SessionNotOnOrAfter.Value) : null; } + set { SessionNotOnOrAfter = string.IsNullOrEmpty(value) ? (DateTime?)null : Saml20Utils.FromUtcString(value); } + } + + #endregion + + #region Elements + + /// + /// Gets or sets the authentication context. + /// The context used by the authenticating authority up to and including the authentication event that + /// yielded this statement. Contains an authentication context class reference, an authentication context + /// declaration or declaration reference, or both. See the Authentication Context specification + /// for a full description of authentication context information. + /// + /// The authentication context. + [XmlElement("AuthnContext", Order = 2)] + public AuthnContext AuthnContext { get; set; } + + /// + /// Gets or sets the subject locality. + /// Specifies the DNS domain name and IP address for the system from which the assertion subject was + /// apparently authenticated. + /// + /// The subject locality. + [XmlElement("SubjectLocality", Order = 1)] + public SubjectLocality SubjectLocality { get; set; } + + #endregion + } +} diff --git a/src/SAML2.Standard/Schema/Core/AuthzDecisionStatement.cs b/src/SAML2.Standard/Schema/Core/AuthzDecisionStatement.cs new file mode 100644 index 0000000..5db6677 --- /dev/null +++ b/src/SAML2.Standard/Schema/Core/AuthzDecisionStatement.cs @@ -0,0 +1,65 @@ +using System; +using System.Xml.Serialization; + +namespace SAML2.Schema.Core +{ + /// + /// The <AuthzDecisionStatement> element describes a statement by the SAML authority asserting that + /// a request for access by the assertion subject to the specified resource has resulted in the specified + /// authorization decision on the basis of some optionally specified evidence. Assertions containing + /// <AuthzDecisionStatement> elements MUST contain a <Subject> element. + /// + [Serializable] + [XmlType(Namespace = Saml20Constants.Assertion)] + [XmlRoot(ElementName, Namespace = Saml20Constants.Assertion, IsNullable = false)] + public class AuthzDecisionStatement : StatementAbstract + { + /// + /// The XML Element name of this class + /// + public new const string ElementName = "AuthzDecisionStatement"; + + #region Attributes + + /// + /// Gets or sets the decision. + /// The decision rendered by the SAML authority with respect to the specified resource. The value is of + /// the DecisionType simple type. + /// + /// The decision. + [XmlAttribute("Decision")] + public DecisionType Decision { get; set; } + + /// + /// Gets or sets the resource. + /// A URI reference identifying the resource to which access authorization is sought. This attribute MAY + /// have the value of the empty URI reference (""), and the meaning is defined to be "the start of the + /// current document", as specified by IETF RFC 2396 [RFC 2396] Section 4.2. + /// + /// The resource. + [XmlAttribute("Resource", DataType = "anyURI")] + public string Resource { get; set; } + + #endregion + + #region Elements + + /// + /// Gets or sets the action. + /// The set of actions authorized to be performed on the specified resource. + /// + /// The action. + [XmlElement("Action", Order = 1)] + public Action[] Action { get; set; } + + /// + /// Gets or sets the evidence. + /// A set of assertions that the SAML authority relied on in making the decision. + /// + /// The evidence. + [XmlElement("Evidence", Order = 2)] + public Evidence Evidence { get; set; } + + #endregion + } +} diff --git a/src/SAML2.Standard/Schema/Core/BaseIdAbstract.cs b/src/SAML2.Standard/Schema/Core/BaseIdAbstract.cs new file mode 100644 index 0000000..a068030 --- /dev/null +++ b/src/SAML2.Standard/Schema/Core/BaseIdAbstract.cs @@ -0,0 +1,43 @@ +using System; +using System.Xml.Serialization; + +namespace SAML2.Schema.Core +{ + /// + /// The <BaseID> element is an extension point that allows applications to add new kinds of identifiers. Its + /// BaseIDAbstractType complex type is abstract and is thus usable only as the base of a derived type. + /// + [Serializable] + [XmlType(Namespace = Saml20Constants.Assertion)] + [XmlRoot(ElementName, Namespace = Saml20Constants.Assertion, IsNullable = false)] + public abstract class BaseIdAbstract + { + /// + /// The XML Element name of this class + /// + public const string ElementName = "BaseID"; + + #region Attributes + + /// + /// Gets or sets the name qualifier. + /// The security or administrative domain that qualifies the identifier. This attribute provides a means + /// to federate identifiers from disparate user stores without collision. + /// + /// The name qualifier. + [XmlAttribute("NameQualifier")] + public string NameQualifier { get; set; } + + /// + /// Gets or sets the SP name qualifier. + /// Further qualifies an identifier with the name of a service provider or affiliation of providers. This + /// attribute provides an additional means to federate identifiers on the basis of the relying party or + /// parties. + /// + /// The SP name qualifier. + [XmlAttribute("SPNameQualifier")] + public string SPNameQualifier { get; set; } + + #endregion + } +} diff --git a/src/SAML2.Standard/Schema/Core/ConditionAbstract.cs b/src/SAML2.Standard/Schema/Core/ConditionAbstract.cs new file mode 100644 index 0000000..4f254b9 --- /dev/null +++ b/src/SAML2.Standard/Schema/Core/ConditionAbstract.cs @@ -0,0 +1,25 @@ +using System; +using System.Diagnostics; +using System.Xml.Serialization; + +namespace SAML2.Schema.Core +{ + /// + /// The SAML ConditionAbstract class. + /// Serves as an extension point for new conditions. + /// + [XmlInclude(typeof(ProxyRestriction))] + [XmlInclude(typeof(OneTimeUse))] + [XmlInclude(typeof(AudienceRestriction))] + [Serializable] + [DebuggerStepThrough] + [XmlType(Namespace = Saml20Constants.Assertion)] + [XmlRoot(ElementName, Namespace = Saml20Constants.Assertion, IsNullable = false)] + public abstract class ConditionAbstract + { + /// + /// The XML Element name of this class + /// + public const string ElementName = "Condition"; + } +} \ No newline at end of file diff --git a/src/SAML2.Standard/Schema/Core/Conditions.cs b/src/SAML2.Standard/Schema/Core/Conditions.cs new file mode 100644 index 0000000..a6c4abd --- /dev/null +++ b/src/SAML2.Standard/Schema/Core/Conditions.cs @@ -0,0 +1,78 @@ +using System; +using System.Collections.Generic; +using System.Xml.Serialization; +using SAML2.Utils; + +namespace SAML2.Schema.Core +{ + /// + /// The SAML20 Conditions class + /// + [Serializable] + [XmlType(Namespace = Saml20Constants.Assertion)] + [XmlRoot(ElementName, Namespace = Saml20Constants.Assertion, IsNullable = false)] + public class Conditions + { + /// + /// The XML Element name of this class + /// + public const string ElementName = "Conditions"; + + /// + /// Gets or sets the not before. + /// Specifies the earliest time instant at which the assertion is valid. The time value is encoded in UTC + /// + /// The not before. + [XmlIgnore] + public DateTime? NotBefore { get; set; } + + /// + /// Gets or sets the not on or after. + /// Specifies the time instant at which the assertion has expired. The time value is encoded in UTC. + /// + /// The not on or after. + [XmlIgnore] + public DateTime? NotOnOrAfter { get; set; } + + #region Attributes + + /// + /// Gets or sets the not before string. + /// + /// The not before string. + [XmlAttribute("NotBefore")] + public string NotBeforeString + { + get { return NotBefore.HasValue ? Saml20Utils.ToUtcString(NotBefore.Value) : null; } + set { NotBefore = string.IsNullOrEmpty(value) ? (DateTime?)null : Saml20Utils.FromUtcString(value); } + } + + /// + /// Gets or sets the not on or after string. + /// + /// The not on or after string. + [XmlAttribute("NotOnOrAfter")] + public string NotOnOrAfterString + { + get { return NotOnOrAfter.HasValue ? Saml20Utils.ToUtcString(NotOnOrAfter.Value) : null; } + set { NotOnOrAfter = string.IsNullOrEmpty(value) ? (DateTime?)null : Saml20Utils.FromUtcString(value); } + } + + #endregion + + #region Elements + + /// + /// Gets or sets the items. + /// Items may be of types AudienceRestriction, Condition, OneTimeUse and ProxyRestriction + /// + /// The items. + [XmlElement("AudienceRestriction", typeof(AudienceRestriction), Order = 1)] + [XmlElement("Condition", typeof(ConditionAbstract), Order = 1)] + [XmlElement("OneTimeUse", typeof(OneTimeUse), Order = 1)] + [XmlElement("ProxyRestriction", typeof(ProxyRestriction), Order = 1)] + public List Items { get; set; } + + #endregion + } +} diff --git a/src/SAML2.Standard/Schema/Core/DecisionType.cs b/src/SAML2.Standard/Schema/Core/DecisionType.cs new file mode 100644 index 0000000..c3b2520 --- /dev/null +++ b/src/SAML2.Standard/Schema/Core/DecisionType.cs @@ -0,0 +1,31 @@ +using System; +using System.Xml.Serialization; + +namespace SAML2.Schema.Core +{ + /// + /// Decision types + /// + [Serializable] + [XmlType(Namespace = Saml20Constants.Assertion)] + public enum DecisionType + { + /// + /// Permit decision type + /// + [XmlEnum("Permit")] + Permit, + + /// + /// Deny decision type + /// + [XmlEnum("Deny")] + Deny, + + /// + /// Indeterminate decision type + /// + [XmlEnum("Indeterminate")] + Indeterminate, + } +} \ No newline at end of file diff --git a/src/SAML2.Standard/Schema/Core/Evidence.cs b/src/SAML2.Standard/Schema/Core/Evidence.cs new file mode 100644 index 0000000..b6f76e0 --- /dev/null +++ b/src/SAML2.Standard/Schema/Core/Evidence.cs @@ -0,0 +1,45 @@ +using System; +using System.Xml.Serialization; +using SAML2.Schema.Protocol; + +namespace SAML2.Schema.Core +{ + /// + /// The <Evidence> element contains one or more assertions or assertion references that the SAML + /// authority relied on in issuing the authorization decision. + /// + [Serializable] + [XmlType(Namespace = Saml20Constants.Assertion)] + [XmlRoot(ElementName, Namespace = Saml20Constants.Assertion, IsNullable = false)] + public class Evidence + { + /// + /// The XML Element name of this class + /// + public const string ElementName = "Evidence"; + + #region Elements + + /// + /// Gets or sets the items. + /// Items may be of types Assertion, AssertionIDRef, AssertionURIRef and EncryptedAssertion + /// + /// The items. + [XmlElement(Assertion.ElementName, typeof(Assertion), Order = 1)] + [XmlElement("AssertionIDRef", typeof(string), DataType = "NCName", Order = 1)] + [XmlElement("AssertionURIRef", typeof(string), DataType = "anyURI", Order = 1)] + [XmlElement("EncryptedAssertion", typeof(EncryptedElement), Order = 1)] + [XmlChoiceIdentifier("ItemsElementName")] + public object[] Items { get; set; } + + /// + /// Gets or sets the name of the items element. + /// + /// The name of the items element. + [XmlElement("ItemsElementName")] + [XmlIgnore] + public EvidenceType[] ItemsElementName { get; set; } + + #endregion + } +} diff --git a/src/SAML2.Standard/Schema/Core/EvidenceType.cs b/src/SAML2.Standard/Schema/Core/EvidenceType.cs new file mode 100644 index 0000000..be860c4 --- /dev/null +++ b/src/SAML2.Standard/Schema/Core/EvidenceType.cs @@ -0,0 +1,37 @@ +using System; +using System.Xml.Serialization; + +namespace SAML2.Schema.Core +{ + /// + /// Item Choices + /// + [Serializable] + [XmlType(Namespace = Saml20Constants.Assertion, IncludeInSchema = false)] + public enum EvidenceType + { + /// + /// Item of type Assertion + /// + [XmlEnum("Assertion")] + Assertion, + + /// + /// Item of type AssertionIDRef + /// + [XmlEnum("AssertionIDRef")] + AssertionIDRef, + + /// + /// Item of type AssertionURIRef + /// + [XmlEnum("AssertionURIRef")] + AssertionURIRef, + + /// + /// Item of type EncryptedAssertion + /// + [XmlEnum("EncryptedAssertion")] + EncryptedAssertion, + } +} \ No newline at end of file diff --git a/src/SAML2.Standard/Schema/Core/KeyInfoConfirmationData.cs b/src/SAML2.Standard/Schema/Core/KeyInfoConfirmationData.cs new file mode 100644 index 0000000..df1c0a5 --- /dev/null +++ b/src/SAML2.Standard/Schema/Core/KeyInfoConfirmationData.cs @@ -0,0 +1,23 @@ +using System; +using System.Xml.Serialization; + +namespace SAML2.Schema.Core +{ + /// + /// The KeyInfoConfirmationDataType complex type constrains a <SubjectConfirmationData> + /// element to contain one or more <ds:KeyInfo> elements that identify cryptographic keys that are used in + /// some way to authenticate an attesting entity. The particular confirmation method MUST define the exact + /// mechanism by which the confirmation data can be used. The optional attributes defined by the + /// SubjectConfirmationDataType complex type MAY also appear. + /// + [Serializable] + [XmlType(Namespace = Saml20Constants.Assertion)] + [XmlRoot(ElementName, Namespace = Saml20Constants.Assertion, IsNullable = false)] + public class KeyInfoConfirmationData : SubjectConfirmationData + { + /// + /// The XML Element name of this class + /// + public new const string ElementName = "KeyInfoConfirmationData"; + } +} \ No newline at end of file diff --git a/src/SAML2.Standard/Schema/Core/NameId.cs b/src/SAML2.Standard/Schema/Core/NameId.cs new file mode 100644 index 0000000..cf36e70 --- /dev/null +++ b/src/SAML2.Standard/Schema/Core/NameId.cs @@ -0,0 +1,81 @@ +using System; +using System.Xml.Serialization; + +namespace SAML2.Schema.Core +{ + /// + /// The NameIDType complex type is used when an element serves to represent an entity by a string-valued + /// name. It is a more restricted form of identifier than the <BaseID> element and is the type underlying both + /// the <NameID> and <Issuer> elements. + /// + [Serializable] + [XmlType(Namespace = Saml20Constants.Assertion)] + [XmlRoot(ElementName, Namespace = Saml20Constants.Assertion, IsNullable = false)] + public class NameId + { + /// + /// The XML Element name of this class + /// + public const string ElementName = "NameID"; + + /// + /// Gets or sets the value. + /// + /// The value. + [XmlText] + public string Value { get; set; } + + #region Attributes + + /// + /// Gets or sets the format. + /// A URI reference representing the classification of string-based identifier information. See Section + /// 8.3 for the SAML-defined URI references that MAY be used as the value of the Format attribute + /// and their associated descriptions and processing rules. Unless otherwise specified by an element + /// based on this type, if no Format value is provided, then the value + /// urn:oasis:names:tc:SAML:1.0:nameid-format:unspecified (see Section 8.3.1) is in + /// effect. + /// When a Format value other than one specified in Section 8.3 is used, the content of an element + /// of this type is to be interpreted according to the definition of that format as provided outside of this + /// specification. If not otherwise indicated by the definition of the format, issues of anonymity, + /// pseudonym, and the persistence of the identifier with respect to the asserting and relying parties + /// are implementation-specific. + /// + /// The format. + [XmlAttribute("Format", DataType = "anyURI")] + public string Format { get; set; } + + /// + /// Gets or sets the name qualifier. + /// The security or administrative domain that qualifies the name. This attribute provides a means to + /// federate names from disparate user stores without collision. + /// + /// The name qualifier. + [XmlAttribute("NameQualifier")] + public string NameQualifier { get; set; } + + /// + /// Gets or sets the SP name qualifier. + /// Further qualifies a name with the name of a service provider or affiliation of providers. This + /// attribute provides an additional means to federate names on the basis of the relying party or + /// parties. + /// + /// The SP name qualifier. + [XmlAttribute("SPNameQualifier")] + public string SPNameQualifier { get; set; } + + /// + /// Gets or sets the SP provided ID. + /// A name identifier established by a service provider or affiliation of providers for the entity, if + /// different from the primary name identifier given in the content of the element. This attribute + /// provides a means of integrating the use of SAML with existing identifiers already in use by a + /// service provider. For example, an existing identifier can be "attached" to the entity using the Name + /// Identifier Management protocol defined in Section 3.6. + /// + /// The SP provided ID. + [XmlAttribute("SPProvidedID")] + public string SPProvidedID { get; set; } + + #endregion + } +} diff --git a/src/SAML2.Standard/Schema/Core/OneTimeUse.cs b/src/SAML2.Standard/Schema/Core/OneTimeUse.cs new file mode 100644 index 0000000..ec1e0a8 --- /dev/null +++ b/src/SAML2.Standard/Schema/Core/OneTimeUse.cs @@ -0,0 +1,53 @@ +using System; +using System.Xml.Serialization; + +namespace SAML2.Schema.Core +{ + /// + /// + /// In general, relying parties may choose to retain assertions, or the information they contain in some other + /// form, for reuse. The <OneTimeUse> condition element allows an authority to indicate that the information + /// in the assertion is likely to change very soon and fresh information should be obtained for each use. An + /// example would be an assertion containing an <AuthzDecisionStatement> which was the result of a + /// policy which specified access control which was a function of the time of day. + /// + /// + /// If system clocks in a distributed environment could be precisely synchronized, then this requirement could + /// be met by careful use of the validity interval. However, since some clock skew between systems will + /// always be present and will be combined with possible transmission delays, there is no convenient way for + /// the issuer to appropriately limit the lifetime of an assertion without running a substantial risk that it will + /// already have expired before it arrives. + /// + /// + /// The <OneTimeUse> element indicates that the assertion SHOULD be used immediately by the relying + /// party and MUST NOT be retained for future use. Relying parties are always free to request a fresh + /// assertion for every use. However, implementations that choose to retain assertions for future use MUST + /// observe the <OneTimeUse> element. This condition is independent from the NotBefore and + /// NotOnOrAfter condition information. + /// + /// + /// To support the single use constraint, a relying party should maintain a cache of the assertions it has + /// processed containing such a condition. Whenever an assertion with this condition is processed, the cache + /// should be checked to ensure that the same assertion has not been previously received and processed by + /// the relying party. + /// + /// + /// A SAML authority MUST NOT include more than one <OneTimeUse> element within a <Conditions> + /// element of an assertion. + /// + /// + /// For the purposes of determining the validity of the <Conditions> element, the <OneTimeUse> is + /// considered to always be valid. That is, this condition does not affect validity but is a condition on use. + /// + /// + [Serializable] + [XmlType(Namespace = Saml20Constants.Assertion)] + [XmlRoot(ElementName, Namespace = Saml20Constants.Assertion, IsNullable = false)] + public class OneTimeUse : ConditionAbstract + { + /// + /// The XML Element name of this class + /// + public new const string ElementName = "OneTimeUse"; + } +} \ No newline at end of file diff --git a/src/SAML2.Standard/Schema/Core/ProxyRestriction.cs b/src/SAML2.Standard/Schema/Core/ProxyRestriction.cs new file mode 100644 index 0000000..34bc8ca --- /dev/null +++ b/src/SAML2.Standard/Schema/Core/ProxyRestriction.cs @@ -0,0 +1,45 @@ +using System; +using System.Xml.Serialization; + +namespace SAML2.Schema.Core +{ + /// + /// The SAML20 ProxyRestriction condition class. + /// Specifies limitations that the asserting party imposes on relying parties that wish to subsequently act + /// as asserting parties themselves and issue assertions of their own on the basis of the information + /// contained in the original assertion. Although the schema permits multiple occurrences, there MUST + /// be at most one instance of this element. + /// + [Serializable] + [XmlType(Namespace = Saml20Constants.Assertion)] + [XmlRoot(ElementName, Namespace = Saml20Constants.Assertion, IsNullable = false)] + public class ProxyRestriction : ConditionAbstract + { + /// + /// The XML Element name of this class + /// + public new const string ElementName = "ProxyRestriction"; + + #region Attributes + + /// + /// Gets or sets the count. + /// + /// The count. + [XmlAttribute("Count", DataType = "nonNegativeInteger")] + public string Count { get; set; } + + #endregion + + #region Elements + + /// + /// Gets or sets the audience. + /// + /// The audience. + [XmlElement("Audience", DataType = "anyURI", Order = 1)] + public string[] Audience { get; set; } + + #endregion + } +} diff --git a/src/SAML2.Standard/Schema/Core/SamlAttribute.cs b/src/SAML2.Standard/Schema/Core/SamlAttribute.cs new file mode 100644 index 0000000..9bb454e --- /dev/null +++ b/src/SAML2.Standard/Schema/Core/SamlAttribute.cs @@ -0,0 +1,87 @@ +using System; +using System.Xml; +using System.Xml.Serialization; +using SAML2.Schema.Metadata; + +namespace SAML2.Schema.Core +{ + /// + /// The <Attribute> element identifies an attribute by name and optionally includes its value(s). It has the + /// AttributeType complex type. It is used within an attribute statement to express particular attributes and + /// values associated with an assertion subject, as described in the previous section. It is also used in an + /// attribute query to request that the values of specific SAML attributes be returned (see Section 3.3.2.3 for + /// more information). + /// + [XmlInclude(typeof(RequestedAttribute))] + [Serializable] + [XmlType(Namespace = Saml20Constants.Assertion)] + [XmlRoot(ElementName, Namespace = Saml20Constants.Assertion, IsNullable = false)] + public class SamlAttribute + { + /// + /// The XML Element name of this class + /// + public const string ElementName = "Attribute"; + + /// + /// Name format "uri". + /// + public const string NameformatUri = "urn:oasis:names:tc:SAML:2.0:attrname-format:uri"; + + /// + /// Name format "basic". + /// + public const string NameformatBasic = "urn:oasis:names:tc:SAML:2.0:attrname-format:basic"; + + #region Attributes + + /// + /// Gets or sets XML any attribute. + /// + /// The XML Any attribute. + [XmlAnyAttribute] + public XmlAttribute[] AnyAttr { get; set; } + + /// + /// Gets or sets the name of the attribute. + /// + [XmlAttribute("Name")] + public string Name { get; set; } + + /// + /// Gets or sets a URI reference representing the classification of the attribute name for purposes of interpreting the + /// name. See Section 8.2 for some URI references that MAY be used as the value of the NameFormat + /// attribute and their associated descriptions and processing rules. If no NameFormat value is provided, + /// the identifier urn:oasis:names:tc:SAML:2.0:attrname-format:unspecified (see Section + /// 8.2.1) is in effect. + /// + [XmlAttribute("NameFormat", DataType = "anyURI")] + public string NameFormat { get; set; } + + /// + /// Gets or sets a string that provides a more human-readable form of the attribute's name, which may be useful in + /// cases in which the actual Name is complex or opaque, such as an OID or a UUID. This attribute's + /// value MUST NOT be used as a basis for formally identifying SAML attributes. + /// + [XmlAttribute("FriendlyName")] + public string FriendlyName { get; set; } + + #endregion + + #region Elements + + /// + /// Gets or sets the attribute value. + /// Contains a value of the attribute. If an attribute contains more than one discrete value, it is + /// RECOMMENDED that each value appear in its own <AttributeValue> element. If more than + /// one <AttributeValue> element is supplied for an attribute, and any of the elements have a + /// data type assigned through xsi:type, then all of the <AttributeValue> elements must have + /// the identical data type assigned. + /// + /// The attribute value. + [XmlElement("AttributeValue", IsNullable = true, Order = 1)] + public string[] AttributeValue { get; set; } + + #endregion + } +} diff --git a/src/SAML2.Standard/Schema/Core/StatementAbstract.cs b/src/SAML2.Standard/Schema/Core/StatementAbstract.cs new file mode 100644 index 0000000..9a4cd18 --- /dev/null +++ b/src/SAML2.Standard/Schema/Core/StatementAbstract.cs @@ -0,0 +1,22 @@ +using System; +using System.Xml.Serialization; + +namespace SAML2.Schema.Core +{ + /// + /// The SAML20 StatementAbstract class. It's the base class for all statements in SAML20. + /// + [XmlInclude(typeof(AttributeStatement))] + [XmlIncludeAttribute(typeof(AuthzDecisionStatement))] + [XmlIncludeAttribute(typeof(AuthnStatement))] + [Serializable] + [XmlType(Namespace = Saml20Constants.Assertion)] + [XmlRoot(ElementName, Namespace = Saml20Constants.Assertion, IsNullable = false)] + public abstract class StatementAbstract + { + /// + /// The XML Element name of this class + /// + public const string ElementName = "Statement"; + } +} \ No newline at end of file diff --git a/src/SAML2.Standard/Schema/Core/Subject.cs b/src/SAML2.Standard/Schema/Core/Subject.cs new file mode 100644 index 0000000..2b01f87 --- /dev/null +++ b/src/SAML2.Standard/Schema/Core/Subject.cs @@ -0,0 +1,35 @@ +using System; +using System.Xml.Serialization; +using SAML2.Schema.Protocol; + +namespace SAML2.Schema.Core +{ + /// + /// The optional <Subject> element specifies the principal that is the subject of all of the (zero or more) + /// statements in the assertion. + /// + [Serializable] + [XmlType(Namespace = Saml20Constants.Assertion)] + [XmlRoot(ElementName, Namespace = Saml20Constants.Assertion, IsNullable = false)] + public class Subject + { + /// + /// The XML Element name of this class + /// + public const string ElementName = "Subject"; + + #region Elements + + /// + /// Gets or sets the items. + /// + /// The items. + [XmlElement(BaseIdAbstract.ElementName, typeof(BaseIdAbstract), Order = 1)] + [XmlElement("EncryptedID", typeof(EncryptedElement), Order = 1)] + [XmlElement(NameId.ElementName, typeof(NameId), Order = 1)] + [XmlElement(SubjectConfirmation.ElementName, typeof(SubjectConfirmation), Order = 1)] + public object[] Items { get; set; } + + #endregion + } +} diff --git a/src/SAML2.Standard/Schema/Core/SubjectConfirmation.cs b/src/SAML2.Standard/Schema/Core/SubjectConfirmation.cs new file mode 100644 index 0000000..615b6f1 --- /dev/null +++ b/src/SAML2.Standard/Schema/Core/SubjectConfirmation.cs @@ -0,0 +1,69 @@ +using System; +using System.Xml.Serialization; +using SAML2.Schema.Protocol; + +namespace SAML2.Schema.Core +{ + /// + /// The <SubjectConfirmation> element provides the means for a relying party to verify the + /// correspondence of the subject of the assertion with the party with whom the relying party is + /// communicating. + /// + [Serializable] + [XmlType(Namespace = Saml20Constants.Assertion)] + [XmlRoot(ElementName, Namespace = Saml20Constants.Assertion, IsNullable = false)] + public class SubjectConfirmation + { + /// + /// The BEARER_METHOD constant + /// + public const string BearerMethod = "urn:oasis:names:tc:SAML:2.0:cm:bearer"; + + /// + /// The XML Element name of this class + /// + public const string ElementName = "SubjectConfirmation"; + + #region Attributes + + /// + /// Gets or sets the method. + /// A URI reference that identifies a protocol or mechanism to be used to confirm the subject. URI + /// references identifying SAML-defined confirmation methods are currently defined in the SAML profiles + /// specification [SAMLProf]. Additional methods MAY be added by defining new URIs and profiles or by + /// private agreement. + /// + /// The method. + [XmlAttribute("Method", DataType = "anyURI")] + public string Method { get; set; } + + #endregion + + #region Elements + + /// + /// Gets or sets the item. + /// Identifies the entity expected to satisfy the enclosing subject confirmation requirements. + /// Valid elements are <BaseID>, <NameID>, or <EncryptedID> + /// + /// The item. + [XmlElement("BaseID", typeof(BaseIdAbstract), Order = 1)] + [XmlElement("EncryptedID", typeof(EncryptedElement), Order = 1)] + [XmlElement("NameID", typeof(NameId), Order = 1)] + public object Item { get; set; } + + /// + /// Gets or sets the subject confirmation data. + /// Additional confirmation information to be used by a specific confirmation method. For example, typical + /// content of this element might be a <ds:KeyInfo> element as defined in the XML Signature Syntax + /// and Processing specification [XMLSig], which identifies a cryptographic key (See also Section + /// 2.4.1.3). Particular confirmation methods MAY define a schema type to describe the elements, + /// attributes, or content that may appear in the <SubjectConfirmationData> element. + /// + /// The subject confirmation data. + [XmlElement("SubjectConfirmationData", Order = 2)] + public SubjectConfirmationData SubjectConfirmationData { get; set; } + + #endregion + } +} diff --git a/src/SAML2.Standard/Schema/Core/SubjectConfirmationData.cs b/src/SAML2.Standard/Schema/Core/SubjectConfirmationData.cs new file mode 100644 index 0000000..d01b2f7 --- /dev/null +++ b/src/SAML2.Standard/Schema/Core/SubjectConfirmationData.cs @@ -0,0 +1,105 @@ +using System; +using System.Xml; +using System.Xml.Serialization; +using SAML2.Utils; + +namespace SAML2.Schema.Core +{ + /// + /// The <SubjectConfirmationData> element has the SubjectConfirmationDataType complex type. It + /// specifies additional data that allows the subject to be confirmed or constrains the circumstances under + /// which the act of subject confirmation can take place. Subject confirmation takes place when a relying + /// party seeks to verify the relationship between an entity presenting the assertion (that is, the attesting + /// entity) and the subject of the assertion's claims. + /// + [XmlInclude(typeof(KeyInfoConfirmationData))] + [Serializable] + [XmlType(Namespace = Saml20Constants.Assertion)] + [XmlRoot(ElementName, Namespace = Saml20Constants.Assertion, IsNullable = false)] + public class SubjectConfirmationData + { + /// + /// The XML Element name of this class + /// + public const string ElementName = "SubjectConfirmationData"; + + /// + /// Gets or sets the not before. + /// + /// The not before. + [XmlIgnore] + public DateTime? NotBefore { get; set; } + + /// + /// Gets or sets the not on or after. + /// + /// The not on or after. + [XmlIgnore] + public DateTime? NotOnOrAfter { get; set; } + + #region Attributes + + /// + /// Gets or sets the address. + /// + /// The address. + [XmlAttribute("Address", DataType = "string")] + public string Address { get; set; } + + /// + /// Gets or sets XML any attribute. + /// + /// The XML Any attribute. + [XmlAnyAttribute] + public XmlAttribute[] AnyAttr { get; set; } + + /// + /// Gets or sets the in response to. + /// + /// The in response to. + [XmlAttribute("InResponseTo", DataType = "NCName")] + public string InResponseTo { get; set; } + + /// + /// Gets or sets the not on or after string. + /// + /// The not on or after string. + [XmlAttribute("NotBefore")] + public string NotBeforeString + { + get { return NotBefore.HasValue ? Saml20Utils.ToUtcString(NotBefore.Value) : null; } + set { NotBefore = string.IsNullOrEmpty(value) ? (DateTime?)null : Saml20Utils.FromUtcString(value); } + } + + /// + /// Gets or sets the not on or after string. + /// + /// The not on or after string. + [XmlAttribute("NotOnOrAfter")] + public string NotOnOrAfterString + { + get { return NotOnOrAfter.HasValue ? Saml20Utils.ToUtcString(NotOnOrAfter.Value) : null; } + set { NotOnOrAfter = string.IsNullOrEmpty(value) ? (DateTime?)null : Saml20Utils.FromUtcString(value); } + } + + /// + /// Gets or sets the recipient. + /// + /// The recipient. + [XmlAttribute("Recipient", DataType = "anyURI")] + public string Recipient { get; set; } + + #endregion + + #region Elements + + /// + /// Gets or sets the any-elements-array. + /// + /// The any-elements-array + [XmlAnyElement(Order = 1)] + public XmlElement[] AnyElements { get; set; } + + #endregion + } +} diff --git a/src/SAML2.Standard/Schema/Core/SubjectLocality.cs b/src/SAML2.Standard/Schema/Core/SubjectLocality.cs new file mode 100644 index 0000000..a8e3377 --- /dev/null +++ b/src/SAML2.Standard/Schema/Core/SubjectLocality.cs @@ -0,0 +1,43 @@ +using System; +using System.Xml.Serialization; + +namespace SAML2.Schema.Core +{ + /// + /// The <SubjectLocality> element specifies the DNS domain name and IP address for the system from + /// which the assertion subject was authenticated. It has the following attributes: + /// + [Serializable] + [XmlType(Namespace = Saml20Constants.Assertion)] + [XmlRoot(ElementName, Namespace = Saml20Constants.Assertion, IsNullable = false)] + public class SubjectLocality + { + /// + /// The XML Element name of this class + /// + public const string ElementName = "SubjectLocality"; + + #region Attributes + + /// + /// Gets or sets the address. + /// The network address of the system from which the principal identified by the subject was + /// authenticated. IPv4 addresses SHOULD be represented in dotted-decimal format (e.g., "1.2.3.4"). + /// IPv6 addresses SHOULD be represented as defined by Section 2.2 of IETF RFC 3513 [RFC 3513] + /// (e.g., "FEDC:BA98:7654:3210:FEDC:BA98:7654:3210"). + /// + /// The address. + [XmlAttribute("Address")] + public string Address { get; set; } + + /// + /// Gets or sets the name of the DNS. + /// The DNS name of the system from which the principal identified by the subject was authenticated. + /// + /// The name of the DNS. + [XmlAttribute("DNSName")] + public string DNSName { get; set; } + + #endregion + } +} diff --git a/src/SAML2.Standard/Schema/Metadata/AdditionalMetadataLocation.cs b/src/SAML2.Standard/Schema/Metadata/AdditionalMetadataLocation.cs new file mode 100644 index 0000000..8772911 --- /dev/null +++ b/src/SAML2.Standard/Schema/Metadata/AdditionalMetadataLocation.cs @@ -0,0 +1,41 @@ +using System; +using System.Xml.Serialization; + +namespace SAML2.Schema.Metadata +{ + /// + /// The <AdditionalMetadataLocation> element is a namespace-qualified URI that specifies where + /// additional XML-based metadata may exist for a SAML entity. Its AdditionalMetadataLocationType + /// complex type extends the anyURI type with a namespace attribute (also of type anyURI). This required + /// attribute MUST contain the XML namespace of the root element of the instance document found at the + /// specified location. + /// + [Serializable] + [XmlType(Namespace = Saml20Constants.Metadata)] + [XmlRoot(ElementName, Namespace = Saml20Constants.Metadata, IsNullable = false)] + public class AdditionalMetadataLocation + { + /// + /// The XML Element name of this class + /// + public const string ElementName = "AdditionalMetadataLocation"; + + /// + /// Gets or sets the value. + /// + /// The value. + [XmlText(DataType = "anyURI")] + public string Value { get; set; } + + #region Attributes + + /// + /// Gets or sets the @namespace. + /// + /// The @namespace. + [XmlAttribute("namespace", DataType = "anyURI")] + public string Namespace { get; set; } + + #endregion + } +} diff --git a/src/SAML2.Standard/Schema/Metadata/AffiliationDescriptor.cs b/src/SAML2.Standard/Schema/Metadata/AffiliationDescriptor.cs new file mode 100644 index 0000000..10182e9 --- /dev/null +++ b/src/SAML2.Standard/Schema/Metadata/AffiliationDescriptor.cs @@ -0,0 +1,125 @@ +using System; +using System.Xml; +using System.Xml.Serialization; +using SAML2.Schema.XmlDSig; +using SAML2.Utils; + +namespace SAML2.Schema.Metadata +{ + /// + /// The <AffiliationDescriptor> element is an alternative to the sequence of role descriptors + /// described in Section 2.4 that is used when an <EntityDescriptor> describes an affiliation of SAML + /// entities (typically service providers) rather than a single entity. The <AffiliationDescriptor> + /// element provides a summary of the individual entities that make up the affiliation along with general + /// information about the affiliation itself. + /// + [Serializable] + [XmlType(Namespace = Saml20Constants.Metadata)] + [XmlRoot(ElementName, Namespace = Saml20Constants.Metadata, IsNullable = false)] + public class AffiliationDescriptor + { + /// + /// The XML Element name of this class + /// + public const string ElementName = "AffiliationDescriptor"; + + /// + /// Gets or sets the valid until. + /// Optional attribute indicates the expiration time of the metadata contained in the element and any + /// contained elements. + /// + /// The valid until. + [XmlIgnore] + public DateTime? ValidUntil { get; set; } + + #region Attributes + + /// + /// Gets or sets the affiliation owner ID. + /// Specifies the unique identifier of the entity responsible for the affiliation. The owner is NOT + /// presumed to be a member of the affiliation; if it is a member, its identifier MUST also appear in an + /// <AffiliateMember> element. + /// + /// The affiliation owner ID. + [XmlAttribute("affiliationOwnerID", DataType = "anyURI")] + public string AffiliationOwnerId { get; set; } + + /// + /// Gets or sets XML any attribute. + /// + /// The XML Any attribute. + [XmlAnyAttribute] + public XmlAttribute[] AnyAttr { get; set; } + + /// + /// Gets or sets the cache duration. + /// Optional attribute indicates the maximum length of time a consumer should cache the metadata + /// contained in the element and any contained elements. + /// + /// The cache duration. + [XmlAttribute("cacheDuration", DataType = "duration")] + public string CacheDuration { get; set; } + + /// + /// Gets or sets the ID. + /// A document-unique identifier for the element, typically used as a reference point when signing. + /// + /// The ID. + [XmlAttribute("ID", DataType = "ID")] + public string ID { get; set; } + + /// + /// Gets or sets the valid until string. + /// + /// The valid until string. + [XmlAttribute("validUntil")] + public string ValidUntilString + { + get { return ValidUntil.HasValue ? Saml20Utils.ToUtcString(ValidUntil.Value) : null; } + set { ValidUntil = string.IsNullOrEmpty(value) ? (DateTime?)null : Saml20Utils.FromUtcString(value); } + } + + #endregion + + #region Elements + + /// + /// Gets or sets the affiliate member. + /// One or more elements enumerating the members of the affiliation by specifying each member's + /// unique identifier. + /// + /// The affiliate member. + [XmlElement("AffiliateMember", DataType = "anyURI", Order = 3)] + public string[] AffiliateMember { get; set; } + + /// + /// Gets or sets the extensions. + /// This contains optional metadata extensions that are agreed upon between a metadata publisher + /// and consumer. Extension elements MUST be namespace-qualified by a non-SAML-defined + /// namespace. + /// + /// The extensions. + [XmlElement("Extensions", Order = 2)] + public ExtensionType Extensions { get; set; } + + /// + /// Gets or sets the key descriptor. + /// Optional sequence of elements that provides information about the cryptographic keys that the + /// affiliation uses as a whole, as distinct from keys used by individual members of the affiliation, + /// which are published in the metadata for those entities. + /// + /// The key descriptor. + [XmlElement("KeyDescriptor", Order = 4)] + public KeyDescriptor[] KeyDescriptor { get; set; } + + /// + /// Gets or sets the signature. + /// An XML signature that authenticates the containing element and its contents + /// + /// The signature. + [XmlElement("Signature", Namespace = Saml20Constants.Xmldsig, Order = 1)] + public Signature Signature { get; set; } + + #endregion + } +} diff --git a/src/SAML2.Standard/Schema/Metadata/AttributeAuthorityDescriptor.cs b/src/SAML2.Standard/Schema/Metadata/AttributeAuthorityDescriptor.cs new file mode 100644 index 0000000..a27d320 --- /dev/null +++ b/src/SAML2.Standard/Schema/Metadata/AttributeAuthorityDescriptor.cs @@ -0,0 +1,73 @@ +using System; +using System.Xml.Serialization; + +namespace SAML2.Schema.Metadata +{ + /// + /// The <AttributeAuthorityDescriptor> element extends RoleDescriptorType with content + /// reflecting profiles specific to attribute authorities, SAML authorities that respond to + /// <samlp:AttributeQuery> messages. + /// + [Serializable] + [XmlType(Namespace = Saml20Constants.Metadata)] + [XmlRoot(ElementName, Namespace = Saml20Constants.Metadata, IsNullable = false)] + public class AttributeAuthorityDescriptor : RoleDescriptor + { + /// + /// The XML Element name of this class + /// + public new const string ElementName = "AttributeAuthorityDescriptor"; + + #region Elements + + /// + /// Gets or sets the attribute. + /// Zero or more elements that identify the SAML attributes supported by the authority. Specific + /// values MAY optionally be included, indicating that only certain values permitted by the attribute's + /// definition are supported. + /// + /// The attribute. + [XmlElement("Attribute", Namespace = Saml20Constants.Metadata, Order = 5)] + public Attribute[] Attribute { get; set; } + + /// + /// Gets or sets the attribute profile. + /// Zero or more elements of type anyURI that enumerate the attribute profiles supported by this + /// authority. + /// + /// The attribute profile. + [XmlElement("AttributeProfile", DataType = "anyURI", Order = 4)] + public string[] AttributeProfile { get; set; } + + /// + /// Gets or sets the attribute service. + /// One or more elements of type EndpointType that describe endpoints that support the profile of + /// the Attribute Query protocol defined in [SAMLProf]. All attribute authorities support at least one + /// such endpoint, by definition. + /// + /// The attribute service. + [XmlElement("AttributeService", Order = 1)] + public Endpoint[] AttributeService { get; set; } + + /// + /// Gets or sets the assertion ID request service. + /// Zero or more elements of type EndpointType that describe endpoints that support the profile of + /// the Assertion Request protocol defined in [SAMLProf] or the special URI binding for assertion + /// requests defined in [SAMLBind]. + /// + /// The assertion ID request service. + [XmlElement("AssertionIDRequestService", Order = 2)] + public Endpoint[] AssertionIdRequestService { get; set; } + + /// + /// Gets or sets the name ID format. + /// Zero or more elements of type anyURI that enumerate the name identifier formats supported by + /// this authority. + /// + /// The name ID format. + [XmlElement("NameIDFormat", DataType = "anyURI", Order = 3)] + public string[] NameIdFormat { get; set; } + + #endregion + } +} diff --git a/src/SAML2.Standard/Schema/Metadata/AttributeConsumingService.cs b/src/SAML2.Standard/Schema/Metadata/AttributeConsumingService.cs new file mode 100644 index 0000000..96515d9 --- /dev/null +++ b/src/SAML2.Standard/Schema/Metadata/AttributeConsumingService.cs @@ -0,0 +1,88 @@ +using System; +using System.Xml; +using System.Xml.Serialization; + +namespace SAML2.Schema.Metadata +{ + /// + /// The <AttributeConsumingService> element defines a particular service offered by the service + /// provider in terms of the attributes the service requires or desires. + /// + [Serializable] + [XmlType(Namespace = Saml20Constants.Metadata)] + [XmlRoot(ElementName, Namespace = Saml20Constants.Metadata, IsNullable = false)] + public class AttributeConsumingService + { + /// + /// The XML Element name of this class + /// + public const string ElementName = "AttributeConsumingService"; + + /// + /// Gets or sets a value indicating whether this instance is default. + /// Identifies the default service supported by the service provider. Useful if the specific service is not + /// otherwise indicated by application context. If omitted, the value is assumed to be false. + /// + /// + /// true if this instance is default; otherwise, false. + /// + [XmlIgnore] + public bool? IsDefault { get; set; } + + #region Attributes + + /// + /// Gets or sets the index. + /// A required attribute that assigns a unique integer value to the element so that it can be referenced + /// in a protocol message. + /// + /// The index. + [XmlAttribute("index")] + public int Index { get; set; } + + /// + /// Gets or sets a value indicating whether this instance is default. + /// Identifies the default service supported by the service provider. Useful if the specific service is not + /// otherwise indicated by application context. If omitted, the value is assumed to be false. + /// + /// + /// true if this instance is default; otherwise, false. + /// + [XmlAttribute("isDefault")] + public string IsDefaultString + { + get { return IsDefault == null ? null : XmlConvert.ToString(IsDefault.Value); } + set { IsDefault = string.IsNullOrEmpty(value) ? (bool?)null : XmlConvert.ToBoolean(value); } + } + + #endregion + + #region Elements + + /// + /// Gets or sets the requested attribute. + /// One or more elements specifying attributes required or desired by this service. + /// + /// The requested attribute. + [XmlElement("RequestedAttribute", Order = 3)] + public RequestedAttribute[] RequestedAttribute { get; set; } + + /// + /// Gets or sets the name of the service. + /// One or more language-qualified names for the service. + /// + /// The name of the service. + [XmlElement("ServiceName", Order = 1)] + public LocalizedName[] ServiceName { get; set; } + + /// + /// Gets or sets the service description. + /// Zero or more language-qualified strings that describe the service. + /// + /// The service description. + [XmlElement("ServiceDescription", Order = 2)] + public LocalizedName[] ServiceDescription { get; set; } + + #endregion + } +} diff --git a/src/SAML2.Standard/Schema/Metadata/AuthnAuthorityDescriptor.cs b/src/SAML2.Standard/Schema/Metadata/AuthnAuthorityDescriptor.cs new file mode 100644 index 0000000..60b0ec6 --- /dev/null +++ b/src/SAML2.Standard/Schema/Metadata/AuthnAuthorityDescriptor.cs @@ -0,0 +1,54 @@ +using System; +using System.Xml.Serialization; + +namespace SAML2.Schema.Metadata +{ + /// + /// The <AuthnAuthorityDescriptor> element extends RoleDescriptorType with content reflecting + /// profiles specific to authentication authorities, SAML authorities that respond to <samlp:AuthnQuery> + /// messages. + /// + [Serializable] + [XmlType(Namespace = Saml20Constants.Metadata)] + [XmlRoot(ElementName, Namespace = Saml20Constants.Metadata, IsNullable = false)] + public class AuthnAuthorityDescriptor : RoleDescriptor + { + /// + /// The XML Element name of this class + /// + public new const string ElementName = "AuthnAuthorityDescriptor"; + + #region Elements + + /// + /// Gets or sets the assertion ID request service. + /// Zero or more elements of type EndpointType that describe endpoints that support the profile of + /// the Assertion Request protocol defined in [SAMLProf] or the special URI binding for assertion + /// requests defined in [SAMLBind]. + /// + /// The assertion ID request service. + [XmlElement("AssertionIDRequestService", Order = 2)] + public Endpoint[] AssertionIdRequestService { get; set; } + + /// + /// Gets or sets the authentication query service. + /// One or more elements of type EndpointType that describe endpoints that support the profile of + /// the Authentication Query protocol defined in [SAMLProf]. All authentication authorities support at + /// least one such endpoint, by definition. + /// + /// The authentication query service. + [XmlElement("AuthnQueryService", Order = 1)] + public Endpoint[] AuthnQueryService { get; set; } + + /// + /// Gets or sets the name ID format. + /// Zero or more elements of type anyURI that enumerate the name identifier formats supported by + /// this authority. + /// + /// The name ID format. + [XmlElement("NameIDFormat", DataType = "anyURI", Order = 3)] + public string[] NameIdFormat { get; set; } + + #endregion + } +} diff --git a/src/SAML2.Standard/Schema/Metadata/Contact.cs b/src/SAML2.Standard/Schema/Metadata/Contact.cs new file mode 100644 index 0000000..12224ab --- /dev/null +++ b/src/SAML2.Standard/Schema/Metadata/Contact.cs @@ -0,0 +1,109 @@ +using System; +using System.Xml; +using System.Xml.Serialization; + +namespace SAML2.Schema.Metadata +{ + /// + /// The <ContactPerson> element specifies basic contact information about a person responsible in some + /// capacity for a SAML entity or role. The use of this element is always optional. Its content is informative in + /// nature and does not directly map to any core SAML elements or attributes. + /// + [Serializable] + [XmlRoot(ElementName, IsNullable = false)] + public class Contact + { + /// + /// The XML Element name of this class + /// + public const string ElementName = "ContactPerson"; + + /// + /// Initializes a new instance of the class. + /// + public Contact() { } + + /// + /// Initializes a new instance of the class. + /// + /// Type of the contact. + public Contact(ContactType contactType) + { + ContactType = contactType; + } + + #region Attributes + + /// + /// Gets or sets XML any attribute. + /// + /// The XML Any attribute. + [XmlAnyAttribute] + public XmlAttribute[] AnyAttr { get; set; } + + /// + /// Gets or sets the type of the contact. + /// Specifies the type of contact using the ContactTypeType enumeration. The possible values are + /// technical, support, administrative, billing, and other. + /// + /// The type of the contact. + [XmlAttribute("contactType")] + public ContactType ContactType { get; set; } + + #endregion + + #region Elements + + /// + /// Gets or sets the company. + /// Optional string element that specifies the name of the company for the contact person. + /// + /// The company. + [XmlElement("Company", Order = 1)] + public string Company { get; set; } + + /// + /// Gets or sets the email address. + /// Zero or more elements containing mailto: URIs representing e-mail addresses belonging to the + /// contact person. + /// + /// The email address. + [XmlElement("EmailAddress", DataType = "anyURI", Order = 4)] + public string[] EmailAddress { get; set; } + + /// + /// Gets or sets the extensions. + /// This contains optional metadata extensions that are agreed upon between a metadata publisher + /// and consumer. Extension elements MUST be namespace-qualified by a non-SAML-defined + /// namespace. + /// + /// The extensions. + [XmlElement("Extensions", Order = 6)] + public ExtensionType Extensions { get; set; } + + /// + /// Gets or sets the name of the given. + /// Optional string element that specifies the given (first) name of the contact person. + /// + /// The name of the given. + [XmlElement("GivenName", Order = 2)] + public string GivenName { get; set; } + + /// + /// Gets or sets optional string element that specifies the surname of the contact person. + /// + /// The name of the sur. + [XmlElement("SurName", Order = 3)] + public string SurName { get; set; } + + /// + /// Gets or sets the telephone number. + /// Zero or more string elements specifying a telephone number of the contact person. + /// + /// The telephone number. + [XmlElement("TelephoneNumber", Order = 5)] + public string[] TelephoneNumber { get; set; } + + #endregion + } +} diff --git a/src/SAML2.Standard/Schema/Metadata/ContactType.cs b/src/SAML2.Standard/Schema/Metadata/ContactType.cs new file mode 100644 index 0000000..fc063a0 --- /dev/null +++ b/src/SAML2.Standard/Schema/Metadata/ContactType.cs @@ -0,0 +1,43 @@ +using System; +using System.Xml.Serialization; + +namespace SAML2.Schema.Metadata +{ + /// + /// Contact type enumeration. + /// + [Serializable] + [XmlType(Namespace = Saml20Constants.Metadata)] + public enum ContactType + { + /// + /// technical contact type. + /// + [XmlEnum("technical")] + Technical, + + /// + /// support contact type. + /// + [XmlEnum("support")] + Support, + + /// + /// administrative contact type. + /// + [XmlEnum("administrative")] + Administrative, + + /// + /// billing contact type. + /// + [XmlEnum("billing")] + Billing, + + /// + /// other contact type. + /// + [XmlEnum("other")] + Other + } +} \ No newline at end of file diff --git a/src/SAML2.Standard/Schema/Metadata/Endpoint.cs b/src/SAML2.Standard/Schema/Metadata/Endpoint.cs new file mode 100644 index 0000000..027bbd5 --- /dev/null +++ b/src/SAML2.Standard/Schema/Metadata/Endpoint.cs @@ -0,0 +1,71 @@ +using System; +using System.Xml; +using System.Xml.Serialization; + +namespace SAML2.Schema.Metadata +{ + /// + /// The complex type EndpointType describes a SAML protocol binding endpoint at which a SAML entity can + /// be sent protocol messages. Various protocol or profile-specific metadata elements are bound to this type. + /// + [XmlInclude(typeof(IndexedEndpoint))] + [Serializable] + [XmlType(Namespace = Saml20Constants.Metadata)] + [XmlRoot(ElementName, Namespace = Saml20Constants.Metadata, IsNullable = false)] + public class Endpoint + { + /// + /// The XML Element name of this class + /// + public const string ElementName = "SingleLogoutService"; + + #region Attributes + + /// + /// Gets or sets XML any attribute. + /// + /// The XML Any attribute. + [XmlAnyAttribute] + public XmlAttribute[] AnyAttr { get; set; } + + /// + /// Gets or sets the binding. + /// A required attribute that specifies the SAML binding supported by the endpoint. Each binding is + /// assigned a URI to identify it. + /// + /// The binding. + [XmlAttribute("Binding", DataType = "anyURI")] + public string Binding { get; set; } + + /// + /// Gets or sets the location. + /// A required URI attribute that specifies the location of the endpoint. The allowable syntax of this + /// URI depends on the protocol binding. + /// + /// The location. + [XmlAttribute("Location", DataType = "anyURI")] + public string Location { get; set; } + + /// + /// Gets or sets the response location. + /// Optionally specifies a different location to which response messages sent as part of the protocol + /// or profile should be sent. The allowable syntax of this URI depends on the protocol binding. + /// + /// The response location. + [XmlAttribute("ResponseLocation", DataType = "anyURI")] + public string ResponseLocation { get; set; } + + #endregion + + #region Elements + + /// + /// Gets or sets any elements. + /// + /// Any elements. + [XmlAnyElement(Order = 1)] + public XmlElement[] Any { get; set; } + + #endregion + } +} diff --git a/src/SAML2.Standard/Schema/Metadata/EntitiesDescriptor.cs b/src/SAML2.Standard/Schema/Metadata/EntitiesDescriptor.cs new file mode 100644 index 0000000..7d81e79 --- /dev/null +++ b/src/SAML2.Standard/Schema/Metadata/EntitiesDescriptor.cs @@ -0,0 +1,108 @@ +using System; +using System.Xml.Serialization; +using SAML2.Schema.XmlDSig; + +namespace SAML2.Schema.Metadata +{ + /// + /// The <EntitiesDescriptor> element contains the metadata for an optionally named group of SAML + /// entities. + /// + [Serializable] + [XmlType(Namespace = Saml20Constants.Metadata)] + [XmlRoot(ElementName, Namespace = Saml20Constants.Metadata, IsNullable = false)] + public class EntitiesDescriptor + { + /// + /// The XML Element name of this class + /// + public const string ElementName = "EntitiesDescriptor"; + + /// + /// Gets or sets the valid until. + /// Optional attribute indicates the expiration time of the metadata contained in the element and any + /// contained elements. + /// + /// The valid until. + [XmlIgnore] + public DateTime ValidUntil { get; set; } + + /// + /// Gets or sets a value indicating whether [valid until specified]. + /// + /// true if [valid until specified]; otherwise, false. + [XmlIgnore] + public bool ValidUntilSpecified { get; set; } + + #region Attributes + + /// + /// Gets or sets the cache duration. + /// Optional attribute indicates the maximum length of time a consumer should cache the metadata + /// contained in the element and any contained elements. + /// + /// The cache duration. + [XmlAttribute("cacheDuration", DataType = "duration")] + public string CacheDuration { get; set; } + + /// + /// Gets or sets the ID. + /// A document-unique identifier for the element, typically used as a reference point when signing + /// + /// The ID. + [XmlAttribute("ID", DataType = "ID")] + public string ID { get; set; } + + /// + /// Gets or sets the name. + /// A string name that identifies a group of SAML entities in the context of some deployment. + /// + /// The name. + [XmlAttribute("Name")] + public string Name { get; set; } + + /// + /// Gets or sets the valid until string. + /// + /// The valid until string. + [XmlAttribute("validUntil")] + public string ValidUntilString + { + get { return ValidUntil.ToUniversalTime().ToString("o"); } + set { ValidUntil = DateTime.Parse(value); } + } + + #endregion + + #region Elements + + /// + /// Gets or sets the extensions. + /// This contains optional metadata extensions that are agreed upon between a metadata publisher + /// and consumer. Extension elements MUST be namespace-qualified by a non-SAML-defined + /// namespace. + /// + /// The extensions. + [XmlElement("Extensions", Order = 2)] + public ExtensionType Extensions { get; set; } + + /// + /// Gets or sets the items. + /// Contains the metadata for one or more SAML entities, or a nested group of additional metadata + /// + /// The items. + [XmlElement(ElementName, typeof(EntitiesDescriptor), Order = 3)] + [XmlElement(EntityDescriptor.ElementName, typeof(EntityDescriptor), Order = 3)] + public object[] Items { get; set; } + + /// + /// Gets or sets the signature. + /// An XML signature that authenticates the containing element and its contents + /// + /// The signature. + [XmlElement("Signature", Namespace = Saml20Constants.Xmldsig, Order = 1)] + public Signature Signature { get; set; } + + #endregion + } +} diff --git a/src/SAML2.Standard/Schema/Metadata/EntityDescriptor.cs b/src/SAML2.Standard/Schema/Metadata/EntityDescriptor.cs new file mode 100644 index 0000000..e03bb5b --- /dev/null +++ b/src/SAML2.Standard/Schema/Metadata/EntityDescriptor.cs @@ -0,0 +1,154 @@ +using System; +using System.Xml; +using System.Xml.Serialization; +using SAML2.Schema.XmlDSig; + +namespace SAML2.Schema.Metadata +{ + /// + /// The <EntityDescriptor> element specifies metadata for a single SAML entity. A single entity may act + /// in many different roles in the support of multiple profiles. This specification directly supports the following + /// concrete roles as well as the abstract <RoleDescriptor> element for extensibility (see subsequent + /// sections for more details): + /// * SSO Identity Provider + /// * SSO Service Provider + /// * Authentication Authority + /// * Attribute Authority + /// * Policy Decision Point + /// * Affiliation + /// + [Serializable] + [XmlType(Namespace = Saml20Constants.Metadata)] + [XmlRoot(ElementName, Namespace = Saml20Constants.Metadata, IsNullable = false)] + public class EntityDescriptor + { + /// + /// The XML Element name of this class + /// + public const string ElementName = "EntityDescriptor"; + + /// + /// Gets or sets the valid until. + /// Optional attribute indicates the expiration time of the metadata contained in the element and any + /// contained elements. + /// + /// The valid until. + [XmlIgnore] + public DateTime? ValidUntil { get; set; } + + #region Attributes + + /// + /// Gets or sets XML any attribute. + /// + /// The XML Any attribute. + [XmlAnyAttribute] + public XmlAttribute[] AnyAttr { get; set; } + + /// + /// Gets or sets the cache duration. + /// Optional attribute indicates the maximum length of time a consumer should cache the metadata + /// contained in the element and any contained elements. + /// + /// The cache duration. + [XmlAttribute("cacheDuration", DataType = "duration")] + public string CacheDuration { get; set; } + + /// + /// Gets or sets the entity ID. + /// Specifies the unique identifier of the SAML entity whose metadata is described by the element's + /// contents. + /// + /// The entity ID. + [XmlAttribute("entityID", DataType = "anyURI")] + public string EntityID { get; set; } + + /// + /// Gets or sets the ID. + /// A document-unique identifier for the element, typically used as a reference point when signing + /// + /// The ID. + [XmlAttribute("ID", DataType = "ID")] + public string ID { get; set; } + + /// + /// Gets or sets the valid until string. + /// Optional attribute indicates the expiration time of the metadata contained in the element and any + /// contained elements. + /// + /// The valid until string. + [XmlAttribute("validUntil")] + public string ValidUntilString + { + get { return ValidUntil == null ? null : ValidUntil.Value.ToUniversalTime().ToString("o"); } + set { ValidUntil = value == null ? (DateTime?)null : DateTime.Parse(value); } + } + + #endregion + + #region Elements + + /// + /// Gets or sets the additional metadata location. + /// Optional sequence of namespace-qualified locations where additional metadata exists for the + /// SAML entity. This may include metadata in alternate formats or describing adherence to other + /// non-SAML specifications. + /// + /// The additional metadata location. + [XmlElement("AdditionalMetadataLocation", Order = 6)] + public AdditionalMetadataLocation[] AdditionalMetadataLocation { get; set; } + + /// + /// Gets or sets the contact person. + /// Optional sequence of elements identifying various kinds of contact personnel. + /// + /// The contact person. + [XmlElement("ContactPerson", Order = 5)] + public Contact[] ContactPerson { get; set; } + + /// + /// Gets or sets the extensions. + /// This contains optional metadata extensions that are agreed upon between a metadata publisher + /// and consumer. Extension elements MUST be namespace-qualified by a non-SAML-defined + /// namespace. + /// + /// The extensions. + [XmlElement("Extensions", Order = 2)] + public ExtensionType Extensions { get; set; } + + /// + /// Gets or sets the items. + /// <RoleDescriptor>, <IdpSsoDescriptor>, <SpSsoDescriptor>, + /// <AuthnAuthorityDescriptor>, <AttributeAuthorityDescriptor>, <PDPDescriptor> + /// <AffiliationDescriptor> + /// + /// The items. + [XmlElement(AffiliationDescriptor.ElementName, typeof(AffiliationDescriptor), Order = 3)] + [XmlElement(AttributeAuthorityDescriptor.ElementName, typeof(AttributeAuthorityDescriptor), Order = 3)] + [XmlElement(AuthnAuthorityDescriptor.ElementName, typeof(AuthnAuthorityDescriptor), Order = 3)] + [XmlElement(IdpSsoDescriptor.ElementName, typeof(IdpSsoDescriptor), Order = 3)] + [XmlElement(PdpDescriptor.ElementName, typeof(PdpDescriptor), Order = 3)] + [XmlElement(RoleDescriptor.ElementName, typeof(RoleDescriptor), Order = 3)] + [XmlElement(SpSsoDescriptor.ElementName, typeof(SpSsoDescriptor), Order = 3)] + public object[] Items { get; set; } + + /// + /// Gets or sets the organization. + /// Optional element identifying the organization responsible for the SAML entity described by the + /// element. + /// + /// The organization. + [XmlElement("Organization", Order = 4)] + public Organization Organization { get; set; } + + /// + /// Gets or sets the signature. + /// An XML signature that authenticates the containing element and its contents + /// + /// The signature. + [XmlElement("Signature", Namespace = Saml20Constants.Xmldsig, Order = 1)] + public Signature Signature { get; set; } + + #endregion + } +} diff --git a/src/SAML2.Standard/Schema/Metadata/Extensions.cs b/src/SAML2.Standard/Schema/Metadata/Extensions.cs new file mode 100644 index 0000000..f78c78c --- /dev/null +++ b/src/SAML2.Standard/Schema/Metadata/Extensions.cs @@ -0,0 +1,31 @@ +using System; +using System.Xml; +using System.Xml.Serialization; + +namespace SAML2.Schema.Metadata +{ + /// + /// Extension type + /// + [Serializable] + [XmlType(TypeName = "ExtensionsType", Namespace = Saml20Constants.Metadata)] + [XmlRoot(ElementName, Namespace = Saml20Constants.Metadata, IsNullable = false)] + public class ExtensionType + { + /// + /// The XML Element name of this class + /// + public const string ElementName = "Extensions"; + + #region Elements + + /// + /// Gets or sets any elements. + /// + /// Any elements. + [XmlAnyElement(Order = 1)] + public XmlElement[] Any { get; set; } + + #endregion + } +} \ No newline at end of file diff --git a/src/SAML2.Standard/Schema/Metadata/IdpSsoDescriptor.cs b/src/SAML2.Standard/Schema/Metadata/IdpSsoDescriptor.cs new file mode 100644 index 0000000..3b77d71 --- /dev/null +++ b/src/SAML2.Standard/Schema/Metadata/IdpSsoDescriptor.cs @@ -0,0 +1,90 @@ +using System; +using System.Xml.Serialization; +using SAML2.Schema.Core; + +namespace SAML2.Schema.Metadata +{ + /// + /// The <IdpSsoDescriptor> element extends SSODescriptorType with content reflecting profiles + /// specific to identity providers supporting SSO. + /// + [Serializable] + [XmlType(Namespace = Saml20Constants.Metadata)] + [XmlRoot(ElementName, Namespace = Saml20Constants.Metadata, IsNullable = false)] + public class IdpSsoDescriptor : SsoDescriptor + { + /// + /// The XML Element name of this class + /// + public new const string ElementName = "IDPSSODescriptor"; + + #region Attributes + + /// + /// Gets or sets a value indicating whether want authentication requests signed. + /// Optional attribute that indicates a requirement for the <samlp:AuthnRequest> messages + /// received by this identity provider to be signed. If omitted, the value is assumed to be false. + /// + /// + /// true if want authentication requests signed; otherwise, false. + /// + [XmlAttribute] + public bool WantAuthnRequestsSigned { get; set; } + + #endregion + + #region Elements + + /// + /// Gets or sets the assertion ID request service. + /// Zero or more elements of type EndpointType that describe endpoints that support the profile of + /// the Assertion Request protocol defined in [SAMLProf] or the special URI binding for assertion + /// requests defined in [SAMLBind]. + /// + /// The assertion ID request service. + [XmlElement("AssertionIDRequestService", Order = 3)] + public Endpoint[] AssertionIdRequestService { get; set; } + + /// + /// Gets or sets the attribute. + /// Zero or more elements that identify the SAML attributes supported by the identity provider. + /// Specific values MAY optionally be included, indicating that only certain values permitted by the + /// attribute's definition are supported. In this context, "support" for an attribute means that the identity + /// provider has the capability to include it when delivering assertions during single sign-on. + /// + /// The attribute. + [XmlElement("Attribute", Namespace = Saml20Constants.Assertion, Order = 5)] + public SamlAttribute[] Attributes { get; set; } + + /// + /// Gets or sets the attribute profile. + /// Zero or more elements of type anyURI that enumerate the attribute profiles supported by this + /// identity provider. See [SAMLProf] for some possible values for this element. + /// + /// The attribute profile. + [XmlElement("AttributeProfile", DataType = "anyURI", Order = 4)] + public string[] AttributeProfile { get; set; } + + /// + /// Gets or sets the name ID mapping service. + /// Zero or more elements of type EndpointType that describe endpoints that support the Name + /// Identifier Mapping profile defined in [SAMLProf]. The ResponseLocation attribute MUST be + /// omitted. + /// + /// The name ID mapping service. + [XmlElement("NameIDMappingService", Order = 2)] + public Endpoint[] NameIdMappingService { get; set; } + + /// + /// Gets or sets the single sign on service. + /// One or more elements of type EndpointType that describe endpoints that support the profiles of + /// the Authentication Request protocol defined in [SAMLProf]. All identity providers support at least + /// one such endpoint, by definition. The ResponseLocation attribute MUST be omitted. + /// + /// The single sign on service. + [XmlElement("SingleSignOnService", Order = 1)] + public Endpoint[] SingleSignOnService { get; set; } + + #endregion + } +} diff --git a/src/SAML2.Standard/Schema/Metadata/IndexedEndpoint.cs b/src/SAML2.Standard/Schema/Metadata/IndexedEndpoint.cs new file mode 100644 index 0000000..36196a0 --- /dev/null +++ b/src/SAML2.Standard/Schema/Metadata/IndexedEndpoint.cs @@ -0,0 +1,58 @@ +using System; +using System.Xml; +using System.Xml.Serialization; + +namespace SAML2.Schema.Metadata +{ + /// + /// The complex type IndexedEndpointType extends EndpointType with a pair of attributes to permit the + /// indexing of otherwise identical endpoints so that they can be referenced by protocol messages. + /// + [Serializable] + [XmlType(Namespace = Saml20Constants.Metadata)] + [XmlRoot(ElementName, Namespace = Saml20Constants.Metadata, IsNullable = false)] + public class IndexedEndpoint : Endpoint + { + /// + /// The XML Element name of this class + /// + public new const string ElementName = "ArtifactResolutionService"; + + /// + /// Gets or sets a value indicating whether this instance is default. + /// An optional boolean attribute used to designate the default endpoint among an indexed set. If + /// omitted, the value is assumed to be false. + /// + /// + /// true if this instance is default; otherwise, false. + /// + [XmlIgnore] + public bool? IsDefault { get; set; } + + #region Attributes + + /// + /// Gets or sets the index. + /// A required attribute that assigns a unique integer value to the endpoint so that it can be + /// referenced in a protocol message. The index value need only be unique within a collection of like + /// elements contained within the same parent element (i.e., they need not be unique across the + /// entire instance). + /// + /// The index. + [XmlAttribute("index")] + public int Index { get; set; } + + /// + /// Gets or sets the isDefault string. + /// + /// The isDefault string. + [XmlAttribute("isDefault")] + public string IsDefaultString + { + get { return IsDefault == null ? null : XmlConvert.ToString(IsDefault.Value); } + set { IsDefault = value == null ? (bool?)null : XmlConvert.ToBoolean(value); } + } + + #endregion + } +} diff --git a/src/SAML2.Standard/Schema/Metadata/KeyDescriptor.cs b/src/SAML2.Standard/Schema/Metadata/KeyDescriptor.cs new file mode 100644 index 0000000..61c01f6 --- /dev/null +++ b/src/SAML2.Standard/Schema/Metadata/KeyDescriptor.cs @@ -0,0 +1,62 @@ +using System; +using System.Xml.Serialization; +using SAML2.Schema.XEnc; +using SAML2.Schema.XmlDSig; + +namespace SAML2.Schema.Metadata +{ + /// + /// The <KeyDescriptor> element provides information about the cryptographic key(s) that an entity uses + /// to sign data or receive encrypted keys, along with additional cryptographic details. + /// + [Serializable] + [XmlType(Namespace = Saml20Constants.Metadata)] + [XmlRoot(ElementName, Namespace = Saml20Constants.Metadata, IsNullable = false)] + public class KeyDescriptor + { + /// + /// The XML Element name of this class + /// + public const string ElementName = "KeyDescriptor"; + + /// + /// Gets or sets a value indicating whether [use specified]. + /// + /// true if [use specified]; otherwise, false. + [XmlIgnore] + public bool UseSpecified { get; set; } + + #region Attributes + + /// + /// Gets or sets the use. + /// Optional attribute specifying the purpose of the key being described. Values are drawn from the + /// KeyTypes enumeration, and consist of the values encryption and signing. + /// + /// The use. + [XmlAttribute("use")] + public KeyTypes Use { get; set; } + + #endregion + + #region Elements + + /// + /// Gets or sets the encryption method. + /// Optional element specifying an algorithm and algorithm-specific settings supported by the entity. + /// The exact content varies based on the algorithm supported. See [XMLEnc] for the definition of this + /// element's xenc:EncryptionMethodType complex type. + /// + /// The encryption method. + [XmlElement("EncryptionMethod", Order = 2)] + public EncryptionMethod[] EncryptionMethod { get; set; } + + /// + /// Gets or sets the XML Signature element KeyInfo. Can be implicitly converted to the .NET class System.Security.Cryptography.Xml.KeyInfo. + /// + [XmlElement("KeyInfo", Namespace = Saml20Constants.Xmldsig, Order = 1)] + public KeyInfo KeyInfo { get; set; } + + #endregion + } +} diff --git a/src/SAML2.Standard/Schema/Metadata/KeyTypes.cs b/src/SAML2.Standard/Schema/Metadata/KeyTypes.cs new file mode 100644 index 0000000..98cfaf5 --- /dev/null +++ b/src/SAML2.Standard/Schema/Metadata/KeyTypes.cs @@ -0,0 +1,25 @@ +using System; +using System.Xml.Serialization; + +namespace SAML2.Schema.Metadata +{ + /// + /// Key types enumeration. + /// + [Serializable] + [XmlType(Namespace = Saml20Constants.Metadata)] + public enum KeyTypes + { + /// + /// Encryption key type. + /// + [XmlEnum("encryption")] + Encryption, + + /// + /// Signing key type. + /// + [XmlEnum("signing")] + Signing + } +} \ No newline at end of file diff --git a/src/SAML2.Standard/Schema/Metadata/LocalizedName.cs b/src/SAML2.Standard/Schema/Metadata/LocalizedName.cs new file mode 100644 index 0000000..8d14ebb --- /dev/null +++ b/src/SAML2.Standard/Schema/Metadata/LocalizedName.cs @@ -0,0 +1,55 @@ +using System; +using System.Xml.Schema; +using System.Xml.Serialization; + +namespace SAML2.Schema.Metadata +{ + /// + /// The localizedNameType complex type extends a string-valued element with a standard XML language + /// attribute. + /// + [Serializable] + [XmlType(Namespace = Saml20Constants.Metadata)] + [XmlRoot(ElementName, Namespace = Saml20Constants.Metadata, IsNullable = false)] + public class LocalizedName + { + /// + /// The XML Element name of this class + /// + public const string ElementName = "OrganizationName"; + + /// + /// Initializes a new instance of the class. + /// + public LocalizedName() { } + + /// + /// Initializes a new instance of the class. + /// + /// The value. + /// The language. + public LocalizedName(string value, string language) + { + Value = value; + Language = language; + } + + /// + /// Gets or sets the value. + /// + /// The value. + [XmlText] + public string Value { get; set; } + + #region Attributes + + /// + /// Gets or sets the lang. + /// + /// The lang. + [XmlAttribute("lang", Form = XmlSchemaForm.Qualified, Namespace = "http://www.w3.org/XML/1998/namespace")] + public string Language { get; set; } + + #endregion + } +} diff --git a/src/SAML2.Standard/Schema/Metadata/LocalizedUri.cs b/src/SAML2.Standard/Schema/Metadata/LocalizedUri.cs new file mode 100644 index 0000000..5dcb4c3 --- /dev/null +++ b/src/SAML2.Standard/Schema/Metadata/LocalizedUri.cs @@ -0,0 +1,55 @@ +using System; +using System.Xml.Schema; +using System.Xml.Serialization; + +namespace SAML2.Schema.Metadata +{ + /// + /// The localizedURIType complex type extends a URI-valued element with a standard XML language + /// attribute. + /// + [Serializable] + [XmlType(Namespace = Saml20Constants.Metadata)] + [XmlRoot(ElementName, Namespace = Saml20Constants.Metadata, IsNullable = false)] + public class LocalizedURI + { + /// + /// The XML Element name of this class + /// + public const string ElementName = "OrganizationURL"; + + /// + /// Initializes a new instance of the class. + /// + public LocalizedURI() { } + + /// + /// Initializes a new instance of the class. + /// + /// The value. + /// The language. + public LocalizedURI(string value, string language) + { + Value = value; + Language = language; + } + + /// + /// Gets or sets the value. + /// + /// The value. + [XmlText(DataType = "anyURI")] + public string Value { get; set; } + + #region Attributes + + /// + /// Gets or sets the lang. + /// + /// The lang. + [XmlAttribute("lang", Form = XmlSchemaForm.Qualified, Namespace = "http://www.w3.org/XML/1998/namespace")] + public string Language { get; set; } + + #endregion + } +} diff --git a/src/SAML2.Standard/Schema/Metadata/Organization.cs b/src/SAML2.Standard/Schema/Metadata/Organization.cs new file mode 100644 index 0000000..f5ab3ab --- /dev/null +++ b/src/SAML2.Standard/Schema/Metadata/Organization.cs @@ -0,0 +1,72 @@ +using System; +using System.Xml; +using System.Xml.Serialization; + +namespace SAML2.Schema.Metadata +{ + /// + /// The <Organization> element specifies basic information about an organization responsible for a SAML + /// entity or role. The use of this element is always optional. Its content is informative in nature and does not + /// directly map to any core SAML elements or attributes. + /// + [Serializable] + [XmlRoot(ElementName, IsNullable = false)] + public class Organization + { + /// + /// The XML Element name of this class + /// + public const string ElementName = "Organization"; + + #region Attributes + + /// + /// Gets or sets XML any attribute. + /// + /// The XML Any attribute. + [XmlAnyAttribute] + public XmlAttribute[] AnyAttr { get; set; } + + #endregion + + #region Elements + + /// + /// Gets or sets the extensions. + /// This contains optional metadata extensions that are agreed upon between a metadata publisher + /// and consumer. Extensions MUST NOT include global (non-namespace-qualified) elements or + /// elements qualified by a SAML-defined namespace within this element. + /// + /// The extensions. + [XmlElement("Extensions", Order = 1)] + public ExtensionType Extensions { get; set; } + + /// + /// Gets or sets the display name of the organization. + /// One or more language-qualified names that are suitable for human consumption. + /// + /// The display name of the organization. + [XmlElement("OrganizationDisplayName", Order = 3)] + public LocalizedName[] OrganizationDisplayName { get; set; } + + /// + /// Gets or sets the name of the organization. + /// One or more language-qualified names that may or may not be suitable for human consumption + /// + /// The name of the organization. + [XmlElement("OrganizationName", Order = 2)] + public LocalizedName[] OrganizationName { get; set; } + + /// + /// Gets or sets the organization URL. + /// One or more language-qualified URIs that specify a location to which to direct a user for additional + /// information. Note that the language qualifier refers to the content of the material at the specified + /// location. + /// + /// The organization URL. + [XmlElement("OrganizationURL", Order = 4)] + public LocalizedURI[] OrganizationURL { get; set; } + + #endregion + } +} diff --git a/src/SAML2.Standard/Schema/Metadata/PdpDescriptor.cs b/src/SAML2.Standard/Schema/Metadata/PdpDescriptor.cs new file mode 100644 index 0000000..45df5f4 --- /dev/null +++ b/src/SAML2.Standard/Schema/Metadata/PdpDescriptor.cs @@ -0,0 +1,53 @@ +using System; +using System.Xml.Serialization; + +namespace SAML2.Schema.Metadata +{ + /// + /// The <PDPDescriptor> element extends RoleDescriptorType with content reflecting profiles specific to + /// policy decision points, SAML authorities that respond to <samlp:AuthzDecisionQuery> messages. + /// + [Serializable] + [XmlType(Namespace = Saml20Constants.Metadata)] + [XmlRoot(ElementName, Namespace = Saml20Constants.Metadata, IsNullable = false)] + public class PdpDescriptor : RoleDescriptor + { + /// + /// The XML Element name of this class + /// + public new const string ElementName = "Organization"; + + #region Elements + + /// + /// Gets or sets the authorize service. + /// One or more elements of type EndpointType that describe endpoints that support the profile of + /// the Authorization Decision Query protocol defined in [SAMLProf]. All policy decision points support + /// at least one such endpoint, by definition. + /// + /// The authorize service. + [XmlElement("AuthzService", Order = 1)] + public Endpoint[] AuthzService { get; set; } + + /// + /// Gets or sets the assertion ID request service. + /// Zero or more elements of type EndpointType that describe endpoints that support the profile of + /// the Assertion Request protocol defined in [SAMLProf] or the special URI binding for assertion + /// requests defined in [SAMLBind]. + /// + /// The assertion ID request service. + [XmlElement("AssertionIDRequestService", Order = 2)] + public Endpoint[] AssertionIdRequestService { get; set; } + + /// + /// Gets or sets the name ID format. + /// Zero or more elements of type anyURI that enumerate the name identifier formats supported by + /// this authority. + /// + /// The name ID format. + [XmlElement("NameIDFormat", DataType = "anyURI", Order = 3)] + public string[] NameIdFormat { get; set; } + + #endregion + } +} diff --git a/src/SAML2.Standard/Schema/Metadata/RequestedAttribute.cs b/src/SAML2.Standard/Schema/Metadata/RequestedAttribute.cs new file mode 100644 index 0000000..cd3e20f --- /dev/null +++ b/src/SAML2.Standard/Schema/Metadata/RequestedAttribute.cs @@ -0,0 +1,48 @@ +using System; +using System.Xml; +using System.Xml.Serialization; +using SAML2.Schema.Core; + +namespace SAML2.Schema.Metadata +{ + /// + /// The <RequestedAttribute> element specifies a service provider's interest in a specific SAML + /// attribute, optionally including specific values. + /// + [Serializable] + [XmlType(Namespace = Saml20Constants.Metadata)] + [XmlRoot(ElementName, Namespace = Saml20Constants.Metadata, IsNullable = false)] + public class RequestedAttribute : SamlAttribute + { + /// + /// The XML Element name of this class + /// + public new const string ElementName = "RequestedAttribute"; + + /// + /// Gets or sets a value indicating whether this instance is required. + /// Optional XML attribute indicates if the service requires the corresponding SAML attribute in order + /// to function at all (as opposed to merely finding an attribute useful or desirable). + /// + /// + /// true if this instance is required; otherwise, false. + /// + [XmlIgnore] + public bool? IsRequired { get; set; } + + #region Attributes + + /// + /// Gets or sets the is required string. + /// + /// The is required string. + [XmlAttribute("isRequired")] + public string IsRequiredString + { + get { return IsRequired.HasValue ? XmlConvert.ToString(IsRequired.Value) : null; } + set { IsRequired = string.IsNullOrEmpty(value) ? (bool?)null : XmlConvert.ToBoolean(value); } + } + + #endregion + } +} diff --git a/src/SAML2.Standard/Schema/Metadata/RoleDescriptor.cs b/src/SAML2.Standard/Schema/Metadata/RoleDescriptor.cs new file mode 100644 index 0000000..fc00371 --- /dev/null +++ b/src/SAML2.Standard/Schema/Metadata/RoleDescriptor.cs @@ -0,0 +1,175 @@ +using System; +using System.Xml; +using System.Xml.Serialization; +using SAML2.Schema.XmlDSig; +using SAML2.Utils; + +namespace SAML2.Schema.Metadata +{ + /// + /// The <RoleDescriptor> element is an abstract extension point that contains common descriptive + /// information intended to provide processing commonality across different roles. New roles can be defined + /// by extending its abstract RoleDescriptorType complex type + /// + [XmlInclude(typeof(AttributeAuthorityDescriptor))] + [XmlInclude(typeof(PdpDescriptor))] + [XmlInclude(typeof(AuthnAuthorityDescriptor))] + [XmlInclude(typeof(SsoDescriptor))] + [XmlInclude(typeof(SpSsoDescriptor))] + [XmlInclude(typeof(IdpSsoDescriptor))] + [Serializable] + [XmlType(Namespace = Saml20Constants.Metadata)] + [XmlRoot(ElementName, Namespace = Saml20Constants.Metadata, IsNullable = false)] + public abstract class RoleDescriptor + { + /// + /// The XML Element name of this class + /// + public const string ElementName = "RoleDescriptor"; + + /// + /// The protocol support enumeration backing field + /// + private string _protocolSupportEnumeration; + + /// + /// Gets or sets the protocol support enumeration. + /// A whitespace-delimited set of URIs that identify the set of protocol specifications supported by the + /// role element. For SAML V2.0 entities, this set MUST include the SAML protocol namespace URI, + /// urn:oasis:names:tc:SAML:2.0:protocol. Note that future SAML specifications might + /// share the same namespace URI, but SHOULD provide alternate "protocol support" identifiers to + /// ensure discrimination when necessary. + /// + /// The protocol support enumeration. + [XmlIgnore] + public string[] ProtocolSupportEnumeration + { + get + { + return _protocolSupportEnumeration.Split(' '); + } + + set + { + _protocolSupportEnumeration = string.Join(" ", value); + } + } + + /// + /// Gets or sets the valid until. + /// Optional attribute indicates the expiration time of the metadata contained in the element and any + /// contained elements. + /// + /// The valid until. + [XmlIgnore] + public DateTime? ValidUntil { get; set; } + + #region Attributes + + /// + /// Gets or sets XML any attribute. + /// + /// The XML Any attribute. + [XmlAnyAttribute] + public XmlAttribute[] AnyAttr { get; set; } + + /// + /// Gets or sets the cache duration. + /// Optional attribute indicates the maximum length of time a consumer should cache the metadata + /// contained in the element and any contained elements. + /// + /// The cache duration. + [XmlAttribute("cacheDuration", DataType = "duration")] + public string CacheDuration { get; set; } + + /// + /// Gets or sets the error URL. + /// Optional URI attribute that specifies a location to direct a user for problem resolution and + /// additional support related to this role. + /// + /// The error URL. + [XmlAttribute("errorUrl", DataType = "anyURI")] + public string ErrorUrl { get; set; } + + /// + /// Gets or sets the ID. + /// A document-unique identifier for the element, typically used as a reference point when signing. + /// + /// The ID. + [XmlAttribute("ID", DataType = "ID")] + public string Id { get; set; } + + /// + /// Gets or sets the protocol support enumeration string. + /// + /// The protocol support enumeration string. + [XmlAttribute("protocolSupportEnumeration", DataType = "anyURI")] + public string ProtocolSupportEnumerationString + { + get { return _protocolSupportEnumeration; } + set { _protocolSupportEnumeration = value; } + } + + /// + /// Gets or sets the valid until string. + /// + /// The valid until string. + [XmlAttribute("validUntil")] + public string ValidUntilString + { + get { return ValidUntil.HasValue ? Saml20Utils.ToUtcString(ValidUntil.Value) : null; } + set { ValidUntil = string.IsNullOrEmpty(value) ? (DateTime?)null : Saml20Utils.FromUtcString(value); } + } + + #endregion + + #region Elements + + /// + /// Gets or sets the contact person. + /// Optional sequence of elements specifying contacts associated with this role. Identical to the + /// element used within the <EntityDescriptor> element. + /// + /// The contact person. + [XmlElement("ContactPerson", Order = 5)] + public Contact[] ContactPerson { get; set; } + + /// + /// Gets or sets the extensions. + /// This contains optional metadata extensions that are agreed upon between a metadata publisher + /// and consumer. Extension elements MUST be namespace-qualified by a non-SAML-defined + /// namespace. + /// + /// The extensions. + [XmlElement("Extensions", Order = 2)] + public ExtensionType Extensions { get; set; } + + /// + /// Gets or sets the key descriptor. + /// Optional sequence of elements that provides information about the cryptographic keys that the + /// entity uses when acting in this role. + /// + /// The key descriptor. + [XmlElement("KeyDescriptor", Order = 3)] + public KeyDescriptor[] KeyDescriptor { get; set; } + + /// + /// Gets or sets the organization. + /// Optional element specifies the organization associated with this role. Identical to the element used + /// within the <EntityDescriptor> element. + /// + /// The organization. + [XmlElement("Organization", Order = 4)] + public Organization Organization { get; set; } + + /// + /// Gets or sets the signature. + /// An XML signature that authenticates the containing element and its contents + /// + /// The signature. + [XmlElement("Signature", Namespace = Saml20Constants.Xmldsig, Order = 1)] + public Signature Signature { get; set; } + + #endregion + } +} diff --git a/src/SAML2.Standard/Schema/Metadata/SpSsoDescriptor.cs b/src/SAML2.Standard/Schema/Metadata/SpSsoDescriptor.cs new file mode 100644 index 0000000..4597f29 --- /dev/null +++ b/src/SAML2.Standard/Schema/Metadata/SpSsoDescriptor.cs @@ -0,0 +1,88 @@ +using System; +using System.Xml; +using System.Xml.Serialization; + +namespace SAML2.Schema.Metadata +{ + /// + /// The SpSsoDescriptor element extends SSODescriptorType with content reflecting profiles specific + /// to service providers. + /// + [Serializable] + [XmlType(Namespace = Saml20Constants.Metadata)] + [XmlRoot(ElementName, Namespace = Saml20Constants.Metadata, IsNullable = false)] + public class SpSsoDescriptor : SsoDescriptor + { + /// + /// The XML Element name of this class + /// + public new const string ElementName = "SPSSODescriptor"; + + /// + /// AuthnRequestsSignedc> backing field. + /// + private bool? _authnRequestsSignedField; + + /// + /// WantAssertionsSigned backing field. + /// + private bool? _wantAssertionsSignedField; + + #region Attributes + + /// + /// Gets or sets a value indicating whether the authentication requests is signed. + /// Optional attribute that indicates whether the samlp:AuthnRequest messages sent by this + /// service provider will be signed. If omitted, the value is assumed to be false. + /// + /// true if authentication requests signed; otherwise, false. + [XmlAttribute] + public string AuthnRequestsSigned + { + get { return _authnRequestsSignedField == null ? null : XmlConvert.ToString(_authnRequestsSignedField.Value); } + set { _authnRequestsSignedField = string.IsNullOrEmpty(value) ? (bool?)null : XmlConvert.ToBoolean(value); } + } + + /// + /// Gets or sets a value indicating whether assertions should be signed. + /// Optional attribute that indicates a requirement for the saml:Assertion elements received by + /// this service provider to be signed. If omitted, the value is assumed to be false. This requirement + /// is in addition to any requirement for signing derived from the use of a particular profile/binding + /// combination. + /// + /// + /// true if want assertions signed; otherwise, false. + /// + [XmlAttribute] + public string WantAssertionsSigned + { + get { return _wantAssertionsSignedField == null ? null : XmlConvert.ToString(_wantAssertionsSignedField.Value); } + set { _wantAssertionsSignedField = string.IsNullOrEmpty(value) ? (bool?)null : XmlConvert.ToBoolean(value); } + } + + #endregion + + #region Elements + + /// + /// Gets or sets the assertion consumer service. + /// One or more elements that describe indexed endpoints that support the profiles of the + /// Authentication Request protocol defined in [SAMLProf]. All service providers support at least one + /// such endpoint, by definition. + /// + /// The assertion consumer service. + [XmlElement("AssertionConsumerService", Order = 1)] + public IndexedEndpoint[] AssertionConsumerService { get; set; } + + /// + /// Gets or sets the attribute consuming service. + /// Zero or more elements that describe an application or service provided by the service provider + /// that requires or desires the use of SAML attributes. + /// + /// The attribute consuming service. + [XmlElement("AttributeConsumingService", Order = 2)] + public AttributeConsumingService[] AttributeConsumingService { get; set; } + + #endregion + } +} diff --git a/src/SAML2.Standard/Schema/Metadata/SsoDescriptor.cs b/src/SAML2.Standard/Schema/Metadata/SsoDescriptor.cs new file mode 100644 index 0000000..13d3a03 --- /dev/null +++ b/src/SAML2.Standard/Schema/Metadata/SsoDescriptor.cs @@ -0,0 +1,62 @@ +using System; +using System.Diagnostics; +using System.Xml.Serialization; + +namespace SAML2.Schema.Metadata +{ + /// + /// The SSODescriptorType abstract type is a common base type for the concrete types + /// SPSSODescriptorType and IDPSSODescriptorType, described in subsequent sections. It extends + /// RoleDescriptorType with elements reflecting profiles common to both identity providers and service + /// providers that support SSO + /// + [XmlInclude(typeof(SpSsoDescriptor))] + [XmlIncludeAttribute(typeof(IdpSsoDescriptor))] + [Serializable] + [DebuggerStepThrough] + [XmlTypeAttribute(Namespace = Saml20Constants.Metadata)] + public abstract class SsoDescriptor : RoleDescriptor + { + #region Elements + + /// + /// Gets or sets the artifact resolution service. + /// Zero or more elements of type IndexedEndpointType that describe indexed endpoints that + /// support the Artifact Resolution profile defined in [SAMLProf]. The ResponseLocation attribute + /// MUST be omitted. + /// + /// The artifact resolution service. + [XmlElement("ArtifactResolutionService", Order = 1)] + public IndexedEndpoint[] ArtifactResolutionService { get; set; } + + /// + /// Gets or sets the manage name ID service. + /// Zero or more elements of type EndpointType that describe endpoints that support the Name + /// Identifier Management profiles defined in [SAMLProf]. + /// + /// The manage name ID service. + [XmlElement("ManageNameIDService", Order = 3)] + public Endpoint[] ManageNameIdService { get; set; } + + /// + /// Gets or sets the name ID format. + /// Zero or more elements of type anyURI that enumerate the name identifier formats supported by + /// this system entity acting in this role. See Section 8.3 of [SAMLCore] for some possible values for + /// this element. + /// + /// The name ID format. + [XmlElement("NameIDFormat", DataType = "anyURI", Order = 4)] + public string[] NameIdFormat { get; set; } + + /// + /// Gets or sets the single logout service. + /// Zero or more elements of type EndpointType that describe endpoints that support the Single + /// Logout profiles defined in [SAMLProf]. + /// + /// The single logout service. + [XmlElement("SingleLogoutService", Order = 2)] + public Endpoint[] SingleLogoutService { get; set; } + + #endregion + } +} diff --git a/src/SAML2.Standard/Schema/Protocol/ArtifactResolve.cs b/src/SAML2.Standard/Schema/Protocol/ArtifactResolve.cs new file mode 100644 index 0000000..4682832 --- /dev/null +++ b/src/SAML2.Standard/Schema/Protocol/ArtifactResolve.cs @@ -0,0 +1,35 @@ +using System; +using System.Xml.Serialization; + +namespace SAML2.Schema.Protocol +{ + /// + /// The <ArtifactResolve> message is used to request that a SAML protocol message be returned in an + /// <ArtifactResponse> message by specifying an artifact that represents the SAML protocol message. + /// The original transmission of the artifact is governed by the specific protocol binding that is being used; see + /// [SAMLBind] for more information on the use of artifacts in bindings + /// + [Serializable] + [XmlType(Namespace = Saml20Constants.Protocol)] + [XmlRoot(ElementName, Namespace = Saml20Constants.Protocol, IsNullable = false)] + public class ArtifactResolve : RequestAbstract + { + /// + /// The XML Element name of this class + /// + public const string ElementName = "ArtifactResolve"; + + #region Elements + + /// + /// Gets or sets the artifact. + /// The artifact value that the requester received and now wishes to translate into the protocol message it + /// represents. See [SAMLBind] for specific artifact format information. + /// + /// The artifact. + [XmlElement("Artifact", Order = 1)] + public string Artifact { get; set; } + + #endregion + } +} diff --git a/src/SAML2.Standard/Schema/Protocol/ArtifactResponse.cs b/src/SAML2.Standard/Schema/Protocol/ArtifactResponse.cs new file mode 100644 index 0000000..4dd38cd --- /dev/null +++ b/src/SAML2.Standard/Schema/Protocol/ArtifactResponse.cs @@ -0,0 +1,34 @@ +using System; +using System.Xml; +using System.Xml.Serialization; + +namespace SAML2.Schema.Protocol +{ + /// + /// The recipient of an <ArtifactResolve> message MUST respond with an <ArtifactResponse> + /// message element. This element is of complex type ArtifactResponseType, which extends + /// StatusResponseType with a single optional wildcard element corresponding to the SAML protocol + /// message being returned. This wrapped message element can be a request or a response. + /// + [Serializable] + [XmlType(Namespace = Saml20Constants.Protocol)] + [XmlRoot(ElementName, Namespace = Saml20Constants.Protocol, IsNullable = false)] + public class ArtifactResponse : StatusResponse + { + /// + /// The XML Element name of this class + /// + public new const string ElementName = "ArtifactResponse"; + + #region Elements + + /// + /// Gets or sets the any XML element. + /// + /// The Any XML element. + [XmlAnyElement(Order = 1)] + public XmlElement Any { get; set; } + + #endregion + } +} diff --git a/src/SAML2.Standard/Schema/Protocol/AssertionIdRequest.cs b/src/SAML2.Standard/Schema/Protocol/AssertionIdRequest.cs new file mode 100644 index 0000000..88166eb --- /dev/null +++ b/src/SAML2.Standard/Schema/Protocol/AssertionIdRequest.cs @@ -0,0 +1,33 @@ +using System; +using System.Xml.Serialization; + +namespace SAML2.Schema.Protocol +{ + /// + /// If the requester knows the unique identifier of one or more assertions, the <AssertionIDRequest> + /// message element can be used to request that they be returned in a <Response> message. The + /// <saml:AssertionIDRef> element is used to specify each assertion to return. See Section 2.3.1 for + /// more information on this element. + /// + [Serializable] + [XmlType(Namespace = Saml20Constants.Protocol)] + [XmlRoot(ElementName, Namespace = Saml20Constants.Protocol, IsNullable = false)] + public class AssertionIdRequest : RequestAbstract + { + /// + /// The XML Element name of this class + /// + public const string ElementName = "AssertionIDRequest"; + + #region Elements + + /// + /// Gets or sets the assertion ID ref. + /// + /// The assertion ID ref. + [XmlElement("AssertionIDRef", Namespace = Saml20Constants.Assertion, DataType = "NCName", Order = 1)] + public string[] AssertionIdRef { get; set; } + + #endregion + } +} diff --git a/src/SAML2.Standard/Schema/Protocol/AttributeQuery.cs b/src/SAML2.Standard/Schema/Protocol/AttributeQuery.cs new file mode 100644 index 0000000..00822a7 --- /dev/null +++ b/src/SAML2.Standard/Schema/Protocol/AttributeQuery.cs @@ -0,0 +1,39 @@ +using System; +using System.Xml.Serialization; +using SAML2.Schema.Core; + +namespace SAML2.Schema.Protocol +{ + /// + /// The <AttributeQuery> element is used to make the query "Return the requested attributes for this + /// subject." A successful response will be in the form of assertions containing attribute statements, to the + /// extent allowed by policy. + /// + [Serializable] + [XmlType(Namespace = Saml20Constants.Protocol)] + [XmlRoot(ElementName, Namespace = Saml20Constants.Protocol, IsNullable = false)] + public class AttributeQuery : SubjectQueryAbstract + { + /// + /// The XML Element name of this class + /// + public new const string ElementName = "AttributeQuery"; + + #region Elements + + /// + /// Gets or sets the attribute. + /// Each <saml:Attribute> element specifies an attribute whose value(s) are to be returned. If no + /// attributes are specified, it indicates that all attributes allowed by policy are requested. If a given + /// <saml:Attribute> element contains one or more <saml:AttributeValue> elements, then if + /// that attribute is returned in the response, it MUST NOT contain any values that are not equal to the + /// values specified in the query. In the absence of equality rules specified by particular profiles or + /// attributes, equality is defined as an identical XML representation of the value + /// + /// The attribute. + [XmlElement("Attribute", Namespace = Saml20Constants.Assertion, Order = 1)] + public SamlAttribute[] SamlAttribute { get; set; } + + #endregion + } +} diff --git a/src/SAML2.Standard/Schema/Protocol/AuthnContextComparisonType.cs b/src/SAML2.Standard/Schema/Protocol/AuthnContextComparisonType.cs new file mode 100644 index 0000000..0b54c51 --- /dev/null +++ b/src/SAML2.Standard/Schema/Protocol/AuthnContextComparisonType.cs @@ -0,0 +1,37 @@ +using System; +using System.Xml.Serialization; + +namespace SAML2.Schema.Protocol +{ + /// + /// AuthContext comparison type enumeration. + /// + [Serializable] + [XmlType(Namespace = Saml20Constants.Protocol)] + public enum AuthnContextComparisonType + { + /// + /// Exact comparison type. + /// + [XmlEnum("exact")] + Exact, + + /// + /// Minimum comparison type. + /// + [XmlEnum("minimum")] + Minimum, + + /// + /// Maximum comparison type. + /// + [XmlEnum("maximum")] + Maximum, + + /// + /// Better comparison type. + /// + [XmlEnum("better")] + Better + } +} diff --git a/src/SAML2.Standard/Schema/Protocol/AuthnContextType.cs b/src/SAML2.Standard/Schema/Protocol/AuthnContextType.cs new file mode 100644 index 0000000..045b179 --- /dev/null +++ b/src/SAML2.Standard/Schema/Protocol/AuthnContextType.cs @@ -0,0 +1,25 @@ +using System; +using System.Xml.Serialization; + +namespace SAML2.Schema.Protocol +{ + /// + /// AuthContext type enumeration. + /// + [Serializable] + [XmlType(Namespace = Saml20Constants.Protocol, IncludeInSchema = false)] + public enum AuthnContextType + { + /// + /// AuthnContextClassRef type. + /// + [XmlEnum("urn:oasis:names:tc:SAML:2.0:assertion:AuthnContextClassRef")] + AuthnContextClassRef, + + /// + /// AuthnContextDeclRef type. + /// + [XmlEnum("urn:oasis:names:tc:SAML:2.0:assertion:AuthnContextDeclRef")] + AuthnContextDeclRef + } +} diff --git a/src/SAML2.Standard/Schema/Protocol/AuthnQuery.cs b/src/SAML2.Standard/Schema/Protocol/AuthnQuery.cs new file mode 100644 index 0000000..498c7fa --- /dev/null +++ b/src/SAML2.Standard/Schema/Protocol/AuthnQuery.cs @@ -0,0 +1,49 @@ +using System; +using System.Xml.Serialization; + +namespace SAML2.Schema.Protocol +{ + /// + /// The <AuthnQuery> message element is used to make the query "What assertions containing + /// authentication statements are available for this subject?" A successful <Response> will contain one or + /// more assertions containing authentication statements. + /// + [Serializable] + [XmlType(Namespace = Saml20Constants.Protocol)] + [XmlRoot(ElementName, Namespace = Saml20Constants.Protocol, IsNullable = false)] + public class AuthnQuery : SubjectQueryAbstract + { + /// + /// The XML Element name of this class + /// + public new const string ElementName = "AuthnQuery"; + + #region Attributes + + /// + /// Gets or sets the index of the session. + /// If present, specifies a filter for possible responses. Such a query asks the question "What assertions + /// containing authentication statements do you have for this subject within the context of the supplied + /// session information?" + /// + /// The index of the session. + [XmlAttribute] + public string SessionIndex { get; set; } + + #endregion + + #region Elements + + /// + /// Gets or sets the requested authentication context. + /// If present, specifies a filter for possible responses. Such a query asks the question "What assertions + /// containing authentication statements do you have for this subject that satisfy the authentication + /// context requirements in this element?" + /// + /// The requested authentication context. + [XmlElement("RequestedAuthnContext", Order = 1)] + public RequestedAuthnContext RequestedAuthnContext { get; set; } + + #endregion + } +} diff --git a/src/SAML2.Standard/Schema/Protocol/AuthnRequest.cs b/src/SAML2.Standard/Schema/Protocol/AuthnRequest.cs new file mode 100644 index 0000000..6f22cb6 --- /dev/null +++ b/src/SAML2.Standard/Schema/Protocol/AuthnRequest.cs @@ -0,0 +1,238 @@ +using System; +using System.Xml.Serialization; +using SAML2.Schema.Core; + +namespace SAML2.Schema.Protocol +{ + /// + /// To request that an identity provider issue an assertion with an authentication statement, a presenter + /// authenticates to that identity provider (or relies on an existing security context) and sends it an + /// <AuthnRequest> message that describes the properties that the resulting assertion needs to have to + /// satisfy its purpose. Among these properties may be information that relates to the content of the assertion + /// and/or information that relates to how the resulting <Response> message should be delivered to the + /// requester. The process of authentication of the presenter may take place before, during, or after the initial + /// delivery of the <AuthnRequest> message. + /// The requester might not be the same as the presenter of the request if, for example, the requester is a + /// relying party that intends to use the resulting assertion to authenticate or authorize the requested subject + /// so that the relying party can decide whether to provide a service. + /// The <AuthnRequest> message SHOULD be signed or otherwise authenticated and integrity protected + /// by the protocol binding used to deliver the message. + /// + [Serializable] + [XmlType(Namespace = Saml20Constants.Protocol)] + [XmlRoot(ElementName, Namespace = Saml20Constants.Protocol, IsNullable = false)] + public class AuthnRequest : RequestAbstract + { + /// + /// The XML Element name of this class + /// + public const string ElementName = "AuthnRequest"; + + /// + /// Gets or sets a value indicating whether [assertion consumer service index specified]. + /// + /// + /// true if [assertion consumer service index specified]; otherwise, false. + /// + [XmlIgnore] + public bool AssertionConsumerServiceIndexSpecified { get; set; } + + /// + /// Gets or sets a value indicating whether [attribute consuming service index specified]. + /// + /// + /// true if [attribute consuming service index specified]; otherwise, false. + /// + [XmlIgnore] + public bool AttributeConsumingServiceIndexSpecified { get; set; } + + /// + /// Gets or sets a value indicating whether force authentication. + /// A Boolean value. If "true", the identity provider MUST authenticate the presenter directly rather than + /// rely on a previous security context. If a value is not provided, the default is "false". However, if both + /// ForceAuthn and IsPassive are "true", the identity provider MUST NOT freshly authenticate the + /// presenter unless the constraints of IsPassive can be met. + /// + /// true if force authentication; otherwise, false. + [XmlIgnore] + public bool? ForceAuthn { get; set; } + + /// + /// Gets or sets a value indicating whether this instance is passive. + /// A Boolean value. If "true", the identity provider and the user agent itself MUST NOT visibly take control + /// of the user interface from the requester and interact with the presenter in a noticeable fashion. If a + /// value is not provided, the default is "false". + /// + /// + /// true if this instance is passive; otherwise, false. + /// + [XmlIgnore] + public bool? IsPassive { get; set; } + + #region Attributes + + /// + /// Gets or sets the index of the assertion consumer service. + /// Indirectly identifies the location to which the <Response> message should be returned to the + /// requester. It applies only to profiles in which the requester is different from the presenter, such as the + /// Web Browser SSO profile in [SAMLProf]. The identity provider MUST have a trusted means to map + /// the index value in the attribute to a location associated with the requester. [SAMLMeta] provides one + /// possible mechanism. If omitted, then the identity provider MUST return the <Response> message to + /// the default location associated with the requester for the profile of use. If the index specified is invalid, + /// then the identity provider MAY return an error <Response> or it MAY use the default location. This + /// attribute is mutually exclusive with the AssertionConsumerServiceURL and ProtocolBinding + /// attributes. + /// + /// The index of the assertion consumer service. + [XmlAttribute("AssertionConsumerServiceIndex")] + public ushort AssertionConsumerServiceIndex { get; set; } + + /// + /// Gets or sets the assertion consumer service URL. + /// Specifies by value the location to which the <Response> message MUST be returned to the + /// requester. The responder MUST ensure by some means that the value specified is in fact associated + /// with the requester. [SAMLMeta] provides one possible mechanism; signing the enclosing + /// <AuthnRequest> message is another. This attribute is mutually exclusive with the + /// AssertionConsumerServiceIndex attribute and is typically accompanied by the + /// ProtocolBinding attribute. + /// + /// The assertion consumer service URL. + [XmlAttribute("AssertionConsumerServiceURL", DataType = "anyURI")] + public string AssertionConsumerServiceUrl { get; set; } + + /// + /// Gets or sets the index of the attribute consuming service. + /// Indirectly identifies information associated with the requester describing the SAML attributes the + /// requester desires or requires to be supplied by the identity provider in the <Response> message. The + /// identity provider MUST have a trusted means to map the index value in the attribute to information + /// associated with the requester. [SAMLMeta] provides one possible mechanism. The identity provider + /// MAY use this information to populate one or more <saml:AttributeStatement> elements in the + /// assertion(s) it returns. + /// + /// The index of the attribute consuming service. + [XmlAttribute("AttributeConsumingServiceIndex")] + public ushort AttributeConsumingServiceIndex { get; set; } + + /// + /// Gets or sets a value indicating whether force authentication specified. + /// + /// true if force authentication specified; otherwise, false. + [XmlAttribute("ForceAuthn")] + public string ForceAuthnString + { + get { return ForceAuthn.HasValue ? ForceAuthn.Value.ToString().ToLower() : null; } + set + { + bool val; + if (!bool.TryParse(value, out val)) + { + return; + } + + ForceAuthn = val; + } + } + + /// + /// Gets or sets a value indicating whether this instance is passive specified. + /// + /// + /// true if this instance is passive specified; otherwise, false. + /// + [XmlAttribute("IsPassive")] + public string IsPassiveString + { + get { return IsPassive.HasValue ? IsPassive.Value.ToString().ToLower() : "false"; } + set + { + bool val; + if (!bool.TryParse(value, out val)) + { + return; + } + + IsPassive = val; + } + } + + /// + /// Gets or sets the protocol binding. + /// A URI reference that identifies a SAML protocol binding to be used when returning the <Response> + /// message. See [SAMLBind] for more information about protocol bindings and URI references defined + /// for them. This attribute is mutually exclusive with the AssertionConsumerServiceIndex attribute + /// and is typically accompanied by the AssertionConsumerServiceURL attribute. + /// + /// The protocol binding. + [XmlAttribute("ProtocolBinding", DataType = "anyURI")] + public string ProtocolBinding { get; set; } + + /// + /// Gets or sets the name of the provider. + /// Specifies the human-readable name of the requester for use by the presenter's user agent or the + /// identity provider. + /// + /// The name of the provider. + [XmlAttribute("ProviderName")] + public string ProviderName { get; set; } + + #endregion + + #region Elements + + /// + /// Gets or sets the conditions. + /// Specifies the SAML conditions the requester expects to limit the validity and/or use of the resulting + /// assertion(s). The responder MAY modify or supplement this set as it deems necessary. The + /// information in this element is used as input to the process of constructing the assertion, rather than as + /// conditions on the use of the request itself. + /// + /// The conditions. + [XmlElement("Conditions", Namespace = Saml20Constants.Assertion, Order = 3)] + public Conditions Conditions { get; set; } + + /// + /// Gets or sets the name ID policy. + /// Specifies constraints on the name identifier to be used to represent the requested subject. If omitted, + /// then any type of identifier supported by the identity provider for the requested subject can be used, + /// constrained by any relevant deployment-specific policies, with respect to privacy, for example + /// + /// The name ID policy. + /// + /// This is here for a reason. Some SAML implementations require that NameIDPolicy is the first element following issuer. + /// + [XmlElement("NameIDPolicy", Order = 2)] + public NameIdPolicy NameIdPolicy { get; set; } + + /// + /// Gets or sets the requested authentication context. + /// Specifies the requirements, if any, that the requester places on the authentication context that applies + /// to the responding provider's authentication of the presenter. See Section 3.3.2.2.1 for processing rules + /// regarding this element. + /// + /// The requested authentication context. + [XmlElement("RequestedAuthnContext", Order = 4)] + public RequestedAuthnContext RequestedAuthnContext { get; set; } + + /// + /// Gets or sets the scoping. + /// Specifies a set of identity providers trusted by the requester to authenticate the presenter, as well as + /// limitations and context related to proxy of the <AuthnRequest> message to subsequent identity + /// providers by the responder + /// + /// The scoping. + [XmlElement("Scoping", Order = 5)] + public Scoping Scoping { get; set; } + + /// + /// Gets or sets the subject. + /// Specifies the requested subject of the resulting assertion(s). This may include one or more + /// <saml:SubjectConfirmation> elements to indicate how and/or by whom the resulting assertions + /// can be confirmed. + /// + /// The subject. + [XmlElement("Subject", Namespace = Saml20Constants.Assertion, Order = 1)] + public Subject Subject { get; set; } + + #endregion + } +} diff --git a/src/SAML2.Standard/Schema/Protocol/AuthzDecisionQuery.cs b/src/SAML2.Standard/Schema/Protocol/AuthzDecisionQuery.cs new file mode 100644 index 0000000..68096cb --- /dev/null +++ b/src/SAML2.Standard/Schema/Protocol/AuthzDecisionQuery.cs @@ -0,0 +1,62 @@ +using System; +using System.Xml.Serialization; +using SAML2.Schema.Core; + +namespace SAML2.Schema.Protocol +{ + /// + /// + /// The <AuthzDecisionQuery> element is used to make the query "Should these actions on this resource + /// be allowed for this subject, given this evidence?" A successful response will be in the form of assertions + /// containing authorization decision statements. + /// + /// + /// Note: The <AuthzDecisionQuery> feature has been frozen as of SAML V2.0, with no + /// future enhancements planned. Users who require additional functionality may want to + /// consider the extensible Access Control Markup Language [XACML], which offers + /// enhanced authorization decision features. + /// + /// + [Serializable] + [XmlType(Namespace = Saml20Constants.Protocol)] + [XmlRoot(ElementName, Namespace = Saml20Constants.Protocol, IsNullable = false)] + public class AuthzDecisionQuery : SubjectQueryAbstract + { + /// + /// The XML Element name of this class + /// + public new const string ElementName = "AuthzDecisionQuery"; + + #region Attributes + + /// + /// Gets or sets the resource. + /// A URI reference indicating the resource for which authorization is requested. + /// + /// The resource. + [XmlAttribute("Resource", DataType = "anyURI")] + public string Resource { get; set; } + + #endregion + + #region Elements + + /// + /// Gets or sets the action. + /// The actions for which authorization is requested. + /// + /// The action. + [XmlElement("Action", Namespace = Saml20Constants.Assertion, Order = 1)] + public Core.Action[] Action { get; set; } + + /// + /// Gets or sets the evidence. + /// A set of assertions that the SAML authority MAY rely on in making its authorization decision + /// + /// The evidence. + [XmlElement("Evidence", Namespace = Saml20Constants.Assertion, Order = 2)] + public Evidence Evidence { get; set; } + + #endregion + } +} diff --git a/src/SAML2.Standard/Schema/Protocol/EncryptedAssertion.cs b/src/SAML2.Standard/Schema/Protocol/EncryptedAssertion.cs new file mode 100644 index 0000000..4aa18d6 --- /dev/null +++ b/src/SAML2.Standard/Schema/Protocol/EncryptedAssertion.cs @@ -0,0 +1,20 @@ +using System; +using System.Xml.Serialization; + +namespace SAML2.Schema.Protocol +{ + /// + /// The <EncryptedAssertion> element represents an assertion in encrypted fashion, as defined by the + /// XML Encryption Syntax and Processing specification [XMLEnc]. + /// + [Serializable] + [XmlType(Namespace = Saml20Constants.Assertion)] + [XmlRoot(ElementName, Namespace = Saml20Constants.Assertion, IsNullable = false)] + public class EncryptedAssertion : EncryptedElement + { + /// + /// The XML Element name of this class + /// + public new const string ElementName = "EncryptedAssertion"; + } +} diff --git a/src/SAML2.Standard/Schema/Protocol/EncryptedElement.cs b/src/SAML2.Standard/Schema/Protocol/EncryptedElement.cs new file mode 100644 index 0000000..eca4058 --- /dev/null +++ b/src/SAML2.Standard/Schema/Protocol/EncryptedElement.cs @@ -0,0 +1,42 @@ +using System; +using System.Xml.Serialization; +using SAML2.Schema.XEnc; + +namespace SAML2.Schema.Protocol +{ + /// + /// Represents an encrypted element + /// + /// + /// NOTE: XmlRoot parameter manually changed from "NewEncryptedID" to "EncryptedElementType". + /// + [Serializable] + [XmlType(Namespace = Saml20Constants.Assertion)] + [XmlRoot(ElementName, Namespace = Saml20Constants.Assertion, IsNullable = false)] + [XmlInclude(typeof(EncryptedAssertion))] + public class EncryptedElement + { + /// + /// The XML Element name of this class + /// + public const string ElementName = "EncryptedElement"; + + #region Elements + + /// + /// Gets or sets the encrypted data. + /// + /// The encrypted data. + [XmlElement("EncryptedData", Order = 1, Namespace = Saml20Constants.Xenc)] + public EncryptedData EncryptedData { get; set; } + + /// + /// Gets or sets the encrypted key. + /// + /// The encrypted key. + [XmlElement("EncryptedKey", Order = 2, Namespace = Saml20Constants.Xenc)] + public EncryptedKey[] EncryptedKey { get; set; } + + #endregion + } +} diff --git a/src/SAML2.Standard/Schema/Protocol/Extensions.cs b/src/SAML2.Standard/Schema/Protocol/Extensions.cs new file mode 100644 index 0000000..20e67f7 --- /dev/null +++ b/src/SAML2.Standard/Schema/Protocol/Extensions.cs @@ -0,0 +1,35 @@ +using System; +using System.Xml; +using System.Xml.Serialization; + +namespace SAML2.Schema.Protocol +{ + /// + /// This extension point contains optional protocol message extension elements that are agreed on + /// between the communicating parties. No extension schema is required in order to make use of this + /// extension point, and even if one is provided, the lax validation setting does not impose a requirement + /// for the extension to be valid. SAML extension elements MUST be namespace-qualified in a non- + /// SAML-defined namespace. + /// + [Serializable] + [XmlType(Namespace = Saml20Constants.Protocol)] + [XmlRoot(ElementName, Namespace = Saml20Constants.Protocol, IsNullable = false)] + public class Extensions + { + /// + /// The XML Element name of this class + /// + public const string ElementName = "Extensions"; + + #region Elements + + /// + /// Gets or sets the any XML element. + /// + /// The Any XML element. + [XmlAnyElement(Order = 1)] + public XmlElement[] Any { get; set; } + + #endregion + } +} diff --git a/src/SAML2.Standard/Schema/Protocol/IdpEntry.cs b/src/SAML2.Standard/Schema/Protocol/IdpEntry.cs new file mode 100644 index 0000000..cc124c6 --- /dev/null +++ b/src/SAML2.Standard/Schema/Protocol/IdpEntry.cs @@ -0,0 +1,49 @@ +using System; +using System.Xml.Serialization; + +namespace SAML2.Schema.Protocol +{ + /// + /// The <IDPEntry> element specifies a single identity provider trusted by the requester to authenticate the + /// presenter. + /// + [Serializable] + [XmlType(Namespace = Saml20Constants.Protocol)] + [XmlRoot(ElementName, Namespace = Saml20Constants.Protocol, IsNullable = false)] + public class IdpEntry + { + /// + /// The XML Element name of this class + /// + public const string ElementName = "IDPEntry"; + + #region Attributes + + /// + /// Gets or sets the provider ID. + /// The unique identifier of the identity provider. + /// + /// The provider ID. + [XmlAttribute("ProviderID", DataType = "anyURI")] + public string ProviderID { get; set; } + + /// + /// Gets or sets the name. + /// A human-readable name for the identity provider + /// + /// The name. + [XmlAttribute("Name")] + public string Name { get; set; } + + /// + /// Gets or sets the location. + /// A URI reference representing the location of a profile-specific endpoint supporting the authentication + /// request protocol. The binding to be used must be understood from the profile of use. + /// + /// The location. + [XmlAttribute("Loc", DataType = "anyURI")] + public string Location { get; set; } + + #endregion + } +} diff --git a/src/SAML2.Standard/Schema/Protocol/IdpList.cs b/src/SAML2.Standard/Schema/Protocol/IdpList.cs new file mode 100644 index 0000000..3b26d73 --- /dev/null +++ b/src/SAML2.Standard/Schema/Protocol/IdpList.cs @@ -0,0 +1,45 @@ +using System; +using System.CodeDom.Compiler; +using System.Xml.Serialization; + +namespace SAML2.Schema.Protocol +{ + /// + /// The <IDPList> element specifies the identity providers trusted by the requester to authenticate the + /// presenter. + /// + [GeneratedCode("xsd", "2.0.50727.42")] + [Serializable] + [XmlType(Namespace = Saml20Constants.Protocol)] + [XmlRoot(ElementName, Namespace = Saml20Constants.Protocol, IsNullable = false)] + public class IdpList + { + /// + /// The XML Element name of this class + /// + public const string ElementName = "IDPList"; + + #region Elements + + /// + /// Gets or sets the IDP entry. + /// Information about a single identity provider. + /// + /// The IDP entry. + [XmlElement("IDPEntry", Order = 1)] + public IdpEntry[] IDPEntry { get; set; } + + /// + /// Gets or sets the get complete. + /// If the <IDPList> is not complete, using this element specifies a URI reference that can be used to + /// retrieve the complete list. Retrieving the resource associated with the URI MUST result in an XML + /// instance whose root element is an <IDPList> that does not itself contain a <GetComplete> + /// element. + /// + /// The get complete. + [XmlElement("GetComplete", DataType = "anyURI", Order = 2)] + public string GetComplete { get; set; } + + #endregion + } +} diff --git a/src/SAML2.Standard/Schema/Protocol/LogoutRequest.cs b/src/SAML2.Standard/Schema/Protocol/LogoutRequest.cs new file mode 100644 index 0000000..4829261 --- /dev/null +++ b/src/SAML2.Standard/Schema/Protocol/LogoutRequest.cs @@ -0,0 +1,84 @@ +using System; +using System.Xml.Serialization; +using SAML2.Schema.Core; +using SAML2.Utils; + +namespace SAML2.Schema.Protocol +{ + /// + /// A session participant or session authority sends a <LogoutRequest> message to indicate that a session + /// has been terminated. + /// + [Serializable] + [XmlType(Namespace = Saml20Constants.Protocol)] + [XmlRoot(ElementName, Namespace = Saml20Constants.Protocol, IsNullable = false)] + public class LogoutRequest : RequestAbstract + { + /// + /// The XML Element name of this class + /// + public const string ElementName = "LogoutRequest"; + + /// + /// Specifies that the message is being sent because the principal wishes to terminate the indicated session. + /// + public const string ReasonUser = "urn:oasis:names:tc:SAML:2.0:logout:user"; + + /// + /// Specifies that the message is being sent because an administrator wishes to terminate the indicated session for + /// the principal. + /// + public const string ReasonAdmin = "urn:oasis:names:tc:SAML:2.0:logout:admin"; + + /// + /// Gets or sets NotOnOrAfter. + /// + /// The not on or after. + [XmlIgnore] + public DateTime? NotOnOrAfter { get; set; } + + #region Attributes + + /// + /// Gets or sets the issue instant string. + /// + /// The issue instant string. + [XmlAttribute("NotOnOrAfter")] + public string NotOnOrAfterString + { + get { return NotOnOrAfter.HasValue ? Saml20Utils.ToUtcString(NotOnOrAfter.Value) : null; } + set { NotOnOrAfter = string.IsNullOrEmpty(value) ? (DateTime?)null : Saml20Utils.FromUtcString(value); } + } + + /// + /// Gets or sets the reason. + /// + /// The reason. + [XmlAttribute("Reason")] + public string Reason { get; set; } + + #endregion + + #region Elements + + /// + /// Gets or sets the item. + /// The identifier and associated attributes (in plaintext or encrypted form) that specify the principal as + /// currently recognized by the identity and service providers prior to this request. + /// + /// The item. + [XmlElement("BaseID", typeof(BaseIdAbstract), Namespace = Saml20Constants.Assertion, Order = 1)] + [XmlElement("EncryptedID", typeof(EncryptedElement), Namespace = Saml20Constants.Assertion, Order = 1)] + [XmlElement("NameID", typeof(NameId), Namespace = Saml20Constants.Assertion, Order = 1)] + public object Item { get; set; } + + /// + /// Gets or sets the index of the session. + /// + /// The index of the session. + [XmlElement("SessionIndex", Order = 2)] + public string[] SessionIndex { get; set; } + + #endregion + } +} diff --git a/src/SAML2.Standard/Schema/Protocol/LogoutResponse.cs b/src/SAML2.Standard/Schema/Protocol/LogoutResponse.cs new file mode 100644 index 0000000..22a0d8c --- /dev/null +++ b/src/SAML2.Standard/Schema/Protocol/LogoutResponse.cs @@ -0,0 +1,19 @@ +using System; +using System.Xml.Serialization; + +namespace SAML2.Schema.Protocol +{ + /// + /// The SAML LogoutResponse + /// + [Serializable] + [XmlType(Namespace = Saml20Constants.Protocol)] + [XmlRoot(ElementName, Namespace = Saml20Constants.Protocol, IsNullable = false)] + public class LogoutResponse : StatusResponse + { + /// + /// The XML Element name of this class + /// + public new const string ElementName = "LogoutResponse"; + } +} diff --git a/src/SAML2.Standard/Schema/Protocol/ManageNameIdRequest.cs b/src/SAML2.Standard/Schema/Protocol/ManageNameIdRequest.cs new file mode 100644 index 0000000..186032a --- /dev/null +++ b/src/SAML2.Standard/Schema/Protocol/ManageNameIdRequest.cs @@ -0,0 +1,49 @@ +using System; +using System.Xml.Serialization; +using SAML2.Schema.Core; + +namespace SAML2.Schema.Protocol +{ + /// + /// Managed NameIDRequest. + /// + [Serializable] + [XmlType(Namespace = Saml20Constants.Protocol)] + [XmlRoot(ElementName, Namespace = Saml20Constants.Protocol, IsNullable = false)] + public class ManageNameIdRequest : RequestAbstract + { + /// + /// The XML Element name of this class + /// + public const string ElementName = "ManageNameIDRequest"; + + #region Elements + + /// + /// Gets or sets the item. + /// The name identifier and associated descriptive data (in plaintext or encrypted form) that specify the + /// principal as currently recognized by the identity and service providers prior to this request. + /// + /// The item. + [XmlElement("EncryptedID", typeof(EncryptedElement), Namespace = Saml20Constants.Assertion, Order = 1)] + [XmlElement("NameID", typeof(NameId), Namespace = Saml20Constants.Assertion, Order = 1)] + public object Item { get; set; } + + /// + /// Gets or sets the item1. + /// The new identifier value (in plaintext or encrypted form) to be used when communicating with the + /// requesting provider concerning this principal, or an indication that the use of the old identifier has + /// been terminated. In the former case, if the requester is the service provider, the new identifier MUST + /// appear in subsequent <NameID> elements in the SPProvidedID attribute. If the requester is the + /// identity provider, the new value will appear in subsequent <NameID> elements as the element's + /// content. + /// + /// The item1. + [XmlElement("NewEncryptedID", typeof(EncryptedElement), Order = 2)] + [XmlElement("NewID", typeof(string), Order = 2)] + [XmlElement("Terminate", typeof(Terminate), Order = 2)] + public object Item1 { get; set; } + + #endregion + } +} diff --git a/src/SAML2.Standard/Schema/Protocol/NameIdMappingRequest.cs b/src/SAML2.Standard/Schema/Protocol/NameIdMappingRequest.cs new file mode 100644 index 0000000..39cabe5 --- /dev/null +++ b/src/SAML2.Standard/Schema/Protocol/NameIdMappingRequest.cs @@ -0,0 +1,44 @@ +using System; +using System.Xml.Serialization; +using SAML2.Schema.Core; + +namespace SAML2.Schema.Protocol +{ + /// + /// To request an alternate name identifier for a principal from an identity provider, a requester sends an + /// <NameIDMappingRequest> message + /// + [Serializable] + [XmlType(Namespace = Saml20Constants.Protocol)] + [XmlRoot(ElementName, Namespace = Saml20Constants.Protocol, IsNullable = false)] + public class NameIdMappingRequest : RequestAbstract + { + /// + /// The XML Element name of this class + /// + public const string ElementName = "NameIDMappingRequest"; + + #region Elements + + /// + /// Gets or sets the item. + /// The identifier and associated descriptive data that specify the principal as currently recognized by the + /// requester and the responder. + /// + /// The item. + [XmlElement("BaseID", typeof(BaseIdAbstract), Namespace = Saml20Constants.Assertion, Order = 1)] + [XmlElement("EncryptedID", typeof(EncryptedElement), Namespace = Saml20Constants.Assertion, Order = 1)] + [XmlElement("NameID", typeof(NameId), Namespace = Saml20Constants.Assertion, Order = 1)] + public object Item { get; set; } + + /// + /// Gets or sets the name ID policy. + /// The requirements regarding the format and optional name qualifier for the identifier to be returned + /// + /// The name ID policy. + [XmlElement("NameIDPolicy", Order = 2)] + public NameIdPolicy NameIdPolicy { get; set; } + + #endregion + } +} diff --git a/src/SAML2.Standard/Schema/Protocol/NameIdMappingResponse.cs b/src/SAML2.Standard/Schema/Protocol/NameIdMappingResponse.cs new file mode 100644 index 0000000..a7cddfe --- /dev/null +++ b/src/SAML2.Standard/Schema/Protocol/NameIdMappingResponse.cs @@ -0,0 +1,35 @@ +using System; +using System.Xml.Serialization; +using SAML2.Schema.Core; + +namespace SAML2.Schema.Protocol +{ + /// + /// The recipient of a <NameIDMappingRequest> message MUST respond with a + /// <NameIDMappingResponse> message. + /// + [Serializable] + [XmlType(Namespace = Saml20Constants.Protocol)] + [XmlRoot(ElementName, Namespace = Saml20Constants.Protocol, IsNullable = false)] + public class NameIdMappingResponse : StatusResponse + { + /// + /// The XML Element name of this class + /// + public new const string ElementName = "NameIDMappingResponse"; + + #region Elements + + /// + /// Gets or sets the item. + /// The identifier and associated attributes that specify the principal in the manner requested, usually in + /// encrypted form + /// + /// The item. + [XmlElement("EncryptedID", typeof(EncryptedElement), Namespace = Saml20Constants.Assertion, Order = 1)] + [XmlElement("NameID", typeof(NameId), Namespace = Saml20Constants.Assertion, Order = 1)] + public object Item { get; set; } + + #endregion + } +} diff --git a/src/SAML2.Standard/Schema/Protocol/NameIdPolicy.cs b/src/SAML2.Standard/Schema/Protocol/NameIdPolicy.cs new file mode 100644 index 0000000..754b0cf --- /dev/null +++ b/src/SAML2.Standard/Schema/Protocol/NameIdPolicy.cs @@ -0,0 +1,69 @@ +using System; +using System.Xml.Serialization; + +namespace SAML2.Schema.Protocol +{ + /// + /// The <NameIDPolicy> element tailors the name identifier in the subjects of assertions resulting from an + /// <AuthnRequest>. + /// + [Serializable] + [XmlType(Namespace = Saml20Constants.Protocol)] + [XmlRoot(ElementName, Namespace = Saml20Constants.Protocol, IsNullable = false)] + public class NameIdPolicy + { + /// + /// The XML Element name of this class + /// + public const string ElementName = "NameIDPolicy"; + + /// + /// Gets or sets a value indicating whether [allow create]. + /// A Boolean value used to indicate whether the identity provider is allowed, in the course of fulfilling the + /// request, to create a new identifier to represent the principal. Defaults to "false". When "false", the + /// requester constrains the identity provider to only issue an assertion to it if an acceptable identifier for + /// the principal has already been established. Note that this does not prevent the identity provider from + /// creating such identifiers outside the context of this specific request (for example, in advance for a + /// large number of principals). + /// + /// true if [allow create]; otherwise, false. + [XmlIgnore] + public bool? AllowCreate { get; set; } + + #region Attributes + + /// + /// Gets or sets the AllowCreate string. + /// + /// The AllowCreate string. + [XmlAttribute("AllowCreate")] + public string AllowCreateString + { + get { return AllowCreate.HasValue ? AllowCreate.ToString().ToLower() : null; } + set { AllowCreate = string.IsNullOrEmpty(value) ? (bool?)null : Convert.ToBoolean(value); } + } + + /// + /// Gets or sets the format. + /// Specifies the URI reference corresponding to a name identifier format defined in this or another + /// specification (see Section 8.3 for examples). The additional value of + /// urn:oasis:names:tc:SAML:2.0:nameid-format:encrypted is defined specifically for use + /// within this attribute to indicate a request that the resulting identifier be encrypted. + /// + /// The format. + [XmlAttribute("Format", DataType = "anyURI")] + public string Format { get; set; } + + /// + /// Gets or sets the SP name qualifier. + /// Optionally specifies that the assertion subject's identifier be returned (or created) in the namespace of + /// a service provider other than the requester, or in the namespace of an affiliation group of service + /// providers. + /// + /// The SP name qualifier. + [XmlAttribute("SPNameQualifier")] + public string SPNameQualifier { get; set; } + + #endregion + } +} diff --git a/src/SAML2.Standard/Schema/Protocol/RequestAbstract.cs b/src/SAML2.Standard/Schema/Protocol/RequestAbstract.cs new file mode 100644 index 0000000..0f8623a --- /dev/null +++ b/src/SAML2.Standard/Schema/Protocol/RequestAbstract.cs @@ -0,0 +1,121 @@ +using System; +using System.Xml.Serialization; +using SAML2.Schema.Core; +using SAML2.Schema.XmlDSig; +using SAML2.Utils; + +namespace SAML2.Schema.Protocol +{ + /// + /// All SAML requests are of types that are derived from the abstract RequestAbstractType complex type. + /// This type defines common attributes and elements that are associated with all SAML requests + /// + [XmlInclude(typeof(NameIdMappingRequest))] + [XmlInclude(typeof(LogoutRequest))] + [XmlInclude(typeof(ManageNameIdRequest))] + [XmlInclude(typeof(ArtifactResolve))] + [XmlInclude(typeof(AuthnRequest))] + [XmlInclude(typeof(SubjectQueryAbstract))] + [XmlInclude(typeof(AuthzDecisionQuery))] + [XmlInclude(typeof(AttributeQuery))] + [XmlInclude(typeof(AuthnQuery))] + [XmlInclude(typeof(AssertionIdRequest))] + [Serializable] + [XmlType(Namespace = Saml20Constants.Protocol)] + public abstract class RequestAbstract + { + /// + /// Gets or sets the issue instant. + /// The time instant of issue of the request. + /// + /// The issue instant. + [XmlIgnore] + public DateTime? IssueInstant { get; set; } + + #region Attributes + + /// + /// Gets or sets the consent. + /// Indicates whether or not (and under what conditions) consent has been obtained from a principal in + /// the sending of this request. + /// + /// The consent. + [XmlAttribute("Consent", DataType = "anyURI")] + public string Consent { get; set; } + + /// + /// Gets or sets the destination. + /// A URI reference indicating the address to which this request has been sent. This is useful to prevent + /// malicious forwarding of requests to unintended recipients, a protection that is required by some + /// protocol bindings. If it is present, the actual recipient MUST check that the URI reference identifies the + /// location at which the message was received. If it does not, the request MUST be discarded. Some + /// protocol bindings may require the use of this attribute (see [SAMLBind]). + /// + /// The destination. + [XmlAttribute("Destination", DataType = "anyURI")] + public string Destination { get; set; } + + /// + /// Gets or sets the identifier for the request. It is of type xs:ID and MUST follow the requirements specified in Section + /// 1.3.4 for identifier uniqueness. The values of the ID attribute in a request and the InResponseTo + /// attribute in the corresponding response MUST match. + /// Gets or sets the ID. + /// + /// The ID. + [XmlAttribute("ID", DataType = "ID")] + public string Id { get; set; } + + /// + /// Gets or sets the issue instant string. + /// + /// The issue instant string. + [XmlAttribute("IssueInstant")] + public string IssueInstantString + { + get { return IssueInstant.HasValue ? Saml20Utils.ToUtcString(IssueInstant.Value) : null; } + set { IssueInstant = string.IsNullOrEmpty(value) ? (DateTime?)null : Saml20Utils.FromUtcString(value); } + } + + /// + /// Gets or sets the version. + /// The version of this request. The identifier for the version of SAML defined in this specification is "2.0". + /// + /// The version. + [XmlAttribute("Version")] + public string Version { get; set; } + + #endregion + + #region Elements + + /// + /// Gets or sets the extensions. + /// This extension point contains optional protocol message extension elements that are agreed on + /// between the communicating parties. No extension schema is required in order to make use of this + /// extension point, and even if one is provided, the lax validation setting does not impose a requirement + /// for the extension to be valid. SAML extension elements MUST be namespace-qualified in a non- + /// SAML-defined namespace + /// + /// The extensions. + [XmlElement("Extensions", Order = 3)] + public Extensions Extensions { get; set; } + + /// + /// Gets or sets the issuer. + /// Identifies the entity that generated the request message. + /// + /// The issuer. + [XmlElement("Issuer", Namespace = Saml20Constants.Assertion, Order = 1)] + public NameId Issuer { get; set; } + + /// + /// Gets or sets the signature. + /// An XML Signature that authenticates the requester and provides message integrity + /// + /// The signature. + [XmlElement("Signature", Namespace = "http://www.w3.org/2000/09/xmldsig#", Order = 2)] + public Signature Signature { get; set; } + + #endregion + } +} diff --git a/src/SAML2.Standard/Schema/Protocol/RequestedAuthnContext.cs b/src/SAML2.Standard/Schema/Protocol/RequestedAuthnContext.cs new file mode 100644 index 0000000..92e21e7 --- /dev/null +++ b/src/SAML2.Standard/Schema/Protocol/RequestedAuthnContext.cs @@ -0,0 +1,73 @@ +using System; +using System.Xml.Serialization; + +namespace SAML2.Schema.Protocol +{ + /// + /// The <RequestedAuthnContext> element specifies the authentication context requirements of + /// authentication statements returned in response to a request or query. + /// + [Serializable] + [XmlType(Namespace = Saml20Constants.Protocol)] + [XmlRoot(ElementName, Namespace = Saml20Constants.Protocol, IsNullable = false)] + public class RequestedAuthnContext + { + /// + /// The XML Element name of this class + /// + public const string ElementName = "RequestedAuthnContext"; + + /// + /// Gets or sets a value indicating whether [comparison specified]. + /// + /// true if [comparison specified]; otherwise, false. + [XmlIgnore] + public bool ComparisonSpecified { get; set; } + + #region Attributes + + /// + /// Gets or sets the comparison. + /// Specifies the comparison method used to evaluate the requested context classes or statements, one + /// of "exact", "minimum", "maximum", or "better". The default is "exact". + /// If Comparison is set to "exact" or omitted, then the resulting authentication context in the authentication + /// statement MUST be the exact match of at least one of the authentication contexts specified. + /// If Comparison is set to "minimum", then the resulting authentication context in the authentication + /// statement MUST be at least as strong (as deemed by the responder) as one of the authentication + /// contexts specified. + /// If Comparison is set to "better", then the resulting authentication context in the authentication + /// statement MUST be stronger (as deemed by the responder) than any one of the authentication contexts + /// specified. + /// If Comparison is set to "maximum", then the resulting authentication context in the authentication + /// statement MUST be as strong as possible (as deemed by the responder) without exceeding the strength + /// of at least one of the authentication contexts specified. + /// + /// The comparison. + [XmlAttribute("Comparison")] + public AuthnContextComparisonType Comparison { get; set; } + + #endregion + + #region Elements + + /// + /// Gets or sets the items. + /// Specifies one or more URI references identifying authentication context classes or declarations. + /// + /// The items. + [XmlElement("AuthnContextClassRef", typeof(string), Namespace = Saml20Constants.Assertion, DataType = "anyURI", Order = 1)] + [XmlElement("AuthnContextDeclRef", typeof(string), Namespace = Saml20Constants.Assertion, DataType = "anyURI", Order = 1)] + [XmlChoiceIdentifier("ItemsElementName")] + public string[] Items { get; set; } + + /// + /// Gets or sets the name of the items element. + /// + /// The name of the items element. + [XmlElement("ItemsElementName")] + [XmlIgnore] + public AuthnContextType[] ItemsElementName { get; set; } + + #endregion + } +} \ No newline at end of file diff --git a/src/SAML2.Standard/Schema/Protocol/Response.cs b/src/SAML2.Standard/Schema/Protocol/Response.cs new file mode 100644 index 0000000..f90ecba --- /dev/null +++ b/src/SAML2.Standard/Schema/Protocol/Response.cs @@ -0,0 +1,34 @@ +using System; +using System.Xml.Serialization; +using SAML2.Schema.Core; + +namespace SAML2.Schema.Protocol +{ + /// + /// The <Response> message element is used when a response consists of a list of zero or more assertions + /// that satisfy the request. It has the complex type ResponseType, which extends StatusResponseType + /// + [Serializable] + [XmlType(Namespace = Saml20Constants.Protocol)] + [XmlRoot(ElementName, Namespace = Saml20Constants.Protocol, IsNullable = false)] + public class Response : StatusResponse + { + /// + /// The XML Element name of this class + /// + public new const string ElementName = "Response"; + + #region Elements + + /// + /// Gets or sets the items. + /// Specifies an assertion by value, or optionally an encrypted assertion by value. + /// + /// The items. + [XmlElement(Assertion.ElementName, typeof(Assertion), Namespace = Saml20Constants.Assertion, Order = 1)] + [XmlElement("EncryptedAssertion", typeof(EncryptedElement), Namespace = Saml20Constants.Assertion, Order = 1)] + public object[] Items { get; set; } + + #endregion + } +} diff --git a/src/SAML2.Standard/Schema/Protocol/Scoping.cs b/src/SAML2.Standard/Schema/Protocol/Scoping.cs new file mode 100644 index 0000000..e4ad710 --- /dev/null +++ b/src/SAML2.Standard/Schema/Protocol/Scoping.cs @@ -0,0 +1,60 @@ +using System; +using System.CodeDom.Compiler; +using System.Xml.Serialization; + +namespace SAML2.Schema.Protocol +{ + /// + /// The <Scoping> element specifies the identity providers trusted by the requester to authenticate the + /// presenter, as well as limitations and context related to a proxy of the <AuthnRequest> message to + /// subsequent identity providers by the responder. + /// + [GeneratedCode("xsd", "2.0.50727.42")] + [Serializable] + [XmlType(Namespace = Saml20Constants.Protocol)] + [XmlRoot(ElementName, Namespace = Saml20Constants.Protocol, IsNullable = false)] + public class Scoping + { + /// + /// The XML Element name of this class + /// + public const string ElementName = "Scoping"; + + #region Attributes + + /// + /// Gets or sets the proxy count. + /// Specifies the number of a proxy indirections permissible between the identity provider that receives + /// this <AuthnRequest> and the identity provider who ultimately authenticates the principal. A count of + /// zero permits no proxy, while omitting this attribute expresses no such restriction. + /// + /// The proxy count. + [XmlAttribute("ProxyCount", DataType = "nonNegativeInteger")] + public string ProxyCount { get; set; } + + #endregion + + #region Elements + + /// + /// Gets or sets the IDP list. + /// An advisory list of identity providers and associated information that the requester deems acceptable + /// to respond to the request. + /// + /// The IDP list. + [XmlElement("IDPList", Order = 1)] + public IdpList IdpList { get; set; } + + /// + /// Gets or sets the requester ID. + /// Identifies the set of requesting entities on whose behalf the requester is acting. Used to communicate + /// the chain of requesters when a proxy occurs, as described in Section 3.4.1.5. See Section 8.3.6 for a + /// description of entity identifiers. + /// + /// The requester ID. + [XmlElement("RequesterID", DataType = "anyURI", Order = 2)] + public string[] RequesterId { get; set; } + + #endregion + } +} diff --git a/src/SAML2.Standard/Schema/Protocol/Status.cs b/src/SAML2.Standard/Schema/Protocol/Status.cs new file mode 100644 index 0000000..9796bd8 --- /dev/null +++ b/src/SAML2.Standard/Schema/Protocol/Status.cs @@ -0,0 +1,47 @@ +using System; +using System.Xml.Serialization; + +namespace SAML2.Schema.Protocol +{ + /// + /// The SAML protocol status class. + /// + [Serializable] + [XmlType(Namespace = Saml20Constants.Protocol)] + [XmlRoot(ElementName, Namespace = Saml20Constants.Protocol, IsNullable = false)] + public class Status + { + /// + /// The XML Element name of this class + /// + public const string ElementName = "Status"; + + #region Elements + + /// + /// Gets or sets the status code. + /// A code representing the status of the activity carried out in response to the corresponding request. + /// + /// The status code. + [XmlElement("StatusCode", Order = 1)] + public StatusCode StatusCode { get; set; } + + /// + /// Gets or sets the status detail. + /// Additional information concerning the status of the request. + /// + /// The status detail. + [XmlElement("StatusDetail", Order = 3)] + public StatusDetail StatusDetail { get; set; } + + /// + /// Gets or sets the status message. + /// A message which MAY be returned to an operator. + /// + /// The status message. + [XmlElement("StatusMessage", Order = 2)] + public string StatusMessage { get; set; } + + #endregion + } +} diff --git a/src/SAML2.Standard/Schema/Protocol/StatusCode.cs b/src/SAML2.Standard/Schema/Protocol/StatusCode.cs new file mode 100644 index 0000000..3b9ae4e --- /dev/null +++ b/src/SAML2.Standard/Schema/Protocol/StatusCode.cs @@ -0,0 +1,47 @@ +using System; +using System.Xml.Serialization; + +namespace SAML2.Schema.Protocol +{ + /// + /// The <StatusCode> element specifies a code or a set of nested codes representing the status of the + /// corresponding request. + /// + [Serializable] + [XmlType(Namespace = Saml20Constants.Protocol)] + [XmlRoot(ElementName, Namespace = Saml20Constants.Protocol, IsNullable = false)] + public class StatusCode + { + /// + /// The XML Element name of this class + /// + public const string ElementName = "StatusCode"; + + #region Attributes + + /// + /// Gets or sets the value. + /// The status code value. This attribute contains a URI reference. The value of the topmost + /// <StatusCode> element MUST be from the top-level list provided in this section. + /// + /// The value. + [XmlAttribute("Value", DataType = "anyURI")] + public string Value { get; set; } + + #endregion + + #region Elements + + /// + /// Gets or sets the sub status code. + /// A subordinate status code that provides more specific information on an error condition. Note that + /// responders MAY omit subordinate status codes in order to prevent attacks that seek to probe for + /// additional information by intentionally presenting erroneous requests. + /// + /// The sub status code. + [XmlElement("StatusCode", Namespace = Saml20Constants.Protocol, Order = 1)] + public StatusCode SubStatusCode { get; set; } + + #endregion + } +} diff --git a/src/SAML2.Standard/Schema/Protocol/StatusDetail.cs b/src/SAML2.Standard/Schema/Protocol/StatusDetail.cs new file mode 100644 index 0000000..1c73586 --- /dev/null +++ b/src/SAML2.Standard/Schema/Protocol/StatusDetail.cs @@ -0,0 +1,33 @@ +using System; +using System.Xml; +using System.Xml.Serialization; + +namespace SAML2.Schema.Protocol +{ + /// + /// The <StatusDetail> element MAY be used to specify additional information concerning the status of + /// the request. The additional information consists of zero or more elements from any namespace, with no + /// requirement for a schema to be present or for schema validation of the <StatusDetail> contents. + /// + [Serializable] + [XmlType(Namespace = Saml20Constants.Protocol)] + [XmlRoot(ElementName, Namespace = Saml20Constants.Protocol, IsNullable = false)] + public class StatusDetail + { + /// + /// The XML Element name of this class + /// + public const string ElementName = "StatusDetail"; + + #region Elements + + /// + /// Gets or sets the any XML element. + /// + /// The Any XML element. + [XmlAnyElement(Order = 1)] + public XmlElement[] Any { get; set; } + + #endregion + } +} diff --git a/src/SAML2.Standard/Schema/Protocol/StatusResponse.cs b/src/SAML2.Standard/Schema/Protocol/StatusResponse.cs new file mode 100644 index 0000000..dc2e2f0 --- /dev/null +++ b/src/SAML2.Standard/Schema/Protocol/StatusResponse.cs @@ -0,0 +1,144 @@ +using System; +using System.Xml.Schema; +using System.Xml.Serialization; +using SAML2.Schema.Core; +using SAML2.Schema.XmlDSig; +using SAML2.Utils; + +namespace SAML2.Schema.Protocol +{ + /// + /// All SAML responses are of types that are derived from the StatusResponseType complex type. This type + /// defines common attributes and elements that are associated with all SAML responses + /// + [XmlInclude(typeof(NameIdMappingResponse))] + [XmlInclude(typeof(ArtifactResponse))] + [XmlInclude(typeof(LogoutResponse))] + [XmlInclude(typeof(Response))] + [Serializable] + [XmlType(Namespace = Saml20Constants.Protocol)] + [XmlRoot(ElementName, Namespace = Saml20Constants.Protocol, IsNullable = false)] + public class StatusResponse + { + /// + /// The XML Element name of this class + /// + public const string ElementName = "ManageNameIDResponse"; + + /// + /// Gets or sets the issue instant. + /// The time instant of issue of the response. + /// + /// The issue instant. + [XmlIgnore] + public DateTime? IssueInstant { get; set; } + + #region Attributes + + /// + /// Gets or sets the consent. + /// Indicates whether or not (and under what conditions) consent has been obtained from a principal in + /// the sending of this response. See Section 8.4 for some URI references that MAY be used as the value + /// of the Consent attribute and their associated descriptions. If no Consent value is provided, the + /// identifier urn:oasis:names:tc:SAML:2.0:consent:unspecified (see Section 8.4.1) is in + /// effect. + /// + /// The consent. + [XmlAttribute("Consent", DataType = "anyURI")] + public string Consent { get; set; } + + /// + /// Gets or sets the destination. + /// A URI reference indicating the address to which this response has been sent. This is useful to prevent + /// malicious forwarding of responses to unintended recipients, a protection that is required by some + /// protocol bindings. If it is present, the actual recipient MUST check that the URI reference identifies the + /// location at which the message was received. If it does not, the response MUST be discarded. Some + /// protocol bindings may require the use of this attribute (see [SAMLBind]). + /// + /// The destination. + [XmlAttribute("Destination", DataType = "anyURI")] + public string Destination { get; set; } + + /// + /// Gets or sets the ID. + /// An identifier for the response. It is of type xs:ID, and MUST follow the requirements specified in + /// Section 1.3.4 for identifier uniqueness. + /// + /// The ID. + [XmlAttribute("ID", DataType = "ID")] + public string ID { get; set; } + + /// + /// Gets or sets the in response to. + /// A reference to the identifier of the request to which the response corresponds, if any. If the response + /// is not generated in response to a request, or if the ID attribute value of a request cannot be + /// determined (for example, the request is malformed), then this attribute MUST NOT be present. + /// Otherwise, it MUST be present and its value MUST match the value of the corresponding request's + /// ID attribute. + /// + /// The in response to. + [XmlAttribute("InResponseTo", DataType = "NCName")] + public string InResponseTo { get; set; } + + /// + /// Gets or sets the issue instant string. + /// + /// The issue instant string. + [XmlAttribute("IssueInstant")] + public string IssueInstantString + { + get { return IssueInstant.HasValue ? Saml20Utils.ToUtcString(IssueInstant.Value) : null; } + set { IssueInstant = string.IsNullOrEmpty(value) ? (DateTime?)null : Saml20Utils.FromUtcString(value); } + } + + /// + /// Gets or sets the version. + /// The version of this response. The identifier for the version of SAML defined in this specification is "2.0". + /// + /// The version. + [XmlAttribute] + public string Version { get; set; } + + #endregion + + #region Elements + + /// + /// Gets or sets the extensions. + /// This extension point contains optional protocol message extension elements that are agreed on + /// between the communicating parties. . No extension schema is required in order to make use of this + /// extension point, and even if one is provided, the lax validation setting does not impose a requirement + /// for the extension to be valid. SAML extension elements MUST be namespace-qualified in a non- + /// SAML-defined namespace. + /// + /// The extensions. + [XmlElement("Extensions", Order = 3)] + public Extensions Extensions { get; set; } + + /// + /// Gets or sets the issuer. + /// Identifies the entity that generated the response message. + /// + /// The issuer. + [XmlElement("Issuer", Namespace = Saml20Constants.Assertion, Form = XmlSchemaForm.Qualified, Order = 1)] + public NameId Issuer { get; set; } + + /// + /// Gets or sets the signature. + /// An XML Signature that authenticates the responder and provides message integrity + /// + /// The signature. + [XmlElement("Signature", Namespace = "http://www.w3.org/2000/09/xmldsig#", Order = 2)] + public Signature Signature { get; set; } + + /// + /// Gets or sets the status. + /// A code representing the status of the corresponding request + /// + /// The status. + [XmlElement("Status", Order = 4)] + public Status Status { get; set; } + + #endregion + } +} diff --git a/src/SAML2.Standard/Schema/Protocol/SubjectQueryAbstract.cs b/src/SAML2.Standard/Schema/Protocol/SubjectQueryAbstract.cs new file mode 100644 index 0000000..b28b6c6 --- /dev/null +++ b/src/SAML2.Standard/Schema/Protocol/SubjectQueryAbstract.cs @@ -0,0 +1,35 @@ +using System; +using System.Xml.Serialization; +using SAML2.Schema.Core; + +namespace SAML2.Schema.Protocol +{ + /// + /// is an extension point that allows new SAML queries to be + /// defined that specify a single SAML subject. + /// + [XmlInclude(typeof(AuthzDecisionQuery))] + [XmlInclude(typeof(AttributeQuery))] + [XmlInclude(typeof(AuthnQuery))] + [Serializable] + [XmlType(Namespace = Saml20Constants.Protocol)] + [XmlRoot(ElementName, Namespace = Saml20Constants.Protocol, IsNullable = false)] + public abstract class SubjectQueryAbstract : RequestAbstract + { + /// + /// The XML Element name of this class + /// + public const string ElementName = "SubjectQuery"; + + #region Elements + + /// + /// Gets or sets the subject. + /// + /// The subject. + [XmlElement("Subject", Namespace = Saml20Constants.Assertion, Order = 1)] + public Subject Subject { get; set; } + + #endregion + } +} diff --git a/src/SAML2.Standard/Schema/Protocol/Terminate.cs b/src/SAML2.Standard/Schema/Protocol/Terminate.cs new file mode 100644 index 0000000..2d4b809 --- /dev/null +++ b/src/SAML2.Standard/Schema/Protocol/Terminate.cs @@ -0,0 +1,21 @@ +using System; +using System.Diagnostics; +using System.Xml.Serialization; + +namespace SAML2.Schema.Protocol +{ + /// + /// The SAML20 protocol Terminate class + /// + [Serializable] + [DebuggerStepThrough] + [XmlType(Namespace = Saml20Constants.Protocol)] + [XmlRoot(ElementName, Namespace = Saml20Constants.Protocol, IsNullable = false)] + public class Terminate + { + /// + /// The XML Element name of this class + /// + public const string ElementName = "Terminate"; + } +} diff --git a/src/SAML2.Standard/Schema/XEnc/AgreementMethod.cs b/src/SAML2.Standard/Schema/XEnc/AgreementMethod.cs new file mode 100644 index 0000000..ee43694 --- /dev/null +++ b/src/SAML2.Standard/Schema/XEnc/AgreementMethod.cs @@ -0,0 +1,71 @@ +using System; +using System.Xml; +using System.Xml.Serialization; +using SAML2.Schema.XmlDSig; + +namespace SAML2.Schema.XEnc +{ + /// + /// The AgreementMethod element appears as the content of a ds:KeyInfo since, like other ds:KeyInfo children, + /// it yields a key. This ds:KeyInfo is in turn a child of an EncryptedData or EncryptedKey element. The + /// Algorithm attribute and KeySize child of the EncryptionMethod element under this EncryptedData or + /// EncryptedKey element are implicit parameters to the key agreement computation. In cases where this + /// EncryptionMethod algorithm URI is insufficient to determine the key length, a KeySize MUST have been + /// included. In addition, the sender may place a KA-Nonce element under AgreementMethod to assure that different + /// keying material is generated even for repeated agreements using the same sender and recipient public keys. + /// + [Serializable] + [XmlType(Namespace = Saml20Constants.Xenc)] + [XmlRoot(ElementName, Namespace = Saml20Constants.Xenc, IsNullable = false)] + public class AgreementMethod + { + /// + /// The XML Element name of this class + /// + public const string ElementName = "AgreementMethod"; + + #region Attributes + + /// + /// Gets or sets the algorithm. + /// + /// The algorithm. + [XmlAttribute(DataType = "anyURI")] + public string Algorithm { get; set; } + + #endregion + + #region Elements + + /// + /// Gets or sets the any XML element. + /// + /// The Any XML element. + [XmlText] + [XmlAnyElement] + public XmlNode[] Any { get; set; } + + /// + /// Gets or sets the KA nonce. + /// + /// The KA nonce. + [XmlElement("KA-Nonce", DataType = "base64Binary")] + public byte[] KANonce { get; set; } + + /// + /// Gets or sets the originator key info. + /// + /// The originator key info. + [XmlElement("OriginatorKeyInfo")] + public KeyInfo OriginatorKeyInfo { get; set; } + + /// + /// Gets or sets the recipient key info. + /// + /// The recipient key info. + [XmlElement("RecipientKeyInfo")] + public KeyInfo RecipientKeyInfo { get; set; } + + #endregion + } +} diff --git a/src/SAML2.Standard/Schema/XEnc/CipherData.cs b/src/SAML2.Standard/Schema/XEnc/CipherData.cs new file mode 100644 index 0000000..05f20a4 --- /dev/null +++ b/src/SAML2.Standard/Schema/XEnc/CipherData.cs @@ -0,0 +1,33 @@ +using System; +using System.Xml.Serialization; + +namespace SAML2.Schema.XEnc +{ + /// + /// The CipherData is a mandatory element that provides the encrypted data. It must either contain the + /// encrypted octet sequence as base64 encoded text of the CipherValue element, or provide a reference to an + /// external location containing the encrypted octet sequence via the CipherReference element. + /// + [Serializable] + [XmlType(Namespace = Saml20Constants.Xenc)] + [XmlRoot(ElementName, Namespace = Saml20Constants.Xenc, IsNullable = false)] + public class CipherData + { + /// + /// The XML Element name of this class + /// + public const string ElementName = "CipherData"; + + #region Elements + + /// + /// Gets or sets the item. + /// + /// The item. + [XmlElement("CipherReference", typeof(CipherReference))] + [XmlElement("CipherValue", typeof(byte[]), DataType = "base64Binary")] + public object Item { get; set; } + + #endregion + } +} diff --git a/src/SAML2.Standard/Schema/XEnc/CipherReference.cs b/src/SAML2.Standard/Schema/XEnc/CipherReference.cs new file mode 100644 index 0000000..129a35f --- /dev/null +++ b/src/SAML2.Standard/Schema/XEnc/CipherReference.cs @@ -0,0 +1,43 @@ +using System; +using System.Xml.Serialization; + +namespace SAML2.Schema.XEnc +{ + /// + /// The CipherData is a mandatory element that provides the encrypted data. It must either contain the + /// encrypted octet sequence as base64 encoded text of the CipherValue element, or provide a reference to + /// an external location containing the encrypted octet sequence via the CipherReference element. + /// + [Serializable] + [XmlType(Namespace = Saml20Constants.Xenc)] + [XmlRoot(ElementName, Namespace = Saml20Constants.Xenc, IsNullable = false)] + public class CipherReference + { + /// + /// The XML Element name of this class + /// + public const string ElementName = "CipherReference"; + + #region Attributes + + /// + /// Gets or sets the URI. + /// + /// The URI. + [XmlAttribute("URI", DataType = "anyURI")] + public string URI { get; set; } + + #endregion + + #region Elements + + /// + /// Gets or sets the item. + /// + /// The item. + [XmlElement("Transforms")] + public TransformsType Item { get; set; } + + #endregion + } +} diff --git a/src/SAML2.Standard/Schema/XEnc/Encrypted.cs b/src/SAML2.Standard/Schema/XEnc/Encrypted.cs new file mode 100644 index 0000000..05668d7 --- /dev/null +++ b/src/SAML2.Standard/Schema/XEnc/Encrypted.cs @@ -0,0 +1,81 @@ +using System; +using System.Xml.Serialization; +using SAML2.Schema.XmlDSig; + +namespace SAML2.Schema.XEnc +{ + /// + /// The base class for EncryptedKey and EncryptedData + /// + [XmlInclude(typeof(EncryptedKey))] + [XmlInclude(typeof(EncryptedData))] + [Serializable] + [XmlType(Namespace = Saml20Constants.Xenc)] + public abstract class Encrypted + { + #region Attributes + + /// + /// Gets or sets the encoding. + /// + /// The encoding. + [XmlAttribute("Encoding", DataType = "anyURI")] + public string Encoding { get; set; } + + /// + /// Gets or sets the id. + /// + /// The id. + [XmlAttribute("Id", DataType = "ID")] + public string Id { get; set; } + + /// + /// Gets or sets the type of the MIME. + /// + /// The type of the MIME. + [XmlAttribute("MimeType")] + public string MimeType { get; set; } + + /// + /// Gets or sets the type. + /// + /// The type. + [XmlAttribute("Type", DataType = "anyURI")] + public string Type { get; set; } + + #endregion + + #region Elements + + /// + /// Gets or sets the encryption method. + /// the RSA public key algorithm. + /// + /// The encryption method. + [XmlElement("EncryptionMethod")] + public EncryptionMethod EncryptionMethod { get; set; } + + /// + /// Gets or sets the key info. + /// + /// The key info. + [XmlElement("KeyInfo", Namespace = Saml20Constants.Xmldsig)] + public KeyInfo KeyInfo { get; set; } + + /// + /// Gets or sets the cipher data. + /// + /// The cipher data. + [XmlElement("CipherData")] + public CipherData CipherData { get; set; } + + /// + /// Gets or sets the encryption properties. + /// + /// The encryption properties. + [XmlElement("EncryptionProperties")] + public EncryptionProperties EncryptionProperties { get; set; } + + #endregion + } +} diff --git a/src/SAML2.Standard/Schema/XEnc/EncryptedData.cs b/src/SAML2.Standard/Schema/XEnc/EncryptedData.cs new file mode 100644 index 0000000..3ba8176 --- /dev/null +++ b/src/SAML2.Standard/Schema/XEnc/EncryptedData.cs @@ -0,0 +1,19 @@ +using System; +using System.Xml.Serialization; + +namespace SAML2.Schema.XEnc +{ + /// + /// Encrypted data class + /// + [Serializable] + [XmlType(Namespace = Saml20Constants.Xenc)] + [XmlRoot(ElementName, Namespace = Saml20Constants.Xenc, IsNullable = false)] + public class EncryptedData : Encrypted + { + /// + /// The XML Element name of this class + /// + public const string ElementName = "EncryptedData"; + } +} diff --git a/src/SAML2.Standard/Schema/XEnc/EncryptedKey.cs b/src/SAML2.Standard/Schema/XEnc/EncryptedKey.cs new file mode 100644 index 0000000..9d6d8be --- /dev/null +++ b/src/SAML2.Standard/Schema/XEnc/EncryptedKey.cs @@ -0,0 +1,52 @@ +using System; +using System.Xml.Serialization; + +namespace SAML2.Schema.XEnc +{ + /// + /// The EncryptedKey element is used to transport encryption keys from the originator to a known recipient(s). + /// It may be used as a stand-alone XML document, be placed within an application document, or appear inside + /// an EncryptedData element as a child of a ds:KeyInfo element. The key value is always encrypted to the + /// recipient(s). When EncryptedKey is decrypted the resulting octets are made available to the EncryptionMethod + /// algorithm without any additional processing. + /// + [Serializable] + [XmlType(Namespace = Saml20Constants.Xenc)] + [XmlRoot(ElementName, Namespace = Saml20Constants.Xenc, IsNullable = false)] + public class EncryptedKey : Encrypted + { + /// + /// The XML Element name of this class + /// + public const string ElementName = "EncryptedKey"; + + #region Attributes + + /// + /// Gets or sets the recipient. + /// + /// The recipient. + [XmlAttribute("Recipient")] + public string Recipient { get; set; } + + #endregion + + #region Elements + + /// + /// Gets or sets the name of the carried key. + /// + /// The name of the carried key. + [XmlElement("CarriedKeyName")] + public string CarriedKeyName { get; set; } + + /// + /// Gets or sets the reference list. + /// + /// The reference list. + [XmlElement("ReferenceList")] + public ReferenceList ReferenceList { get; set; } + + #endregion + } +} diff --git a/src/SAML2.Standard/Schema/XEnc/EncryptionMethod.cs b/src/SAML2.Standard/Schema/XEnc/EncryptionMethod.cs new file mode 100644 index 0000000..91d1425 --- /dev/null +++ b/src/SAML2.Standard/Schema/XEnc/EncryptionMethod.cs @@ -0,0 +1,58 @@ +using System; +using System.Xml; +using System.Xml.Serialization; + +namespace SAML2.Schema.XEnc +{ + /// + /// EncryptionMethod is an optional element that describes the encryption algorithm applied to the cipher data. + /// If the element is absent, the encryption algorithm must be known by the recipient or the decryption will fail. + /// + [Serializable] + [XmlType(Namespace = Saml20Constants.Xenc)] + [XmlRoot(ElementName, Namespace = Saml20Constants.Metadata, IsNullable = false)] + public class EncryptionMethod + { + /// + /// The XML Element name of this class + /// + public const string ElementName = "EncryptionMethod"; + + #region Attributes + + /// + /// Gets or sets the algorithm. + /// + /// The algorithm. + [XmlAttribute("Algorithm", DataType = "anyURI")] + public string Algorithm { get; set; } + + #endregion + + #region Elements + + /// + /// Gets or sets any elements. + /// + /// Any elements. + [XmlText] + [XmlAnyElement] + public XmlNode[] Any { get; set; } + + /// + /// Gets or sets the size of the key. + /// + /// The size of the key. + [XmlElement("KeySize", DataType = "integer")] + public string KeySize { get; set; } + + /// + /// Gets or sets the OAEP parameters. + /// + /// The OAEP parameters. + [XmlElement("OAEPparams", DataType = "base64Binary")] + public byte[] OAEPparams { get; set; } + + #endregion + } +} diff --git a/src/SAML2.Standard/Schema/XEnc/EncryptionProperties.cs b/src/SAML2.Standard/Schema/XEnc/EncryptionProperties.cs new file mode 100644 index 0000000..e4f7ac5 --- /dev/null +++ b/src/SAML2.Standard/Schema/XEnc/EncryptionProperties.cs @@ -0,0 +1,41 @@ +using System; +using System.Xml.Serialization; + +namespace SAML2.Schema.XEnc +{ + /// + /// Holds list of EncryptionProperty elements + /// + [Serializable] + [XmlType(Namespace = Saml20Constants.Xenc)] + [XmlRoot(ElementName, Namespace = Saml20Constants.Xenc, IsNullable = false)] + public class EncryptionProperties + { + /// + /// The XML Element name of this class + /// + public const string ElementName = "EncryptionProperties"; + + #region Attributes + + /// + /// Gets or sets the id. + /// + /// The id. + [XmlAttribute("Id", DataType = "ID")] + public string Id { get; set; } + + #endregion + + #region Elements + + /// + /// Gets or sets the encryption property. + /// + /// The encryption property. + [XmlElement("EncryptionProperty")] + public EncryptionProperty[] EncryptionProperty { get; set; } + + #endregion + } +} diff --git a/src/SAML2.Standard/Schema/XEnc/EncryptionProperty.cs b/src/SAML2.Standard/Schema/XEnc/EncryptionProperty.cs new file mode 100644 index 0000000..8a069dd --- /dev/null +++ b/src/SAML2.Standard/Schema/XEnc/EncryptionProperty.cs @@ -0,0 +1,67 @@ +using System; +using System.Xml; +using System.Xml.Serialization; + +namespace SAML2.Schema.XEnc +{ + /// + /// Additional information items concerning the generation of the EncryptedData or EncryptedKey can be placed + /// in an EncryptionProperty element (e.g., date/time stamp or the serial number of cryptographic hardware used + /// during encryption). The Target attribute identifies the EncryptedType structure being described. anyAttribute + /// permits the inclusion of attributes from the XML namespace to be included (i.e., xml:space, xml:lang, and + /// xml:base). + /// + [Serializable] + [XmlType(Namespace = Saml20Constants.Xenc)] + [XmlRoot(ElementName, Namespace = Saml20Constants.Xenc, IsNullable = false)] + public class EncryptionProperty + { + /// + /// The XML Element name of this class + /// + public const string ElementName = "EncryptionProperty"; + + /// + /// Gets or sets the text. + /// + /// The text. + [XmlText] + public string[] Text { get; set; } + + #region Attributes + + /// + /// Gets or sets XML any attribute. + /// + /// The XML Any attribute. + [XmlAnyAttribute] + public XmlAttribute[] AnyAttr { get; set; } + + /// + /// Gets or sets the id. + /// + /// The id. + [XmlAttribute("Id", DataType = "ID")] + public string Id { get; set; } + + /// + /// Gets or sets the target. + /// + /// The target. + [XmlAttribute("Target", DataType = "anyURI")] + public string Target { get; set; } + + #endregion + + #region Elements + + /// + /// Gets or sets the items. + /// + /// The items. + [XmlAnyElement] + public XmlElement[] Items { get; set; } + + #endregion + } +} diff --git a/src/SAML2.Standard/Schema/XEnc/ReferenceList.cs b/src/SAML2.Standard/Schema/XEnc/ReferenceList.cs new file mode 100644 index 0000000..a0a2c71 --- /dev/null +++ b/src/SAML2.Standard/Schema/XEnc/ReferenceList.cs @@ -0,0 +1,37 @@ +using System; +using System.Xml.Serialization; + +namespace SAML2.Schema.XEnc +{ + /// + /// ReferenceList is an element that contains pointers from a key value of an EncryptedKey to items + /// encrypted by that key value (EncryptedData or EncryptedKey elements). + /// + [Serializable] + [XmlType(AnonymousType = true, Namespace = Saml20Constants.Xenc)] + [XmlRoot(Namespace = Saml20Constants.Xenc, IsNullable = false)] + public class ReferenceList + { + #region Elements + + /// + /// Gets or sets the items. + /// DataReferencee and KeyReference elements + /// + /// The items. + [XmlElement("DataReference", typeof(ReferenceType))] + [XmlElement("KeyReference", typeof(ReferenceType))] + [XmlChoiceIdentifier("ItemsElementName")] + public ReferenceType[] Items { get; set; } + + /// + /// Gets or sets the name of the items element. + /// + /// The name of the items element. + [XmlElement("ItemsElementName")] + [XmlIgnore] + public ReferenceListType[] ItemsElementName { get; set; } + + #endregion + } +} diff --git a/src/SAML2.Standard/Schema/XEnc/ReferenceListType.cs b/src/SAML2.Standard/Schema/XEnc/ReferenceListType.cs new file mode 100644 index 0000000..cce44b0 --- /dev/null +++ b/src/SAML2.Standard/Schema/XEnc/ReferenceListType.cs @@ -0,0 +1,25 @@ +using System; +using System.Xml.Serialization; + +namespace SAML2.Schema.XEnc +{ + /// + /// ItemsChoice for Referencelists + /// + [Serializable] + [XmlType(Namespace = Saml20Constants.Xenc, IncludeInSchema = false)] + public enum ReferenceListType + { + /// + /// DataReference type. + /// + [XmlEnum("DataReference")] + DataReference, + + /// + /// KeyReference type. + /// + [XmlEnum("KeyReference")] + KeyReference, + } +} diff --git a/src/SAML2.Standard/Schema/XEnc/ReferenceType.cs b/src/SAML2.Standard/Schema/XEnc/ReferenceType.cs new file mode 100644 index 0000000..749290e --- /dev/null +++ b/src/SAML2.Standard/Schema/XEnc/ReferenceType.cs @@ -0,0 +1,43 @@ +using System; +using System.Xml; +using System.Xml.Serialization; + +namespace SAML2.Schema.XEnc +{ + /// + /// DataReference elements are used to refer to EncryptedData elements that were encrypted + /// using the key defined in the enclosing EncryptedKey element. Multiple DataReference elements + /// can occur if multiple EncryptedData elements exist that are encrypted by the same key. + /// + [Serializable] + [XmlType(TypeName = "ReferenceType", Namespace = Saml20Constants.Xenc)] + public class ReferenceType + { + /// + /// The XML Element name of this class + /// + public const string ElementName = "ReferenceType"; + + #region Attributes + + /// + /// Gets or sets the URI. + /// + /// The URI. + [XmlAttribute("URI", DataType = "anyURI")] + public string URI { get; set; } + + #endregion + + #region Elements + + /// + /// Gets or sets any elements. + /// + /// Any elements. + [XmlAnyElement] + public XmlElement[] Any { get; set; } + + #endregion + } +} diff --git a/src/SAML2.Standard/Schema/XEnc/TransformsType.cs b/src/SAML2.Standard/Schema/XEnc/TransformsType.cs new file mode 100644 index 0000000..d33a47c --- /dev/null +++ b/src/SAML2.Standard/Schema/XEnc/TransformsType.cs @@ -0,0 +1,30 @@ +using System; +using System.Xml.Serialization; +using SAML2.Schema.XmlDSig; + +namespace SAML2.Schema.XEnc +{ + /// + /// The Transforms type + /// + [Serializable] + [XmlType(TypeName = "TransformsType", Namespace = Saml20Constants.Xenc)] + public class TransformsType + { + /// + /// The XML Element name of this class + /// + public const string ElementName = "TransformsType"; + + #region Elements + + /// + /// Gets or sets the transform. + /// + /// The transform. + [XmlElement("Transform", Namespace = Saml20Constants.Xmldsig)] + public Transform[] Transform { get; set; } + + #endregion + } +} diff --git a/src/SAML2.Standard/Schema/XmlDSig/CanonicalizationMethod.cs b/src/SAML2.Standard/Schema/XmlDSig/CanonicalizationMethod.cs new file mode 100644 index 0000000..974ea1c --- /dev/null +++ b/src/SAML2.Standard/Schema/XmlDSig/CanonicalizationMethod.cs @@ -0,0 +1,87 @@ +using System; +using System.Xml; +using System.Xml.Serialization; + +namespace SAML2.Schema.XmlDSig +{ + /// + /// + /// CanonicalizationMethod is a required element that specifies the canonicalization algorithm applied to + /// the SignedInfo element prior to performing signature calculations. This element uses the general structure + /// for algorithms described in Algorithm Identifiers and Implementation Requirements (section 6.1). + /// Implementations MUST support the REQUIRED canonicalization algorithms. + /// + /// + /// Alternatives to the REQUIRED canonicalization algorithms (section 6.5), such as Canonical XML with + /// Comments (section 6.5.1) or a minimal canonicalization (such as CRLF and charset normalization), may be + /// explicitly specified but are NOT REQUIRED. Consequently, their use may not interoperate with other + /// applications that do not support the specified algorithm (see XML Canonicalization and Syntax Constraint + /// Considerations, section 7). Security issues may also arise in the treatment of entity processing and + /// comments if non-XML aware canonicalization algorithms are not properly constrained (see section 8.2: Only + /// What is "Seen" Should be Signed). + /// + /// + /// The way in which the SignedInfo element is presented to the canonicalization method is dependent on that + /// method. The following applies to algorithms which process XML as nodes or characters: + /// + /// + /// XML based canonicalization implementations MUST be provided with a [XPath] node-set originally formed from + /// the document containing the SignedInfo and currently indicating the SignedInfo, its descendants, and + /// the attribute and namespace nodes of SignedInfo and its descendant elements. + /// Text based canonicalization algorithms (such as CRLF and charset normalization) should be provided with + /// the UTF-8 octets that represent the well-formed SignedInfo element, from the first character to the last + /// character of the XML representation, inclusive. This includes the entire text of the start and end tags + /// of the SignedInfo element as well as all descendant markup and character data (i.e., the text) between + /// those tags. Use of text based canonicalization of SignedInfo is NOT RECOMMENDED. + /// + /// + /// We recommend applications that implement a text-based instead of XML-based canonicalization -- such as + /// resource constrained apps -- generate canonicalized XML as their output serialization so as to mitigate + /// interoperability and security concerns. For instance, such an implementation SHOULD (at least) generate + /// standalone XML instances [XML]. + /// + /// + /// NOTE: The signature application must exercise great care in accepting and executing an arbitrary + /// CanonicalizationMethod. For example, the canonicalization method could rewrite the URIs of the References + /// being validated. Or, the method could massively transform SignedInfo so that validation would always + /// succeed (i.e., converting it to a trivial signature with a known key over trivial data). Since + /// CanonicalizationMethod is inside SignedInfo, in the resulting canonical form it could erase itself from + /// SignedInfo or modify the SignedInfo element so that it appears that a different canonicalization function + /// was used! Thus a Signature which appears to authenticate the desired data with the desired key, + /// DigestMethod, and SignatureMethod, can be meaningless if a capricious CanonicalizationMethod is used. + /// + /// + [Serializable] + [XmlType(Namespace = Saml20Constants.Xmldsig)] + [XmlRoot(ElementName, Namespace = Saml20Constants.Xmldsig, IsNullable = false)] + public class CanonicalizationMethod + { + /// + /// The XML Element name of this class + /// + public const string ElementName = "CanonicalizationMethod"; + + #region Attributes + + /// + /// Gets or sets the algorithm. + /// + /// The algorithm. + [XmlAttribute("Algorithm", DataType = "anyURI")] + public string Algorithm { get; set; } + + #endregion + + #region Elements + + /// + /// Gets or sets the any XML element. + /// + /// The Any XML element. + [XmlText] + [XmlAnyElement] + public XmlNode[] Any { get; set; } + + #endregion + } +} diff --git a/src/SAML2.Standard/Schema/XmlDSig/DigestMethod.cs b/src/SAML2.Standard/Schema/XmlDSig/DigestMethod.cs new file mode 100644 index 0000000..507e763 --- /dev/null +++ b/src/SAML2.Standard/Schema/XmlDSig/DigestMethod.cs @@ -0,0 +1,55 @@ +using System; +using System.Xml; +using System.Xml.Serialization; + +namespace SAML2.Schema.XmlDSig +{ + /// + /// + /// DigestMethod is a required element that identifies the digest algorithm to be applied to the signed + /// object. This element uses the general structure here for algorithms specified in Algorithm Identifiers + /// and Implementation Requirements (section 6.1). + /// + /// + /// If the result of the URI dereference and application of Transforms is an XPath node-set (or sufficiently + /// functional replacement implemented by the application) then it must be converted as described in the + /// Reference Processing Model (section 4.3.3.2). If the result of URI dereference and application of + /// transforms is an octet stream, then no conversion occurs (comments might be present if the Canonical + /// XML with Comments was specified in the Transforms). The digest algorithm is applied to the data octets + /// of the resulting octet stream. + /// + /// + [Serializable] + [XmlType(Namespace = Saml20Constants.Xmldsig)] + [XmlRoot(ElementName, Namespace = Saml20Constants.Xmldsig, IsNullable = false)] + public class DigestMethod + { + /// + /// The XML Element name of this class + /// + public const string ElementName = "DigestMethod"; + + #region Attributes + + /// + /// Gets or sets the algorithm. + /// + /// The algorithm. + [XmlAttribute("Algorithm", DataType = "anyURI")] + public string Algorithm { get; set; } + + #endregion + + #region Elements + + /// + /// Gets or sets any elements. + /// + /// Any elements. + [XmlText] + [XmlAnyElement] + public XmlNode[] Any { get; set; } + + #endregion + } +} diff --git a/src/SAML2.Standard/Schema/XmlDSig/DsaKeyValue.cs b/src/SAML2.Standard/Schema/XmlDSig/DsaKeyValue.cs new file mode 100644 index 0000000..560f515 --- /dev/null +++ b/src/SAML2.Standard/Schema/XmlDSig/DsaKeyValue.cs @@ -0,0 +1,95 @@ +using System; +using System.Xml.Serialization; + +namespace SAML2.Schema.XmlDSig +{ + /// + /// + /// The DSA KeyValue + /// DSA keys and the DSA signature algorithm are specified in [DSS]. DSA public key values can have the following fields: + /// + /// + /// P - a prime modulus meeting the [DSS] requirements + /// Q - an integer in the range 2**159 < Q < 2**160 which is a prime divisor of P-1 + /// G - an integer with certain properties with respect to P and Q + /// Y - G**X mod P (where X is part of the private key and not made public) + /// J - (P - 1) / Q + /// seed - a DSA prime generation seed + /// pgenCounter - a DSA prime generation counter + /// + /// + /// Parameter J is available for inclusion solely for efficiency as it is calculatable from P and Q. + /// Parameters seed and pgenCounter are used in the DSA prime number generation algorithm specified in + /// [DSS]. As such, they are optional but must either both be present or both be absent. This prime + /// generation algorithm is designed to provide assurance that a weak prime is not being used and it yields + /// a P and Q value. Parameters P, Q, and G can be public and common to a group of users. They might be + /// known from application context. As such, they are optional but P and Q must either both appear or both + /// be absent. If all of P, Q, seed, and pgenCounter are present, implementations are not required to check + /// if they are consistent and are free to use either P and Q or seed and pgenCounter. All parameters are + /// encoded as base64 [MIME] values. + /// + /// + [Serializable] + [XmlType(Namespace = Saml20Constants.Xmldsig)] + [XmlRoot(ElementName, Namespace = Saml20Constants.Xmldsig, IsNullable = false)] + public class DsaKeyValue + { + /// + /// The XML Element name of this class + /// + public const string ElementName = "DSAKeyValue"; + + #region Elements + + /// + /// Gets or sets the G. + /// + /// The G. + [XmlElement(DataType = "base64Binary")] + public byte[] G { get; set; } + + /// + /// Gets or sets the J. + /// + /// The J. + [XmlElement(DataType = "base64Binary")] + public byte[] J { get; set; } + + /// + /// Gets or sets the P. + /// + /// The P. + [XmlElement(DataType = "base64Binary")] + public byte[] P { get; set; } + + /// + /// Gets or sets the pgen counter. + /// + /// The pgen counter. + [XmlElement("PgenCounter", DataType = "base64Binary")] + public byte[] PgenCounter { get; set; } + + /// + /// Gets or sets the Q. + /// + /// The Q. + [XmlElement(DataType = "base64Binary")] + public byte[] Q { get; set; } + + /// + /// Gets or sets the seed. + /// + /// The seed. + [XmlElement("Seed", DataType = "base64Binary")] + public byte[] Seed { get; set; } + + /// + /// Gets or sets the Y. + /// + /// The Y. + [XmlElement(DataType = "base64Binary")] + public byte[] Y { get; set; } + + #endregion + } +} diff --git a/src/SAML2.Standard/Schema/XmlDSig/KeyInfo.cs b/src/SAML2.Standard/Schema/XmlDSig/KeyInfo.cs new file mode 100644 index 0000000..25a6441 --- /dev/null +++ b/src/SAML2.Standard/Schema/XmlDSig/KeyInfo.cs @@ -0,0 +1,109 @@ +using System; +using System.Xml; +using System.Xml.Serialization; +using SAML2.Utils; + +namespace SAML2.Schema.XmlDSig +{ + /// + /// + /// KeyInfo is an optional element that enables the recipient(s) to obtain the key needed to validate the + /// signature. KeyInfo may contain keys, names, certificates and other public key management information, + /// such as in-band key distribution or key agreement data. This specification defines a few simple types + /// but applications may extend those types or all together replace them with their own key identification + /// and exchange semantics using the XML namespace facility. [XML-ns] However, questions of trust of such + /// key information (e.g., its authenticity or strength) are out of scope of this specification and left + /// to the application. + /// + /// + /// If KeyInfo is omitted, the recipient is expected to be able to identify the key based on application + /// context. Multiple declarations within KeyInfo refer to the same key. While applications may define and + /// use any mechanism they choose through inclusion of elements from a different namespace, compliant + /// versions MUST implement KeyValue (section 4.4.2) and SHOULD implement RetrievalMethod (section 4.4.3). + /// + /// + /// The schema/DTD specifications of many of KeyInfo's children (e.g., PGPData, SPKIData, X509Data) permit + /// their content to be extended/complemented with elements from another namespace. This may be done only + /// if it is safe to ignore these extension elements while claiming support for the types defined in this + /// specification. Otherwise, external elements, including alternative structures to those defined by this + /// specification, MUST be a child of KeyInfo. For example, should a complete XML-PGP standard be defined, + /// its root element MUST be a child of KeyInfo. (Of course, new structures from external namespaces can + /// incorporate elements from the dsig namespace via features of the type definition language. + /// + /// + [Serializable] + [XmlType(Namespace = Saml20Constants.Xmldsig)] + [XmlRoot(ElementName, Namespace = Saml20Constants.Xmldsig, IsNullable = false)] + public class KeyInfo + { + /// + /// The XML Element name of this class + /// + public const string ElementName = "KeyInfo"; + + /// + /// Gets or sets the text. + /// + /// The text. + [XmlText] + public string[] Text { get; set; } + + #region Attributes + + /// + /// Gets or sets the id. + /// + /// The id. + [XmlAttribute(DataType = "ID")] + public string Id { get; set; } + + #endregion + + #region Elements + + /// + /// Gets or sets the items. + /// Items are of types: + /// KeyName, KeyValue, MgmtData, PGPData, RetrievalMethod, SPKIData, X509Data + /// + /// The items. + [XmlAnyElement] + [XmlElement("KeyName", typeof(string))] + [XmlElement(KeyValue.ElementName, typeof(KeyValue))] + [XmlElement("MgmtData", typeof(string))] + [XmlElement(PgpData.ElementName, typeof(PgpData))] + [XmlElement(RetrievalMethod.ElementName, typeof(RetrievalMethod))] + [XmlElement(SpkiData.ElementName, typeof(SpkiData))] + [XmlElement(X509Data.ElementName, typeof(X509Data))] + [XmlChoiceIdentifier("ItemsElementName")] + public object[] Items { get; set; } + + /// + /// Gets or sets the name of the items element. + /// + /// The name of the items element. + [XmlElement("ItemsElementName")] + [XmlIgnore] + public KeyInfoItemType[] ItemsElementName { get; set; } + + #endregion + + /// + /// An implicit conversion between our Xml Serialization class, and the .NET framework's built-in version of KeyInfo. + /// + /// The key info. + /// The result of the conversion. + public static explicit operator System.Security.Cryptography.Xml.KeyInfo(KeyInfo ki) + { + var result = new System.Security.Cryptography.Xml.KeyInfo(); + var doc = new XmlDocument(); + doc.LoadXml(Serialization.SerializeToXmlString(ki)); + if (doc.DocumentElement != null) + { + result.LoadXml(doc.DocumentElement); + } + + return result; + } + } +} diff --git a/src/SAML2.Standard/Schema/XmlDSig/KeyInfoItemType.cs b/src/SAML2.Standard/Schema/XmlDSig/KeyInfoItemType.cs new file mode 100644 index 0000000..50d556b --- /dev/null +++ b/src/SAML2.Standard/Schema/XmlDSig/KeyInfoItemType.cs @@ -0,0 +1,61 @@ +using System; +using System.Xml.Serialization; + +namespace SAML2.Schema.XmlDSig +{ + /// + /// KeyInfo item types. + /// + [Serializable] + [XmlType(Namespace = Saml20Constants.Xmldsig, IncludeInSchema = false)] + public enum KeyInfoItemType + { + /// + /// Any item type. + /// + [XmlEnum("##any:")] + Item, + + /// + /// KeyName item type. + /// + [XmlEnum("KeyName")] + KeyName, + + /// + /// KeyValue item type. + /// + [XmlEnum("KeyValue")] + KeyValue, + + /// + /// MgmtData item type. + /// + [XmlEnum("MgmtData")] + MgmtData, + + /// + /// PGPData item type. + /// + [XmlEnum("PGPData")] + PGPData, + + /// + /// RetrievalMethod item type. + /// + [XmlEnum("RetrievalMethod")] + RetrievalMethod, + + /// + /// SPKIData item type. + /// + [XmlEnum("SPKIData")] + SPKIData, + + /// + /// X509Data item type. + /// + [XmlEnum("X509Data")] + X509Data + } +} diff --git a/src/SAML2.Standard/Schema/XmlDSig/KeyValue.cs b/src/SAML2.Standard/Schema/XmlDSig/KeyValue.cs new file mode 100644 index 0000000..a106172 --- /dev/null +++ b/src/SAML2.Standard/Schema/XmlDSig/KeyValue.cs @@ -0,0 +1,43 @@ +using System; +using System.Xml.Serialization; + +namespace SAML2.Schema.XmlDSig +{ + /// + /// The KeyValue element contains a single public key that may be useful in validating the signature. + /// Structured formats for defining DSA (REQUIRED) and RSA (RECOMMENDED) public keys are defined in Signature + /// Algorithms (section 6.4). The KeyValue element may include externally defined public keys values + /// represented as PCDATA or element types from an external namespace. + /// + [Serializable] + [XmlType(Namespace = Saml20Constants.Xmldsig)] + [XmlRoot(ElementName, Namespace = Saml20Constants.Xmldsig, IsNullable = false)] + public class KeyValue + { + /// + /// The XML Element name of this class + /// + public const string ElementName = "KeyValue"; + + /// + /// Gets or sets the text. + /// + /// The text. + [XmlText] + public string[] Text { get; set; } + + #region Elements + + /// + /// Gets or sets the item. + /// Item is of type DSAKeyValue or RSAKeyValue + /// + /// The item. + [XmlAnyElement] + [XmlElement("DSAKeyValue", typeof(DsaKeyValue))] + [XmlElement("RSAKeyValue", typeof(RsaKeyValue))] + public object Item { get; set; } + + #endregion + } +} diff --git a/src/SAML2.Standard/Schema/XmlDSig/Manifest.cs b/src/SAML2.Standard/Schema/XmlDSig/Manifest.cs new file mode 100644 index 0000000..daabee3 --- /dev/null +++ b/src/SAML2.Standard/Schema/XmlDSig/Manifest.cs @@ -0,0 +1,46 @@ +using System; +using System.Xml.Serialization; + +namespace SAML2.Schema.XmlDSig +{ + /// + /// The Manifest element provides a list of References. The difference from the list in SignedInfo is that + /// it is application defined which, if any, of the digests are actually checked against the objects referenced + /// and what to do if the object is inaccessible or the digest compare fails. If a Manifest is pointed to from + /// SignedInfo, the digest over the Manifest itself will be checked by the core signature validation behavior. + /// The digests within such a Manifest are checked at the application's discretion. If a Manifest is referenced + /// from another Manifest, even the overall digest of this two level deep Manifest might not be checked. + /// + [Serializable] + [XmlType(Namespace = Saml20Constants.Xmldsig)] + [XmlRoot(ElementName, Namespace = Saml20Constants.Xmldsig, IsNullable = false)] + public class Manifest + { + /// + /// The XML Element name of this class + /// + public const string ElementName = "Manifest"; + + #region Attributes + + /// + /// Gets or sets the id. + /// + /// The id. + [XmlAttribute(DataType = "ID")] + public string Id { get; set; } + + #endregion + + #region Elements + + /// + /// Gets or sets the reference. + /// + /// The reference. + [XmlElement("Reference")] + public Reference[] Reference { get; set; } + + #endregion + } +} diff --git a/src/SAML2.Standard/Schema/XmlDSig/ObjectType.cs b/src/SAML2.Standard/Schema/XmlDSig/ObjectType.cs new file mode 100644 index 0000000..d28abea --- /dev/null +++ b/src/SAML2.Standard/Schema/XmlDSig/ObjectType.cs @@ -0,0 +1,85 @@ +using System; +using System.Xml; +using System.Xml.Serialization; + +namespace SAML2.Schema.XmlDSig +{ + /// + /// + /// Object is an optional element that may occur one or more times. When present, this element may contain + /// any data. The Object element may include optional MIME type, ID, and encoding attributes. + /// + /// + /// The Object's Encoding attributed may be used to provide a URI that identifies the method by which the + /// object is encoded (e.g., a binary file). + /// + /// + /// The MimeType attribute is an optional attribute which describes the data within the Object (independent + /// of its encoding). This is a string with values defined by [MIME]. For example, if the Object contains + /// base64 encoded PNG, the Encoding may be specified as 'base64' and the MimeType as 'image/png'. This + /// attribute is purely advisory; no validation of the MimeType information is required by this specification. + /// Applications which require normative type and encoding information for signature validation should specify + /// Transforms with well defined resulting types and/or encodings. + /// + /// + /// The Object's Id is commonly referenced from a Reference in SignedInfo, or Manifest. This element is + /// typically used for enveloping signatures where the object being signed is to be included in the signature + /// element. The digest is calculated over the entire Object element including start and end tags. + /// + /// + /// Note, if the application wishes to exclude the <Object> tags from the digest calculation the Reference + /// must identify the actual data object (easy for XML documents) or a transform must be used to remove the + /// Object tags (likely where the data object is non-XML). Exclusion of the object tags may be desired for + /// cases where one wants the signature to remain valid if the data object is moved from inside a signature + /// to outside the signature (or vice versa), or where the content of the Object is an encoding of an original + /// binary document and it is desired to extract and decode so as to sign the original bitwise representation. + /// + /// + [Serializable] + [XmlType(Namespace = Saml20Constants.Xmldsig)] + [XmlRoot(ElementName, Namespace = Saml20Constants.Xmldsig, IsNullable = false)] + public class ObjectType + { + /// + /// The XML Element name of this class + /// + public const string ElementName = "Object"; + + #region Attributes + + /// + /// Gets or sets the encoding. + /// + /// The encoding. + [XmlAttribute("Encoding", DataType = "anyURI")] + public string Encoding { get; set; } + + /// + /// Gets or sets the id. + /// + /// The id. + [XmlAttribute("Id", DataType = "ID")] + public string Id { get; set; } + + /// + /// Gets or sets the type of the MIME. + /// + /// The type of the MIME. + [XmlAttribute("MimeType")] + public string MimeType { get; set; } + + #endregion + + #region Elements + + /// + /// Gets or sets any elements. + /// + /// Any elements. + [XmlText] + [XmlAnyElement] + public XmlNode[] Any { get; set; } + + #endregion + } +} diff --git a/src/SAML2.Standard/Schema/XmlDSig/PgpData.cs b/src/SAML2.Standard/Schema/XmlDSig/PgpData.cs new file mode 100644 index 0000000..4c9ef92 --- /dev/null +++ b/src/SAML2.Standard/Schema/XmlDSig/PgpData.cs @@ -0,0 +1,48 @@ +using System; +using System.Xml.Serialization; + +namespace SAML2.Schema.XmlDSig +{ + /// + /// The PGPData element within KeyInfo is used to convey information related to PGP public key pairs and + /// signatures on such keys. The PGPKeyID's value is a base64Binary sequence containing a standard PGP public + /// key identifier as defined in [PGP, section 11.2]. The PgpKeyPacket contains a base64-encoded Key Material + /// Packet as defined in [PGP, section 5.5]. These children element types can be complemented/extended by + /// siblings from an external namespace within PGPData, or PGPData can be replaced all together with an + /// alternative PGP XML structure as a child of KeyInfo. PGPData must contain one PGPKeyID and/or one + /// PgpKeyPacket and 0 or more elements from an external namespace. + /// + [Serializable] + [XmlType(Namespace = Saml20Constants.Xmldsig)] + [XmlRoot(ElementName, Namespace = Saml20Constants.Xmldsig, IsNullable = false)] + public class PgpData + { + /// + /// The XML Element name of this class + /// + public const string ElementName = "PGPData"; + + #region Elements + + /// + /// Gets or sets the items. + /// Items are of type PGPKeyID or PgpKeyPacket + /// + /// The items. + [XmlAnyElement] + [XmlElement("PGPKeyID", typeof(byte[]), DataType = "base64Binary")] + [XmlElement("PGPKeyPacket", typeof(byte[]), DataType = "base64Binary")] + [XmlChoiceIdentifier("ItemsElementName")] + public object[] Items { get; set; } + + /// + /// Gets or sets the name of the items element. + /// + /// The name of the items element. + [XmlElement("ItemsElementName")] + [XmlIgnore] + public PgpItemType[] ItemsElementName { get; set; } + + #endregion + } +} diff --git a/src/SAML2.Standard/Schema/XmlDSig/PgpItemType.cs b/src/SAML2.Standard/Schema/XmlDSig/PgpItemType.cs new file mode 100644 index 0000000..d054c14 --- /dev/null +++ b/src/SAML2.Standard/Schema/XmlDSig/PgpItemType.cs @@ -0,0 +1,31 @@ +using System; +using System.Xml.Serialization; + +namespace SAML2.Schema.XmlDSig +{ + /// + /// PGP item type. + /// + [Serializable] + [XmlType(Namespace = Saml20Constants.Xmldsig, IncludeInSchema = false)] + public enum PgpItemType + { + /// + /// Any item type. + /// + [XmlEnum("##any:")] + Item, + + /// + /// PgpKeyId item type. + /// + [XmlEnum("PGPKeyID")] + PgpKeyId, + + /// + /// PgpKeyPacket item type. + /// + [XmlEnum("PGPKeyPacket")] + PgpKeyPacket, + } +} diff --git a/src/SAML2.Standard/Schema/XmlDSig/Reference.cs b/src/SAML2.Standard/Schema/XmlDSig/Reference.cs new file mode 100644 index 0000000..603f514 --- /dev/null +++ b/src/SAML2.Standard/Schema/XmlDSig/Reference.cs @@ -0,0 +1,75 @@ +using System; +using System.Xml.Serialization; + +namespace SAML2.Schema.XmlDSig +{ + /// + /// Reference is an element that may occur one or more times. It specifies a digest algorithm and digest + /// value, and optionally an identifier of the object being signed, the type of the object, and/or a list + /// of transforms to be applied prior to digesting. The identification (URI) and transforms describe how the + /// digested content (i.e., the input to the digest method) was created. The Type attribute facilitates the + /// processing of referenced data. For example, while this specification makes no requirements over external + /// data, an application may wish to signal that the referent is a Manifest. An optional ID attribute permits + /// a Reference to be referenced from elsewhere. + /// + [Serializable] + [XmlType(Namespace = Saml20Constants.Xmldsig)] + [XmlRoot(ElementName, Namespace = Saml20Constants.Xmldsig, IsNullable = false)] + public class Reference + { + /// + /// The XML Element name of this class + /// + public const string ElementName = "Reference"; + + #region Attributes + + /// + /// Gets or sets the id. + /// + /// The id. + [XmlAttribute("Id", DataType = "ID")] + public string Id { get; set; } + + /// + /// Gets or sets the URI. + /// + /// The URI. + [XmlAttribute("URI", DataType = "anyURI")] + public string URI { get; set; } + + /// + /// Gets or sets the type. + /// + /// The type. + [XmlAttribute("Type", DataType = "anyURI")] + public string Type { get; set; } + + #endregion + + #region Elements + + /// + /// Gets or sets the transforms. + /// + /// The transforms. + [XmlArrayItem("Transform", IsNullable = false)] + public Transform[] Transforms { get; set; } + + /// + /// Gets or sets the digest method. + /// + /// The digest method. + [XmlElement("DigestMethod")] + public DigestMethod DigestMethod { get; set; } + + /// + /// Gets or sets the digest value. + /// + /// The digest value. + [XmlElement("DigestValue", DataType = "base64Binary")] + public byte[] DigestValue { get; set; } + + #endregion + } +} diff --git a/src/SAML2.Standard/Schema/XmlDSig/RetrievalMethod.cs b/src/SAML2.Standard/Schema/XmlDSig/RetrievalMethod.cs new file mode 100644 index 0000000..ad56964 --- /dev/null +++ b/src/SAML2.Standard/Schema/XmlDSig/RetrievalMethod.cs @@ -0,0 +1,65 @@ +using System; +using System.Xml.Serialization; + +namespace SAML2.Schema.XmlDSig +{ + /// + /// + /// A RetrievalMethod element within KeyInfo is used to convey a reference to KeyInfo information that is + /// stored at another location. For example, several signatures in a document might use a key verified by + /// an X.509v3 certificate chain appearing once in the document or remotely outside the document; each + /// signature's KeyInfo can reference this chain using a single RetrievalMethod element instead of including + /// the entire chain with a sequence of X509Certificate elements. + /// + /// + /// RetrievalMethod uses the same syntax and dereferencing behavior as Reference's URI (section 4.3.3.1) and + /// The Reference Processing Model (section 4.3.3.2) except that there is no DigestMethod or DigestValue + /// child elements and presence of the URI is mandatory. + /// + /// + /// Type is an optional identifier for the type of data to be retrieved. The result of dereferencing a + /// RetrievalMethod Reference for all KeyInfo types defined by this specification (section 4.4) with a + /// corresponding XML structure is an XML element or document with that element as the root. The + /// rawX509Certificate KeyInfo (for which there is no XML structure) returns a binary X509 certificate. + /// + /// + [Serializable] + [XmlType(Namespace = Saml20Constants.Xmldsig)] + [XmlRoot(ElementName, Namespace = Saml20Constants.Xmldsig, IsNullable = false)] + public class RetrievalMethod + { + /// + /// The XML Element name of this class + /// + public const string ElementName = "RetrievalMethod"; + + #region Attributes + + /// + /// Gets or sets the URI. + /// + /// The URI. + [XmlAttribute("URI", DataType = "anyURI")] + public string Uri { get; set; } + + /// + /// Gets or sets the type. + /// + /// The type. + [XmlAttribute("Type", DataType = "anyURI")] + public string Type { get; set; } + + #endregion + + #region Elements + + /// + /// Gets or sets the transforms. + /// + /// The transforms. + [XmlArrayItem("Transform", IsNullable = false)] + public Transform[] Transforms { get; set; } + + #endregion + } +} \ No newline at end of file diff --git a/src/SAML2.Standard/Schema/XmlDSig/RsaKeyValue.cs b/src/SAML2.Standard/Schema/XmlDSig/RsaKeyValue.cs new file mode 100644 index 0000000..2454dc4 --- /dev/null +++ b/src/SAML2.Standard/Schema/XmlDSig/RsaKeyValue.cs @@ -0,0 +1,53 @@ +using System; +using System.Xml.Serialization; + +namespace SAML2.Schema.XmlDSig +{ + /// + /// + /// RSA key values have two fields: Modulus and Exponent. + /// + /// + /// + /// <RSAKeyValue> + /// <Modulus>xA7SEU+e0yQH5rm9kbCDN9o3aPIo7HbP7tX6WOocLZAtNfyxSZDU16ksL6W + /// jubafOqNEpcwR3RdFsT7bCqnXPBe5ELh5u4VEy19MzxkXRgrMvavzyBpVRgBUwUlV + /// 5foK5hhmbktQhyNdy/6LpQRhDUDsTvK+g9Ucj47es9AQJ3U= + /// </Modulus> + /// <Exponent>AQAB</Exponent> + /// </RSAKeyValue> + /// + /// + /// + /// Arbitrary-length integers (e.g. "bignums" such as RSA moduli) are represented in XML as octet strings as defined by the ds:CryptoBinary type. + /// + /// + [Serializable] + [XmlType(Namespace = Saml20Constants.Xmldsig)] + [XmlRoot(ElementName, Namespace = Saml20Constants.Xmldsig, IsNullable = false)] + public class RsaKeyValue + { + /// + /// The XML Element name of this class + /// + public const string ElementName = "RSAKeyValue"; + + #region Elements + + /// + /// Gets or sets the exponent. + /// + /// The exponent. + [XmlElement("Exponent", DataType = "base64Binary")] + public byte[] Exponent { get; set; } + + /// + /// Gets or sets the modulus. + /// + /// The modulus. + [XmlElement("Modulus", DataType = "base64Binary")] + public byte[] Modulus { get; set; } + + #endregion + } +} diff --git a/src/SAML2.Standard/Schema/XmlDSig/Signature.cs b/src/SAML2.Standard/Schema/XmlDSig/Signature.cs new file mode 100644 index 0000000..ca3994c --- /dev/null +++ b/src/SAML2.Standard/Schema/XmlDSig/Signature.cs @@ -0,0 +1,65 @@ +using System; +using System.Xml.Serialization; + +namespace SAML2.Schema.XmlDSig +{ + /// + /// The Signature element is the root element of an XML Signature + /// + [Serializable] + [XmlType(Namespace = Saml20Constants.Xmldsig)] + [XmlRoot(ElementName, Namespace = Saml20Constants.Xmldsig, IsNullable = false)] + public class Signature + { + /// + /// The XML Element name of this class + /// + public const string ElementName = "Signature"; + + #region Attributes + + /// + /// Gets or sets the id. + /// + /// The id. + [XmlAttribute("Id", DataType = "ID")] + public string Id { get; set; } + + #endregion + + #region Elements + + /// + /// Gets or sets the signed info. + /// The structure of SignedInfo includes the canonicalization algorithm, a signature algorithm, and one or + /// more references. The SignedInfo element may contain an optional ID attribute that will allow it to be + /// referenced by other signatures and objects. + /// + /// The signed info. + [XmlElement("SignedInfo")] + public SignedInfo SignedInfo { get; set; } + + /// + /// Gets or sets the signature value. + /// + /// The signature value. + [XmlElement("SignatureValue")] + public SignatureValue SignatureValue { get; set; } + + /// + /// Gets or sets the key info. + /// + /// The key info. + [XmlElement("KeyInfo")] + public KeyInfo KeyInfo { get; set; } + + /// + /// Gets or sets the object. + /// + /// The object. + [XmlElement("Object")] + public ObjectType[] Object { get; set; } + + #endregion + } +} diff --git a/src/SAML2.Standard/Schema/XmlDSig/SignatureMethod.cs b/src/SAML2.Standard/Schema/XmlDSig/SignatureMethod.cs new file mode 100644 index 0000000..1a5b16e --- /dev/null +++ b/src/SAML2.Standard/Schema/XmlDSig/SignatureMethod.cs @@ -0,0 +1,55 @@ +using System; +using System.Xml; +using System.Xml.Serialization; + +namespace SAML2.Schema.XmlDSig +{ + /// + /// SignatureMethod is a required element that specifies the algorithm used for signature generation and + /// validation. This algorithm identifies all cryptographic functions involved in the signature operation + /// (e.g. hashing, public key algorithms, MACs, padding, etc.). This element uses the general structure + /// here for algorithms described in section 6.1: Algorithm Identifiers and Implementation Requirements. + /// While there is a single identifier, that identifier may specify a format containing multiple distinct + /// signature values. + /// + [Serializable] + [XmlType(Namespace = Saml20Constants.Xmldsig)] + [XmlRoot(ElementName, Namespace = Saml20Constants.Xmldsig, IsNullable = false)] + public class SignatureMethod + { + /// + /// The XML Element name of this class + /// + public const string ElementName = "SignatureMethod"; + + #region Attributes + + /// + /// Gets or sets the algorithm. + /// + /// The algorithm. + [XmlAttribute("v", DataType = "anyURI")] + public string Algorithm { get; set; } + + #endregion + + #region Elements + + /// + /// Gets or sets the any XML element. + /// + /// The Any XML element. + [XmlText] + [XmlAnyElement] + public XmlNode[] Any { get; set; } + + /// + /// Gets or sets the length of the HMAC output. + /// + /// The length of the HMAC output. + [XmlElement("HMACOutputLength", DataType = "integer")] + public string HMACOutputLength { get; set; } + + #endregion + } +} diff --git a/src/SAML2.Standard/Schema/XmlDSig/SignatureProperties.cs b/src/SAML2.Standard/Schema/XmlDSig/SignatureProperties.cs new file mode 100644 index 0000000..b8fabd0 --- /dev/null +++ b/src/SAML2.Standard/Schema/XmlDSig/SignatureProperties.cs @@ -0,0 +1,41 @@ +using System; +using System.Xml.Serialization; + +namespace SAML2.Schema.XmlDSig +{ + /// + /// Contains a list of SignatureProperty instances + /// + [Serializable] + [XmlType(Namespace = Saml20Constants.Xmldsig)] + [XmlRoot(ElementName, Namespace = Saml20Constants.Xmldsig, IsNullable = false)] + public class SignatureProperties + { + /// + /// The XML Element name of this class + /// + public const string ElementName = "SignatureProperties"; + + #region Attributes + + /// + /// Gets or sets the id. + /// + /// The id. + [XmlAttribute("Id", DataType = "ID")] + public string Id { get; set; } + + #endregion + + #region Elements + + /// + /// Gets or sets the signature property. + /// + /// The signature property. + [XmlElement("SignatureProperty")] + public SignatureProperty[] SignatureProperty { get; set; } + + #endregion + } +} diff --git a/src/SAML2.Standard/Schema/XmlDSig/SignatureProperty.cs b/src/SAML2.Standard/Schema/XmlDSig/SignatureProperty.cs new file mode 100644 index 0000000..c2bbcab --- /dev/null +++ b/src/SAML2.Standard/Schema/XmlDSig/SignatureProperty.cs @@ -0,0 +1,62 @@ +using System; +using System.Xml; +using System.Xml.Serialization; + +namespace SAML2.Schema.XmlDSig +{ + /// + /// For the inclusion of assertions about the signature itself (e.g., signature semantics, the time of signing + /// or the serial number of hardware used in cryptographic processes). Such assertions may be signed by including + /// a Reference for the SignatureProperties in SignedInfo. While the signing application should be very careful + /// about what it signs (it should understand what is in the SignatureProperty) a receiving application has no + /// obligation to understand that semantic (though its parent trust engine may wish to). Any content about the + /// signature generation may be located within the SignatureProperty element. The mandatory Target attribute + /// references the Signature element to which the property applies. + /// + [Serializable] + [XmlType(Namespace = Saml20Constants.Xmldsig)] + [XmlRoot(ElementName, Namespace = Saml20Constants.Xmldsig, IsNullable = false)] + public class SignatureProperty + { + /// + /// The XML Element name of this class + /// + public const string ElementName = "SignatureProperty"; + + /// + /// Gets or sets the text. + /// + /// The text. + [XmlText] + public string[] Text { get; set; } + + #region Attributes + + /// + /// Gets or sets the id. + /// + /// The id. + [XmlAttribute("Id", DataType = "ID")] + public string Id { get; set; } + + /// + /// Gets or sets the target. + /// + /// The target. + [XmlAttribute("Target", DataType = "anyURI")] + public string Target { get; set; } + + #endregion + + #region Elements + + /// + /// Gets or sets the items. + /// + /// The items. + [XmlAnyElement] + public XmlElement[] Items { get; set; } + + #endregion + } +} diff --git a/src/SAML2.Standard/Schema/XmlDSig/SignatureValue.cs b/src/SAML2.Standard/Schema/XmlDSig/SignatureValue.cs new file mode 100644 index 0000000..4709176 --- /dev/null +++ b/src/SAML2.Standard/Schema/XmlDSig/SignatureValue.cs @@ -0,0 +1,41 @@ +using System; +using System.Diagnostics; +using System.Xml.Serialization; + +namespace SAML2.Schema.XmlDSig +{ + /// + /// The SignatureValue element contains the actual value of the digital signature; it is always encoded using + /// base64 [MIME]. While we identify two SignatureMethod algorithms, one mandatory and one optional to + /// implement, user specified algorithms may be used as well. + /// + [Serializable] + [DebuggerStepThrough] + [XmlType(Namespace = Saml20Constants.Xmldsig)] + [XmlRoot(ElementName, Namespace = Saml20Constants.Xmldsig, IsNullable = false)] + public class SignatureValue + { + /// + /// The XML Element name of this class + /// + public const string ElementName = "SignatureValue"; + + /// + /// Gets or sets the value. + /// + /// The value. + [XmlText(DataType = "base64Binary")] + public byte[] Value { get; set; } + + #region Attributes + + /// + /// Gets or sets the id. + /// + /// The id. + [XmlAttribute(DataType = "ID")] + public string Id { get; set; } + + #endregion + } +} diff --git a/src/SAML2.Standard/Schema/XmlDSig/SignedInfo.cs b/src/SAML2.Standard/Schema/XmlDSig/SignedInfo.cs new file mode 100644 index 0000000..96ee890 --- /dev/null +++ b/src/SAML2.Standard/Schema/XmlDSig/SignedInfo.cs @@ -0,0 +1,65 @@ +using System; +using System.Xml.Serialization; + +namespace SAML2.Schema.XmlDSig +{ + /// + /// + /// The structure of SignedInfo includes the canonicalization algorithm, a signature algorithm, and one or + /// more references. The SignedInfo element may contain an optional ID attribute that will allow it to be + /// referenced by other signatures and objects. + /// + /// + /// SignedInfo does not include explicit signature or digest properties (such as calculation time, + /// cryptographic device serial number, etc.). If an application needs to associate properties with the + /// signature or digest, it may include such information in a SignatureProperties element within an Object + /// element. + /// + /// + [Serializable] + [XmlType(Namespace = Saml20Constants.Xmldsig)] + [XmlRoot(ElementName, Namespace = Saml20Constants.Xmldsig, IsNullable = false)] + public class SignedInfo + { + /// + /// The XML Element name of this class + /// + public const string ElementName = "SignedInfo"; + + #region Attributes + + /// + /// Gets or sets the id. + /// + /// The id. + [XmlAttribute("Id", DataType = "ID")] + public string Id { get; set; } + + #endregion + + #region Elements + + /// + /// Gets or sets the canonicalization method. + /// + /// The canonicalization method. + [XmlElement("CanonicalizationMethod")] + public CanonicalizationMethod CanonicalizationMethod { get; set; } + + /// + /// Gets or sets the reference. + /// + /// The reference. + [XmlElement("Reference")] + public Reference[] Reference { get; set; } + + /// + /// Gets or sets the signature method. + /// + /// The signature method. + [XmlElement("SignatureMethod")] + public SignatureMethod SignatureMethod { get; set; } + + #endregion + } +} diff --git a/src/SAML2.Standard/Schema/XmlDSig/SpkiData.cs b/src/SAML2.Standard/Schema/XmlDSig/SpkiData.cs new file mode 100644 index 0000000..3f99d0f --- /dev/null +++ b/src/SAML2.Standard/Schema/XmlDSig/SpkiData.cs @@ -0,0 +1,46 @@ +using System; +using System.Xml; +using System.Xml.Serialization; + +namespace SAML2.Schema.XmlDSig +{ + /// + /// The SPKIData element within KeyInfo is used to convey information related to SPKI public key pairs, + /// certificates and other SPKI data. SPKISexp is the base64 encoding of a SPKI canonical S-expression. + /// SPKIData must have at least one SPKISexp; SPKISexp can be complemented/extended by siblings from an + /// external namespace within SPKIData, or SPKIData can be entirely replaced with an alternative SPKI XML + /// structure as a child of KeyInfo. + /// + [Serializable] + [XmlType(Namespace = Saml20Constants.Xmldsig)] + [XmlRoot(ElementName, Namespace = Saml20Constants.Xmldsig, IsNullable = false)] + public class SpkiData + { + /// + /// The XML Element name of this class + /// + public const string ElementName = "SPKIData"; + + #region Attributes + + /// + /// Gets or sets any attributes. + /// + /// Any attributes. + [XmlAnyElement] + public XmlElement Any { get; set; } + + #endregion + + #region Elements + + /// + /// Gets or sets the SPKIS expression. + /// + /// The SPKIS expression. + [XmlElement("SPKISexp", DataType = "base64Binary")] + public byte[][] SPKISexp { get; set; } + + #endregion + } +} diff --git a/src/SAML2.Standard/Schema/XmlDSig/Transform.cs b/src/SAML2.Standard/Schema/XmlDSig/Transform.cs new file mode 100644 index 0000000..35688a7 --- /dev/null +++ b/src/SAML2.Standard/Schema/XmlDSig/Transform.cs @@ -0,0 +1,84 @@ +using System; +using System.Xml.Serialization; + +namespace SAML2.Schema.XmlDSig +{ + /// + /// + /// The optional Transforms element contains an ordered list of Transform elements; these describe how the + /// signer obtained the data object that was digested. The output of each Transform serves as input to the + /// next Transform. The input to the first Transform is the result of dereferencing the URI attribute of the + /// Reference element. The output from the last Transform is the input for the DigestMethod algorithm. When + /// transforms are applied the signer is not signing the native (original) document but the resulting + /// (transformed) document. (See Only What is Signed is Secure (section 8.1).) + /// + /// + /// Each Transform consists of an Algorithm attribute and content parameters, if any, appropriate for the + /// given algorithm. The Algorithm attribute value specifies the name of the algorithm to be performed, and + /// the Transform content provides additional data to govern the algorithm's processing of the transform + /// input. (See Algorithm Identifiers and Implementation Requirements (section 6).) + /// + /// + /// As described in The Reference Processing Model (section 4.3.3.2), some transforms take an XPath node-set + /// as input, while others require an octet stream. If the actual input matches the input needs of the + /// transform, then the transform operates on the unaltered input. If the transform input requirement differs + /// from the format of the actual input, then the input must be converted. + /// + /// + /// Some Transforms may require explicit MIME type, charset (IANA registered "character set"), or other such + /// information concerning the data they are receiving from an earlier Transform or the source data, although + /// no Transform algorithm specified in this document needs such explicit information. Such data + /// characteristics are provided as parameters to the Transform algorithm and should be described in the + /// specification for the algorithm. + /// + /// + /// Examples of transforms include but are not limited to base64 decoding [MIME], canonicalization [XML-C14N], + /// XPath filtering [XPath], and XSLT [XSLT]. The generic definition of the Transform element also allows + /// application-specific transform algorithms. For example, the transform could be a decompression routine + /// given by a Java class appearing as a base64 encoded parameter to a Java Transform algorithm. However, + /// applications should refrain from using application-specific transforms if they wish their signatures to + /// be verifiable outside of their application domain. Transform Algorithms (section 6.6) defines the list + /// of standard transformations. + /// + /// + [Serializable] + [XmlType(Namespace = Saml20Constants.Xmldsig)] + [XmlRoot(ElementName, Namespace = Saml20Constants.Xmldsig, IsNullable = false)] + public class Transform + { + /// + /// The XML Element name of this class + /// + public const string ElementName = "Transform"; + + /// + /// Gets or sets the text. + /// + /// The text. + [XmlText] + public string[] Text { get; set; } + + #region Attributes + + /// + /// Gets or sets the algorithm. + /// + /// The algorithm. + [XmlAttribute("Algorithm", DataType = "anyURI")] + public string Algorithm { get; set; } + + #endregion + + #region Elements + + /// + /// Gets or sets the items. + /// + /// The items. + [XmlAnyElement] + [XmlElement("XPath", typeof(string))] + public object[] Items { get; set; } + + #endregion + } +} diff --git a/src/SAML2.Standard/Schema/XmlDSig/Transforms.cs b/src/SAML2.Standard/Schema/XmlDSig/Transforms.cs new file mode 100644 index 0000000..ee3b029 --- /dev/null +++ b/src/SAML2.Standard/Schema/XmlDSig/Transforms.cs @@ -0,0 +1,30 @@ +using System; +using System.Xml.Serialization; + +namespace SAML2.Schema.XmlDSig +{ + /// + /// Holds a list of transform classes + /// + [Serializable] + [XmlType(Namespace = Saml20Constants.Xmldsig)] + [XmlRoot(ElementName, Namespace = Saml20Constants.Xmldsig, IsNullable = false)] + public class Transforms + { + /// + /// The XML Element name of this class + /// + public const string ElementName = "Transforms"; + + #region Elements + + /// + /// Gets or sets the transform. + /// + /// The transform. + [XmlElement("Transform")] + public Transform[] Transform { get; set; } + + #endregion + } +} diff --git a/src/SAML2.Standard/Schema/XmlDSig/X509Data.cs b/src/SAML2.Standard/Schema/XmlDSig/X509Data.cs new file mode 100644 index 0000000..4fb3537 --- /dev/null +++ b/src/SAML2.Standard/Schema/XmlDSig/X509Data.cs @@ -0,0 +1,74 @@ +using System; +using System.Xml.Serialization; + +namespace SAML2.Schema.XmlDSig +{ + /// + /// + /// An X509Data element within KeyInfo contains one or more identifiers of keys or X509 certificates + /// (or certificates' identifiers or a revocation list). The content of X509Data is: + /// + /// + /// At least one element, from the following set of element types; any of these may appear together or more + /// than once if and only if each instance describes or is related to the same certificate: + /// + /// + /// *) The X509IssuerSerial element, which contains an X.509 issuer distinguished name/serial number pair that SHOULD be compliant with RFC2253 [LDAP-DN], + /// *) The X509SubjectName element, which contains an X.509 subject distinguished name that SHOULD be compliant with RFC2253 [LDAP-DN], + /// *) The X509SKI element, which contains the base64 encoded plain (i.e. non-DER-encoded) value of a X509 V.3 SubjectKeyIdentifier extension. + /// *) The X509Certificate element, which contains a base64-encoded [X509v3] certificate, and + /// *) Elements from an external namespace which accompanies/complements any of the elements above. + /// *) The X509CRL element, which contains a base64-encoded certificate revocation list (CRL) [X509v3]. + /// + /// + /// Any X509IssuerSerial, X509SKI, and X509SubjectName elements that appear MUST refer to the certificate + /// or certificates containing the validation key. All such elements that refer to a particular individual + /// certificate MUST be grouped inside a single X509Data element and if the certificate to which they refer + /// appears, it MUST also be in that X509Data element. + /// + /// + /// Any X509IssuerSerial, X509SKI, and X509SubjectName elements that relate to the same key but different + /// certificates MUST be grouped within a single KeyInfo but MAY occur in multiple X509Data elements. + /// + /// + /// All certificates appearing in an X509Data element MUST relate to the validation key by either containing + /// it or being part of a certification chain that terminates in a certificate containing the validation key. + /// + /// + [Serializable] + [XmlType(Namespace = Saml20Constants.Xmldsig)] + [XmlRoot(ElementName, Namespace = Saml20Constants.Xmldsig, IsNullable = false)] + public class X509Data + { + /// + /// The XML Element name of this class + /// + public const string ElementName = "X509Data"; + + #region Elements + + /// + /// Gets or sets the items. + /// Items can be of types: X509CRL, X509Certificate, X509IssuerSerial, X509SKI, X509SubjectName + /// + /// The items. + [XmlAnyElement] + [XmlElement("X509CRL", typeof(byte[]), DataType = "base64Binary")] + [XmlElement("X509Certificate", typeof(byte[]), DataType = "base64Binary")] + [XmlElement(X509IssuerSerial.ElementName, typeof(X509IssuerSerial))] + [XmlElement("X509SKI", typeof(byte[]), DataType = "base64Binary")] + [XmlElement("X509SubjectName", typeof(string))] + [XmlChoiceIdentifier("ItemsElementName")] + public object[] Items { get; set; } + + /// + /// Gets or sets the name of the items element. + /// + /// The name of the items element. + [XmlElement("ItemsElementName")] + [XmlIgnore] + public X509ItemType[] ItemsElementName { get; set; } + + #endregion + } +} diff --git a/src/SAML2.Standard/Schema/XmlDSig/X509IssuerSerial.cs b/src/SAML2.Standard/Schema/XmlDSig/X509IssuerSerial.cs new file mode 100644 index 0000000..6c637a5 --- /dev/null +++ b/src/SAML2.Standard/Schema/XmlDSig/X509IssuerSerial.cs @@ -0,0 +1,36 @@ +using System; +using System.Xml.Serialization; + +namespace SAML2.Schema.XmlDSig +{ + /// + /// contains an X.509 issuer distinguished name/serial number pair that SHOULD be compliant with RFC2253 + /// + [Serializable] + [XmlType(Namespace = Saml20Constants.Xmldsig)] + public class X509IssuerSerial + { + /// + /// The XML Element name of this class + /// + public const string ElementName = "X509IssuerSerial"; + + #region Elements + + /// + /// Gets or sets the name of the X509 issuer. + /// + /// The name of the X509 issuer. + [XmlElement("X509IssuerName")] + public string X509IssuerName { get; set; } + + /// + /// Gets or sets the X509 serial number. + /// + /// The X509 serial number. + [XmlElement("X509SerialNumber", DataType = "integer")] + public string X509SerialNumber { get; set; } + + #endregion + } +} diff --git a/src/SAML2.Standard/Schema/XmlDSig/X509ItemType.cs b/src/SAML2.Standard/Schema/XmlDSig/X509ItemType.cs new file mode 100644 index 0000000..3160a5a --- /dev/null +++ b/src/SAML2.Standard/Schema/XmlDSig/X509ItemType.cs @@ -0,0 +1,49 @@ +using System; +using System.Xml.Serialization; + +namespace SAML2.Schema.XmlDSig +{ + /// + /// X509 item types. + /// + [Serializable] + [XmlType(Namespace = Saml20Constants.Xmldsig, IncludeInSchema = false)] + public enum X509ItemType + { + /// + /// Any item type. + /// + [XmlEnum("##any:")] + Item, + + /// + /// X509CRL item type. + /// + [XmlEnum("X509CRL")] + X509CRL, + + /// + /// X509Certificate item type. + /// + [XmlEnum("X509Certificate")] + X509Certificate, + + /// + /// X509IssuerSerial item type. + /// + [XmlEnum("X509IssuerSerial")] + X509IssuerSerial, + + /// + /// X509SKI item type. + /// + [XmlEnum("X509SKI")] + X509SKI, + + /// + /// X509SubjectName item type. + /// + [XmlEnum("X509SubjectName")] + X509SubjectName + } +} \ No newline at end of file diff --git a/src/SAML2.Standard/Specification/DefaultCertificateSpecification.cs b/src/SAML2.Standard/Specification/DefaultCertificateSpecification.cs new file mode 100644 index 0000000..8ddaa88 --- /dev/null +++ b/src/SAML2.Standard/Specification/DefaultCertificateSpecification.cs @@ -0,0 +1,34 @@ +using SAML2.Standard; +using System; +using System.Security.Cryptography.X509Certificates; + +namespace SAML2.Specification +{ + /// + /// Checks if a certificate is within its validity period + /// Performs an online revocation check if the certificate contains a CRL url (oid: 2.5.29.31) + /// + public class DefaultCertificateSpecification : ICertificateSpecification + { + /// + /// Determines whether the specified certificate is considered valid according to the RFC3280 specification. + /// + /// The certificate to validate. + /// + /// true if valid; otherwise, false. + /// + public bool IsSatisfiedBy(X509Certificate2 certificate) + { + try + { + return certificate.Verify(); + } + catch (Exception e) + { + Logging.LoggerProvider.LoggerFor(GetType()).Warn(string.Format(ErrorMessages.CertificateIsNotRFC3280Valid, certificate.SubjectName.Name, certificate.Thumbprint), e); + } + + return false; + } + } +} diff --git a/src/SAML2.Standard/Specification/ICertificateSpecification.cs b/src/SAML2.Standard/Specification/ICertificateSpecification.cs new file mode 100644 index 0000000..7cd7ed6 --- /dev/null +++ b/src/SAML2.Standard/Specification/ICertificateSpecification.cs @@ -0,0 +1,19 @@ +using System.Security.Cryptography.X509Certificates; + +namespace SAML2.Specification +{ + /// + /// Specification interface for certificate validation + /// + public interface ICertificateSpecification + { + /// + /// Determines whether the specified certificate is considered valid by this specification. + /// + /// The certificate to validate. + /// + /// true if valid; otherwise, false. + /// + bool IsSatisfiedBy(X509Certificate2 certificate); + } +} diff --git a/src/SAML2.Standard/Specification/SelfIssuedCertificateSpecification.cs b/src/SAML2.Standard/Specification/SelfIssuedCertificateSpecification.cs new file mode 100644 index 0000000..4a8c9eb --- /dev/null +++ b/src/SAML2.Standard/Specification/SelfIssuedCertificateSpecification.cs @@ -0,0 +1,35 @@ +using SAML2.Standard; +using System; +using System.IdentityModel.Selectors; +using System.Security.Cryptography.X509Certificates; + +namespace SAML2.Specification +{ + /// + /// Validates a self-signed certificate + /// + public class SelfIssuedCertificateSpecification : ICertificateSpecification + { + /// + /// Determines whether the specified certificate is considered valid by this specification. + /// Always returns true. No online validation attempted. + /// + /// The certificate to validate. + /// + /// true. + /// + public bool IsSatisfiedBy(X509Certificate2 certificate) + { + try + { + return certificate.Verify(); + } + catch (Exception e) + { + Logging.LoggerProvider.LoggerFor(GetType()).Warn(string.Format(ErrorMessages.CertificateIsNotRFC3280Valid, certificate.SubjectName.Name, certificate.Thumbprint), e); + } + + return false; + } + } +} diff --git a/src/SAML2.Standard/Specification/SpecificationFactory.cs b/src/SAML2.Standard/Specification/SpecificationFactory.cs new file mode 100644 index 0000000..fc7aea0 --- /dev/null +++ b/src/SAML2.Standard/Specification/SpecificationFactory.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections.Generic; +using SAML2.Config; + +namespace SAML2.Specification +{ + /// + /// Specification factory for getting certificate specification. + /// + public class SpecificationFactory + { + /// + /// Gets the certificate specifications. + /// + /// The endpoint. + /// A list of certificate validation specifications for this endpoint + public static List GetCertificateSpecifications(IdentityProvider endpoint) + { + var specs = new List(); + if (endpoint.CertificateValidationTypes != null && endpoint.CertificateValidationTypes.Count > 0) + { + foreach (var elem in endpoint.CertificateValidationTypes) + { + try + { + var val = (ICertificateSpecification)Activator.CreateInstance(Type.GetType(elem)); + specs.Add(val); + } + catch (Exception e) + { + Logging.LoggerProvider.LoggerFor(typeof(SpecificationFactory)).Error(e.Message, e); + } + } + } + + if (specs.Count == 0) + { + // Add default specification + specs.Add(new DefaultCertificateSpecification()); + } + + return specs; + } + } +} diff --git a/src/SAML2.Standard/TraceMessages.resx b/src/SAML2.Standard/TraceMessages.resx new file mode 100644 index 0000000..1f40883 --- /dev/null +++ b/src/SAML2.Standard/TraceMessages.resx @@ -0,0 +1,276 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Artifact created: {0} + + + Artifact redirect received: {0} + + + Artifact resolved: {0} + + + Resolving artifact "{0}" from identity provider "{1}" endpoint "{2}" + + + Artifact resolve received: {0} + + + Sending response to artifact resolve request "{0}": {1} + + + Artifact response received: {0} + + + AttrQuery assertion received: {0} + + + AttrQuery sent to "{0}": {1} + + + AuthRequest sent for identity provider "{0}" using binding "{1}" + + + Common domain cookie received: {0} + + + Redirect to SignOn endpoint found in Common Domain Cookie + + + Redirect to SignOn endpoint "{0}" + + + Executing Logout Actions + + + Logout request received: {0} + + + Logout request sent for identity provider "{0}" using "{1}" binding: {2} + + + Executing SignOn Actions + + + Successfully processed signon request for "{1}" using NameIdFormat "{2}" for session "{0}" + + + Parsing SOAP message: {0} + + + Assertion found: {0} + + + Assertion being parsed + + + Successfully parsed Assertion: {0} + + + Assertion prehandler called + + + Processing assertion: {0} + + + Audience restriction validated for intended URIs {0} against allowed URIs {1} + + + AuthnRequest sent: {0} + + + Redirecting to Common Domain for identity provider discovery + + + EncryptedAssertion Decrypted: {0} + + + Decrypting EncryptedAssertion + + + EncryptedAssertion found: {0} + + + Identity provider not found. Redirecting for identity provider selection + + + Identity provider retreived from Common Domain Cookie: {0} + + + Identity provider retreived from known providers: {0} + + + Identity provider retreived from IDPChoiceParamater: {0} + + + Redirecting to idpSelectionUrl for selection of identity provider: {0} + + + Logout handler called + + + Successfully parsed Logout request: {0} + + + Parsing Logout request POST binding message: {0} + + + Parsing Logout request Redirect binding message with signature algorithm {1} and signature {2}: {0} + + + Successfully parsed Logout response: {0} + + + Parsing Logout response POST binding message: {0} + + + Logout response received + + + Parsing Logout response Redirect binding message with signature algorithm {1} and signature {2}: {0} + + + Logout response sent: {0} + + + Metadata document being created + + + Metadata document successfully created + + + No replay attack detected + + + Checking for replay attack + + + Successfully decoded SamlResponse: {0} + + + SamlResponse decoding + + + SamlResponse received: {0} + + + SignOn handler called + + \ No newline at end of file diff --git a/src/SAML2.Standard/Utils/ArtifactUtil.cs b/src/SAML2.Standard/Utils/ArtifactUtil.cs new file mode 100644 index 0000000..17205e4 --- /dev/null +++ b/src/SAML2.Standard/Utils/ArtifactUtil.cs @@ -0,0 +1,183 @@ +using System; +using System.Security.Cryptography; +using System.Text; + +namespace SAML2.Utils +{ + /// + /// Contains functions to generate and parse artifacts, as defined in "Bindings for the OASIS + /// Security Assertion Markup Language (SAML) v. 2.0" specification. + /// + public class ArtifactUtil + { + /// + /// Argument length error format. + /// + private const string ArgumentLengthErrorFmt = "Unexpected length of byte[] parameter: {0}. Should be {1}"; + + /// + /// Artifact length. + /// + private const int ArtifactLength = 44; + + /// + /// Length of message handle + /// + private const int MessageHandleLength = 20; + + /// + /// Length of source id + /// + private const int SourceIdLength = 20; + + /// + /// Creates the artifact. + /// + /// The type code value. + /// The endpoint index value. + /// The source id hash. + /// The message handle. + /// A Base64 encoded string containing the artifact + public static string CreateArtifact(short typeCodeValue, short endpointIndexValue, byte[] sourceIdHash, byte[] messageHandle) + { + if (sourceIdHash.Length != SourceIdLength) + { + throw new ArgumentException(string.Format(ArgumentLengthErrorFmt, sourceIdHash.Length, SourceIdLength), "sourceIdHash"); + } + + if (messageHandle.Length != MessageHandleLength) + { + throw new ArgumentException(string.Format(ArgumentLengthErrorFmt, messageHandle.Length, MessageHandleLength), "messageHandle"); + } + + var typeCode = new byte[2]; + typeCode[0] = (byte)(typeCodeValue >> 8); + typeCode[1] = (byte)typeCodeValue; + + var endpointIndex = new byte[2]; + endpointIndex[0] = (byte)(endpointIndexValue >> 8); + endpointIndex[1] = (byte)endpointIndexValue; + + var result = new byte[2 + 2 + SourceIdLength + MessageHandleLength]; + + typeCode.CopyTo(result, 0); + endpointIndex.CopyTo(result, 2); + sourceIdHash.CopyTo(result, 4); + messageHandle.CopyTo(result, 4 + SourceIdLength); + + return Convert.ToBase64String(result); + } + + /// + /// Generates the message handle. + /// + /// The message handle. + public static byte[] GenerateMessageHandle() + { + var rng = RandomNumberGenerator.Create(); + + var messageHandle = new byte[MessageHandleLength]; + rng.GetNonZeroBytes(messageHandle); + + return messageHandle; + } + + /// + /// Generates the source id hash. + /// + /// The source id URL. + /// The source id hash. + public static byte[] GenerateSourceIdHash(string sourceIdUrl) + { + var sha = SHA1Managed.Create(); + var sourceId = sha.ComputeHash(Encoding.ASCII.GetBytes(sourceIdUrl)); + + return sourceId; + } + + /// + /// Parses the artifact. + /// + /// The artifact. + /// The type code value. + /// Index of the endpoint. + /// The source id hash. + /// The message handle. + public static void ParseArtifact(string artifact, ref short typeCodeValue, ref short endpointIndex, ref byte[] sourceIdHash, ref byte[] messageHandle) + { + if (sourceIdHash.Length != SourceIdLength) + { + throw new ArgumentException(string.Format(ArgumentLengthErrorFmt, sourceIdHash.Length, SourceIdLength), "sourceIdHash"); + } + + if (messageHandle.Length != MessageHandleLength) + { + throw new ArgumentException(string.Format(ArgumentLengthErrorFmt, messageHandle.Length, MessageHandleLength), "messageHandle"); + } + + var bytes = Convert.FromBase64String(artifact); + if (bytes.Length != ArtifactLength) + { + throw new ArgumentException("Unexpected artifact length", "artifact"); + } + + typeCodeValue = (short)(bytes[0] << 8 | bytes[1]); + endpointIndex = (short)(bytes[2] << 8 | bytes[3]); + + var index = 4; + for (var i = 0; i < SourceIdLength; i++) + { + sourceIdHash[i] = bytes[i + index]; + } + + index += SourceIdLength; + for (var i = 0; i < MessageHandleLength; i++) + { + messageHandle[i] = bytes[i + index]; + } + } + + /// + /// Tries to parse artifact. + /// + /// The artifact. + /// The type code value. + /// Index of the endpoint. + /// The source id hash. + /// The message handle. + /// True of parsing was successful, else false. + public static bool TryParseArtifact(string artifact, ref short typeCodeValue, ref short endpointIndex, ref byte[] sourceIdHash, ref byte[] messageHandle) + { + try + { + ParseArtifact(artifact, ref typeCodeValue, ref endpointIndex, ref sourceIdHash, ref messageHandle); + } + catch + { + return false; + } + + return true; + } + + /// + /// Retrieves the endpoint index from an artifact + /// + /// The artifact. + /// The endpoint index. + public static ushort GetEndpointIndex(string artifact) + { + short parsedTypeCode = -1; + short parsedEndpointIndex = -1; + var parsedSourceIdHash = new byte[20]; + var parsedMessageHandle = new byte[20]; + + if (TryParseArtifact(artifact, ref parsedTypeCode, ref parsedEndpointIndex, ref parsedSourceIdHash, ref parsedMessageHandle)) + { + return (ushort)parsedEndpointIndex; + } + + throw new ArgumentException("Malformed artifact", "artifact"); + } + } +} diff --git a/src/SAML2.Standard/Utils/Compression.cs b/src/SAML2.Standard/Utils/Compression.cs new file mode 100644 index 0000000..2422731 --- /dev/null +++ b/src/SAML2.Standard/Utils/Compression.cs @@ -0,0 +1,52 @@ +using System; +using System.IO; +using System.IO.Compression; +using System.Text; + +namespace SAML2.Utils +{ + public class Compression + { + /// + /// Take a Base64-encoded string, decompress the result using the DEFLATE algorithm and return the resulting + /// string. + /// + /// The value. + /// The decompressed value. + public static string DeflateDecompress(string value) + { + var encoded = Convert.FromBase64String(value); + var memoryStream = new MemoryStream(encoded); + + var result = new StringBuilder(); + using (var stream = new DeflateStream(memoryStream, CompressionMode.Decompress)) { + var testStream = new StreamReader(new BufferedStream(stream), Encoding.UTF8); + + // It seems we need to "peek" on the StreamReader to get it started. If we don't do this, the first call to + // ReadToEnd() will return string.empty. + testStream.Peek(); + result.Append(testStream.ReadToEnd()); + + stream.Close(); + } + + return result.ToString(); + } + + /// + /// Uses DEFLATE compression to compress the input value. Returns the result as a Base64 encoded string. + /// + /// The val. + /// The compressed string. + public static string DeflateEncode(string val) + { + var memoryStream = new MemoryStream(); + using (var writer = new StreamWriter(new DeflateStream(memoryStream, CompressionMode.Compress, true), new UTF8Encoding(false))) { + writer.Write(val); + writer.Close(); + + return Convert.ToBase64String(memoryStream.GetBuffer(), 0, (int)memoryStream.Length, Base64FormattingOptions.None); + } + } + } +} diff --git a/src/SAML2.Standard/Utils/IDPSelectionUtil.cs b/src/SAML2.Standard/Utils/IDPSelectionUtil.cs new file mode 100644 index 0000000..e0e1e29 --- /dev/null +++ b/src/SAML2.Standard/Utils/IDPSelectionUtil.cs @@ -0,0 +1,168 @@ +using SAML2.Config; +using SAML2.Logging; +using SAML2.Standard; +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Linq; + +namespace SAML2.Utils +{ + /// + /// This delegate is used handling events, where the framework have several configured IDP's to choose from + /// and needs information on, which one to use. + /// + /// List of configured endpoints + /// The for the IDP that should be used for authentication + public delegate IdentityProvider IdpSelectionEventHandler(IdentityProviders ep); + + /// + /// Contains helper functionality for selection of IDP when more than one is configured + /// + public class IdpSelectionUtil + { + public const string IdpChoiceParameterName = "cidp"; + + private readonly IInternalLogger logger; + + public IdpSelectionUtil(IInternalLogger logger) + { + this.logger = logger ?? throw new ArgumentNullException("logger"); + } + /// + /// The event handler will be called, when no Common Domain Cookie is set, + /// no IdentityProviderEndpointElement is marked as default in the configuration, + /// and no idpSelectionUrl is configured. + /// Make sure that only one event handler is added, since only the last result of the event handler invocation will be used. + /// + public static event IdpSelectionEventHandler IdpSelectionEvent; + + /// + /// Helper method for generating URL to a link, that the user can click to select that particular IdentityProviderEndpointElement for authorization. + /// Usually not called directly, but called from IdentityProviderEndpointElement.GetIDPLoginUrl() + /// + /// Id of IDP that an authentication URL is needed for + /// A URL that can be used for logging in at the IDP + public static string GetIdpLoginUrl(string idpId, Saml2Configuration config) + { + throw new System.NotImplementedException(); + // TODO: This should be needed eventually, but probably has to be handled outside of core + //return string.Format("{0}?{1}={2}", config.ServiceProvider.Endpoints.DefaultSignOnEndpoint.LocalPath, Saml20SignonHandler.IdpChoiceParameterName, HttpUtility.UrlEncode(idpId)); + } + + /// + /// Invokes the IDP selection event handler. + /// + /// The endpoints. + /// The . + public static IdentityProvider InvokeIDPSelectionEventHandler(IdentityProviders endpoints) + { + return IdpSelectionEvent != null ? IdpSelectionEvent(endpoints) : null; + } + + /// + /// Looks through the Identity Provider configurations and + /// + /// The identity provider id. + /// The . + public static IdentityProvider RetrieveIDPConfiguration(string idpId, Saml2Configuration config) + { + return config.IdentityProviders.FirstOrDefault(x => x.Id == idpId); + } + + /// + /// Handles the selection of an IDP. If only one IDP is found, the user is automatically redirected to it. + /// If several are found, and nothing indicates to which one the user should be sent, this method returns null. + /// + /// The context. + /// Configuration. If null, configuration will be populated from application config + /// The . + public IdentityProvider RetrieveIDP(NameValueCollection allparams, NameValueCollection queryString, Saml2Configuration config, Action redirectToSelection) + { + // If idpChoice is set, use it value + if (!string.IsNullOrEmpty(allparams[IdpChoiceParameterName])) { + logger.DebugFormat(TraceMessages.IdentityProviderRetreivedFromQueryString, allparams[IdpChoiceParameterName]); + var endPoint = config.IdentityProviders.FirstOrDefault(x => x.Id == allparams[IdpChoiceParameterName]); + if (endPoint != null) { + return endPoint; + } + } + + // If we have a common domain cookie, use it's value + // It must have been returned from the local common domain cookie reader endpoint. + if (!string.IsNullOrEmpty(queryString["_saml_idp"])) { + var cdc = new Protocol.CommonDomainCookie(queryString["_saml_idp"]); + if (cdc.IsSet) { + var endPoint = config.IdentityProviders.FirstOrDefault(x => x.Id == cdc.PreferredIDP); + if (endPoint != null) { + logger.DebugFormat(TraceMessages.IdentityProviderRetreivedFromCommonDomainCookie, cdc.PreferredIDP); + return endPoint; + } + + logger.WarnFormat(ErrorMessages.CommonDomainCookieIdentityProviderInvalid, cdc.PreferredIDP); + } + } + + // If there is only one configured IdentityProviderEndpointElement lets just use that + if (config.IdentityProviders.Count == 1 && config.IdentityProviders[0].Metadata != null) { + logger.DebugFormat(TraceMessages.IdentityProviderRetreivedFromDefault, config.IdentityProviders[0].Name); + return config.IdentityProviders[0]; + } + + // If one of the endpoints are marked with default, use that one + var defaultIDP = config.IdentityProviders.FirstOrDefault(idp => idp.Default); + if (defaultIDP != null) { + logger.DebugFormat(TraceMessages.IdentityProviderRetreivedFromDefault, defaultIDP.Id); + return defaultIDP; + } + + // In case an IDP selection url has been configured, redirect to that one. + if (!string.IsNullOrEmpty(config.IdentityProviders.SelectionUrl)) { + logger.DebugFormat(TraceMessages.IdentityProviderRetreivedFromSelection, config.IdentityProviders.SelectionUrl); + redirectToSelection(config.IdentityProviders.SelectionUrl); + return null; + } + + // If an IDPSelectionEvent handler is present, request the handler for an IDP endpoint to use. + return IdpSelectionUtil.InvokeIDPSelectionEventHandler(config.IdentityProviders); + } + + /// + /// Determine which endpoint to use based on the protocol defaults, configuration data and metadata. + /// + /// The binding to use if none has been specified in the configuration and the metadata allows all bindings. + /// The endpoint as described in the configuration. May be null. + /// A list of endpoints of the given type (e.g. SSO or SLO) that the metadata contains. + /// The . + public static IdentityProviderEndpoint DetermineEndpointConfiguration(BindingType defaultBinding, IdentityProviderEndpoint config, List metadata) + { + var result = new IdentityProviderEndpoint { Binding = defaultBinding }; + + // Determine which binding to use. + if (config != null) { + result.Binding = config.Binding; + } else { + // Verify that the metadata allows the default binding. + var allowed = metadata.Exists(el => el.Binding == defaultBinding); + if (!allowed) { + result.Binding = result.Binding == BindingType.Post + ? BindingType.Redirect + : BindingType.Post; + } + } + + if (config != null && !string.IsNullOrEmpty(config.Url)) { + result.Url = config.Url; + } else { + var endpoint = metadata.Find(el => el.Binding == result.Binding); + if (endpoint == null) { + throw new FormatException(string.Format("No IdentityProvider supporting SAML binding {0} found in metadata", result.Binding)); + } + + result.Url = endpoint.Url; + } + + return result; + } + } +} diff --git a/src/SAML2.Standard/Utils/MetadataUtils.cs b/src/SAML2.Standard/Utils/MetadataUtils.cs new file mode 100644 index 0000000..e6683e8 --- /dev/null +++ b/src/SAML2.Standard/Utils/MetadataUtils.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using SAML2.Config; +using SAML2.Logging; +using System.Security.Cryptography.X509Certificates; +using SAML2.Standard; + +namespace SAML2.Utils +{ + public class MetadataUtils + { + private readonly Saml2Configuration configuration; + private readonly IInternalLogger logger; + + public MetadataUtils(Config.Saml2Configuration configuration, Logging.IInternalLogger logger) + { + this.configuration = configuration ?? throw new ArgumentNullException("configuration"); + this.logger = logger ?? throw new ArgumentNullException("logger"); + } + /// + /// Creates the metadata document. + /// + /// The context. + /// if set to true sign the document. + public string CreateMetadataDocument(Encoding encoding, bool sign) + { + logger.Debug(TraceMessages.MetadataDocumentBeingCreated); + + var keyinfo = new System.Security.Cryptography.Xml.KeyInfo(); + var keyClause = new System.Security.Cryptography.Xml.KeyInfoX509Data(configuration.ServiceProvider.SigningCertificate, X509IncludeOption.EndCertOnly); + keyinfo.AddClause(keyClause); + + var doc = new Saml20MetadataDocument(configuration, keyinfo, sign); + + logger.Debug(TraceMessages.MetadataDocumentCreated); + return doc.ToXml(encoding, configuration.ServiceProvider.SigningCertificate); + } + } +} diff --git a/src/SAML2.Standard/Utils/Saml20Utils.cs b/src/SAML2.Standard/Utils/Saml20Utils.cs new file mode 100644 index 0000000..3869df8 --- /dev/null +++ b/src/SAML2.Standard/Utils/Saml20Utils.cs @@ -0,0 +1,69 @@ +using System; +using System.Xml; + +namespace SAML2.Utils +{ + /// + /// Helpers for converting between string and DateTime representations of UTC date-times + /// and for enforcing the UTC-string-format demand for xml strings in SAML2.0 + /// + public static class Saml20Utils + { + /// + /// Converts from the UTC string. + /// + /// The value. + /// The represented DateTime. + public static DateTime FromUtcString(string value) + { + try + { + return XmlConvert.ToDateTime(value, XmlDateTimeSerializationMode.Utc); + } + catch (FormatException fe) + { + throw new Saml20FormatException("Invalid DateTime-string (non-UTC) found in saml:Assertion", fe); + } + } + + /// + /// Toes the UTC string. + /// + /// The value. + /// The DateTime represented as a UTC string. + public static string ToUtcString(DateTime value) + { + return XmlConvert.ToString(value, XmlDateTimeSerializationMode.Utc); + } + + /// + /// Make sure that the ID elements is at least 128 bits in length (SAML2.0 standard section 1.3.4) + /// + /// The id. + /// True if the id is valid, else false. + public static bool ValidateIdString(string id) + { + return id != null && id.Length >= 16; + } + + /// + /// A string value marked as OPTIONAL in [SAML2.0 standard] must contain at least one non-whitespace character + /// + /// The opt string. + /// True if the string is value, else false. + public static bool ValidateOptionalString(string optString) + { + return optString == null || ValidateRequiredString(optString); + } + + /// + /// A string value marked as REQUIRED in [SAML2.0 standard] must contain at least one non-whitespace character + /// + /// The required string. + /// True if the string is value, else false. + public static bool ValidateRequiredString(string reqString) + { + return !(string.IsNullOrEmpty(reqString) || reqString.Trim().Length == 0); + } + } +} diff --git a/src/SAML2.Standard/Utils/Serialization.cs b/src/SAML2.Standard/Utils/Serialization.cs new file mode 100644 index 0000000..efcb0bf --- /dev/null +++ b/src/SAML2.Standard/Utils/Serialization.cs @@ -0,0 +1,105 @@ +using System.IO; +using System.Xml; +using System.Xml.Serialization; + +namespace SAML2.Utils +{ + /// + /// Functions for typed serialization and deserialization of objects. + /// + public static class Serialization + { + /// + /// Initializes static members of the class. + /// + static Serialization() + { + XmlNamespaces = new XmlSerializerNamespaces(); + XmlNamespaces.Add("samlp", Saml20Constants.Protocol); + XmlNamespaces.Add("saml", Saml20Constants.Assertion); + } + + /// + /// Gets the instance of XmlSerializerNamespaces that is used by this class. + /// + /// The XmlSerializerNamespaces instance. + public static XmlSerializerNamespaces XmlNamespaces { get; private set; } + + /// + /// Reads and deserializes an item from the reader + /// + /// The type of object to deserialize to. + /// The reader. + /// The deserialized item. + public static T Deserialize(XmlReader reader) + { + var serializer = new XmlSerializer(typeof(T)); + var item = (T)serializer.Deserialize(reader); + + return item; + } + + /// + /// Deserializes an item from an XML string. + /// + /// The type of object to deserialize to. + /// The XML. + /// The deserialized item. + public static T DeserializeFromXmlString(string xml) + { + var reader = new XmlTextReader(new StringReader(xml)); + return Deserialize(reader); + } + + /// + /// Serializes the specified item to a stream. + /// + /// The items type + /// The item to serialize. + /// The stream to serialize to. + public static void Serialize(T item, Stream stream) + { + var serializer = new XmlSerializer(typeof(T)); + serializer.Serialize(stream, item, XmlNamespaces); + stream.Flush(); + } + + /// + /// Serializes the specified item. + /// + /// The items type + /// The item. + /// An XmlDocument containing the serialized form of the item + public static XmlDocument Serialize(T item) + { + var stream = new MemoryStream(); + Serialize(item, stream); + + // create the XmlDocument to return + var doc = new XmlDocument(); + stream.Seek(0, SeekOrigin.Begin); + doc.Load(stream); + + stream.Close(); + + return doc; + } + + /// + /// Serializes an item to an XML string. + /// + /// The type of object to serialize. + /// The item. + /// The serialized string. + public static string SerializeToXmlString(T item) + { + var stream = new MemoryStream(); + Serialize(item, stream); + + var reader = new StreamReader(stream); + stream.Seek(0, SeekOrigin.Begin); + + return reader.ReadToEnd(); + } + } +} diff --git a/src/SAML2.Standard/Utils/TimeRestrictionValidation.cs b/src/SAML2.Standard/Utils/TimeRestrictionValidation.cs new file mode 100644 index 0000000..427e237 --- /dev/null +++ b/src/SAML2.Standard/Utils/TimeRestrictionValidation.cs @@ -0,0 +1,34 @@ +using System; + +namespace SAML2.Utils +{ + /// + /// Utility functions for validating SAML message time stamps + /// + public static class TimeRestrictionValidation + { + /// + /// Handle allowed clock skew by decreasing notBefore with allowedClockSkew + /// + /// The not before. + /// The now. + /// The allowed clock skew. + /// True if the not before value is valid, else false. + public static bool NotBeforeValid(DateTime notBefore, DateTime now, TimeSpan allowedClockSkew) + { + return notBefore.Subtract(allowedClockSkew) <= now; + } + + /// + /// Handle allowed clock skew by increasing notOnOrAfter with allowedClockSkew + /// + /// The not on or after. + /// The now. + /// The allowed clock skew. + /// True if the not on or after value is valid, else false. + public static bool NotOnOrAfterValid(DateTime notOnOrAfter, DateTime now, TimeSpan allowedClockSkew) + { + return now < notOnOrAfter.Add(allowedClockSkew); + } + } +} \ No newline at end of file diff --git a/src/SAML2.Standard/Utils/XmlSignatureUtils.cs b/src/SAML2.Standard/Utils/XmlSignatureUtils.cs new file mode 100644 index 0000000..3ea68a7 --- /dev/null +++ b/src/SAML2.Standard/Utils/XmlSignatureUtils.cs @@ -0,0 +1,555 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; +using System.Security.Cryptography.Xml; +using System.Xml; +using SAML2.Config; + +namespace SAML2.Utils +{ + /// + /// This class contains methods that creates and validates signatures on XmlDocuments. + /// + public class XmlSignatureUtils + { + #region Public methods + + /// + /// Verifies the signature of the XmlDocument instance using the key enclosed with the signature. + /// + /// The doc. + /// true if the document's signature can be verified. false if the signature could + /// not be verified. + /// if the XmlDocument instance does not contain a signed XML document. + public static bool CheckSignature(XmlDocument doc) + { + CheckDocument(doc); + var signedXml = RetrieveSignature(doc); + + if (signedXml.SignatureMethod.Contains("rsa-sha256")) + { + // SHA256 keys must be obtained from message manually + var trustedCertificates = GetCertificates(doc); + foreach (var cert in trustedCertificates) + { + if (signedXml.CheckSignature(cert.PublicKey.Key)) + { + return true; + } + } + + return false; + } + + return signedXml.CheckSignature(); + } + + /// + /// Verifies the signature of the XmlDocument instance using the key given as a parameter. + /// + /// The doc. + /// The algorithm. + /// true if the document's signature can be verified. false if the signature could + /// not be verified. + /// if the XmlDocument instance does not contain a signed XML document. + public static bool CheckSignature(XmlDocument doc, AsymmetricAlgorithm alg) + { + CheckDocument(doc); + var signedXml = RetrieveSignature(doc); + + return signedXml.CheckSignature(alg); + } + + /// + /// Verifies the signature of the XmlElement instance using the key given as a parameter. + /// + /// The element. + /// The algorithm. + /// true if the element's signature can be verified. false if the signature could + /// not be verified. + /// if the XmlDocument instance does not contain a signed XML element. + public static bool CheckSignature(XmlElement el, AsymmetricAlgorithm alg) + { + // CheckDocument(element); + var signedXml = RetrieveSignature(el); + + return signedXml.CheckSignature(alg); + } + + /// + /// Verify the given document using a KeyInfo instance. The KeyInfo instance's KeyClauses will be traversed for + /// elements that can verify the signature, e.g. certificates or keys. If nothing is found, an exception is thrown. + /// + /// The doc. + /// The key info. + /// true if the element's signature can be verified. false if the signature could + /// not be verified. + public static bool CheckSignature(XmlDocument doc, KeyInfo keyinfo) + { + CheckDocument(doc); + var signedXml = RetrieveSignature(doc); + + AsymmetricAlgorithm alg = null; + X509Certificate2 cert = null; + foreach (KeyInfoClause clause in keyinfo) + { + if (clause is RSAKeyValue) + { + var key = (RSAKeyValue)clause; + alg = key.Key; + break; + } + + if (clause is KeyInfoX509Data) + { + var x509Data = (KeyInfoX509Data)clause; + var count = x509Data.Certificates.Count; + cert = (X509Certificate2)x509Data.Certificates[count - 1]; + } + else if (clause is DSAKeyValue) + { + var key = (DSAKeyValue)clause; + alg = key.Key; + break; + } + } + + if (alg == null && cert == null) + { + throw new InvalidOperationException("Unable to locate the key or certificate to verify the signature."); + } + + return alg != null ? signedXml.CheckSignature(alg) : signedXml.CheckSignature(cert, true); + } + + /// + /// Attempts to retrieve an asymmetric key from the KeyInfoClause given as parameter. + /// + /// The key info clause. + /// null if the key could not be found. + public static AsymmetricAlgorithm ExtractKey(KeyInfoClause keyInfoClause) + { + if (keyInfoClause is RSAKeyValue) + { + var key = (RSAKeyValue)keyInfoClause; + return key.Key; + } + + if (keyInfoClause is KeyInfoX509Data) + { + var cert = GetCertificateFromKeyInfo((KeyInfoX509Data)keyInfoClause); + return cert != null ? cert.PublicKey.Key : null; + } + + if (keyInfoClause is DSAKeyValue) + { + var key = (DSAKeyValue)keyInfoClause; + return key.Key; + } + + return null; + } + + /// + /// Returns the KeyInfo element that is included with the signature in the document. + /// + /// The doc. + /// The signature . + /// if the document is not signed. + public static KeyInfo ExtractSignatureKeys(XmlDocument doc) + { + CheckDocument(doc); + if (doc.DocumentElement != null) + { + var signedXml = new SignedXml(doc.DocumentElement); + + var nodeList = doc.GetElementsByTagName(Schema.XmlDSig.Signature.ElementName, Saml20Constants.Xmldsig); + if (nodeList.Count == 0) + { + throw new InvalidOperationException("The XmlDocument does not contain a signature."); + } + + signedXml.LoadXml((XmlElement)nodeList[0]); + + return signedXml.KeyInfo; + } + + return null; + } + + /// + /// Returns the KeyInfo element that is included with the signature in the element. + /// + /// The element. + /// The signature . + /// if the document is not signed. + public static KeyInfo ExtractSignatureKeys(XmlElement element) + { + CheckDocument(element); + var signedXml = new SignedXml(element); + + var nodeList = element.GetElementsByTagName(Schema.XmlDSig.Signature.ElementName, Saml20Constants.Xmldsig); + if (nodeList.Count == 0) + { + throw new InvalidOperationException("The XmlDocument does not contain a signature."); + } + + signedXml.LoadXml((XmlElement)nodeList[0]); + + return signedXml.KeyInfo; + } + + /// + /// Gets the certificate from key info. + /// + /// The key info. + /// The last certificate in the chain + public static X509Certificate2 GetCertificateFromKeyInfo(KeyInfoX509Data keyInfo) + { + var count = keyInfo.Certificates.Count; + if (count == 0) + { + return null; + } + + var cert = (X509Certificate2)keyInfo.Certificates[count - 1]; + + return cert; + } + + /// + /// Checks if a document contains a signature. + /// + /// The doc. + /// true if the specified doc is signed; otherwise, false. + public static bool IsSigned(XmlDocument doc) + { + CheckDocument(doc); + var nodeList = doc.GetElementsByTagName(Schema.XmlDSig.Signature.ElementName, Saml20Constants.Xmldsig); + + return nodeList.Count > 0; + } + + /// + /// Checks if an element contains a signature. + /// + /// The element. + /// true if the specified element is signed; otherwise, false. + public static bool IsSigned(XmlElement el) + { + CheckDocument(el); + var nodeList = el.GetElementsByTagName(Schema.XmlDSig.Signature.ElementName, Saml20Constants.Xmldsig); + + return nodeList.Count > 0; + } + + /// + /// Signs an XmlDocument with an xml signature using the signing certificate specified in the + /// configuration file. + /// + /// The XmlDocument to be signed + /// The id of the topmost element in the XmlDocument + public static void SignDocument(XmlDocument doc, string id, Saml2Configuration config) + { + SignDocument(doc, id, config.ServiceProvider.SigningCertificate); + } + + /// + /// Signs an XmlDocument with an xml signature using the signing certificate given as argument to the method. + /// + /// The XmlDocument to be signed + /// The id of the topmost element in the XmlDocument + /// The certificate used to sign the document + public static void SignDocument(XmlDocument doc, string id, X509Certificate2 cert) + { + var signedXml = new SignedXml(doc); + signedXml.SignedInfo.CanonicalizationMethod = SignedXml.XmlDsigExcC14NTransformUrl; + signedXml.SigningKey = cert.PrivateKey; + + // Retrieve the value of the "ID" attribute on the root assertion element. + var reference = new Reference("#" + id); + + reference.AddTransform(new XmlDsigEnvelopedSignatureTransform()); + reference.AddTransform(new XmlDsigExcC14NTransform()); + + signedXml.AddReference(reference); + + // Include the public key of the certificate in the assertion. + signedXml.KeyInfo = new KeyInfo(); + signedXml.KeyInfo.AddClause(new KeyInfoX509Data(cert, X509IncludeOption.WholeChain)); + + signedXml.ComputeSignature(); + + // Append the computed signature. The signature must be placed as the sibling of the Issuer element. + if (doc.DocumentElement != null) + { + var nodes = doc.DocumentElement.GetElementsByTagName("Issuer", Saml20Constants.Assertion); + + // doc.DocumentElement.InsertAfter(doc.ImportNode(signedXml.GetXml(), true), nodes[0]); + var parentNode = nodes[0].ParentNode; + if (parentNode != null) + { + parentNode.InsertAfter(doc.ImportNode(signedXml.GetXml(), true), nodes[0]); + } + } + } + + #endregion + + #region Private methods + + /// + /// Do checks on the document given. Every public method accepting a XmlDocument instance as parameter should + /// call this method before continuing. + /// + /// The doc. + private static void CheckDocument(XmlDocument doc) + { + if (doc == null) + { + throw new ArgumentNullException("doc"); + } + + if (!doc.PreserveWhitespace) + { + throw new InvalidOperationException("The XmlDocument must have its \"PreserveWhitespace\" property set to true when a signed document is loaded."); + } + } + + /// + /// Do checks on the element given. Every public method accepting a XmlElement instance as parameter should + /// call this method before continuing. + /// + /// The element. + private static void CheckDocument(XmlElement el) + { + if (el == null) + { + throw new ArgumentNullException("el"); + } + + if (el.OwnerDocument != null && !el.OwnerDocument.PreserveWhitespace) + { + throw new InvalidOperationException("The XmlDocument must have its \"PreserveWhitespace\" property set to true when a signed document is loaded."); + } + } + + /// + /// Gets the certificates. + /// + /// The document. + /// List of . + private static List GetCertificates(XmlDocument doc) + { + var certificates = new List(); + var nodeList = doc.GetElementsByTagName("ds:X509Certificate"); + if (nodeList.Count == 0) + { + nodeList = doc.GetElementsByTagName("X509Certificate"); + } + + foreach (XmlNode xn in nodeList) + { + try + { + var xc = new X509Certificate2(Convert.FromBase64String(xn.InnerText)); + certificates.Add(xc); + } + catch + { + // Swallow the certificate parse error + } + } + + return certificates; + } + + /// + /// Digs the <Signature> element out of the document. + /// + /// The doc. + /// The . + /// if the document does not contain a signature. + private static SignedXml RetrieveSignature(XmlDocument doc) + { + return RetrieveSignature(doc.DocumentElement); + } + + /// + /// Digs the <Signature> element out of the document. + /// + /// The element. + /// The . + /// if the document does not contain a signature. + private static SignedXml RetrieveSignature(XmlElement el) + { + if (el.OwnerDocument.DocumentElement == null) + { + var doc = new XmlDocument() { PreserveWhitespace = true }; + doc.LoadXml(el.OuterXml); + el = doc.DocumentElement; + } + + SignedXml signedXml = new SignedXmlWithIdResolvement(el); + var nodeList = el.GetElementsByTagName(Schema.XmlDSig.Signature.ElementName, Saml20Constants.Xmldsig); + if (nodeList.Count == 0) + { + throw new InvalidOperationException("Document does not contain a signature to verify."); + } + + signedXml.LoadXml((XmlElement)nodeList[0]); + + // To support SHA256 for XML signatures, an additional algorithm must be enabled. + // This is not supported in .Net versions older than 4.0. In older versions, + // an exception will be raised if an SHA256 signature method is attempted to be used. + if (signedXml.SignatureMethod.Contains("rsa-sha256")) + { + var addAlgorithmMethod = typeof(CryptoConfig).GetMethod("AddAlgorithm", BindingFlags.Public | BindingFlags.Static); + if (addAlgorithmMethod == null) + { + throw new InvalidOperationException("This version of .Net does not support CryptoConfig.AddAlgorithm. Enabling sha256 not psosible."); + } + + addAlgorithmMethod.Invoke(null, new object[] { typeof(RSAPKCS1SHA256SignatureDescription), new[] { signedXml.SignatureMethod } }); + } + + // verify that the inlined signature has a valid reference uri + VerifyReferenceUri(signedXml, el.GetAttribute("ID")); + + return signedXml; + } + + /// + /// Verifies that the reference uri (if any) points to the correct element. + /// + /// the ds:signature element + /// the expected id referenced by the ds:signature element + private static void VerifyReferenceUri(SignedXml signedXml, string id) + { + if (id == null) + { + throw new InvalidOperationException("Cannot match null id"); + } + + if (signedXml.SignedInfo.References.Count <= 0) + { + throw new InvalidOperationException("No references in Signature element"); + } + + var reference = (Reference)signedXml.SignedInfo.References[0]; + var uri = reference.Uri; + + // empty uri is okay - indicates that everything is signed + if (!string.IsNullOrEmpty(uri)) + { + if (!uri.StartsWith("#")) + { + throw new InvalidOperationException("Signature reference URI is not a document fragment reference. Uri = '" + uri + "'"); + } + + if (uri.Length < 2 || !id.Equals(uri.Substring(1))) + { + throw new InvalidOperationException("Rererence URI = '" + uri.Substring(1) + "' does not match expected id = '" + id + "'"); + } + } + } + + #endregion + + /// + /// Used to validate SHA256 signatures + /// + public class RSAPKCS1SHA256SignatureDescription : SignatureDescription + { + /// + /// Initializes a new instance of the class. + /// + public RSAPKCS1SHA256SignatureDescription() + { + KeyAlgorithm = "System.Security.Cryptography.RSACryptoServiceProvider"; + DigestAlgorithm = "System.Security.Cryptography.SHA256Managed"; + FormatterAlgorithm = "System.Security.Cryptography.RSAPKCS1SignatureFormatter"; + DeformatterAlgorithm = "System.Security.Cryptography.RSAPKCS1SignatureDeformatter"; + } + + /// + /// Creates signature deformatter + /// + /// The key to use in the . + /// The newly created instance. + public override AsymmetricSignatureDeformatter CreateDeformatter(AsymmetricAlgorithm key) + { + var asymmetricSignatureDeformatter = (AsymmetricSignatureDeformatter) + CryptoConfig.CreateFromName(DeformatterAlgorithm); + asymmetricSignatureDeformatter.SetKey(key); + asymmetricSignatureDeformatter.SetHashAlgorithm("SHA256"); + + return asymmetricSignatureDeformatter; + } + } + + /// + /// Signed XML with Id Resolvement class. + /// + public class SignedXmlWithIdResolvement : SignedXml + { + /// + /// Initializes a new instance of the class. + /// + /// The document. + public SignedXmlWithIdResolvement(XmlDocument document) : base(document) { } + + /// + /// Initializes a new instance of the class from the specified object. + /// + /// The object to use to initialize the new instance of . + /// + /// The parameter is null. + /// + public SignedXmlWithIdResolvement(XmlElement elem) : base(elem) { } + + /// + /// Initializes a new instance of the class. + /// + public SignedXmlWithIdResolvement() { } + + /// + /// Returns the object with the specified ID from the specified object. + /// + /// The object to retrieve the object from. + /// The ID of the object to retrieve from the object. + /// The object with the specified ID from the specified object, or null if it could not be found. + public override XmlElement GetIdElement(XmlDocument document, string idValue) + { + var elem = base.GetIdElement(document, idValue); + if (elem == null) + { + var nl = document.GetElementsByTagName("*"); + var enumerator = nl.GetEnumerator(); + while (enumerator != null && enumerator.MoveNext()) + { + var node = (XmlNode)enumerator.Current; + if (node == null || node.Attributes == null) + { + continue; + } + + var nodeEnum = node.Attributes.GetEnumerator(); + while (nodeEnum != null && nodeEnum.MoveNext()) + { + var attr = (XmlAttribute)nodeEnum.Current; + if (attr != null && (attr.LocalName.ToLower() == "id" && attr.Value == idValue && node is XmlElement)) + { + return (XmlElement)node; + } + } + } + } + + return elem; + } + } + } +} diff --git a/src/SAML2.Standard/Validation/ISaml20AssertionValidator.cs b/src/SAML2.Standard/Validation/ISaml20AssertionValidator.cs new file mode 100644 index 0000000..921ef69 --- /dev/null +++ b/src/SAML2.Standard/Validation/ISaml20AssertionValidator.cs @@ -0,0 +1,24 @@ +using System; +using SAML2.Schema.Core; + +namespace SAML2.Validation +{ + /// + /// SAML2 Assertion Validator interface. + /// + public interface ISaml20AssertionValidator + { + /// + /// Validates the assertion. + /// + /// The assertion. + void ValidateAssertion(Assertion assertion); + + /// + /// Validates the time restrictions. + /// + /// The assertion. + /// The allowed clock skew. + void ValidateTimeRestrictions(Assertion assertion, TimeSpan allowedClockSkew); + } +} diff --git a/src/SAML2.Standard/Validation/ISaml20AttributeValidator.cs b/src/SAML2.Standard/Validation/ISaml20AttributeValidator.cs new file mode 100644 index 0000000..169d124 --- /dev/null +++ b/src/SAML2.Standard/Validation/ISaml20AttributeValidator.cs @@ -0,0 +1,23 @@ +using SAML2.Schema.Core; +using SAML2.Schema.Protocol; + +namespace SAML2.Validation +{ + /// + /// SAML2 Attributes Validator interface. + /// + public interface ISaml20AttributeValidator + { + /// + /// Validates the attribute. + /// + /// The SAML attribute. + void ValidateAttribute(SamlAttribute samlAttribute); + + /// + /// Validates the encrypted attribute. + /// + /// The encrypted element. + void ValidateEncryptedAttribute(EncryptedElement encryptedElement); + } +} diff --git a/src/SAML2.Standard/Validation/ISaml20NameIDValidator.cs b/src/SAML2.Standard/Validation/ISaml20NameIDValidator.cs new file mode 100644 index 0000000..887175b --- /dev/null +++ b/src/SAML2.Standard/Validation/ISaml20NameIDValidator.cs @@ -0,0 +1,23 @@ +using SAML2.Schema.Core; +using SAML2.Schema.Protocol; + +namespace SAML2.Validation +{ + /// + /// SAML2 NameID validator interface. + /// + public interface ISaml20NameIdValidator + { + /// + /// Validates the name ID. + /// + /// The name ID. + void ValidateNameId(NameId nameId); + + /// + /// Validates the encrypted ID. + /// + /// The encrypted ID. + void ValidateEncryptedId(EncryptedElement encryptedId); + } +} diff --git a/src/SAML2.Standard/Validation/ISaml20StatementValidator.cs b/src/SAML2.Standard/Validation/ISaml20StatementValidator.cs new file mode 100644 index 0000000..0ebaadf --- /dev/null +++ b/src/SAML2.Standard/Validation/ISaml20StatementValidator.cs @@ -0,0 +1,16 @@ +using SAML2.Schema.Core; + +namespace SAML2.Validation +{ + /// + /// SAML2 Statement Validator interface. + /// + public interface ISaml20StatementValidator + { + /// + /// Validates the statement. + /// + /// The statement. + void ValidateStatement(StatementAbstract statement); + } +} diff --git a/src/SAML2.Standard/Validation/ISaml20SubjectConfirmationDataValidator.cs b/src/SAML2.Standard/Validation/ISaml20SubjectConfirmationDataValidator.cs new file mode 100644 index 0000000..9daa629 --- /dev/null +++ b/src/SAML2.Standard/Validation/ISaml20SubjectConfirmationDataValidator.cs @@ -0,0 +1,16 @@ +using SAML2.Schema.Core; + +namespace SAML2.Validation +{ + /// + /// SAML2 Subject Confirmation Data Validator interface. + /// + public interface ISaml20SubjectConfirmationDataValidator + { + /// + /// Validates the subject confirmation data. + /// + /// The data. + void ValidateSubjectConfirmationData(SubjectConfirmationData data); + } +} diff --git a/src/SAML2.Standard/Validation/ISaml20SubjectConfirmationValidator.cs b/src/SAML2.Standard/Validation/ISaml20SubjectConfirmationValidator.cs new file mode 100644 index 0000000..e4cbe1c --- /dev/null +++ b/src/SAML2.Standard/Validation/ISaml20SubjectConfirmationValidator.cs @@ -0,0 +1,16 @@ +using SAML2.Schema.Core; + +namespace SAML2.Validation +{ + /// + /// SAML2 Subject Confirmation Validator interface. + /// + public interface ISaml20SubjectConfirmationValidator + { + /// + /// Validates the subject confirmation. + /// + /// The subject confirmation. + void ValidateSubjectConfirmation(SubjectConfirmation subjectConfirmation); + } +} \ No newline at end of file diff --git a/src/SAML2.Standard/Validation/ISaml20SubjectValidator.cs b/src/SAML2.Standard/Validation/ISaml20SubjectValidator.cs new file mode 100644 index 0000000..d2e8f1a --- /dev/null +++ b/src/SAML2.Standard/Validation/ISaml20SubjectValidator.cs @@ -0,0 +1,16 @@ +using SAML2.Schema.Core; + +namespace SAML2.Validation +{ + /// + /// SAML2 Subject Validator interface. + /// + public interface ISaml20SubjectValidator + { + /// + /// Validates the subject. + /// + /// The subject. + void ValidateSubject(Subject subject); + } +} diff --git a/src/SAML2.Standard/Validation/Saml20AssertionValidator.cs b/src/SAML2.Standard/Validation/Saml20AssertionValidator.cs new file mode 100644 index 0000000..85bc6f0 --- /dev/null +++ b/src/SAML2.Standard/Validation/Saml20AssertionValidator.cs @@ -0,0 +1,393 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using SAML2.Schema.Core; +using SAML2.Standard; +using SAML2.Utils; + +namespace SAML2.Validation +{ + /// + /// SAML2 Assertion validator. + /// + public class Saml20AssertionValidator : ISaml20AssertionValidator + { + /// + /// Use quirksMode. + /// + private readonly bool _quirksMode; + + /// + /// The allowed audience URIs. + /// + private readonly List _allowedAudienceUris; + + /// + /// NameIDValidator backing field. + /// + private readonly ISaml20NameIdValidator _nameIdValidator = new Saml20NameIdValidator(); + + /// + /// StatementValidator backing field. + /// + private readonly ISaml20StatementValidator _statementValidator = new Saml20StatementValidator(); + + /// + /// SubjectValidator backing field. + /// + private readonly ISaml20SubjectValidator _subjectValidator = new Saml20SubjectValidator(); + + /// + /// Initializes a new instance of the class. + /// + /// The allowed audience uris. + /// if set to true [quirks mode]. + public Saml20AssertionValidator(List allowedAudienceUris, bool quirksMode) + { + _allowedAudienceUris = allowedAudienceUris; + _quirksMode = quirksMode; + } + + #region ISaml20AssertionValidator interface + + /// + /// Validates the assertion. + /// + /// The assertion. + public virtual void ValidateAssertion(Assertion assertion) + { + if (assertion == null) + { + throw new ArgumentNullException("assertion"); + } + + ValidateAssertionAttributes(assertion); + ValidateSubject(assertion); + ValidateConditions(assertion); + ValidateStatements(assertion); + } + + /// + /// Validates the time restrictions. + /// + /// The assertion. + /// The allowed clock skew. + public void ValidateTimeRestrictions(Assertion assertion, TimeSpan allowedClockSkew) + { + // Conditions are not required + if (assertion.Conditions == null) + { + return; + } + + var conditions = assertion.Conditions; + var now = DateTime.UtcNow; + + // Negative allowed clock skew does not make sense - we are trying to relax the restriction interval, not restrict it any further + if (allowedClockSkew < TimeSpan.Zero) + { + allowedClockSkew = allowedClockSkew.Negate(); + } + + // NotBefore must not be in the future + if (!ValidateNotBefore(conditions.NotBefore, now, allowedClockSkew)) + { + throw new Saml20FormatException("Conditions.NotBefore must not be in the future"); + } + + // NotOnOrAfter must not be in the past + if (!ValidateNotOnOrAfter(conditions.NotOnOrAfter, now, allowedClockSkew)) + { + throw new Saml20FormatException("Conditions.NotOnOrAfter must not be in the past"); + } + + foreach (var statement in assertion.GetAuthnStatements()) + { + if (statement.SessionNotOnOrAfter != null && statement.SessionNotOnOrAfter <= now) + { + throw new Saml20FormatException("AuthnStatement attribute SessionNotOnOrAfter MUST be in the future"); + } + + // TODO: Consider validating that authnStatement.AuthnInstant is in the past + } + + if (assertion.Subject != null) + { + foreach (var subjectConfirmation in assertion.Subject.Items.OfType().Where(subjectConfirmation => subjectConfirmation.SubjectConfirmationData != null)) + { + if (!ValidateNotBefore(subjectConfirmation.SubjectConfirmationData.NotBefore, now, allowedClockSkew)) + { + throw new Saml20FormatException("SubjectConfirmationData.NotBefore must not be in the future"); + } + + if (!ValidateNotOnOrAfter(subjectConfirmation.SubjectConfirmationData.NotOnOrAfter, now, allowedClockSkew)) + { + throw new Saml20FormatException("SubjectConfirmationData.NotOnOrAfter must not be in the past"); + } + } + } + } + + #endregion + + /// + /// If both conditions.NotBefore and conditions.NotOnOrAfter are specified, NotBefore + /// MUST BE less than NotOnOrAfter + /// + /// The conditions. + /// If .NotBefore is not less than .NotOnOrAfter + private static void ValidateConditionsInterval(Conditions conditions) + { + // No settings? No restrictions + if (conditions.NotBefore == null && conditions.NotOnOrAfter == null) + { + return; + } + + if (conditions.NotBefore != null && conditions.NotOnOrAfter != null && conditions.NotBefore.Value >= conditions.NotOnOrAfter.Value) + { + throw new Saml20FormatException(string.Format("NotBefore {0} MUST BE less than NotOnOrAfter {1} on Conditions", Saml20Utils.ToUtcString(conditions.NotBefore.Value), Saml20Utils.ToUtcString(conditions.NotOnOrAfter.Value))); + } + } + + /// + /// Null fields are considered to be valid + /// + /// The not before. + /// The now. + /// The allowed clock skew. + /// True if the not before value is valid, else false. + private static bool ValidateNotBefore(DateTime? notBefore, DateTime now, TimeSpan allowedClockSkew) + { + return notBefore == null || TimeRestrictionValidation.NotBeforeValid(notBefore.Value, now, allowedClockSkew); + } + + /// + /// Handle allowed clock skew by increasing notOnOrAfter with allowedClockSkew + /// + /// The not on or after. + /// The now. + /// The allowed clock skew. + /// True if the not on or after value is valid, else false. + private static bool ValidateNotOnOrAfter(DateTime? notOnOrAfter, DateTime now, TimeSpan allowedClockSkew) + { + return notOnOrAfter == null || TimeRestrictionValidation.NotOnOrAfterValid(notOnOrAfter.Value, now, allowedClockSkew); + } + + /// + /// Validates that all the required attributes are present on the assertion. + /// Furthermore it validates validity of the Issuer element. + /// + /// The assertion. + private void ValidateAssertionAttributes(Assertion assertion) + { + // There must be a Version + if (!Saml20Utils.ValidateRequiredString(assertion.Version)) + { + throw new Saml20FormatException("Assertion element must have the Version attribute set."); + } + + // Version must be 2.0 + if (assertion.Version != Saml20Constants.Version) + { + throw new Saml20FormatException("Wrong value of version attribute on Assertion element"); + } + + // Assertion must have an ID + if (!Saml20Utils.ValidateRequiredString(assertion.Id)) + { + throw new Saml20FormatException("Assertion element must have the ID attribute set."); + } + + // Make sure that the ID elements is at least 128 bits in length (SAML2.0 std section 1.3.4) + if (!Saml20Utils.ValidateIdString(assertion.Id)) + { + throw new Saml20FormatException("Assertion element must have an ID attribute with at least 16 characters (the equivalent of 128 bits)"); + } + + // IssueInstant must be set. + if (!assertion.IssueInstant.HasValue) + { + throw new Saml20FormatException("Assertion element must have the IssueInstant attribute set."); + } + + // There must be an Issuer + if (assertion.Issuer == null) + { + throw new Saml20FormatException("Assertion element must have an issuer element."); + } + + // The Issuer element must be valid + _nameIdValidator.ValidateNameId(assertion.Issuer); + } + + /// + /// Validates the Assertion's conditions + /// Audience restrictions processing rules are: + /// - Within a single audience restriction condition in the assertion, the service must be configured + /// with an audience-list that contains at least one of the restrictions in the assertion ("OR" filter) + /// - When multiple audience restrictions are present within the same assertion, all individual audience + /// restriction conditions must be met ("AND" filter) + /// + /// The assertion. + private void ValidateConditions(Assertion assertion) + { + // Conditions are not required + if (assertion.Conditions == null) + { + return; + } + + var oneTimeUseSeen = false; + var proxyRestrictionsSeen = false; + + ValidateConditionsInterval(assertion.Conditions); + + foreach (var cat in assertion.Conditions.Items) + { + if (cat is OneTimeUse) + { + if (oneTimeUseSeen) + { + throw new Saml20FormatException("Assertion contained more than one condition of type OneTimeUse"); + } + + oneTimeUseSeen = true; + continue; + } + + if (cat is ProxyRestriction) + { + if (proxyRestrictionsSeen) + { + throw new Saml20FormatException("Assertion contained more than one condition of type ProxyRestriction"); + } + + proxyRestrictionsSeen = true; + + var proxyRestriction = (ProxyRestriction)cat; + if (!string.IsNullOrEmpty(proxyRestriction.Count)) + { + uint res; + if (!uint.TryParse(proxyRestriction.Count, out res)) + { + throw new Saml20FormatException("Count attribute of ProxyRestriction MUST BE a non-negative integer"); + } + } + + if (proxyRestriction.Audience != null) + { + foreach (var audience in proxyRestriction.Audience) + { + if (!Uri.IsWellFormedUriString(audience, UriKind.Absolute)) + { + throw new Saml20FormatException("ProxyRestriction Audience MUST BE a wellformed uri"); + } + } + } + } + + // AudienceRestriction processing goes here (section 2.5.1.4 of [SAML2.0 standard]) + if (cat is AudienceRestriction) + { + // No audience restrictions? No problems... + var audienceRestriction = (AudienceRestriction)cat; + if (audienceRestriction.Audience == null || audienceRestriction.Audience.Count == 0) + { + continue; + } + + // If there are no allowed audience uris configured for the service, the assertion is not + // valid for this service + if (_allowedAudienceUris == null || _allowedAudienceUris.Count < 1) + { + throw new Saml20FormatException("The service is not configured to meet any audience restrictions"); + } + + Uri match = null; + foreach (var audience in audienceRestriction.Audience) + { + // In QuirksMode this validation is omitted + if (!_quirksMode) + { + // The given audience value MUST BE a valid URI + if (!Uri.IsWellFormedUriString(audience, UriKind.Absolute)) + { + throw new Saml20FormatException("Audience element has value which is not a wellformed absolute uri"); + } + } + + match = _allowedAudienceUris.Find(allowedUri => allowedUri.Equals(new Uri(audience))); + if (match != null) + { + break; + } + } + + var logger = Logging.LoggerProvider.LoggerFor(GetType()); + if (logger.IsDebugEnabled) + { + var intended = string.Join(", ", audienceRestriction.Audience.ToArray()); + var allowed = string.Join(", ", _allowedAudienceUris.Select(u => u.ToString()).ToArray()); + logger.DebugFormat(TraceMessages.AudienceRestrictionValidated, intended, allowed); + } + + if (match == null) + { + throw new Saml20FormatException("The service is not configured to meet the given audience restrictions"); + } + } + } + } + + /// + /// Validates the details of the Statements present in the assertion ([SAML2.0 standard] section 2.7) + /// NOTE: the rules relating to the enforcement of a Subject element are handled during Subject validation + /// + /// The assertion. + private void ValidateStatements(Assertion assertion) + { + // Statements are not required + if (assertion.Items == null) + { + return; + } + + foreach (var o in assertion.Items) + { + _statementValidator.ValidateStatement(o); + } + } + + /// + /// Validates the subject of an Assertion + /// + /// The assertion. + private void ValidateSubject(Assertion assertion) + { + if (assertion.Subject == null) + { + // If there is no statements there must be a subject + // as specified in [SAML2.0 standard] section 2.3.3 + if (assertion.Items == null || assertion.Items.Length == 0) + { + throw new Saml20FormatException("Assertion with no Statements must have a subject."); + } + + foreach (var o in assertion.Items) + { + // If any of the below types are present there must be a subject. + if (o is AuthnStatement || o is AuthzDecisionStatement || o is AttributeStatement) + { + throw new Saml20FormatException("AuthnStatement, AuthzDecisionStatement and AttributeStatement require a subject."); + } + } + } + else + { + // If a subject is present, validate it + _subjectValidator.ValidateSubject(assertion.Subject); + } + } + } +} diff --git a/src/SAML2.Standard/Validation/Saml20AttributeValidator.cs b/src/SAML2.Standard/Validation/Saml20AttributeValidator.cs new file mode 100644 index 0000000..a300278 --- /dev/null +++ b/src/SAML2.Standard/Validation/Saml20AttributeValidator.cs @@ -0,0 +1,71 @@ +using System; +using SAML2.Schema.Core; +using SAML2.Schema.Protocol; +using SAML2.Utils; + +namespace SAML2.Validation +{ + /// + /// SAML2 Attribute validator. + /// + public class Saml20AttributeValidator : ISaml20AttributeValidator + { + /// + /// AnyAttributeValidator backing field. + /// + private readonly Saml20XmlAnyAttributeValidator _anyAttributeValidator = new Saml20XmlAnyAttributeValidator(); + + /// + /// EncryptedElementValidator backing field. + /// + private readonly Saml20EncryptedElementValidator _encryptedElementValidator = new Saml20EncryptedElementValidator(); + + /// + /// Validates the attribute. + /// + /// + /// [SAML2.0 standard] section 2.7.3.1 + /// + /// The SAML attribute. + public void ValidateAttribute(SamlAttribute samlAttribute) + { + if (samlAttribute == null) + { + throw new ArgumentNullException("samlAttribute"); + } + + if (!Saml20Utils.ValidateRequiredString(samlAttribute.Name)) + { + throw new Saml20FormatException("Name attribute of Attribute element MUST contain at least one non-whitespace character"); + } + + if (samlAttribute.AttributeValue != null) + { + foreach (object o in samlAttribute.AttributeValue) + { + if (o == null) + { + throw new Saml20FormatException("null-AttributeValue elements are not supported"); + } + } + } + + if (samlAttribute.AnyAttr != null) + { + _anyAttributeValidator.ValidateXmlAnyAttributes(samlAttribute.AnyAttr); + } + } + + /// + /// Validates the encrypted attribute. + /// + /// + /// [SAML2.0 standard] section 2.7.3.2 + /// + /// The encrypted element. + public void ValidateEncryptedAttribute(EncryptedElement encryptedElement) + { + _encryptedElementValidator.ValidateEncryptedElement(encryptedElement, "EncryptedAttribute"); + } + } +} diff --git a/src/SAML2.Standard/Validation/Saml20EncryptedElementValidator.cs b/src/SAML2.Standard/Validation/Saml20EncryptedElementValidator.cs new file mode 100644 index 0000000..3cf26a8 --- /dev/null +++ b/src/SAML2.Standard/Validation/Saml20EncryptedElementValidator.cs @@ -0,0 +1,36 @@ +using System; +using SAML2.Schema.Protocol; + +namespace SAML2.Validation +{ + /// + /// SAML2 Encrypted Element validator. + /// + public class Saml20EncryptedElementValidator + { + /// + /// Validates the encrypted element. + /// + /// The encrypted element. + /// Name of the parent node. + public void ValidateEncryptedElement(EncryptedElement encryptedElement, string parentNodeName) + { + if (encryptedElement == null) + { + throw new ArgumentNullException("encryptedElement"); + } + + if (encryptedElement.EncryptedData == null) + { + throw new Saml20FormatException(string.Format("An {0} MUST contain an xenc:EncryptedData element", parentNodeName)); + } + + if (encryptedElement.EncryptedData.Type != null + && !string.IsNullOrEmpty(encryptedElement.EncryptedData.Type) + && encryptedElement.EncryptedData.Type != Saml20Constants.Xenc + "Element") + { + throw new Saml20FormatException(string.Format("Type attribute of EncryptedData MUST have value {0} if it is present", Saml20Constants.Xenc + "Element")); + } + } + } +} diff --git a/src/SAML2.Standard/Validation/Saml20KeyInfoValidator.cs b/src/SAML2.Standard/Validation/Saml20KeyInfoValidator.cs new file mode 100644 index 0000000..722dad4 --- /dev/null +++ b/src/SAML2.Standard/Validation/Saml20KeyInfoValidator.cs @@ -0,0 +1,52 @@ +using System; +using SAML2.Schema.Core; +using SAML2.Schema.XmlDSig; + +namespace SAML2.Validation +{ + /// + /// SAML2 KeyInfo validator. + /// + public class Saml20KeyInfoValidator + { + /// + /// Validates the presence and correctness of a among the any-xml-elements of a SubjectConfirmationData + /// + /// The subject confirmation data. + public void ValidateKeyInfo(SubjectConfirmationData subjectConfirmationData) + { + if (subjectConfirmationData == null) + { + throw new Saml20FormatException("SubjectConfirmationData cannot be null when KeyInfo subelements are required"); + } + + if (subjectConfirmationData.AnyElements == null) + { + throw new Saml20FormatException(string.Format("SubjectConfirmationData element MUST have at least one {0} subelement", KeyInfo.ElementName)); + } + + var keyInfoFound = false; + foreach (var element in subjectConfirmationData.AnyElements) + { + if (element.NamespaceURI != Saml20Constants.Xmldsig || element.LocalName != KeyInfo.ElementName) + { + continue; + } + + keyInfoFound = true; + + // A KeyInfo element MUST identify a cryptographic key + if (!element.HasChildNodes) + { + throw new Saml20FormatException(string.Format("{0} subelement of SubjectConfirmationData MUST NOT be empty", KeyInfo.ElementName)); + } + } + + // There MUST BE at least one element present (from the arbitrary elements list on SubjectConfirmationData + if (!keyInfoFound) + { + throw new Saml20FormatException(string.Format("SubjectConfirmationData element MUST contain at least one {0} in namespace {1}", KeyInfo.ElementName, Saml20Constants.Xmldsig)); + } + } + } +} diff --git a/src/SAML2.Standard/Validation/Saml20NameIDValidator.cs b/src/SAML2.Standard/Validation/Saml20NameIDValidator.cs new file mode 100644 index 0000000..435b7c1 --- /dev/null +++ b/src/SAML2.Standard/Validation/Saml20NameIDValidator.cs @@ -0,0 +1,166 @@ +using System; +using System.Net.Mail; +using SAML2.Schema.Core; +using SAML2.Schema.Protocol; +using SAML2.Utils; + +namespace SAML2.Validation +{ + /// + /// SAML2 NameID validator. + /// + public class Saml20NameIdValidator : ISaml20NameIdValidator + { + /// + /// The encrypted element validator. + /// + private readonly Saml20EncryptedElementValidator _encryptedElementValidator = new Saml20EncryptedElementValidator(); + + /// + /// Validates the name ID. + /// + /// The name ID. + public void ValidateNameId(NameId nameId) + { + if (nameId == null) + { + throw new ArgumentNullException("nameId"); + } + + if (string.IsNullOrEmpty(nameId.Format)) + { + return; + } + + if (!Uri.IsWellFormedUriString(nameId.Format, UriKind.Absolute)) + { + throw new Saml20FormatException("NameID element has Format attribute which is not a wellformed absolute uri."); + } + + // The processing rules from [SAML2.0 standard] section 8.3 are implemented here + if (nameId.Format == Saml20Constants.NameIdentifierFormats.Email) + { + if (!Saml20Utils.ValidateRequiredString(nameId.Value)) + { + throw new Saml20FormatException("NameID with Email Format attribute MUST contain a Value that contains more than whitespace characters"); + } + + try + { + new MailAddress(nameId.Value); + } + catch (FormatException fe) + { + throw new Saml20FormatException("Value of NameID is not a valid email address according to the IETF RFC 2822 specification", fe); + } + catch (IndexOutOfRangeException ie) + { + throw new Saml20FormatException("Value of NameID is not a valid email address according to the IETF RFC 2822 specification", ie); + } + } + else if (nameId.Format == Saml20Constants.NameIdentifierFormats.X509SubjectName) + { + if (!Saml20Utils.ValidateRequiredString(nameId.Value)) + { + throw new Saml20FormatException("NameID with X509SubjectName Format attribute MUST contain a Value that contains more than whitespace characters"); + } + + // TODO: Consider checking for correct encoding of the Value according to the + // XML Signature Recommendation (http://www.w3.org/TR/xmldsig-core/) section 4.4.4 + } + else if (nameId.Format == Saml20Constants.NameIdentifierFormats.Windows) + { + // Required format is 'DomainName\UserName' but the domain name and the '\' are optional + if (!Saml20Utils.ValidateRequiredString(nameId.Value)) + { + throw new Saml20FormatException("NameID with Windows Format attribute MUST contain a Value that contains more than whitespace characters"); + } + } + else if (nameId.Format == Saml20Constants.NameIdentifierFormats.Kerberos) + { + // Required format is 'name[/instance]@REALM' + if (!Saml20Utils.ValidateRequiredString(nameId.Value)) + { + throw new Saml20FormatException("NameID with Kerberos Format attribute MUST contain a Value that contains more than whitespace characters"); + } + + if (nameId.Value.Length < 3) + { + throw new Saml20FormatException("NameID with Kerberos Format attribute MUST contain a Value with at least 3 characters"); + } + + if (nameId.Value.IndexOf("@") < 0) + { + throw new Saml20FormatException("NameID with Kerberos Format attribute MUST contain a Value that contains a '@'"); + } + + // TODO: Consider implementing the rules for 'name', 'instance' and 'REALM' found in IETF RFC 1510 (http://www.ietf.org/rfc/rfc1510.txt) here + } + else if (nameId.Format == Saml20Constants.NameIdentifierFormats.Entity) + { + if (!Saml20Utils.ValidateRequiredString(nameId.Value)) + { + throw new Saml20FormatException("NameID with Entity Format attribute MUST contain a Value that contains more than whitespace characters"); + } + + if (nameId.Value.Length > 1024) + { + throw new Saml20FormatException("NameID with Entity Format attribute MUST have a Value that contains no more than 1024 characters"); + } + + if (nameId.NameQualifier != null) + { + throw new Saml20FormatException("NameID with Entity Format attribute MUST NOT set the NameQualifier attribute"); + } + + if (nameId.SPNameQualifier != null) + { + throw new Saml20FormatException("NameID with Entity Format attribute MUST NOT set the SPNameQualifier attribute"); + } + + if (nameId.SPProvidedID != null) + { + throw new Saml20FormatException("NameID with Entity Format attribute MUST NOT set the SPProvidedID attribute"); + } + } + else if (nameId.Format == Saml20Constants.NameIdentifierFormats.Persistent) + { + if (!Saml20Utils.ValidateRequiredString(nameId.Value)) + { + throw new Saml20FormatException("NameID with Persistent Format attribute MUST contain a Value that contains more than whitespace characters"); + } + + if (nameId.Value.Length > 256) + { + throw new Saml20FormatException("NameID with Persistent Format attribute MUST have a Value that contains no more than 256 characters"); + } + } + else if (nameId.Format == Saml20Constants.NameIdentifierFormats.Transient) + { + if (!Saml20Utils.ValidateRequiredString(nameId.Value)) + { + throw new Saml20FormatException("NameID with Transient Format attribute MUST contain a Value that contains more than whitespace characters"); + } + + if (nameId.Value.Length > 256) + { + throw new Saml20FormatException("NameID with Transient Format attribute MUST have a Value that contains no more than 256 characters"); + } + + if (!Saml20Utils.ValidateIdString(nameId.Value)) + { + throw new Saml20FormatException("NameID with Transient Format attribute MUST have a Value with at least 16 characters (the equivalent of 128 bits)"); + } + } + } + + /// + /// Validates the encrypted ID. + /// + /// The encrypted ID. + public void ValidateEncryptedId(EncryptedElement encryptedId) + { + _encryptedElementValidator.ValidateEncryptedElement(encryptedId, "EncryptedID"); + } + } +} diff --git a/src/SAML2.Standard/Validation/Saml20StatementValidator.cs b/src/SAML2.Standard/Validation/Saml20StatementValidator.cs new file mode 100644 index 0000000..5bdb7be --- /dev/null +++ b/src/SAML2.Standard/Validation/Saml20StatementValidator.cs @@ -0,0 +1,252 @@ +using System; +using SAML2.Schema.Core; +using SAML2.Schema.Protocol; +using SAML2.Utils; + +namespace SAML2.Validation +{ + /// + /// SAML2 Statement validator. + /// + public class Saml20StatementValidator : ISaml20StatementValidator + { + /// + /// The attribute validator. + /// + private readonly ISaml20AttributeValidator _attributeValidator = new Saml20AttributeValidator(); + + /// + /// Validates the statement. + /// + /// The statement. + public virtual void ValidateStatement(StatementAbstract statement) + { + if (statement == null) + { + throw new ArgumentNullException("statement"); + } + + // Validate all possible statements in the assertion + if (statement is AuthnStatement) + { + ValidateAuthnStatement((AuthnStatement)statement); + } + else if (statement is AuthzDecisionStatement) + { + ValidateAuthzDecisionStatement((AuthzDecisionStatement)statement); + } + else if (statement is AttributeStatement) + { + ValidateAttributeStatement((AttributeStatement)statement); + } + else + { + throw new Saml20FormatException(string.Format("Unsupported Statement type {0}", statement.GetType())); + } + } + + /// + /// Validate AttributeStatement. + /// + /// + /// [SAML2.0 standard] section 2.7.3 + /// + /// The statement. + private void ValidateAttributeStatement(AttributeStatement statement) + { + if (statement.Items == null || statement.Items.Length == 0) + { + throw new Saml20FormatException("AttributeStatement MUST contain at least one Attribute or EncryptedAttribute"); + } + + foreach (var o in statement.Items) + { + if (o == null) + { + throw new Saml20FormatException("null-Attributes are not supported"); + } + + if (o is SamlAttribute) + { + _attributeValidator.ValidateAttribute((SamlAttribute)o); + } + else if (o is EncryptedElement) + { + _attributeValidator.ValidateEncryptedAttribute((EncryptedElement)o); + } + else + { + throw new Saml20FormatException(string.Format("Subelement {0} of AttributeStatement is not supported", o.GetType())); + } + } + } + + /// + /// Validate AuthnStatement. + /// + /// + /// [SAML2.0 standard] section 2.7.2 + /// + /// The statement. + private void ValidateAuthnStatement(AuthnStatement statement) + { + if (statement.AuthnInstant == null) + { + throw new Saml20FormatException("AuthnStatement MUST have an AuthnInstant attribute"); + } + + if (!Saml20Utils.ValidateOptionalString(statement.SessionIndex)) + { + throw new Saml20FormatException("SessionIndex attribute of AuthnStatement must contain at least one non-whitespace character"); + } + + if (statement.SubjectLocality != null) + { + if (!Saml20Utils.ValidateOptionalString(statement.SubjectLocality.Address)) + { + throw new Saml20FormatException("Address attribute of SubjectLocality must contain at least one non-whitespace character"); + } + + if (!Saml20Utils.ValidateOptionalString(statement.SubjectLocality.DNSName)) + { + throw new Saml20FormatException("DNSName attribute of SubjectLocality must contain at least one non-whitespace character"); + } + } + + ValidateAuthnContext(statement.AuthnContext); + } + + /// + /// Validate AuthzContext. + /// + /// + /// [SAML2.0 standard] section 2.7.2.2 + /// + /// The authentication context. + private void ValidateAuthnContext(AuthnContext authnContext) + { + if (authnContext == null) + { + throw new Saml20FormatException("AuthnStatement MUST have an AuthnContext element"); + } + + // There must be at least one item if an authentication statement is present + if (authnContext.Items == null || authnContext.Items.Length == 0) + { + throw new Saml20FormatException("AuthnContext element MUST contain at least one AuthnContextClassRef, AuthnContextDecl or AuthnContextDeclRef element"); + } + + // Cannot happen when using .NET auto-generated serializer classes, but other implementations may fail to enforce the + // correspondence on the size of the arrays involved + if (authnContext.Items.Length != authnContext.ItemsElementName.Length) + { + throw new Saml20FormatException("AuthnContext parse error: Mismathing Items vs ItemElementNames counts"); + } + + // Validate the anyUri xsi schema type demands on context reference types + // We do not support by-value authentication types (since Geneva does not allow it) + if (authnContext.Items.Length > 2) + { + throw new Saml20FormatException("AuthnContext MUST NOT contain more than two elements."); + } + + var authnContextDeclRefFound = false; + for (var i = 0; i < authnContext.ItemsElementName.Length; ++i) + { + switch (authnContext.ItemsElementName[i]) + { + case Schema.Core.AuthnContextType.AuthnContextClassRef: + if (i > 0) + { + throw new Saml20FormatException("AuthnContextClassRef must be in the first element"); + } + + if (!Uri.IsWellFormedUriString((string)authnContext.Items[i], UriKind.Absolute)) + { + throw new Saml20FormatException("AuthnContextClassRef has a value which is not a wellformed absolute uri"); + } + + break; + case Schema.Core.AuthnContextType.AuthnContextDeclRef: + if (authnContextDeclRefFound) + { + throw new Saml20FormatException("AuthnContextDeclRef MUST only be present once."); + } + + authnContextDeclRefFound = true; + + // There is some concern about this being a valid check. + // See: https://lists.oasis-open.org/archives/security-services/200703/msg00004.html + // http://saml2.codeplex.com/SourceControl/network/forks/etlerch/saml2/contribution/5740 + if (!Uri.IsWellFormedUriString((string)authnContext.Items[i], UriKind.Absolute)) { + throw new Saml20FormatException("AuthnContextDeclRef has a value which is not a wellformed absolute uri"); + } + + break; + case Schema.Core.AuthnContextType.AuthnContextDecl: + throw new Saml20FormatException("AuthnContextDecl elements are not allowed in this implementation"); + default: + throw new Saml20FormatException(string.Format("Subelement {0} of AuthnContext is not supported", authnContext.ItemsElementName[i])); + } + } + + // No authenticating authorities? We are done + if (authnContext.AuthenticatingAuthority == null || authnContext.AuthenticatingAuthority.Length == 0) + { + return; + } + + // Values MUST have xsi schema type anyUri: + foreach (var authnAuthority in authnContext.AuthenticatingAuthority) + { + if (!Uri.IsWellFormedUriString(authnAuthority, UriKind.Absolute)) + { + throw new Saml20FormatException("AuthenticatingAuthority array contains a value which is not a wellformed absolute uri"); + } + } + } + + /// + /// Validate AuthzDecisionStatement. + /// + /// + /// [SAML2.0 standard] section 2.7.4 + /// + /// The statement. + private void ValidateAuthzDecisionStatement(AuthzDecisionStatement statement) + { + // This has type anyURI, and can be empty (special case in the standard), but not null. + if (statement.Resource == null) + { + throw new Saml20FormatException("Resource attribute of AuthzDecisionStatement is REQUIRED"); + } + + // If it is not empty, it MUST BE a valid URI + if (statement.Resource.Length > 0 && !Uri.IsWellFormedUriString(statement.Resource, UriKind.Absolute)) + { + throw new Saml20FormatException("Resource attribute of AuthzDecisionStatement has a value which is not a wellformed absolute uri"); + } + + // NOTE: Decision property validation is done implicitly be the deserializer since it is represented by an enumeration + if (statement.Action == null || statement.Action.Length == 0) + { + throw new Saml20FormatException("At least one Action subelement must be present for an AuthzDecisionStatement element"); + } + + foreach (var action in statement.Action) + { + // NOTE: [SAML2.0 standard] claims that the Namespace is [Optional], but according to the schema definition (and Geneva) + // NOTE: it has use="required" + if (!Saml20Utils.ValidateRequiredString(action.Namespace)) + { + throw new Saml20FormatException("Namespace attribute of Action element must contain at least one non-whitespace character"); + } + + if (!Uri.IsWellFormedUriString(action.Namespace, UriKind.Absolute)) + { + throw new Saml20FormatException("Namespace attribute of Action element has a value which is not a wellformed absolute uri"); + } + } + } + } +} diff --git a/src/SAML2.Standard/Validation/Saml20SubjectConfirmationDataValidator.cs b/src/SAML2.Standard/Validation/Saml20SubjectConfirmationDataValidator.cs new file mode 100644 index 0000000..69cc17a --- /dev/null +++ b/src/SAML2.Standard/Validation/Saml20SubjectConfirmationDataValidator.cs @@ -0,0 +1,63 @@ +using System; +using SAML2.Schema.Core; +using SAML2.Utils; + +namespace SAML2.Validation +{ + /// + /// SAML2 SubjectConfirmationData validator. + /// + public class Saml20SubjectConfirmationDataValidator : ISaml20SubjectConfirmationDataValidator + { + /// + /// The AnyAttrc> validator. + /// + private readonly Saml20XmlAnyAttributeValidator _anyAttrValidator = new Saml20XmlAnyAttributeValidator(); + + /// + /// The KeyInfo validator. + /// + private readonly Saml20KeyInfoValidator _keyInfoValidator = new Saml20KeyInfoValidator(); + + #region ISaml20SubjectConfirmationDataValidator Members + + /// + /// Validate SubjectConfirmationData. + /// + /// The subject confirmation data. + /// + /// [SAML2.0 standard] section 2.4.1.2 + /// + public void ValidateSubjectConfirmationData(SubjectConfirmationData subjectConfirmationData) + { + // If present it must be anyUri + if (subjectConfirmationData.Recipient != null) + { + if (!Uri.IsWellFormedUriString(subjectConfirmationData.Recipient, UriKind.Absolute)) + { + throw new Saml20FormatException("Recipient of SubjectConfirmationData must be a wellformed absolute URI."); + } + } + + // NotBefore MUST BE striclty less than NotOnOrAfter if they are both set + if (subjectConfirmationData.NotBefore != null && subjectConfirmationData.NotOnOrAfter != null && !(subjectConfirmationData.NotBefore < subjectConfirmationData.NotOnOrAfter)) + { + throw new Saml20FormatException(string.Format("NotBefore {0} MUST BE less than NotOnOrAfter {1} on SubjectConfirmationData", Saml20Utils.ToUtcString(subjectConfirmationData.NotBefore.Value), Saml20Utils.ToUtcString(subjectConfirmationData.NotOnOrAfter.Value))); + } + + // Make sure the extension-attributes are namespace-qualified and do not use reserved namespaces + if (subjectConfirmationData.AnyAttr != null) + { + _anyAttrValidator.ValidateXmlAnyAttributes(subjectConfirmationData.AnyAttr); + } + + // Standards-defined extension type which has stricter rules than it's base type + if (subjectConfirmationData is KeyInfoConfirmationData) + { + _keyInfoValidator.ValidateKeyInfo(subjectConfirmationData); + } + } + + #endregion + } +} diff --git a/src/SAML2.Standard/Validation/Saml20SubjectConfirmationValidator.cs b/src/SAML2.Standard/Validation/Saml20SubjectConfirmationValidator.cs new file mode 100644 index 0000000..a86f432 --- /dev/null +++ b/src/SAML2.Standard/Validation/Saml20SubjectConfirmationValidator.cs @@ -0,0 +1,75 @@ +using System; +using SAML2.Schema.Core; +using SAML2.Schema.Protocol; +using SAML2.Utils; + +namespace SAML2.Validation +{ + /// + /// SAML2 SubjectConfirmation validator. + /// + public class Saml20SubjectConfirmationValidator : ISaml20SubjectConfirmationValidator + { + /// + /// KeyInfo validator. + /// + private readonly Saml20KeyInfoValidator _keyInfoValidator = new Saml20KeyInfoValidator(); + + /// + /// NameID validator. + /// + private readonly ISaml20NameIdValidator _nameIdValidator = new Saml20NameIdValidator(); + + /// + /// SubjectConfirmationData validator. + /// + private readonly ISaml20SubjectConfirmationDataValidator _subjectConfirmationDataValidator = new Saml20SubjectConfirmationDataValidator(); + + /// + /// Validates the subject confirmation. + /// + /// The subject confirmation. + public void ValidateSubjectConfirmation(SubjectConfirmation subjectConfirmation) + { + if (subjectConfirmation == null) + { + throw new ArgumentNullException("subjectConfirmation"); + } + + if (!Saml20Utils.ValidateRequiredString(subjectConfirmation.Method)) + { + throw new Saml20FormatException("Method attribute of SubjectConfirmation MUST contain at least one non-whitespace character"); + } + + if (!Uri.IsWellFormedUriString(subjectConfirmation.Method, UriKind.Absolute)) + { + throw new Saml20FormatException("SubjectConfirmation element has Method attribute which is not a wellformed absolute uri."); + } + + if (subjectConfirmation.Method == Saml20Constants.SubjectConfirmationMethods.HolderOfKey) + { + _keyInfoValidator.ValidateKeyInfo(subjectConfirmation.SubjectConfirmationData); + } + + if (subjectConfirmation.Item != null) + { + if (subjectConfirmation.Item is NameId) + { + _nameIdValidator.ValidateNameId((NameId)subjectConfirmation.Item); + } + else if (subjectConfirmation.Item is EncryptedElement) + { + _nameIdValidator.ValidateEncryptedId((EncryptedElement)subjectConfirmation.Item); + } + else + { + throw new Saml20FormatException(string.Format("Identifier of type {0} is not supported for SubjectConfirmation", subjectConfirmation.Item.GetType())); + } + } + else if (subjectConfirmation.SubjectConfirmationData != null) + { + _subjectConfirmationDataValidator.ValidateSubjectConfirmationData(subjectConfirmation.SubjectConfirmationData); + } + } + } +} diff --git a/src/SAML2.Standard/Validation/Saml20SubjectValidator.cs b/src/SAML2.Standard/Validation/Saml20SubjectValidator.cs new file mode 100644 index 0000000..c41cff6 --- /dev/null +++ b/src/SAML2.Standard/Validation/Saml20SubjectValidator.cs @@ -0,0 +1,64 @@ +using System; +using SAML2.Schema.Core; +using SAML2.Schema.Protocol; + +namespace SAML2.Validation +{ + /// + /// SAML Subject validator. + /// + public class Saml20SubjectValidator : ISaml20SubjectValidator + { + /// + /// NameID validator. + /// + private readonly ISaml20NameIdValidator _nameIdValidator = new Saml20NameIdValidator(); + + /// + /// SubjectConfirmation validator. + /// + private readonly ISaml20SubjectConfirmationValidator _subjectConfirmationValidator = new Saml20SubjectConfirmationValidator(); + + /// + /// Validates the subject. + /// + /// The subject. + public virtual void ValidateSubject(Subject subject) + { + if (subject == null) + { + throw new ArgumentNullException("subject"); + } + + var validContentFound = false; + if (subject.Items == null || subject.Items.Length == 0) + { + throw new Saml20FormatException("Subject MUST contain either an identifier or a subject confirmation"); + } + + foreach (var o in subject.Items) + { + if (o is NameId) + { + validContentFound = true; + _nameIdValidator.ValidateNameId((NameId)o); + } + else if (o is EncryptedElement) + { + validContentFound = true; + _nameIdValidator.ValidateEncryptedId((EncryptedElement)o); + } + else if (o is SubjectConfirmation) + { + validContentFound = true; + _subjectConfirmationValidator.ValidateSubjectConfirmation((SubjectConfirmation)o); + } + } + + if (!validContentFound) + { + throw new Saml20FormatException("Subject must have either NameID, EncryptedID or SubjectConfirmation subelement."); + } + } + } +} diff --git a/src/SAML2.Standard/Validation/Saml20XmlAnyAttributeValidator.cs b/src/SAML2.Standard/Validation/Saml20XmlAnyAttributeValidator.cs new file mode 100644 index 0000000..8e32b42 --- /dev/null +++ b/src/SAML2.Standard/Validation/Saml20XmlAnyAttributeValidator.cs @@ -0,0 +1,45 @@ +using System; +using System.Xml; +using SAML2.Utils; + +namespace SAML2.Validation +{ + /// + /// SAML2 Xml AnyAttribute validator. + /// + public class Saml20XmlAnyAttributeValidator + { + /// + /// Validates the XML any attributes. + /// + /// Any attributes. + public void ValidateXmlAnyAttributes(XmlAttribute[] anyAttributes) + { + if (anyAttributes == null) + { + throw new ArgumentNullException("anyAttributes"); + } + + if (anyAttributes.Length == 0) + { + return; + } + + foreach (var attr in anyAttributes) + { + if (!Saml20Utils.ValidateRequiredString(attr.Prefix)) + { + throw new Saml20FormatException("Attribute extension xml attributes MUST BE namespace qualified"); + } + + foreach (var samlns in Saml20Constants.SamlNamespaces) + { + if (attr.NamespaceURI == samlns) + { + throw new Saml20FormatException("Attribute extension xml attributes MUST NOT use a namespace reserved by SAML"); + } + } + } + } + } +} diff --git a/src/SAML2.sln b/src/SAML2.sln index e721495..f802905 100644 --- a/src/SAML2.sln +++ b/src/SAML2.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 14 -VisualStudioVersion = 14.0.22310.1 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.29418.71 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SAML2.Core", "SAML2.Core\SAML2.Core.csproj", "{75E5BAD2-A20C-43CC-B5C8-38004CEDBDFD}" EndProject @@ -15,6 +15,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SelfHostOwinSPExample", "Se EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Owin.Security.Saml.Test", "Owin.Security.Saml.Test\Owin.Security.Saml.Test.csproj", "{9577677C-0E39-45C5-945D-8B18B8A761E4}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SAML2.Standard", "SAML2.Standard\SAML2.Standard.csproj", "{5F6561FA-DA97-49CD-902D-3B8F4280EB0C}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -45,8 +47,15 @@ Global {9577677C-0E39-45C5-945D-8B18B8A761E4}.Debug|Any CPU.Build.0 = Debug|Any CPU {9577677C-0E39-45C5-945D-8B18B8A761E4}.Release|Any CPU.ActiveCfg = Release|Any CPU {9577677C-0E39-45C5-945D-8B18B8A761E4}.Release|Any CPU.Build.0 = Release|Any CPU + {5F6561FA-DA97-49CD-902D-3B8F4280EB0C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5F6561FA-DA97-49CD-902D-3B8F4280EB0C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5F6561FA-DA97-49CD-902D-3B8F4280EB0C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5F6561FA-DA97-49CD-902D-3B8F4280EB0C}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {CB82CA51-A451-4606-B495-847A98B60743} + EndGlobalSection EndGlobal