Skip to content

Commit c1966b7

Browse files
committed
Enhance parsing of StatusCode in SAML Responses (#38628)
* Enhance parsing of StatusCode in SAML Responses <Status> elements in a failed response might contain two nested <StatusCode> elements. We currently only parse the first one in order to create a message that we attach to the Exception we return and log. However this is generic and only gives out informarion about whether the SAML IDP believes it's an error with the request or if it couldn't handle the request for other reasons. The encapsulated StatusCode has a more interesting error message that potentially gives out the actual error as in Invalid nameid policy, authentication failure etc. This change ensures that we print that information also, and removes Message and Details fields from the message when these are not part of the Status element (which quite often is the case)
1 parent dd0dffb commit c1966b7

File tree

2 files changed

+63
-37
lines changed

2 files changed

+63
-37
lines changed

x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/saml/SamlAuthenticator.java

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,8 +106,7 @@ private SamlAttributes authenticateResponse(Element element, Collection<String>
106106
throw samlException("SAML Response has no status code");
107107
}
108108
if (isSuccess(status) == false) {
109-
throw samlException("SAML Response is not a 'success' response: Code={} Message={} Detail={}",
110-
status.getStatusCode().getValue(), getMessage(status), getDetail(status));
109+
throw samlException("SAML Response is not a 'success' response: {}", getStatusCodeMessage(status));
111110
}
112111
checkIssuer(response.getIssuer(), response);
113112
checkResponseDestination(response);
@@ -137,6 +136,32 @@ private SamlAttributes authenticateResponse(Element element, Collection<String>
137136
return new SamlAttributes(nameId, session, attributes);
138137
}
139138

139+
private String getStatusCodeMessage(Status status) {
140+
StatusCode firstLevel = status.getStatusCode();
141+
StatusCode subLevel = firstLevel.getStatusCode();
142+
StringBuilder sb = new StringBuilder();
143+
if (StatusCode.REQUESTER.equals(firstLevel.getValue())) {
144+
sb.append("The SAML IdP did not grant the request. It indicated that the Elastic Stack side sent something invalid (");
145+
} else if (StatusCode.RESPONDER.equals(firstLevel.getValue())) {
146+
sb.append("The request could not be granted due to an error in the SAML IDP side (");
147+
} else if (StatusCode.VERSION_MISMATCH.equals(firstLevel.getValue())) {
148+
sb.append("The request could not be granted because the SAML IDP doesn't support SAML 2.0 (");
149+
} else {
150+
sb.append("The request could not be granted, the SAML IDP responded with a non-standard Status code (");
151+
}
152+
sb.append(firstLevel.getValue()).append(").");
153+
if (getMessage(status) != null) {
154+
sb.append(" Message: [").append(getMessage(status)).append("]");
155+
}
156+
if (getDetail(status) != null) {
157+
sb.append(" Detail: [").append(getDetail(status)).append("]");
158+
}
159+
if (null != subLevel) {
160+
sb.append(" Specific status code which might indicate what the issue is: [").append(subLevel.getValue()).append("]");
161+
}
162+
return sb.toString();
163+
}
164+
140165
private String getMessage(Status status) {
141166
final StatusMessage sm = status.getStatusMessage();
142167
return sm == null ? null : sm.getMessage();

x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/saml/SamlAuthenticatorTests.java

Lines changed: 36 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1374,7 +1374,7 @@ public void testContentIsAcceptedIfRestrictedToOurAudience() throws Exception {
13741374
}
13751375

13761376
public void testContentIsRejectedIfNotMarkedAsSuccess() throws Exception {
1377-
final String xml = getSimpleResponse(clock.instant()).replace(StatusCode.SUCCESS, StatusCode.REQUESTER);
1377+
final String xml = getStatusFailedResponse();
13781378
final SamlToken token = token(signDoc(xml));
13791379
final ElasticsearchSecurityException exception = expectSamlException(() -> authenticator.authenticate(token));
13801380
assertThat(exception.getMessage(), containsString("not a 'success' response"));
@@ -1408,8 +1408,7 @@ public void testSignatureWrappingAttackOne() throws Exception {
14081408
<ForgedAssertion></ForgedAssertion>
14091409
</ForgedResponse>
14101410
*/
1411-
final Element response = (Element) legitimateDocument.
1412-
getElementsByTagNameNS(SAML20P_NS, "Response").item(0);
1411+
final Element response = (Element) legitimateDocument.getElementsByTagNameNS(SAML20P_NS, "Response").item(0);
14131412
final Element clonedResponse = (Element) response.cloneNode(true);
14141413
final Element clonedSignature = (Element) clonedResponse.
14151414
getElementsByTagNameNS("http://www.w3.org/2000/09/xmldsig#", "Signature").item(0);
@@ -1443,8 +1442,7 @@ public void testSignatureWrappingAttackTwo() throws Exception {
14431442
<ForgedAssertion></ForgedAssertion>
14441443
</ForgedResponse>
14451444
*/
1446-
final Element response = (Element) legitimateDocument.
1447-
getElementsByTagNameNS(SAML20P_NS, "Response").item(0);
1445+
final Element response = (Element) legitimateDocument.getElementsByTagNameNS(SAML20P_NS, "Response").item(0);
14481446
final Element clonedResponse = (Element) response.cloneNode(true);
14491447
final Element clonedSignature = (Element) clonedResponse.
14501448
getElementsByTagNameNS("http://www.w3.org/2000/09/xmldsig#", "Signature").item(0);
@@ -1482,8 +1480,7 @@ public void testSignatureWrappingAttackThree() throws Exception {
14821480
</LegitimateAssertion>
14831481
</Response>
14841482
*/
1485-
final Element response = (Element) legitimateDocument.
1486-
getElementsByTagNameNS(SAML20P_NS, "Response").item(0);
1483+
final Element response = (Element) legitimateDocument.getElementsByTagNameNS(SAML20P_NS, "Response").item(0);
14871484
final Element assertion = (Element) legitimateDocument.
14881485
getElementsByTagNameNS(SAML20_NS, "Assertion").item(0);
14891486
final Element forgedAssertion = (Element) assertion.cloneNode(true);
@@ -1522,10 +1519,8 @@ public void testSignatureWrappingAttackFour() throws Exception {
15221519
</ForgedAssertion>
15231520
</Response>
15241521
*/
1525-
final Element response = (Element) legitimateDocument.
1526-
getElementsByTagNameNS(SAML20P_NS, "Response").item(0);
1527-
final Element assertion = (Element) legitimateDocument.
1528-
getElementsByTagNameNS(SAML20_NS, "Assertion").item(0);
1522+
final Element response = (Element) legitimateDocument.getElementsByTagNameNS(SAML20P_NS, "Response").item(0);
1523+
final Element assertion = (Element) legitimateDocument.getElementsByTagNameNS(SAML20_NS, "Assertion").item(0);
15291524
final Element forgedAssertion = (Element) assertion.cloneNode(true);
15301525
forgedAssertion.setAttribute("ID", "_forged_assertion_id");
15311526
final Element clonedSignature = (Element) forgedAssertion.
@@ -1559,17 +1554,14 @@ public void testSignatureWrappingAttackFive() throws Exception {
15591554
<LegitimateAssertion></LegitimateAssertion>
15601555
</Response>
15611556
*/
1562-
final Element response = (Element) legitimateDocument.
1563-
getElementsByTagNameNS(SAML20P_NS, "Response").item(0);
1564-
final Element assertion = (Element) legitimateDocument.
1565-
getElementsByTagNameNS(SAML20_NS, "Assertion").item(0);
1557+
final Element response = (Element) legitimateDocument.getElementsByTagNameNS(SAML20P_NS, "Response").item(0);
1558+
final Element assertion = (Element) legitimateDocument.getElementsByTagNameNS(SAML20_NS, "Assertion").item(0);
15661559
final Element signature = (Element) assertion.
1567-
getElementsByTagNameNS("http://www.w3.org/2000/09/xmldsig#", "Signature").item(0);
1560+
getElementsByTagNameNS("http://www.w3.org/2000/09/xmldsig#", "Signature").item(0);
15681561
assertion.removeChild(signature);
15691562
final Element forgedAssertion = (Element) assertion.cloneNode(true);
15701563
forgedAssertion.setAttribute("ID", "_forged_assertion_id");
1571-
final Element issuer = (Element) forgedAssertion.
1572-
getElementsByTagNameNS(SAML20_NS, "Issuer").item(0);
1564+
final Element issuer = (Element) forgedAssertion.getElementsByTagNameNS(SAML20_NS, "Issuer").item(0);
15731565
forgedAssertion.insertBefore(signature, issuer.getNextSibling());
15741566
response.insertBefore(forgedAssertion, assertion);
15751567
final SamlToken forgedToken = token(SamlUtils.toString((legitimateDocument.getDocumentElement())));
@@ -1598,10 +1590,8 @@ public void testSignatureWrappingAttackSix() throws Exception {
15981590
</ForgedAssertion>
15991591
</Response>
16001592
*/
1601-
final Element response = (Element) legitimateDocument.
1602-
getElementsByTagNameNS(SAML20P_NS, "Response").item(0);
1603-
final Element assertion = (Element) legitimateDocument.
1604-
getElementsByTagNameNS(SAML20_NS, "Assertion").item(0);
1593+
final Element response = (Element) legitimateDocument.getElementsByTagNameNS(SAML20P_NS, "Response").item(0);
1594+
final Element assertion = (Element) legitimateDocument.getElementsByTagNameNS(SAML20_NS, "Assertion").item(0);
16051595
final Element forgedAssertion = (Element) assertion.cloneNode(true);
16061596
forgedAssertion.setAttribute("ID", "_forged_assertion_id");
16071597
final Element signature = (Element) assertion.
@@ -1610,8 +1600,7 @@ public void testSignatureWrappingAttackSix() throws Exception {
16101600
getElementsByTagNameNS("http://www.w3.org/2000/09/xmldsig#", "Signature").item(0);
16111601
forgedAssertion.removeChild(forgedSignature);
16121602
assertion.removeChild(signature);
1613-
final Element issuer = (Element) forgedAssertion.
1614-
getElementsByTagNameNS(SAML20_NS, "Issuer").item(0);
1603+
final Element issuer = (Element) forgedAssertion.getElementsByTagNameNS(SAML20_NS, "Issuer").item(0);
16151604
forgedAssertion.insertBefore(signature, issuer.getNextSibling());
16161605
signature.appendChild(assertion);
16171606
response.appendChild(forgedAssertion);
@@ -1642,11 +1631,9 @@ public void testSignatureWrappingAttackSeven() throws Exception {
16421631
</LegitimateAssertion>
16431632
</Response>
16441633
*/
1645-
final Element response = (Element) legitimateDocument.
1646-
getElementsByTagNameNS(SAML20P_NS, "Response").item(0);
1634+
final Element response = (Element) legitimateDocument.getElementsByTagNameNS(SAML20P_NS, "Response").item(0);
16471635
final Element extensions = legitimateDocument.createElement("Extensions");
1648-
final Element assertion = (Element) legitimateDocument.
1649-
getElementsByTagNameNS(SAML20_NS, "Assertion").item(0);
1636+
final Element assertion = (Element) legitimateDocument.getElementsByTagNameNS(SAML20_NS, "Assertion").item(0);
16501637
response.insertBefore(extensions, assertion);
16511638
final Element forgedAssertion = (Element) assertion.cloneNode(true);
16521639
forgedAssertion.setAttribute("ID", "_forged_assertion_id");
@@ -1683,10 +1670,8 @@ public void testSignatureWrappingAttackEight() throws Exception {
16831670
</ForgedAssertion>
16841671
</Response>
16851672
*/
1686-
final Element response = (Element) legitimateDocument.
1687-
getElementsByTagNameNS(SAML20P_NS, "Response").item(0);
1688-
final Element assertion = (Element) legitimateDocument.
1689-
getElementsByTagNameNS(SAML20_NS, "Assertion").item(0);
1673+
final Element response = (Element) legitimateDocument.getElementsByTagNameNS(SAML20P_NS, "Response").item(0);
1674+
final Element assertion = (Element) legitimateDocument.getElementsByTagNameNS(SAML20_NS, "Assertion").item(0);
16901675
final Element forgedAssertion = (Element) assertion.cloneNode(true);
16911676
forgedAssertion.setAttribute("ID", "_forged_assertion_id");
16921677
final Element signature = (Element) assertion.
@@ -1695,8 +1680,7 @@ public void testSignatureWrappingAttackEight() throws Exception {
16951680
getElementsByTagNameNS("http://www.w3.org/2000/09/xmldsig#", "Signature").item(0);
16961681
forgedAssertion.removeChild(forgedSignature);
16971682
assertion.removeChild(signature);
1698-
final Element issuer = (Element) forgedAssertion.
1699-
getElementsByTagNameNS(SAML20_NS, "Issuer").item(0);
1683+
final Element issuer = (Element) forgedAssertion.getElementsByTagNameNS(SAML20_NS, "Issuer").item(0);
17001684
forgedAssertion.insertBefore(signature, issuer.getNextSibling());
17011685
Element object = legitimateDocument.createElement("Object");
17021686
object.appendChild(assertion);
@@ -2034,7 +2018,7 @@ private void encryptElement(Element element, X509Certificate certificate, boolea
20342018
}
20352019

20362020
private Element buildEncryptedKeyElement(Document document, EncryptedKey encryptedKey, X509Certificate certificate)
2037-
throws XMLSecurityException {
2021+
throws XMLSecurityException {
20382022
final XMLCipher cipher = XMLCipher.getInstance();
20392023
final org.apache.xml.security.keys.KeyInfo keyInfo = new org.apache.xml.security.keys.KeyInfo(document);
20402024
final X509Data x509Data = new X509Data(document);
@@ -2054,6 +2038,23 @@ private Response toResponse(String xml) throws SAXException, IOException, Parser
20542038
return authenticator.buildXmlObject(doc.getDocumentElement(), Response.class);
20552039
}
20562040

2041+
private String getStatusFailedResponse() {
2042+
final Instant now = clock.instant();
2043+
return "<?xml version='1.0' encoding='UTF-8'?>\n" +
2044+
"<proto:Response Destination='" + SP_ACS_URL + "' ID='" + randomId() + "' InResponseTo='" + requestId +
2045+
"' IssueInstant='" + now + "' Version='2.0'" +
2046+
" xmlns:proto='urn:oasis:names:tc:SAML:2.0:protocol'" +
2047+
" xmlns:assert='urn:oasis:names:tc:SAML:2.0:assertion'" +
2048+
" xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance'" +
2049+
" xmlns:xs='http://www.w3.org/2001/XMLSchema'" +
2050+
" xmlns:ds='http://www.w3.org/2000/09/xmldsig#' >" +
2051+
"<assert:Issuer>" + IDP_ENTITY_ID + "</assert:Issuer>" +
2052+
"<proto:Status><proto:StatusCode Value='urn:oasis:names:tc:SAML:2.0:status:Requester'>" +
2053+
"<proto:StatusCode Value='urn:oasis:names:tc:SAML:2.0:status:InvalidNameIDPolicy'/></proto:StatusCode>" +
2054+
"</proto:Status>" +
2055+
"</proto:Response>";
2056+
}
2057+
20572058
private String getSimpleResponse(Instant now) {
20582059
return getSimpleResponse(now, randomAlphaOfLengthBetween(12, 18), randomId());
20592060
}

0 commit comments

Comments
 (0)