Skip to content
This repository was archived by the owner on Apr 10, 2025. It is now read-only.

Commit deb17cc

Browse files
committed
Adds ability to validate signatures of AuthnRequest and SAMLResponse
1 parent fb880a3 commit deb17cc

16 files changed

+236
-21
lines changed
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
using System.Security.Cryptography;
2+
using System.Security.Cryptography.Xml;
3+
using Microsoft.IdentityModel.Tokens;
4+
using Microsoft.IdentityModel.Xml;
5+
using KeyInfo = System.Security.Cryptography.Xml.KeyInfo;
6+
using Reference = System.Security.Cryptography.Xml.Reference;
7+
using RSAKeyValue = System.Security.Cryptography.Xml.RSAKeyValue;
8+
9+
namespace System.Xml;
10+
11+
public static class XmlDocumentExtensions
12+
{
13+
public static void SignXml(this XmlDocument document, RSA key, string id = null)
14+
{
15+
var signer = new SignedXml(document);
16+
var root = document.DocumentElement;
17+
if (root == null)
18+
throw new ArgumentException("Invalid document", nameof(document));
19+
20+
var reference = new Reference(id ?? string.Empty);
21+
reference.AddTransform(new XmlDsigExcC14NTransform());
22+
signer.Signature.SignedInfo.CanonicalizationMethod = SignedXml.XmlDsigExcC14NTransformUrl;
23+
signer.Signature.SignedInfo.AddReference(reference);
24+
25+
var keyInfo = new KeyInfo();
26+
keyInfo.AddClause(new RSAKeyValue(key));
27+
signer.Signature.KeyInfo = keyInfo;
28+
29+
signer.SigningKey = key;
30+
signer.ComputeSignature();
31+
32+
var signature = signer.GetXml();
33+
34+
// Append the element to the XML document.
35+
root.AppendChild(document.ImportNode(signature, true));
36+
37+
if (document.FirstChild is XmlDeclaration)
38+
document.RemoveChild(document.FirstChild);
39+
}
40+
}

src/Solid.Identity.Protocols.Sam2p.Tests/Saml2pSerializerTests.cs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,17 @@
66
using System;
77
using System.Collections.Generic;
88
using System.IO;
9+
using System.Linq.Expressions;
10+
using System.Security.Cryptography;
11+
using System.Security.Cryptography.Xml;
912
using System.Text;
1013
using System.Xml;
1114
using System.Xml.Linq;
1215
using Xunit;
1316
using FluentAssertions;
1417
using FluentAssertions.Extensions;
1518
using Microsoft.Extensions.Logging;
19+
using Microsoft.IdentityModel.Logging;
1620

1721
namespace Solid.Identity.Protocols.Saml2p.Tests
1822
{
@@ -26,6 +30,8 @@ public class Saml2pSerializerTests
2630

2731
public Saml2pSerializerTests()
2832
{
33+
IdentityModelEventSource.ShowPII = true;
34+
2935
_mockHandler = new Mock<Saml2SecurityTokenHandler>();
3036
_mockHandler.CallBase = true;
3137
var mockWriterFactory = new Mock<IXmlWriterFactory>();
@@ -283,6 +289,22 @@ public void ShouldReadSamlResponseElement()
283289
var request = _serializer.DeserializeSamlResponse(xml);
284290
Assert.NotNull(request);
285291
}
292+
293+
294+
[Fact]
295+
public void ShouldReadSamlResponseSignature()
296+
{
297+
using var key = RSA.Create(2048);
298+
var xml = $"<samlp:Response xmlns:samlp=\"{_protocolNamespace}\" Version=\"2.0\"></samlp:Response>";
299+
var document = new XmlDocument();
300+
document.LoadXml(xml);
301+
document.SignXml(key);
302+
303+
var signed = document.OuterXml;
304+
305+
var request = _serializer.DeserializeSamlResponse(signed);
306+
Assert.NotNull(request.Signature);
307+
}
286308

287309
[Fact]
288310
public void ShouldReadSamlResponseVersionAttribute()
@@ -832,6 +854,21 @@ public void ShouldReadAuthnRequestElement()
832854
Assert.NotNull(request);
833855
}
834856

857+
[Fact]
858+
public void ShouldReadAuthnRequestSignature()
859+
{
860+
using var key = RSA.Create(2048);
861+
var xml = $"<samlp:AuthnRequest xmlns:samlp=\"{_protocolNamespace}\"></samlp:AuthnRequest>";
862+
var document = new XmlDocument();
863+
document.LoadXml(xml);
864+
document.SignXml(key);
865+
866+
var signed = document.OuterXml;
867+
868+
var request = _serializer.DeserializeAuthnRequest(signed);
869+
Assert.NotNull(request.Signature);
870+
}
871+
835872
[Theory]
836873
[InlineData("2.0")]
837874
public void ShouldReadAuthnRequestVersionAttribute(string expected)

src/Solid.Identity.Protocols.Saml2p/Abstractions/ISaml2pIdentityProvider.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using System.Collections.Generic;
88
using System.Text;
99
using System.Threading.Tasks;
10+
using Solid.IdentityModel.Tokens;
1011

1112
namespace Solid.Identity.Protocols.Saml2p.Abstractions
1213
{
@@ -49,5 +50,15 @@ public interface ISaml2pIdentityProvider : ISaml2pPartner<Saml2pServiceProviderE
4950
/// A flag indicating whether the SP can initiate SSO.
5051
/// </summary>
5152
bool AllowsSpInitiatedSso { get; }
53+
54+
/// <summary>
55+
/// A flag indicating whether signing AuthnRequest is required.
56+
/// </summary>
57+
bool RequiresSignedAuthnRequest { get; }
58+
59+
/// <summary>
60+
/// The <see cref="SignatureMethod"/> used to sign the AuthnRequest.
61+
/// </summary>
62+
SignatureMethod AuthnRequestSigningMethod { get; }
5263
}
5364
}

src/Solid.Identity.Protocols.Saml2p/Abstractions/ISaml2pPartner.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using System;
44
using System.Collections.Generic;
55
using System.Text;
6+
using Microsoft.IdentityModel.Tokens;
67

78
namespace Solid.Identity.Protocols.Saml2p.Abstractions
89
{
@@ -51,5 +52,15 @@ public interface ISaml2pPartner<TEvents>
5152
/// The events object.
5253
/// </summary>
5354
TEvents Events { get; }
55+
56+
/// <summary>
57+
/// Signing key used to sign and validate SAMLResponse
58+
/// </summary>
59+
SecurityKey SamlResponseSigningKey { get; }
60+
61+
/// <summary>
62+
/// Signing key used to sign and validate AuthnRequest
63+
/// </summary>
64+
SecurityKey AuthnRequestSigningKey { get; }
5465
}
5566
}

src/Solid.Identity.Protocols.Saml2p/Abstractions/ISaml2pServiceProvider.cs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -75,8 +75,14 @@ public interface ISaml2pServiceProvider : ISaml2pPartner<Saml2pIdentityProviderE
7575
/// </summary>
7676
bool RequiresEncryptedAssertion { get; }
7777

78-
//SecurityKey ResponseSigningKey { get; }
79-
//string ResponseSigningAlgorithm { get; }
80-
//string ResponseDigestAlgorithm { get; }
78+
/// <summary>
79+
/// A flag indicating whether signing SAMLResponse is required.
80+
/// </summary>
81+
bool RequiresSignedSamlResponse { get; }
82+
83+
/// <summary>
84+
/// The <see cref="SignatureMethod"/> used to sign the SAMLResponse.
85+
/// </summary>
86+
SignatureMethod SamlResponseSigningMethod { get; }
8187
}
8288
}

src/Solid.Identity.Protocols.Saml2p/Factories/AuthnRequestFactory.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,15 @@ public async Task<AuthnRequest> CreateAuthnRequestAsync(HttpContext context, ISa
7272
Comparison = idp.RequestedAuthnContextClassRefComparison
7373
}
7474
};
75+
76+
if (idp.RequiresSignedAuthnRequest)
77+
{
78+
if(idp is { AuthnRequestSigningKey: not null, AuthnRequestSigningMethod: not null })
79+
request.SigningCredentials = idp.AuthnRequestSigningMethod.CreateCredentials(idp.AuthnRequestSigningKey);
80+
else
81+
throw new InvalidOperationException($"Partner '{idp.Id}' requires a signed AuthnRequest, but has misconfigured signing credentials.");
82+
}
83+
7584
var generateContext = new GenerateRelayStateContext
7685
{
7786
Partner = idp,

src/Solid.Identity.Protocols.Saml2p/Factories/SamlResponseFactory.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using System.Collections.Generic;
77
using System.Text;
88
using Microsoft.Extensions.Options;
9+
using Microsoft.IdentityModel.Tokens;
910
using Solid.Identity.Protocols.Saml2p.Abstractions;
1011

1112
namespace Solid.Identity.Protocols.Saml2p.Factories
@@ -46,6 +47,14 @@ public SamlResponse Create(ISaml2pServiceProvider partner, Status status, string
4647
RelayState = relayState
4748
};
4849

50+
if (partner.RequiresSignedSamlResponse)
51+
{
52+
if (partner is { SamlResponseSigningKey: not null, SamlResponseSigningMethod: not null })
53+
response.SigningCredentials = partner.SamlResponseSigningMethod.CreateCredentials(partner.SamlResponseSigningKey);
54+
else
55+
throw new InvalidOperationException($"Partner '{partner.Id}' requires a signed SAMLResponse, but has misconfigured signing credentials.");
56+
}
57+
4958
return response;
5059
}
5160

src/Solid.Identity.Protocols.Saml2p/Factories/SecurityTokenDescriptorFactory.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ public async ValueTask<SecurityTokenDescriptor> CreateSecurityTokenDescriptorAsy
119119
IssuedAt = issuedAt,
120120
NotBefore = issuedAt.Subtract(tolerance),
121121
Expires = expires,
122-
SigningCredentials = GetSigningCredentials(partner),
122+
SigningCredentials = GetAssertionSigningCredentials(partner),
123123
EncryptingCredentials = GetEncryptingCredentials(partner)
124124
};
125125

@@ -163,7 +163,7 @@ private EncryptingCredentials GetEncryptingCredentials(ISaml2pServiceProvider pa
163163
return credentials;
164164
}
165165

166-
private SigningCredentials GetSigningCredentials(ISaml2pServiceProvider partner)
166+
private SigningCredentials GetAssertionSigningCredentials(ISaml2pServiceProvider partner)
167167
{
168168
if (partner.AssertionSigningKey == null)
169169
throw new ArgumentNullException(nameof(partner.AssertionSigningKey));

src/Solid.Identity.Protocols.Saml2p/Middleware/Idp/AcceptSsoEndpointMiddleware.cs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,13 +101,15 @@ private bool IsValid(AcceptSsoContext context, out SamlResponseStatus? status, o
101101

102102
if (context.Partner == null)
103103
{
104+
Logger.LogInformation("No partner found for requesting id");
104105
status = SamlResponseStatus.Requester;
105106
subStatus = SamlResponseStatus.RequestDenied;
106107
return false;
107108
}
108109

109110
if (!context.Partner.Enabled || !context.Partner.CanInitiateSso)
110111
{
112+
Logger.LogInformation("Requesting partner is disabled or is not authorized to initiate sso");
111113
status = SamlResponseStatus.Requester;
112114
subStatus = SamlResponseStatus.RequestDenied;
113115
return false;
@@ -120,6 +122,28 @@ private bool IsValid(AcceptSsoContext context, out SamlResponseStatus? status, o
120122
return false;
121123
}
122124

125+
if (context.Partner.AuthnRequestSigningKey != null)
126+
{
127+
if(context.Request.Signature == null)
128+
{
129+
Logger.LogInformation("Signature is missing in AuthnRequest");
130+
status = SamlResponseStatus.Requester;
131+
subStatus = SamlResponseStatus.RequestDenied;
132+
return false;
133+
}
134+
135+
if (context.Request.Signature.KeyInfo.KeyName != context.Partner.AuthnRequestSigningKey.KeyId)
136+
{
137+
Logger.LogInformation("AuthnRequest is signed by unexpected key");
138+
status = SamlResponseStatus.Requester;
139+
subStatus = SamlResponseStatus.RequestDenied;
140+
return false;
141+
}
142+
}
143+
144+
145+
146+
123147
status = null;
124148
subStatus = null;
125149
return true;

src/Solid.Identity.Protocols.Saml2p/Middleware/Saml2pEndpointMiddleware.cs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,9 +68,7 @@ protected string SerializeSamlResponse(SamlResponse response, BindingType bindin
6868
using (var memory = new MemoryStream())
6969
{
7070
using (var writer = XmlWriter.Create(memory, new XmlWriterSettings { OmitXmlDeclaration = true, Indent = false, CloseOutput = false, Encoding = new UTF8Encoding(false) }))
71-
{
7271
Serializer.SerializeSamlResponse(writer, response);
73-
}
7472
memory.Position = 0;
7573
return Encoder.Encode(memory, binding);
7674
}

0 commit comments

Comments
 (0)