[KEYCLOAK-18419] Support SAML 2.0 Encrypted IDs in Assertion

This commit is contained in:
Sebastian Kanzow 2021-07-30 16:47:08 +02:00 committed by Hynek Mlnařík
parent 443bd4a1ba
commit 4e8e4592ca
6 changed files with 361 additions and 21 deletions

View file

@ -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();
}
}

View file

@ -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);

View file

@ -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();
}
}

View file

@ -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>

View file

@ -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);

View file

@ -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));
}
}