Skip to content

Commit

Permalink
Validate Saml response issuer to stored request state
Browse files Browse the repository at this point in the history
  • Loading branch information
AndersAbel committed Sep 18, 2023
1 parent 05febb4 commit af646c1
Show file tree
Hide file tree
Showing 2 changed files with 81 additions and 8 deletions.
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
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 af646c1

Please sign in to comment.