Skip to content

Commit

Permalink
Issuer validation
Browse files Browse the repository at this point in the history
- Fixes #712
  • Loading branch information
AndersAbel committed Sep 18, 2023
2 parents 05febb4 + b92d951 commit 855f94d
Show file tree
Hide file tree
Showing 4 changed files with 114 additions and 8 deletions.
2 changes: 2 additions & 0 deletions Sustainsys.Saml2/SAML2P/Saml2Response.cs
Original file line number Diff line number Diff line change
Expand Up @@ -605,6 +605,8 @@ private IEnumerable<ClaimsIdentity> CreateClaims(IOptions options, IdentityProvi
validationParameters.ValidAudience = options.SPOptions.EntityId.Id;
validationParameters.TokenReplayCache = options.SPOptions.TokenReplayCache;
validationParameters.ValidateTokenReplay = true;
validationParameters.ValidIssuer = idp.EntityId.Id;
validationParameters.ValidateIssuer = true;

options.Notifications.Unsafe.TokenValidationParametersCreated(validationParameters, idp, XmlElement);

Expand Down
25 changes: 17 additions & 8 deletions Sustainsys.Saml2/WebSSO/AcsCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,12 @@ public class AcsCommand : ICommand
/// <returns>CommandResult</returns>
public CommandResult Run(HttpRequestData request, IOptions options)
{
if(request == null)
if (request == null)
{
throw new ArgumentNullException(nameof(request));
}

if(options == null)
if (options == null)
{
throw new ArgumentNullException(nameof(options));
}
Expand All @@ -51,7 +51,7 @@ public CommandResult Run(HttpRequestData request, IOptions options)
options.Notifications.MessageUnbound(unbindResult);

var samlResponse = new Saml2Response(unbindResult.Data, request.StoredRequestState?.MessageId, options);

var idpContext = GetIdpContext(unbindResult.Data, request, options);

var result = ProcessResponse(options, samlResponse, request.StoredRequestState, idpContext, unbindResult.RelayState);
Expand Down Expand Up @@ -146,22 +146,24 @@ private static CommandResult ProcessResponse(
IdentityProvider identityProvider,
string relayState)
{
ValidateIssuer(storedRequestState?.Idp, samlResponse);

var principal = new ClaimsPrincipal(samlResponse.GetClaims(options, storedRequestState?.RelayData));

if (options.SPOptions.ReturnUrl == null && !identityProvider.RelayStateUsedAsReturnUrl)
{
if (storedRequestState == null)
{
throw new ConfigurationErrorsException(UnsolicitedMissingReturnUrlMessage);
}
if(storedRequestState.ReturnUrl == null)
if (storedRequestState.ReturnUrl == null)
{
throw new ConfigurationErrorsException(SpInitiatedMissingReturnUrl);
}
}

options.SPOptions.Logger.WriteInformation("Successfully processed SAML response "
+ samlResponse.Id.Value + " and authenticated "
options.SPOptions.Logger.WriteInformation("Successfully processed SAML response "
+ samlResponse.Id.Value + " and authenticated "
+ principal.FindFirst(ClaimTypes.NameIdentifier)?.Value);

return new CommandResult()
Expand All @@ -174,7 +176,14 @@ private static CommandResult ProcessResponse(
};
}


private static void ValidateIssuer(EntityId idp, Saml2Response samlResponse)
{
if (idp != null && idp.Id != samlResponse.Issuer.Id)
{
throw new Saml2ResponseFailedValidationException(
$"Unexpected issuer {samlResponse.Issuer.Id} found in response, request was sent to {idp.Id}");
}
}

internal const string UnsolicitedMissingReturnUrlMessage =
@"Unsolicited SAML response received, but no ReturnUrl is configured.
Expand Down
31 changes: 31 additions & 0 deletions Tests/Tests.Shared/Saml2P/Saml2ResponseTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
using X509SecurityKey = Microsoft.IdentityModel.Tokens.X509SecurityKey;
using System.Collections.Generic;
using Microsoft.IdentityModel.Logging;
using Microsoft.IdentityModel.Tokens;

namespace Sustainsys.Saml2.Tests.Saml2P
{
Expand Down Expand Up @@ -2506,5 +2507,35 @@ public void Saml2Response_SessionNotOnOrAfter_ThrowsIfCalledBeforeGetClaims()
.Should().Throw<InvalidOperationException>()
.WithMessage("*GetClaims*");
}

[TestMethod]
public void Saml2Response_GetClaims_DifferentIssuers()
{
var response =
@"<?xml version=""1.0"" encoding=""UTF-8""?>
<saml2p:Response xmlns:saml2p=""urn:oasis:names:tc:SAML:2.0:protocol""
xmlns:saml2=""urn:oasis:names:tc:SAML:2.0:assertion""
ID = """ + MethodBase.GetCurrentMethod().Name + @""" Version=""2.0"" IssueInstant=""2013-01-01T00:00:00Z"">
<saml2:Issuer>https://idp.example.com</saml2:Issuer>
<saml2p:Status>
<saml2p:StatusCode Value=""urn:oasis:names:tc:SAML:2.0:status:Success"" />
</saml2p:Status>
<saml2:Assertion xmlns:saml2=""urn:oasis:names:tc:SAML:2.0:assertion""
Version=""2.0"" ID=""" + MethodBase.GetCurrentMethod().Name + @"_Assertion1""
IssueInstant=""2013-09-25T00:00:00Z"">
<saml2:Issuer>https://other.example.com</saml2:Issuer>
<saml2:Subject>
<saml2:NameID>SomeUser</saml2:NameID>
<saml2:SubjectConfirmation Method=""urn:oasis:names:tc:SAML:2.0:cm:bearer"" />
</saml2:Subject>
<saml2:Conditions NotOnOrAfter=""2100-01-01T00:00:00Z"" />
</saml2:Assertion>
</saml2p:Response>";

var signedResponse = SignedXmlHelper.SignXml(response);

Saml2Response.Read(signedResponse).Invoking(r => r.GetClaims(Options.FromConfiguration))
.Should().Throw<SecurityTokenInvalidIssuerException>();
}
}
}
64 changes: 64 additions & 0 deletions Tests/Tests.Shared/WebSSO/AcsCommandTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -852,5 +852,69 @@ public void AcsCommand_Run_WithRelayStateUserAsReturnUrl_AbsolutUrlValidatesThro

called.Should().BeTrue("Notifaction should have been called");
}

[TestMethod]
public void AcsCommand_Run_ValidatesIssuerWithStoredRequestState()
{
var idp = Options.FromConfiguration.IdentityProviders.Default;

var response =
@"<saml2p:Response xmlns:saml2p=""urn:oasis:names:tc:SAML:2.0:protocol""
xmlns:saml2=""urn:oasis:names:tc:SAML:2.0:assertion""
ID = """ + MethodBase.GetCurrentMethod().Name + @""" InResponseTo = ""InResponseToId"" Version=""2.0"" IssueInstant=""2013-01-01T00:00:00Z"">
<saml2:Issuer>
https://idp.example.com
</saml2:Issuer>
<saml2p:Status>
<saml2p:StatusCode Value=""urn:oasis:names:tc:SAML:2.0:status:Success"" />
</saml2p:Status>
<saml2:Assertion
Version=""2.0"" ID=""" + MethodBase.GetCurrentMethod().Name + @"_Assertion2""
IssueInstant=""2013-09-25T00:00:00Z"">
<saml2:Issuer>https://idp.example.com</saml2:Issuer>
<saml2:Subject>
<saml2:NameID>SomeUser</saml2:NameID>
<saml2:SubjectConfirmation Method=""urn:oasis:names:tc:SAML:2.0:cm:bearer"" />
</saml2:Subject>
<saml2:Conditions NotOnOrAfter=""2100-01-01T00:00:00Z"" />
</saml2:Assertion>
</saml2p:Response>";

var responseFormValue = Convert.ToBase64String
(Encoding.UTF8.GetBytes(SignedXmlHelper.SignXml(response)));
var relayStateFormValue = "rs1234";

var r = new HttpRequestData(
"POST",
new Uri("http://localhost"),
"/ModulePath",
new KeyValuePair<string, IEnumerable<string>>[]
{
new KeyValuePair<string, IEnumerable<string>>("SAMLResponse", new string[] { responseFormValue }),
new KeyValuePair<string, IEnumerable<string>>("RelayState", new string[] { relayStateFormValue })
},
new StoredRequestState(
new EntityId("https://other.example.com"),
new Uri("http://localhost/testUrl.aspx"),
new Saml2Id("InResponseToId"),
null)
);

var ids = new ClaimsIdentity[] { new ClaimsIdentity("Federation") };
ids[0].AddClaim(new Claim(ClaimTypes.NameIdentifier, "SomeUser", null, "https://idp.example.com"));

var expected = new CommandResult()
{
Principal = new ClaimsPrincipal(ids),
HttpStatusCode = HttpStatusCode.SeeOther,
Location = new Uri("http://localhost/testUrl.aspx"),
ClearCookieName = StoredRequestState.CookieNameBase + relayStateFormValue
};

var subject = new AcsCommand();
subject.Invoking(s => s.Run(r, StubFactory.CreateOptions()))
.Should().Throw<Saml2ResponseFailedValidationException>()
.WithMessage("Unexpected issuer*idp*other*");
}
}
}

0 comments on commit 855f94d

Please sign in to comment.