Skip to content

Commit dd2c3c3

Browse files
committed
First attempt to build IdP-initiated logout request
1 parent 0044fc5 commit dd2c3c3

File tree

1 file changed

+74
-13
lines changed

1 file changed

+74
-13
lines changed

AspNetSaml/Saml.cs

+74-13
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
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

1818
namespace 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

Comments
 (0)