KEYCLOAK-7852 Use original NameId value in logout requests
This commit is contained in:
parent
cc8cfd4269
commit
ca4e14fbfa
13 changed files with 469 additions and 29 deletions
|
@ -20,7 +20,9 @@ package org.keycloak.adapters.saml;
|
||||||
import org.keycloak.common.util.MultivaluedHashMap;
|
import org.keycloak.common.util.MultivaluedHashMap;
|
||||||
import org.keycloak.dom.saml.v2.assertion.AssertionType;
|
import org.keycloak.dom.saml.v2.assertion.AssertionType;
|
||||||
|
|
||||||
|
import org.keycloak.dom.saml.v2.assertion.NameIDType;
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
|
import java.net.URI;
|
||||||
import java.security.Principal;
|
import java.security.Principal;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
@ -81,6 +83,27 @@ public class SamlPrincipal implements Serializable, Principal {
|
||||||
return nameIDFormat;
|
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
|
@Override
|
||||||
public String getName() {
|
public String getName() {
|
||||||
return name;
|
return name;
|
||||||
|
|
|
@ -108,7 +108,7 @@ public class WebBrowserSsoAuthenticationHandler extends AbstractSamlAuthenticati
|
||||||
.assertionExpiration(30)
|
.assertionExpiration(30)
|
||||||
.issuer(deployment.getEntityID())
|
.issuer(deployment.getEntityID())
|
||||||
.sessionIndex(account.getSessionIndex())
|
.sessionIndex(account.getSessionIndex())
|
||||||
.userPrincipal(account.getPrincipal().getSamlSubject(), account.getPrincipal().getNameIDFormat())
|
.nameId(account.getPrincipal().getNameID())
|
||||||
.destination(deployment.getIDP().getSingleLogoutService().getRequestBindingUrl());
|
.destination(deployment.getIDP().getSingleLogoutService().getRequestBindingUrl());
|
||||||
BaseSAML2BindingBuilder binding = new BaseSAML2BindingBuilder();
|
BaseSAML2BindingBuilder binding = new BaseSAML2BindingBuilder();
|
||||||
if (deployment.getIDP().getSingleLogoutService().signRequest()) {
|
if (deployment.getIDP().getSingleLogoutService().signRequest()) {
|
||||||
|
|
|
@ -63,6 +63,11 @@
|
||||||
<artifactId>junit</artifactId>
|
<artifactId>junit</artifactId>
|
||||||
<scope>test</scope>
|
<scope>test</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.hamcrest</groupId>
|
||||||
|
<artifactId>hamcrest-all</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
<build>
|
<build>
|
||||||
<resources>
|
<resources>
|
||||||
|
|
|
@ -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<Class<?>, 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> T getObjectFrom(String escapedString, Class<T> 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> T next(Class<T> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -16,6 +16,8 @@
|
||||||
*/
|
*/
|
||||||
package org.keycloak.dom.saml.v2.assertion;
|
package org.keycloak.dom.saml.v2.assertion;
|
||||||
|
|
||||||
|
import org.keycloak.common.util.StringSerialization;
|
||||||
|
import org.keycloak.common.util.StringSerialization.Deserializer;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -69,4 +71,25 @@ public class NameIDType extends BaseIDAbstractType {
|
||||||
public void setSPProvidedID(String sPProvidedID) {
|
public void setSPProvidedID(String sPProvidedID) {
|
||||||
this.sPProvidedID = 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;
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -36,8 +36,7 @@ import org.keycloak.dom.saml.v2.protocol.ExtensionsType;
|
||||||
* @version $Revision: 1 $
|
* @version $Revision: 1 $
|
||||||
*/
|
*/
|
||||||
public class SAML2LogoutRequestBuilder implements SamlProtocolExtensionsAwareBuilder<SAML2LogoutRequestBuilder> {
|
public class SAML2LogoutRequestBuilder implements SamlProtocolExtensionsAwareBuilder<SAML2LogoutRequestBuilder> {
|
||||||
protected String userPrincipal;
|
protected NameIDType nameId;
|
||||||
protected String userPrincipalFormat;
|
|
||||||
protected String sessionIndex;
|
protected String sessionIndex;
|
||||||
protected long assertionExpiration;
|
protected long assertionExpiration;
|
||||||
protected String destination;
|
protected String destination;
|
||||||
|
@ -72,10 +71,26 @@ public class SAML2LogoutRequestBuilder implements SamlProtocolExtensionsAwareBui
|
||||||
return this;
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
public SAML2LogoutRequestBuilder userPrincipal(String nameID, String nameIDformat) {
|
return nameId(nid);
|
||||||
this.userPrincipal = nameID;
|
}
|
||||||
this.userPrincipalFormat = nameIDformat;
|
|
||||||
|
public SAML2LogoutRequestBuilder nameId(NameIDType nameId) {
|
||||||
|
this.nameId = nameId;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -92,14 +107,7 @@ public class SAML2LogoutRequestBuilder implements SamlProtocolExtensionsAwareBui
|
||||||
private LogoutRequestType createLogoutRequest() throws ConfigurationException {
|
private LogoutRequestType createLogoutRequest() throws ConfigurationException {
|
||||||
LogoutRequestType lort = SAML2Request.createLogoutRequest(issuer);
|
LogoutRequestType lort = SAML2Request.createLogoutRequest(issuer);
|
||||||
|
|
||||||
NameIDType nameID = new NameIDType();
|
lort.setNameID(nameId);
|
||||||
nameID.setValue(userPrincipal);
|
|
||||||
//Deal with NameID Format
|
|
||||||
String nameIDFormat = userPrincipalFormat;
|
|
||||||
if (nameIDFormat != null) {
|
|
||||||
nameID.setFormat(URI.create(nameIDFormat));
|
|
||||||
}
|
|
||||||
lort.setNameID(nameID);
|
|
||||||
|
|
||||||
if (issuer != null) {
|
if (issuer != null) {
|
||||||
NameIDType issuerID = new NameIDType();
|
NameIDType issuerID = new NameIDType();
|
||||||
|
|
|
@ -104,8 +104,11 @@ import org.w3c.dom.NodeList;
|
||||||
public class SAMLEndpoint {
|
public class SAMLEndpoint {
|
||||||
protected static final Logger logger = Logger.getLogger(SAMLEndpoint.class);
|
protected static final Logger logger = Logger.getLogger(SAMLEndpoint.class);
|
||||||
public static final String SAML_FEDERATED_SESSION_INDEX = "SAML_FEDERATED_SESSION_INDEX";
|
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";
|
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_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_LOGIN_RESPONSE = "SAML_LOGIN_RESPONSE";
|
||||||
public static final String SAML_ASSERTION = "SAML_ASSERTION";
|
public static final String SAML_ASSERTION = "SAML_ASSERTION";
|
||||||
public static final String SAML_IDP_INITIATED_CLIENT_ID = "SAML_IDP_INITIATED_CLIENT_ID";
|
public static final String SAML_IDP_INITIATED_CLIENT_ID = "SAML_IDP_INITIATED_CLIENT_ID";
|
||||||
|
|
|
@ -129,8 +129,7 @@ public class SAMLIdentityProvider extends AbstractIdentityProvider<SAMLIdentityP
|
||||||
SubjectType subject = assertion.getSubject();
|
SubjectType subject = assertion.getSubject();
|
||||||
SubjectType.STSubType subType = subject.getSubType();
|
SubjectType.STSubType subType = subject.getSubType();
|
||||||
NameIDType subjectNameID = (NameIDType) subType.getBaseID();
|
NameIDType subjectNameID = (NameIDType) subType.getBaseID();
|
||||||
authSession.setUserSessionNote(SAMLEndpoint.SAML_FEDERATED_SUBJECT, subjectNameID.getValue());
|
authSession.setUserSessionNote(SAMLEndpoint.SAML_FEDERATED_SUBJECT_NAMEID, subjectNameID.serializeAsString());
|
||||||
if (subjectNameID.getFormat() != null) authSession.setUserSessionNote(SAMLEndpoint.SAML_FEDERATED_SUBJECT_NAMEFORMAT, subjectNameID.getFormat().toString());
|
|
||||||
AuthnStatementType authn = (AuthnStatementType)context.getContextData().get(SAMLEndpoint.SAML_AUTHN_STATEMENT);
|
AuthnStatementType authn = (AuthnStatementType)context.getContextData().get(SAMLEndpoint.SAML_AUTHN_STATEMENT);
|
||||||
if (authn != null && authn.getSessionIndex() != null) {
|
if (authn != null && authn.getSessionIndex() != null) {
|
||||||
authSession.setUserSessionNote(SAMLEndpoint.SAML_FEDERATED_SESSION_INDEX, authn.getSessionIndex());
|
authSession.setUserSessionNote(SAMLEndpoint.SAML_FEDERATED_SESSION_INDEX, authn.getSessionIndex());
|
||||||
|
@ -191,7 +190,7 @@ public class SAMLIdentityProvider extends AbstractIdentityProvider<SAMLIdentityP
|
||||||
.assertionExpiration(realm.getAccessCodeLifespan())
|
.assertionExpiration(realm.getAccessCodeLifespan())
|
||||||
.issuer(getEntityId(uriInfo, realm))
|
.issuer(getEntityId(uriInfo, realm))
|
||||||
.sessionIndex(userSession.getNote(SAMLEndpoint.SAML_FEDERATED_SESSION_INDEX))
|
.sessionIndex(userSession.getNote(SAMLEndpoint.SAML_FEDERATED_SESSION_INDEX))
|
||||||
.userPrincipal(userSession.getNote(SAMLEndpoint.SAML_FEDERATED_SUBJECT), userSession.getNote(SAMLEndpoint.SAML_FEDERATED_SUBJECT_NAMEFORMAT))
|
.nameId(NameIDType.deserializeFromString(userSession.getNote(SAMLEndpoint.SAML_FEDERATED_SUBJECT_NAMEID)))
|
||||||
.destination(singleLogoutServiceUrl);
|
.destination(singleLogoutServiceUrl);
|
||||||
return logoutBuilder;
|
return logoutBuilder;
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
package org.keycloak.testsuite.util;
|
package org.keycloak.testsuite.util;
|
||||||
|
|
||||||
import org.keycloak.saml.processing.core.saml.v2.common.SAMLDocumentHolder;
|
import org.keycloak.saml.processing.core.saml.v2.common.SAMLDocumentHolder;
|
||||||
|
import org.keycloak.testsuite.page.AbstractPage;
|
||||||
import org.keycloak.testsuite.util.SamlClient.Binding;
|
import org.keycloak.testsuite.util.SamlClient.Binding;
|
||||||
import org.keycloak.testsuite.util.SamlClient.DoNotFollowRedirectStep;
|
import org.keycloak.testsuite.util.SamlClient.DoNotFollowRedirectStep;
|
||||||
import org.keycloak.testsuite.util.SamlClient.ResultExtractor;
|
import org.keycloak.testsuite.util.SamlClient.ResultExtractor;
|
||||||
|
@ -194,6 +195,10 @@ public class SamlClientBuilder {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public SamlClientBuilder navigateTo(AbstractPage page) {
|
||||||
|
return navigateTo(page.buildUri());
|
||||||
|
}
|
||||||
|
|
||||||
public SamlClientBuilder navigateTo(URI httpGetUri) {
|
public SamlClientBuilder navigateTo(URI httpGetUri) {
|
||||||
steps.add((client, currentURI, currentResponse, context) -> new HttpGet(httpGetUri));
|
steps.add((client, currentURI, currentResponse, context) -> new HttpGet(httpGetUri));
|
||||||
return this;
|
return this;
|
||||||
|
|
|
@ -97,13 +97,8 @@ public class CreateLogoutRequestStepBuilder extends SamlDocumentStepBuilder<Logo
|
||||||
SAML2LogoutRequestBuilder builder = new SAML2LogoutRequestBuilder()
|
SAML2LogoutRequestBuilder builder = new SAML2LogoutRequestBuilder()
|
||||||
.destination(authServerSamlUrl.toString())
|
.destination(authServerSamlUrl.toString())
|
||||||
.issuer(issuer)
|
.issuer(issuer)
|
||||||
.sessionIndex(sessionIndex());
|
.sessionIndex(sessionIndex())
|
||||||
|
.nameId(nameId());
|
||||||
final NameIDType nameIdValue = nameId();
|
|
||||||
|
|
||||||
if (nameIdValue != null) {
|
|
||||||
builder = builder.userPrincipal(nameIdValue.getValue(), nameIdValue.getFormat() == null ? null : nameIdValue.getFormat().toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
String documentAsString = DocumentUtil.getDocumentAsString(builder.buildDocument());
|
String documentAsString = DocumentUtil.getDocumentAsString(builder.buildDocument());
|
||||||
String transformed = getTransformer().transform(documentAsString);
|
String transformed = getTransformer().transform(documentAsString);
|
||||||
|
|
|
@ -0,0 +1,111 @@
|
||||||
|
package org.keycloak.testsuite.adapter.servlet;
|
||||||
|
|
||||||
|
import org.keycloak.dom.saml.v2.SAML2Object;
|
||||||
|
import org.keycloak.dom.saml.v2.assertion.AssertionType;
|
||||||
|
import org.keycloak.dom.saml.v2.assertion.NameIDType;
|
||||||
|
import org.keycloak.dom.saml.v2.protocol.LogoutRequestType;
|
||||||
|
import org.keycloak.dom.saml.v2.protocol.ResponseType;
|
||||||
|
import org.keycloak.representations.idm.RealmRepresentation;
|
||||||
|
import org.keycloak.saml.common.constants.JBossSAMLURIConstants;
|
||||||
|
import org.keycloak.saml.processing.core.saml.v2.common.SAMLDocumentHolder;
|
||||||
|
import org.keycloak.testsuite.adapter.AbstractServletsAdapterTest;
|
||||||
|
import org.keycloak.testsuite.adapter.page.EmployeeServlet;
|
||||||
|
import org.keycloak.testsuite.arquillian.annotation.AppServerContainer;
|
||||||
|
import org.keycloak.testsuite.util.SamlClient.Binding;
|
||||||
|
import org.keycloak.testsuite.util.SamlClientBuilder;
|
||||||
|
import org.keycloak.testsuite.utils.arquillian.ContainerConstants;
|
||||||
|
import org.keycloak.testsuite.utils.io.IOUtil;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
import org.hamcrest.Matchers;
|
||||||
|
import org.jboss.arquillian.container.test.api.Deployment;
|
||||||
|
import org.jboss.arquillian.graphene.page.Page;
|
||||||
|
import org.jboss.shrinkwrap.api.spec.WebArchive;
|
||||||
|
import org.junit.Test;
|
||||||
|
import static org.hamcrest.Matchers.instanceOf;
|
||||||
|
import static org.hamcrest.Matchers.is;
|
||||||
|
import static org.junit.Assert.assertThat;
|
||||||
|
import static org.keycloak.testsuite.adapter.AbstractServletsAdapterTest.samlServletDeployment;
|
||||||
|
import static org.keycloak.testsuite.util.Matchers.isSamlResponse;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @author hmlnarik
|
||||||
|
*/
|
||||||
|
@AppServerContainer(ContainerConstants.APP_SERVER_UNDERTOW)
|
||||||
|
@AppServerContainer(ContainerConstants.APP_SERVER_WILDFLY)
|
||||||
|
@AppServerContainer(ContainerConstants.APP_SERVER_WILDFLY_DEPRECATED)
|
||||||
|
@AppServerContainer(ContainerConstants.APP_SERVER_EAP)
|
||||||
|
@AppServerContainer(ContainerConstants.APP_SERVER_EAP6)
|
||||||
|
@AppServerContainer(ContainerConstants.APP_SERVER_EAP71)
|
||||||
|
@AppServerContainer(ContainerConstants.APP_SERVER_TOMCAT7)
|
||||||
|
@AppServerContainer(ContainerConstants.APP_SERVER_TOMCAT8)
|
||||||
|
@AppServerContainer(ContainerConstants.APP_SERVER_TOMCAT9)
|
||||||
|
public class SAMLLogoutAdapterTest extends AbstractServletsAdapterTest {
|
||||||
|
|
||||||
|
private static final String SP_PROVIDED_ID = "spProvidedId";
|
||||||
|
private static final String SP_NAME_QUALIFIER = "spNameQualifier";
|
||||||
|
private static final String NAME_QUALIFIER = "nameQualifier";
|
||||||
|
|
||||||
|
@Deployment(name = EmployeeServlet.DEPLOYMENT_NAME)
|
||||||
|
protected static WebArchive employee() {
|
||||||
|
return samlServletDeployment(EmployeeServlet.DEPLOYMENT_NAME, SendUsernameServlet.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Page
|
||||||
|
private EmployeeServlet employeeServletPage;
|
||||||
|
|
||||||
|
private final AtomicReference<NameIDType> nameIdRef = new AtomicReference<>();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addAdapterTestRealms(List<RealmRepresentation> 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));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -71,6 +71,14 @@ import static org.keycloak.testsuite.util.SamlClient.Binding.*;
|
||||||
*/
|
*/
|
||||||
public class LogoutTest extends AbstractSamlTest {
|
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 salesRep;
|
||||||
private ClientRepresentation sales2Rep;
|
private ClientRepresentation sales2Rep;
|
||||||
|
|
||||||
|
@ -315,8 +323,8 @@ public class LogoutTest extends AbstractSamlTest {
|
||||||
.providerId(SAMLIdentityProviderFactory.PROVIDER_ID)
|
.providerId(SAMLIdentityProviderFactory.PROVIDER_ID)
|
||||||
.alias(SAML_BROKER_ALIAS)
|
.alias(SAML_BROKER_ALIAS)
|
||||||
.displayName("SAML")
|
.displayName("SAML")
|
||||||
.setAttribute(SAMLIdentityProviderConfig.SINGLE_SIGN_ON_SERVICE_URL, "http://saml.idp/saml")
|
.setAttribute(SAMLIdentityProviderConfig.SINGLE_SIGN_ON_SERVICE_URL, BROKER_SIGN_ON_SERVICE_URL)
|
||||||
.setAttribute(SAMLIdentityProviderConfig.SINGLE_LOGOUT_SERVICE_URL, "http://saml.idp/saml")
|
.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.NAME_ID_POLICY_FORMAT, "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress")
|
||||||
.setAttribute(SAMLIdentityProviderConfig.POST_BINDING_RESPONSE, "false")
|
.setAttribute(SAMLIdentityProviderConfig.POST_BINDING_RESPONSE, "false")
|
||||||
.setAttribute(SAMLIdentityProviderConfig.POST_BINDING_AUTHN_REQUEST, "false")
|
.setAttribute(SAMLIdentityProviderConfig.POST_BINDING_AUTHN_REQUEST, "false")
|
||||||
|
@ -328,10 +336,10 @@ public class LogoutTest extends AbstractSamlTest {
|
||||||
private SAML2Object createAuthnResponse(SAML2Object so) {
|
private SAML2Object createAuthnResponse(SAML2Object so) {
|
||||||
AuthnRequestType req = (AuthnRequestType) so;
|
AuthnRequestType req = (AuthnRequestType) so;
|
||||||
try {
|
try {
|
||||||
return new SAML2LoginResponseBuilder()
|
final ResponseType res = new SAML2LoginResponseBuilder()
|
||||||
.requestID(req.getID())
|
.requestID(req.getID())
|
||||||
.destination(req.getAssertionConsumerServiceURL().toString())
|
.destination(req.getAssertionConsumerServiceURL().toString())
|
||||||
.issuer("http://saml.idp/saml")
|
.issuer(BROKER_SERVICE_ID)
|
||||||
.assertionExpiration(1000000)
|
.assertionExpiration(1000000)
|
||||||
.subjectExpiration(1000000)
|
.subjectExpiration(1000000)
|
||||||
.requestIssuer(getAuthServerRealmBase(REALM_NAME).toString())
|
.requestIssuer(getAuthServerRealmBase(REALM_NAME).toString())
|
||||||
|
@ -339,6 +347,13 @@ public class LogoutTest extends AbstractSamlTest {
|
||||||
.authMethod(JBossSAMLURIConstants.AC_UNSPECIFIED.get())
|
.authMethod(JBossSAMLURIConstants.AC_UNSPECIFIED.get())
|
||||||
.sessionIndex("idp:" + UUID.randomUUID())
|
.sessionIndex("idp:" + UUID.randomUUID())
|
||||||
.buildModel();
|
.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) {
|
} catch (ConfigurationException | ProcessingException ex) {
|
||||||
throw new RuntimeException(ex);
|
throw new RuntimeException(ex);
|
||||||
}
|
}
|
||||||
|
@ -350,7 +365,7 @@ public class LogoutTest extends AbstractSamlTest {
|
||||||
return new SAML2LogoutResponseBuilder()
|
return new SAML2LogoutResponseBuilder()
|
||||||
.logoutRequestID(req.getID())
|
.logoutRequestID(req.getID())
|
||||||
.destination(getSamlBrokerUrl(REALM_NAME).toString())
|
.destination(getSamlBrokerUrl(REALM_NAME).toString())
|
||||||
.issuer("http://saml.idp/saml")
|
.issuer(BROKER_SERVICE_ID)
|
||||||
.buildModel();
|
.buildModel();
|
||||||
} catch (ConfigurationException ex) {
|
} catch (ConfigurationException ex) {
|
||||||
throw new RuntimeException(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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue