From 69fdfc482b15fe6cfb0f31dd81cb960a3e1a40fa Mon Sep 17 00:00:00 2001 From: Anders Revsgaard Date: Fri, 22 Aug 2025 13:25:38 +0000 Subject: [PATCH] Add CacheDuration property to EntitiesDescriptor and EntityDescriptor for metadata caching --- .../Schemas/Metadata/EntitiesDescriptor.cs | 29 ++++++++++++++++ .../Schemas/Metadata/EntityDescriptor.cs | 33 +++++++++++++++++++ .../Metadata/Saml2MetadataConstants.cs | 2 ++ 3 files changed, 64 insertions(+) diff --git a/src/ITfoxtec.Identity.Saml2/Schemas/Metadata/EntitiesDescriptor.cs b/src/ITfoxtec.Identity.Saml2/Schemas/Metadata/EntitiesDescriptor.cs index dd63c7d..adff7d8 100644 --- a/src/ITfoxtec.Identity.Saml2/Schemas/Metadata/EntitiesDescriptor.cs +++ b/src/ITfoxtec.Identity.Saml2/Schemas/Metadata/EntitiesDescriptor.cs @@ -4,6 +4,7 @@ using System.Security.Cryptography.X509Certificates; using System.Xml; using System.Xml.Linq; +using System.Text.RegularExpressions; #if NETFULL using System.IdentityModel.Tokens; #else @@ -49,6 +50,30 @@ public string IdAsString /// public int? ValidUntil { get; set; } + /// + /// [Optional] + /// Optional attribute indicates how long a metadata consumer should cache this metadata before attempting to re-fetch. + /// Value must be an XML Schema duration (https://www.w3.org/TR/xmlschema-2/#duration). Example: P1D, PT12H, P2Y3M. + /// Regex used for validation: ^-?P(\d*Y)?(\d*M)?(\d*D)?(T(\d*H)?(\d*M)?(\d*S)?)?$ + /// Throws if set to a non-empty value that does not match the duration pattern. + /// + public string CacheDuration + { + get => _cacheDuration; + set + { + if(!string.IsNullOrEmpty(value) && !CacheDurationRegex.IsMatch(value)) + { + throw new ArgumentException($"Invalid cacheDuration format. See https://www.w3.org/TR/xmlschema-2/#duration. Value: '{value}'"); + } + _cacheDuration = value; + } + } + + private string _cacheDuration; + // Require at least one date or time component after 'P' using a lookahead. See https://www.w3.org/TR/xmlschema-2/#duration + private static readonly Regex CacheDurationRegex = new Regex(@"^-?P(?=\d|T)(\d+Y)?(\d+M)?(\d+W)?(\d+D)?(T(\d+H)?(\d+M)?(\d+S)?)?$", RegexOptions.Compiled); + /// /// [Optional] /// An metadata XML signature that authenticates the containing element and its contents. @@ -118,6 +143,10 @@ protected IEnumerable GetXContent() { yield return new XAttribute(Saml2MetadataConstants.Message.ValidUntil, DateTimeOffset.UtcNow.AddDays(ValidUntil.Value).UtcDateTime.ToString(Saml2Constants.DateTimeFormat, CultureInfo.InvariantCulture)); } + if (!string.IsNullOrEmpty(CacheDuration)) + { + yield return new XAttribute(Saml2MetadataConstants.Message.CacheDuration, CacheDuration); + } yield return new XAttribute(Saml2MetadataConstants.MetadataNamespaceNameX, Saml2MetadataConstants.MetadataNamespace); if (Extensions != null) diff --git a/src/ITfoxtec.Identity.Saml2/Schemas/Metadata/EntityDescriptor.cs b/src/ITfoxtec.Identity.Saml2/Schemas/Metadata/EntityDescriptor.cs index 3c0788b..31baa8c 100644 --- a/src/ITfoxtec.Identity.Saml2/Schemas/Metadata/EntityDescriptor.cs +++ b/src/ITfoxtec.Identity.Saml2/Schemas/Metadata/EntityDescriptor.cs @@ -9,6 +9,7 @@ using System.Threading.Tasks; using System.Xml; using System.Xml.Linq; +using System.Text.RegularExpressions; #if NETFULL using System.IdentityModel.Tokens; #else @@ -53,6 +54,30 @@ public string IdAsString /// public int? ValidUntil { get; set; } + /// + /// [Optional] + /// Optional attribute indicates how long a metadata consumer should cache this metadata before attempting to re-fetch. + /// Value must be an XML Schema duration (https://www.w3.org/TR/xmlschema-2/#duration). Example: P1D, PT12H, P2Y3M. + /// Regex used for validation: ^-?P(\d*Y)?(\d*M)?(\d*D)?(T(\d*H)?(\d*M)?(\d*S)?)?$ + /// Throws if set to a non-empty value that does not match the duration pattern. + /// + public string CacheDuration + { + get => _cacheDuration; + set + { + if(!string.IsNullOrEmpty(value) && !CacheDurationRegex.IsMatch(value)) + { + throw new ArgumentException($"Invalid cacheDuration format. See https://www.w3.org/TR/xmlschema-2/#duration. Value: '{value}'"); + } + _cacheDuration = value; + } + } + + private string _cacheDuration; + // Require at least one date or time component after 'P' using a lookahead. See https://www.w3.org/TR/xmlschema-2/#duration + private static readonly Regex CacheDurationRegex = new Regex(@"^-?P(?=\d|T)(\d+Y)?(\d+M)?(\d+W)?(\d+D)?(T(\d+H)?(\d+M)?(\d+S)?)?$", RegexOptions.Compiled); + /// /// [Optional] /// An metadata XML signature that authenticates the containing element and its contents. @@ -154,6 +179,10 @@ protected IEnumerable GetXContent() { yield return new XAttribute(Saml2MetadataConstants.Message.ValidUntil, DateTimeOffset.UtcNow.AddDays(ValidUntil.Value).UtcDateTime.ToString(Saml2Constants.DateTimeFormat, CultureInfo.InvariantCulture)); } + if (!string.IsNullOrEmpty(CacheDuration)) + { + yield return new XAttribute(Saml2MetadataConstants.Message.CacheDuration, CacheDuration); + } yield return new XAttribute(Saml2MetadataConstants.MetadataNamespaceNameX, Saml2MetadataConstants.MetadataNamespace); if (Extensions != null) @@ -208,6 +237,8 @@ public virtual EntityDescriptor ReadIdPSsoDescriptor(string idPMetadataXml) Id = entityDescriptorElement.Attributes[Saml2MetadataConstants.Message.Id].GetValueOrNull(); + CacheDuration = entityDescriptorElement.Attributes[Saml2MetadataConstants.Message.CacheDuration].GetValueOrNull(); + var idPSsoDescriptorElement = entityDescriptorElement[Saml2MetadataConstants.Message.IdPSsoDescriptor, Saml2MetadataConstants.MetadataNamespace.OriginalString]; if (idPSsoDescriptorElement != null) { @@ -236,6 +267,8 @@ public virtual EntityDescriptor ReadSPSsoDescriptor(string spMetadataXml) Id = entityDescriptorElement.Attributes[Saml2MetadataConstants.Message.Id].GetValueOrNull(); + CacheDuration = entityDescriptorElement.Attributes[Saml2MetadataConstants.Message.CacheDuration].GetValueOrNull(); + var spSsoDescriptorElement = entityDescriptorElement[Saml2MetadataConstants.Message.SPSsoDescriptor, Saml2MetadataConstants.MetadataNamespace.OriginalString]; if (spSsoDescriptorElement != null) { diff --git a/src/ITfoxtec.Identity.Saml2/Schemas/Metadata/Saml2MetadataConstants.cs b/src/ITfoxtec.Identity.Saml2/Schemas/Metadata/Saml2MetadataConstants.cs index 4c5ca41..9f022ee 100644 --- a/src/ITfoxtec.Identity.Saml2/Schemas/Metadata/Saml2MetadataConstants.cs +++ b/src/ITfoxtec.Identity.Saml2/Schemas/Metadata/Saml2MetadataConstants.cs @@ -63,6 +63,8 @@ public class Message public const string ValidUntil = "validUntil"; + public const string CacheDuration = "cacheDuration"; + public const string ContactType = "contactType"; public const string Company = "Company";