11/* Jitbit's simple SAML 2.0 component for ASP.NET
22 https://github.com/jitbit/AspNetSaml/
3- (c) Jitbit LP, 2016-2023
3+ (c) Jitbit LP, 2016-2025
44 Use this freely under the Apache license (see https://choosealicense.com/licenses/apache-2.0/)
55*/
66
@@ -17,17 +17,17 @@ Use this freely under the Apache license (see https://choosealicense.com/license
1717
1818namespace Saml
1919{
20- public abstract class BaseResponse
20+ public abstract class BaseSamlMessage
2121 {
2222 protected XmlDocument _xmlDoc ;
2323 protected readonly X509Certificate2 _certificate ;
2424 protected XmlNamespaceManager _xmlNameSpaceManager ; //we need this one to run our XPath queries on the SAML XML
2525
2626 public string Xml { get { return _xmlDoc . OuterXml ; } }
2727
28- public BaseResponse ( string certificateStr , string responseString = null ) : this ( Encoding . ASCII . GetBytes ( EnsureCertFormat ( certificateStr ) ) , responseString ) { }
28+ public BaseSamlMessage ( string certificateStr , string responseString = null ) : this ( Encoding . ASCII . GetBytes ( EnsureCertFormat ( certificateStr ) ) , responseString ) { }
2929
30- public BaseResponse ( byte [ ] certificateBytes , string responseString = null )
30+ public BaseSamlMessage ( byte [ ] certificateBytes , string responseString = null )
3131 {
3232 _certificate = new X509Certificate2 ( certificateBytes ) ;
3333 if ( responseString != null )
@@ -121,7 +121,7 @@ public bool IsValid()
121121 return ValidateSignatureReference ( signedXml ) && signedXml . CheckSignature ( _certificate , true ) && ! IsExpired ( ) ;
122122 }
123123
124- private bool IsExpired ( )
124+ protected virtual bool IsExpired ( )
125125 {
126126 DateTime expirationDate = DateTime . MaxValue ;
127127 XmlNode node = _xmlDoc . SelectSingleNode ( "/samlp:Response/saml:Assertion[1]/saml:Subject/saml:SubjectConfirmation/saml:SubjectConfirmationData" , _xmlNameSpaceManager ) ;
@@ -135,7 +135,7 @@ private bool IsExpired()
135135 public DateTime ? CurrentTime { get ; set ; } = null ; //mostly for unit-testing. STUPID I KNOW, will fix later
136136 }
137137
138- public class Response : BaseResponse
138+ public class Response : BaseSamlMessage
139139 {
140140 public Response ( string certificateStr , string responseString = null ) : base ( certificateStr , responseString ) { }
141141
@@ -222,7 +222,10 @@ public List<string> GetCustomAttributeAsList(string attr)
222222 }
223223 }
224224
225- public class SignoutResponse : BaseResponse
225+ /// <summary>
226+ /// Represents IdP-generated Logout Response in response to a SP-initiated Logout Request.
227+ /// </summary>
228+ public class SignoutResponse : BaseSamlMessage
226229 {
227230 public SignoutResponse ( string certificateStr , string responseString = null ) : base ( certificateStr , responseString ) { }
228231
@@ -235,6 +238,49 @@ public string GetLogoutStatus()
235238 }
236239 }
237240
241+ /// <summary>
242+ /// Represents an IdP-initiated Logout Request received by the SP.
243+ /// </summary>
244+ public class IdpLogoutRequest : BaseSamlMessage
245+ {
246+ public IdpLogoutRequest ( string certificateStr , string responseString = null ) : base ( certificateStr , responseString ) { }
247+
248+ public IdpLogoutRequest ( byte [ ] certificateBytes , string responseString = null ) : base ( certificateBytes , responseString ) { }
249+
250+ /// <summary>
251+ /// Gets the NameID from the IdP-initiated LogoutRequest.
252+ /// </summary>
253+ public string GetNameID ( )
254+ {
255+ // LogoutRequest typically uses /samlp:LogoutRequest/saml:NameID
256+ XmlNode node = _xmlDoc . SelectSingleNode ( "/samlp:LogoutRequest/saml:NameID" , _xmlNameSpaceManager ) ;
257+ return node ? . InnerText ;
258+ }
259+
260+ /// <summary>
261+ /// Gets the SessionIndex from the IdP-initiated LogoutRequest.
262+ /// </summary>
263+ /// <returns>The SessionIndex string, or null if not found.</returns>
264+ public string GetSessionIndex ( )
265+ {
266+ // SessionIndex is optional in the SAML spec for LogoutRequest
267+ XmlNode node = _xmlDoc . SelectSingleNode ( "/samlp:LogoutRequest/samlp:SessionIndex" , _xmlNameSpaceManager ) ;
268+ return node ? . InnerText ;
269+ }
270+
271+ /// <summary>
272+ /// Checks the validity of the SAML IdP-initiated LogoutRequest (validate signature).
273+ /// This class relies on the base IsValid() method but overrides IsExpired() to always return false,
274+ /// effectively bypassing the expiration check which is not relevant for LogoutRequests.
275+ /// </summary>
276+ protected override bool IsExpired ( )
277+ {
278+ // LogoutRequests don't have the standard expiration elements.
279+ // Return false to ensure the base IsValid() check doesn't fail due to expiration.
280+ return false ;
281+ }
282+ }
283+
238284 public abstract class BaseRequest
239285 {
240286 public string _id ;
@@ -362,6 +408,9 @@ public override string GetRequest()
362408 }
363409 }
364410
411+ /// <summary>
412+ /// Represents an SP-initiated Logout Request to be sent to the IdP.
413+ /// </summary>
365414 public class SignoutRequest : BaseRequest
366415 {
367416 private string _nameId ;
@@ -405,14 +454,26 @@ public static class MetaData
405454 /// <summary>
406455 /// generates XML string describing service provider metadata based on provided EntiytID and Consumer URL
407456 /// </summary>
408- /// <param name="entityId"></param>
409- /// <param name="assertionConsumerServiceUrl"></param>
410- /// <returns></returns>
411- public static string Generate ( string entityId , string assertionConsumerServiceUrl )
457+ /// <param name="entityId">Your SP EntityID</param>
458+ /// <param name="assertionConsumerServiceUrl">Your Assertion Consumer Service URL (where IdP sends responses)</param>
459+ /// <param name="singleLogoutServiceUrl">Optional: Your Single Logout Service URL (where IdP sends LogoutRequests)</param>
460+ /// <returns>XML metadata string</returns>
461+ public static string Generate ( string entityId , string assertionConsumerServiceUrl , string singleLogoutServiceUrl = null )
412462 {
463+ string sloServiceElement = "" ;
464+ if ( ! string . IsNullOrEmpty ( singleLogoutServiceUrl ) )
465+ {
466+ // We advertise HTTP-POST binding as IdpLogoutRequest handles POST
467+ sloServiceElement = $@ "
468+ <md:SingleLogoutService Binding=""urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"" Location=""{ singleLogoutServiceUrl } "" />" ;
469+ }
470+
471+ // Construct the final metadata XML
472+ // NOTE: Using string interpolation with $@ can be tricky with complex XML and quotes.
473+ // Consider using XmlWriter or Linq to XML for more robust XML generation if needed.
413474 return $@ "<?xml version=""1.0""?>
414475<md:EntityDescriptor xmlns:md=""urn:oasis:names:tc:SAML:2.0:metadata""
415- validUntil=""{ DateTime . UtcNow . ToString ( "s" ) } Z""
476+ validUntil=""{ DateTime . UtcNow . AddYears ( 1 ) . ToString ( "s" ) } Z""
416477 entityID=""{ entityId } "">
417478
418479 <md:SPSSODescriptor AuthnRequestsSigned=""false"" WantAssertionsSigned=""true"" protocolSupportEnumeration=""urn:oasis:names:tc:SAML:2.0:protocol"">
@@ -421,7 +482,7 @@ public static string Generate(string entityId, string assertionConsumerServiceUr
421482
422483 <md:AssertionConsumerService Binding=""urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST""
423484 Location=""{ assertionConsumerServiceUrl } ""
424- index=""1"" />
485+ index=""1"" />{ sloServiceElement }
425486 </md:SPSSODescriptor>
426487</md:EntityDescriptor>" ;
427488 }
0 commit comments