[KEYCLOAK-18419] Support SAML 2.0 Encrypted IDs in Assertion
This commit is contained in:
parent
443bd4a1ba
commit
4e8e4592ca
6 changed files with 361 additions and 21 deletions
|
@ -27,24 +27,30 @@ import org.keycloak.dom.saml.v2.assertion.AttributeStatementType.ASTChoiceType;
|
|||
import org.keycloak.dom.saml.v2.assertion.AttributeType;
|
||||
import org.keycloak.dom.saml.v2.assertion.ConditionsType;
|
||||
import org.keycloak.dom.saml.v2.assertion.EncryptedAssertionType;
|
||||
import org.keycloak.dom.saml.v2.assertion.EncryptedElementType;
|
||||
import org.keycloak.dom.saml.v2.assertion.NameIDType;
|
||||
import org.keycloak.dom.saml.v2.assertion.StatementAbstractType;
|
||||
import org.keycloak.dom.saml.v2.assertion.SubjectType;
|
||||
import org.keycloak.dom.saml.v2.assertion.SubjectType.STSubType;
|
||||
import org.keycloak.dom.saml.v2.protocol.ResponseType;
|
||||
import org.keycloak.rotation.HardcodedKeyLocator;
|
||||
import org.keycloak.rotation.KeyLocator;
|
||||
import org.keycloak.saml.common.ErrorCodes;
|
||||
import org.keycloak.saml.common.PicketLinkLogger;
|
||||
import org.keycloak.saml.common.PicketLinkLoggerFactory;
|
||||
import org.keycloak.saml.common.constants.GeneralConstants;
|
||||
import org.keycloak.saml.common.constants.JBossSAMLConstants;
|
||||
import org.keycloak.saml.common.exceptions.ConfigurationException;
|
||||
import org.keycloak.saml.common.exceptions.ParsingException;
|
||||
import org.keycloak.saml.common.exceptions.ProcessingException;
|
||||
import org.keycloak.saml.common.exceptions.fed.IssueInstantMissingException;
|
||||
import org.keycloak.saml.common.util.DocumentUtil;
|
||||
import org.keycloak.saml.common.util.StaxParserUtil;
|
||||
import org.keycloak.saml.common.util.StaxUtil;
|
||||
import org.keycloak.saml.processing.api.saml.v2.sig.SAML2Signature;
|
||||
import org.keycloak.saml.processing.core.parsers.saml.SAMLParser;
|
||||
import org.keycloak.saml.processing.core.parsers.util.SAMLParserUtil;
|
||||
import org.keycloak.saml.processing.core.saml.v2.common.SAMLDocumentHolder;
|
||||
import org.keycloak.saml.processing.core.saml.v2.writers.SAMLAssertionWriter;
|
||||
import org.keycloak.saml.processing.core.util.JAXPValidationUtil;
|
||||
import org.keycloak.saml.processing.core.util.XMLEncryptionUtil;
|
||||
|
@ -56,6 +62,8 @@ import org.w3c.dom.Node;
|
|||
import javax.xml.crypto.dsig.XMLSignature;
|
||||
import javax.xml.datatype.XMLGregorianCalendar;
|
||||
import javax.xml.namespace.QName;
|
||||
import javax.xml.stream.XMLEventReader;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.security.PrivateKey;
|
||||
|
@ -64,10 +72,6 @@ import java.util.ArrayList;
|
|||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import org.keycloak.rotation.HardcodedKeyLocator;
|
||||
import org.keycloak.saml.common.constants.GeneralConstants;
|
||||
import org.keycloak.saml.processing.core.saml.v2.common.SAMLDocumentHolder;
|
||||
|
||||
/**
|
||||
* Utility to deal with assertions
|
||||
*
|
||||
|
@ -282,11 +286,11 @@ public class AssertionUtil {
|
|||
*
|
||||
* @return true if signature is present and valid
|
||||
*/
|
||||
|
||||
|
||||
public static boolean isSignatureValid(Element element, KeyLocator keyLocator) {
|
||||
try {
|
||||
SAML2Signature.configureIdAttribute(element);
|
||||
|
||||
|
||||
Element signature = getSignature(element);
|
||||
if(signature != null) {
|
||||
return XMLSignatureUtil.validateSingleNode(signature, keyLocator);
|
||||
|
@ -296,11 +300,11 @@ public class AssertionUtil {
|
|||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
* Given an {@linkplain Element}, check if there is a Signature direct child element
|
||||
*
|
||||
*
|
||||
* @param element parent {@linkplain Element}
|
||||
* @return true if signature is present
|
||||
*/
|
||||
|
@ -308,11 +312,11 @@ public class AssertionUtil {
|
|||
public static boolean isSignedElement(Element element) {
|
||||
return getSignature(element) != null;
|
||||
}
|
||||
|
||||
|
||||
protected static Element getSignature(Element element) {
|
||||
return DocumentUtil.getDirectChildElement(element, XMLSignature.XMLNS, "Signature");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Check whether the assertion has expired.
|
||||
* Processing rules defined in Section 2.5.1.2 of saml-core-2.0-os.pdf.
|
||||
|
@ -608,4 +612,44 @@ public class AssertionUtil {
|
|||
|
||||
return decryptedDocumentElement;
|
||||
}
|
||||
|
||||
public static boolean isIdEncrypted(final ResponseType responseType) {
|
||||
final STSubType subTypeElement = getSubTypeElement(responseType);
|
||||
return subTypeElement != null && subTypeElement.getEncryptedID() != null;
|
||||
}
|
||||
|
||||
public static void decryptId(final ResponseType responseType, final PrivateKey privateKey) throws ConfigurationException, ProcessingException, ParsingException {
|
||||
final STSubType subTypeElement = getSubTypeElement(responseType);
|
||||
if(subTypeElement == null) {
|
||||
return;
|
||||
}
|
||||
final EncryptedElementType encryptedID = subTypeElement.getEncryptedID();
|
||||
if (encryptedID == null) {
|
||||
return;
|
||||
}
|
||||
Element encryptedElement = encryptedID.getEncryptedElement();
|
||||
Document newDoc = DocumentUtil.createDocument();
|
||||
Node importedNode = newDoc.importNode(encryptedElement, true);
|
||||
newDoc.appendChild(importedNode);
|
||||
Element decryptedNameIdElement = XMLEncryptionUtil.decryptElementInDocument(newDoc, privateKey);
|
||||
|
||||
final XMLEventReader xmlEventReader = StaxParserUtil.getXMLEventReader(DocumentUtil.getNodeAsStream(decryptedNameIdElement));
|
||||
NameIDType nameIDType = SAMLParserUtil.parseNameIDType(xmlEventReader);
|
||||
|
||||
// Add unencrypted id, remove encrypted
|
||||
subTypeElement.addBaseID(nameIDType);
|
||||
subTypeElement.setEncryptedID(null);
|
||||
}
|
||||
|
||||
private static STSubType getSubTypeElement(final ResponseType responseType) {
|
||||
final List<ResponseType.RTChoiceType> assertions = responseType.getAssertions();
|
||||
if (assertions.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
final AssertionType assertion = assertions.get(0).getAssertion();
|
||||
if (assertion.getSubject() == null) {
|
||||
return null;
|
||||
}
|
||||
return assertion.getSubject().getSubType();
|
||||
}
|
||||
}
|
|
@ -98,13 +98,12 @@ import static org.hamcrest.Matchers.is;
|
|||
import static org.hamcrest.Matchers.not;
|
||||
import static org.hamcrest.Matchers.notNullValue;
|
||||
import static org.hamcrest.Matchers.nullValue;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertThat;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
/**
|
||||
* Test class for SAML parser.
|
||||
*
|
||||
|
@ -187,6 +186,18 @@ public class SAMLParserTest {
|
|||
assertThat(ea.getEncryptedElement().getLocalName(), is("EncryptedAssertion"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSaml20EncryptedId() throws Exception {
|
||||
ResponseType rt = assertParsed("saml20-encrypted-id-response.xml", ResponseType.class);
|
||||
|
||||
assertThat(rt, notNullValue());
|
||||
assertThat(rt.getAssertions(), notNullValue());
|
||||
assertThat(rt.getAssertions().size(), is(1));
|
||||
assertThat(rt.getAssertions().get(0).getAssertion().getSubject(), notNullValue());
|
||||
assertThat(rt.getAssertions().get(0).getAssertion().getSubject().getSubType(), notNullValue());
|
||||
assertThat(rt.getAssertions().get(0).getAssertion().getSubject().getSubType().getEncryptedID(), notNullValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSaml20EncryptedAssertionWithNewlines() throws Exception {
|
||||
SAMLDocumentHolder holder = assertParsed("KEYCLOAK-4489-encrypted-assertion-with-newlines.xml", SAMLDocumentHolder.class);
|
||||
|
|
|
@ -1,16 +1,33 @@
|
|||
package org.keycloak.saml.processing.core.saml.v2.util;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.StringReader;
|
||||
import java.security.KeyPair;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.Scanner;
|
||||
|
||||
import org.bouncycastle.openssl.PEMKeyPair;
|
||||
import org.bouncycastle.openssl.PEMParser;
|
||||
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
|
||||
import org.bouncycastle.util.Arrays;
|
||||
import org.junit.Test;
|
||||
import org.keycloak.common.util.Base64;
|
||||
import org.keycloak.common.util.DerUtils;
|
||||
import org.keycloak.dom.saml.v2.assertion.NameIDType;
|
||||
import org.keycloak.dom.saml.v2.assertion.SubjectType.STSubType;
|
||||
import org.keycloak.dom.saml.v2.protocol.ResponseType;
|
||||
import org.keycloak.saml.processing.core.parsers.saml.SAMLParser;
|
||||
import org.keycloak.saml.processing.core.parsers.saml.SAMLParserTest;
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Element;
|
||||
|
||||
|
@ -25,28 +42,28 @@ public class AssertionUtilTest {
|
|||
|
||||
@Test
|
||||
public void testSaml20Signed() throws Exception {
|
||||
|
||||
|
||||
X509Certificate decodeCertificate = DerUtils.decodeCertificate(new ByteArrayInputStream(Base64.decode(PUBLIC_CERT)));
|
||||
|
||||
|
||||
try (InputStream st = AssertionUtilTest.class.getResourceAsStream("saml20-signed-response.xml")) {
|
||||
Document document = DocumentUtil.getDocument(st);
|
||||
|
||||
|
||||
Element assertion = DocumentUtil.getDirectChildElement(document.getDocumentElement(), "urn:oasis:names:tc:SAML:2.0:assertion", "Assertion");
|
||||
|
||||
|
||||
assertTrue(AssertionUtil.isSignatureValid(assertion, decodeCertificate.getPublicKey()));
|
||||
|
||||
|
||||
// test manipulation of signature
|
||||
Element signatureElement = AssertionUtil.getSignature(assertion);
|
||||
byte[] validSignature = Base64.decode(signatureElement.getTextContent());
|
||||
|
||||
|
||||
// change the signature value slightly
|
||||
byte[] invalidSignature = Arrays.clone(validSignature);
|
||||
invalidSignature[0] ^= invalidSignature[0];
|
||||
signatureElement.setTextContent(Base64.encodeBytes(invalidSignature));
|
||||
|
||||
|
||||
// check that signature now is invalid
|
||||
assertFalse(AssertionUtil.isSignatureValid(document.getDocumentElement(), decodeCertificate.getPublicKey()));
|
||||
|
||||
|
||||
// restore valid signature, but remove Signature element, check that still invalid
|
||||
signatureElement.setTextContent(Base64.encodeBytes(validSignature));
|
||||
|
||||
|
@ -55,4 +72,54 @@ public class AssertionUtilTest {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSaml20DecryptId() throws Exception {
|
||||
try (InputStream st = getEncryptedIdTestFileInputStream()) {
|
||||
ResponseType responseType = (ResponseType) SAMLParser.getInstance().parse(st);
|
||||
|
||||
STSubType subType = responseType.getAssertions().get(0).getAssertion().getSubject().getSubType();
|
||||
|
||||
assertNotNull(subType.getEncryptedID());
|
||||
assertNull(subType.getBaseID());
|
||||
|
||||
AssertionUtil.decryptId(responseType, extractPrivateKey());
|
||||
|
||||
assertNull(subType.getEncryptedID());
|
||||
assertNotNull(subType.getBaseID());
|
||||
assertTrue(subType.getBaseID() instanceof NameIDType);
|
||||
assertEquals("myTestId",
|
||||
((NameIDType) subType.getBaseID()).getValue());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private InputStream getEncryptedIdTestFileInputStream() {
|
||||
return SAMLParserTest.class.getResourceAsStream("saml20-encrypted-id-response.xml");
|
||||
}
|
||||
|
||||
private PrivateKey extractPrivateKey() throws IOException {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
try (Scanner sc = new Scanner(getEncryptedIdTestFileInputStream())) {
|
||||
while (sc.hasNextLine()) {
|
||||
if (sc.nextLine().contains("BEGIN RSA PRIVATE KEY")) {
|
||||
sb.append("-----BEGIN RSA PRIVATE KEY-----").append("\n");
|
||||
while (sc.hasNextLine()) {
|
||||
String line = sc.nextLine();
|
||||
if (line.contains("END RSA PRIVATE KEY")) {
|
||||
sb.append("-----END RSA PRIVATE KEY-----");
|
||||
break;
|
||||
}
|
||||
sb.append(line).append("\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
assertNotEquals("PEM certificate not found in test data", 0, sb.length());
|
||||
PEMParser pp = new PEMParser(new StringReader(sb.toString()));
|
||||
PEMKeyPair pemKeyPair = (PEMKeyPair) pp.readObject();
|
||||
KeyPair kp = new JcaPEMKeyConverter().getKeyPair(pemKeyPair);
|
||||
pp.close();
|
||||
return kp.getPrivate();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,98 @@
|
|||
<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="_8e8dc5f69a98cc4c1ff3427e5ce34606fd672f91e6" Version="2.0" IssueInstant="2014-07-17T01:01:48Z" Destination="http://sp.example.com/demo1/index.php?acs" InResponseTo="ONELOGIN_4fee3b046395c4e751011e97f8900b5273d56685">
|
||||
<saml:Issuer>http://idp.example.com/metadata.php</saml:Issuer>
|
||||
<samlp:Status>
|
||||
<samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/>
|
||||
</samlp:Status>
|
||||
<saml:Assertion xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xs="http://www.w3.org/2001/XMLSchema" ID="_d71a3a8e9fcc45c9e9d248ef7049393fc8f04e5f75" Version="2.0" IssueInstant="2014-07-17T01:01:48Z">
|
||||
<saml:Issuer>http://idp.example.com/metadata.php</saml:Issuer>
|
||||
<saml:Subject>
|
||||
<saml:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
|
||||
<saml:SubjectConfirmationData NotOnOrAfter="2024-01-18T06:21:48Z" Recipient="http://sp.example.com/demo1/index.php?acs" InResponseTo="ONELOGIN_4fee3b046395c4e751011e97f8900b5273d56685"/>
|
||||
</saml:SubjectConfirmation>
|
||||
<!-- PEM Certificate containing keys for this test
|
||||
-BEGIN RSA PRIVATE KEY-
|
||||
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
|
||||
-END RSA PRIVATE KEY-
|
||||
|
||||
Unencrypted NameID was (mind the namespace declaration!)
|
||||
<saml:NameID xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
|
||||
SPNameQualifier="http://sp.example.com/demo1/metadata.php"
|
||||
Format="urn:oasis:names:tc:SAML:2.0:nameid-format:transient">myTestId</saml:NameID>
|
||||
-->
|
||||
<saml:EncryptedID><xenc:EncryptedData xmlns:xenc="http://www.w3.org/2001/04/xmlenc#" xmlns:dsig="http://www.w3.org/2000/09/xmldsig#" Type="http://www.w3.org/2001/04/xmlenc#Element"><xenc:EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#aes128-cbc"/><dsig:KeyInfo xmlns:dsig="http://www.w3.org/2000/09/xmldsig#"><xenc:EncryptedKey><xenc:EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p"/><xenc:CipherData><xenc:CipherValue>jx39rtgIfnKhdSDYqPoj/heRt60HWRztXvnsp8cx3ETN9FkdKunzl2UcfHy1s7YIQQzFj5duLHpTePyYunHzyRGq+s5oZvJs78me1M1RfxZ7dwb+3Grb66fshF7fHnBvSogmGlo9e6+q0a2TTiaTO+GIXkJdbRzZGgeThTdz6kjZwMw9vJ7/Aq2cmbSCgjQMy8yT9JlDgcyGn6pyL51L6OuHNQ6UmTmZ1qRR8lghLo78hCQWkkt9MNHF4Z5BkSKovjlZgzef5GYIXNczA2kQuDoX/Pdm21tkE63HlluqpjSALGFmvgv7fGQ3t/QImtal992osnaAkE18pjm8wndvdlQIiYDKHezwtkhcdtzVRtvSXrN68Vnwhyl0vw3g4My8XiK0OgVYrP5vwRQZ+/MV0sd8YiG4Qtb1KxqLQY1L3aRnXOLeYwArtktTSmE537SS0189GeDngwFOOiRhkFrJ7xeVnLQ1wdDYsc3+ktFmXf7lQptCG1f93+erh66OFL3IFT/9uYWEqBsZLEY//Xd4bAYUQm6gKYgTqT9a+lztu/k55fzkuiSSNcY5v1xhHLdHeZVPz1BBDsDIVjJa+yb9G4X8qURMv5DqEq3X+Gnui/r2r5+mTZwUNaGx2HJ8CKwixHaEeI8FBKaA93dy7SqZKQl4YgRyjnsprGmlSFG+RbM=</xenc:CipherValue></xenc:CipherData></xenc:EncryptedKey></dsig:KeyInfo>
|
||||
<xenc:CipherData>
|
||||
<xenc:CipherValue>yqORcbX52pH6GNSnqB4niJVqx02itNmoY6/IGr909sv18Cx9pTLyrPqWWeqdBF2tomkKKp7ZE+rzGxzdV7dR1RWELE7Q/r/s2HQXJgA26pOCCfXnQ7drxcMMjpsUU46qVsekyTPlUp+Tmn4ubqP/aGBeIJCXmvgkMijYFhRmt640ianfpLYSW2wxwmfrgVQP7CenxTkKKnNT56N2LYBPFcMEoIb0tvR3tHKmW2uA10S8QagWaWY8b+WEWJ7IYW49zS8zp94JZElQGnzZ/d0ay+iNDTBWFNaJsDQaykbayds=</xenc:CipherValue>
|
||||
</xenc:CipherData>
|
||||
</xenc:EncryptedData></saml:EncryptedID></saml:Subject>
|
||||
<saml:Conditions NotBefore="2014-07-17T01:01:18Z" NotOnOrAfter="2024-01-18T06:21:48Z">
|
||||
<saml:AudienceRestriction>
|
||||
<saml:Audience>http://sp.example.com/demo1/metadata.php</saml:Audience>
|
||||
</saml:AudienceRestriction>
|
||||
</saml:Conditions>
|
||||
<saml:AuthnStatement AuthnInstant="2014-07-17T01:01:48Z" SessionNotOnOrAfter="2024-07-17T09:01:48Z" SessionIndex="_be9967abd904ddcae3c0eb4189adbe3f71e327cf93">
|
||||
<saml:AuthnContext>
|
||||
<saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:Password</saml:AuthnContextClassRef>
|
||||
</saml:AuthnContext>
|
||||
</saml:AuthnStatement>
|
||||
<saml:AttributeStatement>
|
||||
<saml:Attribute Name="uid" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic">
|
||||
<saml:AttributeValue xsi:type="xs:string">test</saml:AttributeValue>
|
||||
</saml:Attribute>
|
||||
<saml:Attribute Name="mail" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic">
|
||||
<saml:AttributeValue xsi:type="xs:string">test@example.com</saml:AttributeValue>
|
||||
</saml:Attribute>
|
||||
<saml:Attribute Name="eduPersonAffiliation" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic">
|
||||
<saml:AttributeValue xsi:type="xs:string">users</saml:AttributeValue>
|
||||
<saml:AttributeValue xsi:type="xs:string">examplerole1</saml:AttributeValue>
|
||||
</saml:Attribute>
|
||||
</saml:AttributeStatement>
|
||||
</saml:Assertion>
|
||||
</samlp:Response>
|
|
@ -466,6 +466,10 @@ public class SAMLEndpoint {
|
|||
return ErrorPage.error(session, authSession, Response.Status.BAD_REQUEST, Messages.INVALID_REQUESTER);
|
||||
}
|
||||
|
||||
if(AssertionUtil.isIdEncrypted(responseType)) {
|
||||
// This methods writes the parsed and decrypted id back on the responseType parameter:
|
||||
AssertionUtil.decryptId(responseType, keys.getPrivateKey());
|
||||
}
|
||||
AssertionType assertion = responseType.getAssertions().get(0).getAssertion();
|
||||
NameIDType subjectNameID = getSubjectNameID(assertion);
|
||||
String principal = getPrincipal(assertion);
|
||||
|
|
|
@ -0,0 +1,116 @@
|
|||
package org.keycloak.testsuite.broker;
|
||||
|
||||
import org.hamcrest.Matchers;
|
||||
import org.junit.Test;
|
||||
import org.keycloak.common.util.PemUtils;
|
||||
import org.keycloak.dom.saml.v2.protocol.AuthnRequestType;
|
||||
import org.keycloak.saml.RandomSecret;
|
||||
import org.keycloak.saml.common.constants.JBossSAMLConstants;
|
||||
import org.keycloak.saml.common.constants.JBossSAMLURIConstants;
|
||||
import org.keycloak.saml.common.exceptions.ConfigurationException;
|
||||
import org.keycloak.saml.common.exceptions.ParsingException;
|
||||
import org.keycloak.saml.common.exceptions.ProcessingException;
|
||||
import org.keycloak.saml.common.util.DocumentUtil;
|
||||
import org.keycloak.saml.processing.api.saml.v2.request.SAML2Request;
|
||||
import org.keycloak.saml.processing.core.saml.v2.common.SAMLDocumentHolder;
|
||||
import org.keycloak.saml.processing.core.util.XMLEncryptionUtil;
|
||||
import org.keycloak.testsuite.admin.ApiUtil;
|
||||
import org.keycloak.testsuite.util.SamlClient;
|
||||
import org.keycloak.testsuite.util.SamlClientBuilder;
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Element;
|
||||
import org.w3c.dom.Node;
|
||||
|
||||
import javax.crypto.SecretKey;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import javax.xml.namespace.QName;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.containsString;
|
||||
import static org.hamcrest.Matchers.hasSize;
|
||||
import static org.hamcrest.Matchers.not;
|
||||
import static org.keycloak.saml.common.constants.JBossSAMLURIConstants.ASSERTION_NSURI;
|
||||
import static org.keycloak.testsuite.saml.AbstractSamlTest.SAML_ASSERTION_CONSUMER_URL_SALES_POST;
|
||||
import static org.keycloak.testsuite.saml.AbstractSamlTest.SAML_CLIENT_ID_SALES_POST;
|
||||
import static org.keycloak.testsuite.util.Matchers.isSamlResponse;
|
||||
|
||||
public class KcSamlEncryptedIdTest extends AbstractBrokerTest {
|
||||
@Override
|
||||
protected BrokerConfiguration getBrokerConfiguration() {
|
||||
return KcSamlBrokerConfiguration.INSTANCE;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEncryptedIdIsReadable() throws ConfigurationException, ParsingException, ProcessingException {
|
||||
createRolesForRealm(bc.consumerRealmName());
|
||||
|
||||
AuthnRequestType loginRep = SamlClient.createLoginRequestDocument(SAML_CLIENT_ID_SALES_POST, SAML_ASSERTION_CONSUMER_URL_SALES_POST, null);
|
||||
|
||||
Document doc = SAML2Request.convert(loginRep);
|
||||
|
||||
final AtomicReference<String> username = new AtomicReference<>();
|
||||
assertThat(adminClient.realm(bc.consumerRealmName()).users().search(username.get()), hasSize(0));
|
||||
|
||||
SAMLDocumentHolder samlResponse = new SamlClientBuilder()
|
||||
.authnRequest(getConsumerSamlEndpoint(bc.consumerRealmName()), doc, SamlClient.Binding.POST).build() // Request to consumer IdP
|
||||
.login().idp(bc.getIDPAlias()).build()
|
||||
|
||||
.processSamlResponse(SamlClient.Binding.POST) // AuthnRequest to producer IdP
|
||||
.targetAttributeSamlRequest()
|
||||
.build()
|
||||
|
||||
.login().user(bc.getUserLogin(), bc.getUserPassword()).build()
|
||||
|
||||
.processSamlResponse(SamlClient.Binding.POST) // Response from producer IdP
|
||||
.transformDocument(document -> { // Replace Subject -> NameID with EncryptedId
|
||||
Node assertionElement = document.getDocumentElement()
|
||||
.getElementsByTagNameNS(ASSERTION_NSURI.get(), JBossSAMLConstants.ASSERTION.get()).item(0);
|
||||
|
||||
if (assertionElement == null) {
|
||||
throw new IllegalStateException("Unable to find assertion in saml response document");
|
||||
}
|
||||
|
||||
String samlNSPrefix = assertionElement.getPrefix();
|
||||
|
||||
try {
|
||||
QName encryptedIdElementQName = new QName(ASSERTION_NSURI.get(), JBossSAMLConstants.ENCRYPTED_ID.get(), samlNSPrefix);
|
||||
QName nameIdQName = new QName(ASSERTION_NSURI.get(),
|
||||
JBossSAMLConstants.NAMEID.get(), samlNSPrefix);
|
||||
|
||||
// Add xmlns:saml attribute to NameId element,
|
||||
// this is necessary as it is decrypted as a separate doc and saml namespace is not know
|
||||
// unless added to NameId element
|
||||
Element nameIdElement = DocumentUtil.getElement(document, nameIdQName);
|
||||
if (nameIdElement == null) {
|
||||
throw new RuntimeException("Assertion doesn't contain NameId " + DocumentUtil.asString(document));
|
||||
}
|
||||
nameIdElement.setAttribute("xmlns:" + samlNSPrefix, ASSERTION_NSURI.get());
|
||||
username.set(nameIdElement.getTextContent());
|
||||
|
||||
byte[] secret = RandomSecret.createRandomSecret(128 / 8);
|
||||
SecretKey secretKey = new SecretKeySpec(secret, "AES");
|
||||
|
||||
// encrypt the Assertion element and replace it with a EncryptedAssertion element.
|
||||
XMLEncryptionUtil.encryptElement(nameIdQName, document, PemUtils.decodePublicKey(ApiUtil.findActiveSigningKey(adminClient.realm(bc.consumerRealmName())).getPublicKey()),
|
||||
secretKey, 128, encryptedIdElementQName, true);
|
||||
} catch (Exception e) {
|
||||
throw new ProcessingException("failed to encrypt", e);
|
||||
}
|
||||
|
||||
assertThat(DocumentUtil.asString(document), not(containsString(username.get())));
|
||||
return document;
|
||||
})
|
||||
.build()
|
||||
|
||||
// first-broker flow
|
||||
.updateProfile().firstName("a").lastName("b").email(bc.getUserEmail()).build()
|
||||
.followOneRedirect()
|
||||
.getSamlResponse(SamlClient.Binding.POST); // Response from consumer IdP
|
||||
|
||||
assertThat(samlResponse, Matchers.notNullValue());
|
||||
assertThat(samlResponse.getSamlObject(), isSamlResponse(JBossSAMLURIConstants.STATUS_SUCCESS));
|
||||
|
||||
assertThat(adminClient.realm(bc.consumerRealmName()).users().search(username.get()), hasSize(1));
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue