1
1
/* Jitbit's simple SAML 2.0 component for ASP.NET
2
2
https://github.com/jitbit/AspNetSaml/
3
- (c) Jitbit LP, 2016-2023
3
+ (c) Jitbit LP, 2016-2025
4
4
Use this freely under the Apache license (see https://choosealicense.com/licenses/apache-2.0/)
5
5
*/
6
6
@@ -17,17 +17,17 @@ Use this freely under the Apache license (see https://choosealicense.com/license
17
17
18
18
namespace Saml
19
19
{
20
- public abstract class BaseResponse
20
+ public abstract class BaseSamlMessage
21
21
{
22
22
protected XmlDocument _xmlDoc ;
23
23
protected readonly X509Certificate2 _certificate ;
24
24
protected XmlNamespaceManager _xmlNameSpaceManager ; //we need this one to run our XPath queries on the SAML XML
25
25
26
26
public string Xml { get { return _xmlDoc . OuterXml ; } }
27
27
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 ) { }
29
29
30
- public BaseResponse ( byte [ ] certificateBytes , string responseString = null )
30
+ public BaseSamlMessage ( byte [ ] certificateBytes , string responseString = null )
31
31
{
32
32
_certificate = new X509Certificate2 ( certificateBytes ) ;
33
33
if ( responseString != null )
@@ -121,7 +121,7 @@ public bool IsValid()
121
121
return ValidateSignatureReference ( signedXml ) && signedXml . CheckSignature ( _certificate , true ) && ! IsExpired ( ) ;
122
122
}
123
123
124
- private bool IsExpired ( )
124
+ protected virtual bool IsExpired ( )
125
125
{
126
126
DateTime expirationDate = DateTime . MaxValue ;
127
127
XmlNode node = _xmlDoc . SelectSingleNode ( "/samlp:Response/saml:Assertion[1]/saml:Subject/saml:SubjectConfirmation/saml:SubjectConfirmationData" , _xmlNameSpaceManager ) ;
@@ -135,7 +135,7 @@ private bool IsExpired()
135
135
public DateTime ? CurrentTime { get ; set ; } = null ; //mostly for unit-testing. STUPID I KNOW, will fix later
136
136
}
137
137
138
- public class Response : BaseResponse
138
+ public class Response : BaseSamlMessage
139
139
{
140
140
public Response ( string certificateStr , string responseString = null ) : base ( certificateStr , responseString ) { }
141
141
@@ -222,7 +222,10 @@ public List<string> GetCustomAttributeAsList(string attr)
222
222
}
223
223
}
224
224
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
226
229
{
227
230
public SignoutResponse ( string certificateStr , string responseString = null ) : base ( certificateStr , responseString ) { }
228
231
@@ -235,6 +238,49 @@ public string GetLogoutStatus()
235
238
}
236
239
}
237
240
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
+
238
284
public abstract class BaseRequest
239
285
{
240
286
public string _id ;
@@ -362,6 +408,9 @@ public override string GetRequest()
362
408
}
363
409
}
364
410
411
+ /// <summary>
412
+ /// Represents an SP-initiated Logout Request to be sent to the IdP.
413
+ /// </summary>
365
414
public class SignoutRequest : BaseRequest
366
415
{
367
416
private string _nameId ;
@@ -405,14 +454,26 @@ public static class MetaData
405
454
/// <summary>
406
455
/// generates XML string describing service provider metadata based on provided EntiytID and Consumer URL
407
456
/// </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 )
412
462
{
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.
413
474
return $@ "<?xml version=""1.0""?>
414
475
<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""
416
477
entityID=""{ entityId } "">
417
478
418
479
<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
421
482
422
483
<md:AssertionConsumerService Binding=""urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST""
423
484
Location=""{ assertionConsumerServiceUrl } ""
424
- index=""1"" />
485
+ index=""1"" />{ sloServiceElement }
425
486
</md:SPSSODescriptor>
426
487
</md:EntityDescriptor>" ;
427
488
}
0 commit comments