diff --git a/adapters/saml/core-public/src/main/java/org/keycloak/adapters/saml/SamlPrincipal.java b/adapters/saml/core-public/src/main/java/org/keycloak/adapters/saml/SamlPrincipal.java
index 280c3fe459..6c3c932b04 100755
--- a/adapters/saml/core-public/src/main/java/org/keycloak/adapters/saml/SamlPrincipal.java
+++ b/adapters/saml/core-public/src/main/java/org/keycloak/adapters/saml/SamlPrincipal.java
@@ -20,7 +20,9 @@ package org.keycloak.adapters.saml;
import org.keycloak.common.util.MultivaluedHashMap;
import org.keycloak.dom.saml.v2.assertion.AssertionType;
+import org.keycloak.dom.saml.v2.assertion.NameIDType;
import java.io.Serializable;
+import java.net.URI;
import java.security.Principal;
import java.util.Collections;
import java.util.List;
@@ -81,6 +83,27 @@ public class SamlPrincipal implements Serializable, Principal {
return nameIDFormat;
}
+ /**
+ * Subject nameID format
+ *
+ * @return
+ */
+ public NameIDType getNameID() {
+ if (assertion != null
+ && assertion.getSubject() != null
+ && assertion.getSubject().getSubType() != null
+ && assertion.getSubject().getSubType().getBaseID() instanceof NameIDType) {
+ return (NameIDType) assertion.getSubject().getSubType().getBaseID();
+ }
+
+ NameIDType res = new NameIDType();
+ res.setValue(getSamlSubject());
+ if (getNameIDFormat() != null) {
+ res.setFormat(URI.create(getNameIDFormat()));
+ }
+ return res;
+ }
+
@Override
public String getName() {
return name;
diff --git a/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/profile/webbrowsersso/WebBrowserSsoAuthenticationHandler.java b/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/profile/webbrowsersso/WebBrowserSsoAuthenticationHandler.java
index 231c425337..abbe89b693 100755
--- a/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/profile/webbrowsersso/WebBrowserSsoAuthenticationHandler.java
+++ b/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/profile/webbrowsersso/WebBrowserSsoAuthenticationHandler.java
@@ -108,7 +108,7 @@ public class WebBrowserSsoAuthenticationHandler extends AbstractSamlAuthenticati
.assertionExpiration(30)
.issuer(deployment.getEntityID())
.sessionIndex(account.getSessionIndex())
- .userPrincipal(account.getPrincipal().getSamlSubject(), account.getPrincipal().getNameIDFormat())
+ .nameId(account.getPrincipal().getNameID())
.destination(deployment.getIDP().getSingleLogoutService().getRequestBindingUrl());
BaseSAML2BindingBuilder binding = new BaseSAML2BindingBuilder();
if (deployment.getIDP().getSingleLogoutService().signRequest()) {
diff --git a/common/pom.xml b/common/pom.xml
index 218f0066e9..008a6549a2 100755
--- a/common/pom.xml
+++ b/common/pom.xml
@@ -63,6 +63,11 @@
junit
test
+
+ org.hamcrest
+ hamcrest-all
+ test
+
diff --git a/common/src/main/java/org/keycloak/common/util/StringSerialization.java b/common/src/main/java/org/keycloak/common/util/StringSerialization.java
new file mode 100644
index 0000000000..d2290e792e
--- /dev/null
+++ b/common/src/main/java/org/keycloak/common/util/StringSerialization.java
@@ -0,0 +1,112 @@
+package org.keycloak.common.util;
+
+import java.net.URI;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Objects;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Utilities to serialize objects to string. Type safety is not guaranteed here and is responsibility of the caller.
+ * @author hmlnarik
+ */
+public class StringSerialization {
+
+ // Since there is still need to support JDK 7, we have to work without functional interfaces
+ private static enum DeSerializer {
+ OBJECT {
+ @Override public String serialize(Object o) { return o.toString(); };
+ @Override public Object deserialize(String s) { return s; };
+ },
+ URI {
+ @Override public String serialize(Object o) { return o.toString(); };
+ @Override public Object deserialize(String s) { return java.net.URI.create(s); };
+ },
+ ;
+
+ /** Serialize value which is guaranteed to be non-null */
+ public abstract String serialize(Object o);
+ public abstract Object deserialize(String s);
+ }
+
+ private static final Map, DeSerializer> WELL_KNOWN_DESERIALIZERS = new LinkedHashMap<>();
+ private static final String SEPARATOR = ";";
+ private static final Pattern ESCAPE_PATTERN = Pattern.compile(SEPARATOR);
+ private static final Pattern UNESCAPE_PATTERN = Pattern.compile(SEPARATOR + SEPARATOR);
+ private static final Pattern VALUE_PATTERN = Pattern.compile("([NV])" +
+ "(" +
+ "(?:[^" + SEPARATOR + "]|" + SEPARATOR + SEPARATOR + ")*?" +
+ ")($|" + SEPARATOR + "(?!" + SEPARATOR + "))",
+ Pattern.DOTALL
+ );
+
+ static {
+ WELL_KNOWN_DESERIALIZERS.put(URI.class, DeSerializer.URI);
+ WELL_KNOWN_DESERIALIZERS.put(String.class, DeSerializer.OBJECT);
+ }
+
+ /**
+ * Serialize given objects as strings separated by {@link #SEPARATOR} according to the {@link #WELL_KNOWN_SERIALIZERS}.
+ * @param toSerialize
+ * @return
+ */
+ public static String serialize(Object... toSerialize) {
+ StringBuilder sb = new StringBuilder();
+
+ for (int i = 0; i < toSerialize.length; i ++) {
+ Object o = toSerialize[i];
+ String stringO = getStringFrom(o);
+ String escapedStringO = ESCAPE_PATTERN.matcher(stringO).replaceAll(SEPARATOR + SEPARATOR);
+ sb.append(escapedStringO);
+
+ if (i < toSerialize.length - 1) {
+ sb.append(SEPARATOR);
+ }
+ }
+
+ return sb.toString();
+ }
+
+ public static Deserializer deserialize(String what) {
+ return new Deserializer(what);
+ }
+
+ private static String getStringFrom(Object o) {
+ if (o == null) {
+ return "N";
+ }
+
+ Class> c = o.getClass();
+ DeSerializer f = WELL_KNOWN_DESERIALIZERS.get(c);
+ return "V" + (f == null ? o : f.serialize(o));
+ }
+
+ private static T getObjectFrom(String escapedString, Class clazz) {
+ DeSerializer f = WELL_KNOWN_DESERIALIZERS.get(clazz);
+ Object res = f == null ? escapedString : f.deserialize(escapedString);
+ return clazz.cast(res);
+ }
+
+ public static class Deserializer {
+
+ private final Matcher valueMatcher;
+
+ public Deserializer(String what) {
+ this.valueMatcher = VALUE_PATTERN.matcher(what);
+ }
+
+ public T next(Class clazz) {
+ if (! this.valueMatcher.find()) {
+ return null;
+ }
+ String valueOrNull = this.valueMatcher.group(1);
+ if (valueOrNull == null || Objects.equals(valueOrNull, "N")) {
+ return null;
+ }
+ String escapedStringO = this.valueMatcher.group(2);
+ String unescapedStringO = UNESCAPE_PATTERN.matcher(escapedStringO).replaceAll(SEPARATOR);
+ return getObjectFrom(unescapedStringO, clazz);
+ }
+ }
+}
diff --git a/common/src/test/java/org/keycloak/common/util/StringSerializationTest.java b/common/src/test/java/org/keycloak/common/util/StringSerializationTest.java
new file mode 100644
index 0000000000..43e746d8a5
--- /dev/null
+++ b/common/src/test/java/org/keycloak/common/util/StringSerializationTest.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright 2019 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.common.util;
+
+import org.keycloak.common.util.StringSerialization.Deserializer;
+import java.net.URI;
+import org.junit.Test;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.nullValue;
+import static org.junit.Assert.assertThat;
+
+/**
+ *
+ * @author hmlnarik
+ */
+public class StringSerializationTest {
+
+ @Test
+ public void testString() {
+ String a = "aa";
+ String b = "a:\na";
+ String c = null;
+ String d = "a::a";
+ String e = "::";
+
+ String serialized = StringSerialization.serialize(a, b, c, d, e);
+ Deserializer deserializer = StringSerialization.deserialize(serialized);
+
+ assertThat(deserializer.next(String.class), is(a));
+ assertThat(deserializer.next(String.class), is(b));
+ assertThat(deserializer.next(String.class), is(c));
+ assertThat(deserializer.next(String.class), is(d));
+ assertThat(deserializer.next(String.class), is(e));
+ assertThat(deserializer.next(String.class), nullValue());
+ }
+
+ @Test
+ public void testStringWithSeparators() {
+ String a = ";;";
+ String b = "a;a";
+ String c = null;
+ String d = "a;;a";
+ String e = ";;";
+
+ String serialized = StringSerialization.serialize(a, b, c, d, e);
+ Deserializer deserializer = StringSerialization.deserialize(serialized);
+
+ assertThat(deserializer.next(String.class), is(a));
+ assertThat(deserializer.next(String.class), is(b));
+ assertThat(deserializer.next(String.class), is(c));
+ assertThat(deserializer.next(String.class), is(d));
+ assertThat(deserializer.next(String.class), is(e));
+ assertThat(deserializer.next(String.class), nullValue());
+ }
+
+ @Test
+ public void testStringUri() {
+ String a = ";;";
+ String b = "a;a";
+ String c = null;
+ URI d = URI.create("http://my.domain.com");
+ String e = ";;";
+
+ String serialized = StringSerialization.serialize(a, b, c, d, e);
+ Deserializer deserializer = StringSerialization.deserialize(serialized);
+
+ assertThat(deserializer.next(String.class), is(a));
+ assertThat(deserializer.next(String.class), is(b));
+ assertThat(deserializer.next(String.class), is(c));
+ assertThat(deserializer.next(URI.class), is(d));
+ assertThat(deserializer.next(String.class), is(e));
+ assertThat(deserializer.next(String.class), nullValue());
+ }
+
+}
diff --git a/saml-core-api/src/main/java/org/keycloak/dom/saml/v2/assertion/NameIDType.java b/saml-core-api/src/main/java/org/keycloak/dom/saml/v2/assertion/NameIDType.java
index 7ab7c795a3..f2f3497d53 100755
--- a/saml-core-api/src/main/java/org/keycloak/dom/saml/v2/assertion/NameIDType.java
+++ b/saml-core-api/src/main/java/org/keycloak/dom/saml/v2/assertion/NameIDType.java
@@ -16,6 +16,8 @@
*/
package org.keycloak.dom.saml.v2.assertion;
+import org.keycloak.common.util.StringSerialization;
+import org.keycloak.common.util.StringSerialization.Deserializer;
import java.net.URI;
/**
@@ -69,4 +71,25 @@ public class NameIDType extends BaseIDAbstractType {
public void setSPProvidedID(String sPProvidedID) {
this.sPProvidedID = sPProvidedID;
}
+
+ public String serializeAsString() {
+ return StringSerialization.serialize(
+ getNameQualifier(),
+ getSPNameQualifier(),
+ value,
+ format,
+ sPProvidedID
+ );
+ }
+
+ public static NameIDType deserializeFromString(String s) {
+ NameIDType res = new NameIDType();
+ Deserializer d = StringSerialization.deserialize(s);
+ res.setNameQualifier(d.next(String.class));
+ res.setSPNameQualifier(d.next(String.class));
+ res.setValue(d.next(String.class));
+ res.setFormat(d.next(URI.class));
+ res.setSPProvidedID(d.next(String.class));
+ return res;
+ }
}
\ No newline at end of file
diff --git a/saml-core/src/main/java/org/keycloak/saml/SAML2LogoutRequestBuilder.java b/saml-core/src/main/java/org/keycloak/saml/SAML2LogoutRequestBuilder.java
index b1cd113e09..5b0d7b3828 100755
--- a/saml-core/src/main/java/org/keycloak/saml/SAML2LogoutRequestBuilder.java
+++ b/saml-core/src/main/java/org/keycloak/saml/SAML2LogoutRequestBuilder.java
@@ -36,8 +36,7 @@ import org.keycloak.dom.saml.v2.protocol.ExtensionsType;
* @version $Revision: 1 $
*/
public class SAML2LogoutRequestBuilder implements SamlProtocolExtensionsAwareBuilder {
- protected String userPrincipal;
- protected String userPrincipalFormat;
+ protected NameIDType nameId;
protected String sessionIndex;
protected long assertionExpiration;
protected String destination;
@@ -72,10 +71,26 @@ public class SAML2LogoutRequestBuilder implements SamlProtocolExtensionsAwareBui
return this;
}
+ /**
+ *
+ * @param userPrincipal
+ * @param userPrincipalFormat
+ * @return
+ * @deprecated Prefer {@link #nameId(org.keycloak.dom.saml.v2.assertion.NameIDType)}
+ */
+ @Deprecated
+ public SAML2LogoutRequestBuilder userPrincipal(String userPrincipal, String userPrincipalFormat) {
+ NameIDType nid = new NameIDType();
+ nid.setValue(userPrincipal);
+ if (userPrincipalFormat != null) {
+ nid.setFormat(URI.create(userPrincipalFormat));
+ }
+
+ return nameId(nid);
+ }
- public SAML2LogoutRequestBuilder userPrincipal(String nameID, String nameIDformat) {
- this.userPrincipal = nameID;
- this.userPrincipalFormat = nameIDformat;
+ public SAML2LogoutRequestBuilder nameId(NameIDType nameId) {
+ this.nameId = nameId;
return this;
}
@@ -92,14 +107,7 @@ public class SAML2LogoutRequestBuilder implements SamlProtocolExtensionsAwareBui
private LogoutRequestType createLogoutRequest() throws ConfigurationException {
LogoutRequestType lort = SAML2Request.createLogoutRequest(issuer);
- NameIDType nameID = new NameIDType();
- nameID.setValue(userPrincipal);
- //Deal with NameID Format
- String nameIDFormat = userPrincipalFormat;
- if (nameIDFormat != null) {
- nameID.setFormat(URI.create(nameIDFormat));
- }
- lort.setNameID(nameID);
+ lort.setNameID(nameId);
if (issuer != null) {
NameIDType issuerID = new NameIDType();
diff --git a/services/src/main/java/org/keycloak/broker/saml/SAMLEndpoint.java b/services/src/main/java/org/keycloak/broker/saml/SAMLEndpoint.java
index 4f0e49b047..3759f17819 100755
--- a/services/src/main/java/org/keycloak/broker/saml/SAMLEndpoint.java
+++ b/services/src/main/java/org/keycloak/broker/saml/SAMLEndpoint.java
@@ -104,8 +104,11 @@ import org.w3c.dom.NodeList;
public class SAMLEndpoint {
protected static final Logger logger = Logger.getLogger(SAMLEndpoint.class);
public static final String SAML_FEDERATED_SESSION_INDEX = "SAML_FEDERATED_SESSION_INDEX";
+ @Deprecated // in favor of SAML_FEDERATED_SUBJECT_NAMEID
public static final String SAML_FEDERATED_SUBJECT = "SAML_FEDERATED_SUBJECT";
+ @Deprecated // in favor of SAML_FEDERATED_SUBJECT_NAMEID
public static final String SAML_FEDERATED_SUBJECT_NAMEFORMAT = "SAML_FEDERATED_SUBJECT_NAMEFORMAT";
+ public static final String SAML_FEDERATED_SUBJECT_NAMEID = "SAML_FEDERATED_SUBJECT_NAME_ID";
public static final String SAML_LOGIN_RESPONSE = "SAML_LOGIN_RESPONSE";
public static final String SAML_ASSERTION = "SAML_ASSERTION";
public static final String SAML_IDP_INITIATED_CLIENT_ID = "SAML_IDP_INITIATED_CLIENT_ID";
diff --git a/services/src/main/java/org/keycloak/broker/saml/SAMLIdentityProvider.java b/services/src/main/java/org/keycloak/broker/saml/SAMLIdentityProvider.java
index f326263a3b..a86bfd3ace 100755
--- a/services/src/main/java/org/keycloak/broker/saml/SAMLIdentityProvider.java
+++ b/services/src/main/java/org/keycloak/broker/saml/SAMLIdentityProvider.java
@@ -129,8 +129,7 @@ public class SAMLIdentityProvider extends AbstractIdentityProvider new HttpGet(httpGetUri));
return this;
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/saml/CreateLogoutRequestStepBuilder.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/saml/CreateLogoutRequestStepBuilder.java
index ceeb5fbbb4..5775a9da05 100644
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/saml/CreateLogoutRequestStepBuilder.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/saml/CreateLogoutRequestStepBuilder.java
@@ -97,13 +97,8 @@ public class CreateLogoutRequestStepBuilder extends SamlDocumentStepBuilder nameIdRef = new AtomicReference<>();
+
+ @Override
+ public void addAdapterTestRealms(List testRealms) {
+ testRealms.add(IOUtil.loadRealm("/adapter-test/keycloak-saml/testsaml.json"));
+ }
+
+ private SAML2Object extractNameId(SAML2Object so) {
+ assertThat(so, isSamlResponse(JBossSAMLURIConstants.STATUS_SUCCESS));
+ ResponseType loginResp1 = (ResponseType) so;
+ final AssertionType firstAssertion = loginResp1.getAssertions().get(0).getAssertion();
+ assertThat(firstAssertion, org.hamcrest.Matchers.notNullValue());
+ assertThat(firstAssertion.getSubject().getSubType().getBaseID(), instanceOf(NameIDType.class));
+
+ NameIDType nameId = (NameIDType) firstAssertion.getSubject().getSubType().getBaseID();
+
+ nameIdRef.set(nameId);
+
+ return so;
+ }
+
+ @Test
+ public void employeeTest() {
+ SAMLDocumentHolder b = new SamlClientBuilder()
+ .navigateTo(employeeServletPage)
+ .processSamlResponse(Binding.POST)
+ .build()
+ .login().user(bburkeUser).build()
+ .processSamlResponse(Binding.POST)
+ .targetAttributeSamlResponse()
+ .transformObject(this::extractNameId)
+ .transformObject((SAML2Object o) -> {
+ assertThat(o, isSamlResponse(JBossSAMLURIConstants.STATUS_SUCCESS));
+ ResponseType rt = (ResponseType) o;
+ NameIDType t = (NameIDType) rt.getAssertions().get(0).getAssertion().getSubject().getSubType().getBaseID();
+ t.setNameQualifier(NAME_QUALIFIER);
+ t.setSPNameQualifier(SP_NAME_QUALIFIER);
+ t.setSPProvidedID(SP_PROVIDED_ID);
+ return o;
+ }).build()
+ .navigateTo(employeeServletPage.getUriBuilder().clone().queryParam("GLO", "true").build())
+ .getSamlResponse(Binding.POST);
+
+ assertThat(b.getSamlObject(), instanceOf(LogoutRequestType.class));
+ LogoutRequestType lr = (LogoutRequestType) b.getSamlObject();
+ NameIDType logoutRequestNameID = lr.getNameID();
+ assertThat(logoutRequestNameID.getFormat(), is(nameIdRef.get().getFormat()));
+ assertThat(logoutRequestNameID.getValue(), is(nameIdRef.get().getValue()));
+ assertThat(logoutRequestNameID.getNameQualifier(), is(NAME_QUALIFIER));
+ assertThat(logoutRequestNameID.getSPProvidedID(), is(SP_PROVIDED_ID));
+ assertThat(logoutRequestNameID.getSPNameQualifier(), is(SP_NAME_QUALIFIER));
+ }
+
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/saml/LogoutTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/saml/LogoutTest.java
index 69e4ecd25a..e1fa36fc35 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/saml/LogoutTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/saml/LogoutTest.java
@@ -71,6 +71,14 @@ import static org.keycloak.testsuite.util.SamlClient.Binding.*;
*/
public class LogoutTest extends AbstractSamlTest {
+ private static final String SP_PROVIDED_ID = "spProvidedId";
+ private static final String SP_NAME_QUALIFIER = "spNameQualifier";
+ private static final String NAME_QUALIFIER = "nameQualifier";
+
+ private static final String BROKER_SIGN_ON_SERVICE_URL = "http://saml.idp/saml";
+ private static final String BROKER_LOGOUT_SERVICE_URL = "http://saml.idp/SLO/saml";
+ private static final String BROKER_SERVICE_ID = "http://saml.idp/saml";
+
private ClientRepresentation salesRep;
private ClientRepresentation sales2Rep;
@@ -315,8 +323,8 @@ public class LogoutTest extends AbstractSamlTest {
.providerId(SAMLIdentityProviderFactory.PROVIDER_ID)
.alias(SAML_BROKER_ALIAS)
.displayName("SAML")
- .setAttribute(SAMLIdentityProviderConfig.SINGLE_SIGN_ON_SERVICE_URL, "http://saml.idp/saml")
- .setAttribute(SAMLIdentityProviderConfig.SINGLE_LOGOUT_SERVICE_URL, "http://saml.idp/saml")
+ .setAttribute(SAMLIdentityProviderConfig.SINGLE_SIGN_ON_SERVICE_URL, BROKER_SIGN_ON_SERVICE_URL)
+ .setAttribute(SAMLIdentityProviderConfig.SINGLE_LOGOUT_SERVICE_URL, BROKER_LOGOUT_SERVICE_URL)
.setAttribute(SAMLIdentityProviderConfig.NAME_ID_POLICY_FORMAT, "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress")
.setAttribute(SAMLIdentityProviderConfig.POST_BINDING_RESPONSE, "false")
.setAttribute(SAMLIdentityProviderConfig.POST_BINDING_AUTHN_REQUEST, "false")
@@ -328,10 +336,10 @@ public class LogoutTest extends AbstractSamlTest {
private SAML2Object createAuthnResponse(SAML2Object so) {
AuthnRequestType req = (AuthnRequestType) so;
try {
- return new SAML2LoginResponseBuilder()
+ final ResponseType res = new SAML2LoginResponseBuilder()
.requestID(req.getID())
.destination(req.getAssertionConsumerServiceURL().toString())
- .issuer("http://saml.idp/saml")
+ .issuer(BROKER_SERVICE_ID)
.assertionExpiration(1000000)
.subjectExpiration(1000000)
.requestIssuer(getAuthServerRealmBase(REALM_NAME).toString())
@@ -339,6 +347,13 @@ public class LogoutTest extends AbstractSamlTest {
.authMethod(JBossSAMLURIConstants.AC_UNSPECIFIED.get())
.sessionIndex("idp:" + UUID.randomUUID())
.buildModel();
+
+ NameIDType nameId = (NameIDType) res.getAssertions().get(0).getAssertion().getSubject().getSubType().getBaseID();
+ nameId.setNameQualifier(NAME_QUALIFIER);
+ nameId.setSPNameQualifier(SP_NAME_QUALIFIER);
+ nameId.setSPProvidedID(SP_PROVIDED_ID);
+
+ return res;
} catch (ConfigurationException | ProcessingException ex) {
throw new RuntimeException(ex);
}
@@ -350,7 +365,7 @@ public class LogoutTest extends AbstractSamlTest {
return new SAML2LogoutResponseBuilder()
.logoutRequestID(req.getID())
.destination(getSamlBrokerUrl(REALM_NAME).toString())
- .issuer("http://saml.idp/saml")
+ .issuer(BROKER_SERVICE_ID)
.buildModel();
} catch (ConfigurationException ex) {
throw new RuntimeException(ex);
@@ -409,4 +424,56 @@ public class LogoutTest extends AbstractSamlTest {
}
}
+ @Test
+ public void testLogoutPropagatesToSamlIdentityProviderNameIdPreserved() throws IOException {
+ final RealmResource realm = adminClient.realm(REALM_NAME);
+
+ try (
+ Closeable sales = ClientAttributeUpdater.forClient(adminClient, REALM_NAME, SAML_CLIENT_ID_SALES_POST)
+ .setFrontchannelLogout(true)
+ .removeAttribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_POST_ATTRIBUTE)
+ .setAttribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_REDIRECT_ATTRIBUTE, "http://url")
+ .update();
+
+ Closeable idp = new IdentityProviderCreator(realm, addIdentityProvider())
+ ) {
+ SAMLDocumentHolder samlResponse = new SamlClientBuilder()
+ .authnRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST, SAML_ASSERTION_CONSUMER_URL_SALES_POST, POST).build()
+
+ // Virtually perform login at IdP (return artificial SAML response)
+ .login().idp(SAML_BROKER_ALIAS).build()
+ .processSamlResponse(REDIRECT)
+ .transformObject(this::createAuthnResponse)
+ .targetAttributeSamlResponse()
+ .targetUri(getSamlBrokerUrl(REALM_NAME))
+ .build()
+ .updateProfile().username("a").email("a@b.c").firstName("A").lastName("B").build()
+ .followOneRedirect()
+
+ // Now returning back to the app
+ .processSamlResponse(POST)
+ .transformObject(this::extractNameIdAndSessionIndexAndTerminate)
+ .build()
+
+ // ----- Logout phase ------
+
+ // Logout initiated from the app
+ .logoutRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST, REDIRECT)
+ .nameId(nameIdRef::get)
+ .sessionIndex(sessionIndexRef::get)
+ .build()
+
+ .getSamlResponse(REDIRECT);
+
+ assertThat(samlResponse.getSamlObject(), isSamlLogoutRequest(BROKER_LOGOUT_SERVICE_URL));
+ LogoutRequestType lr = (LogoutRequestType) samlResponse.getSamlObject();
+ NameIDType logoutRequestNameID = lr.getNameID();
+ assertThat(logoutRequestNameID.getFormat(), is(JBossSAMLURIConstants.NAMEID_FORMAT_EMAIL.getUri()));
+ assertThat(logoutRequestNameID.getValue(), is("a@b.c"));
+ assertThat(logoutRequestNameID.getNameQualifier(), is(NAME_QUALIFIER));
+ assertThat(logoutRequestNameID.getSPProvidedID(), is(SP_PROVIDED_ID));
+ assertThat(logoutRequestNameID.getSPNameQualifier(), is(SP_NAME_QUALIFIER));
+ }
+ }
+
}