KEYCLOAK-6708 Fix NPE when email not set for email NameIDFormat
This commit is contained in:
parent
771d7f1724
commit
f43519a16e
11 changed files with 257 additions and 53 deletions
|
@ -107,7 +107,7 @@ public class SAML2Request {
|
||||||
// Create a default NameIDPolicy
|
// Create a default NameIDPolicy
|
||||||
NameIDPolicyType nameIDPolicy = new NameIDPolicyType();
|
NameIDPolicyType nameIDPolicy = new NameIDPolicyType();
|
||||||
nameIDPolicy.setAllowCreate(Boolean.TRUE);
|
nameIDPolicy.setAllowCreate(Boolean.TRUE);
|
||||||
nameIDPolicy.setFormat(URI.create(this.nameIDFormat));
|
nameIDPolicy.setFormat(this.nameIDFormat == null ? null : URI.create(this.nameIDFormat));
|
||||||
|
|
||||||
authnRequest.setNameIDPolicy(nameIDPolicy);
|
authnRequest.setNameIDPolicy(nameIDPolicy);
|
||||||
|
|
||||||
|
|
|
@ -244,7 +244,7 @@ public class SAML2Response {
|
||||||
|
|
||||||
// subject -> nameid
|
// subject -> nameid
|
||||||
NameIDType nameIDType = new NameIDType();
|
NameIDType nameIDType = new NameIDType();
|
||||||
nameIDType.setFormat(URI.create(idp.getNameIDFormat()));
|
nameIDType.setFormat(idp.getNameIDFormat() == null ? null : URI.create(idp.getNameIDFormat()));
|
||||||
nameIDType.setValue(idp.getNameIDFormatValue());
|
nameIDType.setValue(idp.getNameIDFormatValue());
|
||||||
|
|
||||||
SubjectType.STSubType subType = new SubjectType.STSubType();
|
SubjectType.STSubType subType = new SubjectType.STSubType();
|
||||||
|
|
|
@ -165,7 +165,7 @@ public class SamlProtocol implements LoginProtocol {
|
||||||
try {
|
try {
|
||||||
ClientModel client = authSession.getClient();
|
ClientModel client = authSession.getClient();
|
||||||
|
|
||||||
if ("true".equals(client.getAttribute(SAML_IDP_INITIATED_LOGIN))) {
|
if ("true".equals(authSession.getClientNote(SAML_IDP_INITIATED_LOGIN))) {
|
||||||
if (error == Error.CANCELLED_BY_USER) {
|
if (error == Error.CANCELLED_BY_USER) {
|
||||||
UriBuilder builder = RealmsResource.protocolUrl(uriInfo).path(SamlService.class, "idpInitiatedSSO");
|
UriBuilder builder = RealmsResource.protocolUrl(uriInfo).path(SamlService.class, "idpInitiatedSSO");
|
||||||
Map<String, String> params = new HashMap<>();
|
Map<String, String> params = new HashMap<>();
|
||||||
|
@ -178,10 +178,21 @@ public class SamlProtocol implements LoginProtocol {
|
||||||
return ErrorPage.error(session, authSession, Response.Status.BAD_REQUEST, translateErrorToIdpInitiatedErrorMessage(error));
|
return ErrorPage.error(session, authSession, Response.Status.BAD_REQUEST, translateErrorToIdpInitiatedErrorMessage(error));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
SAML2ErrorResponseBuilder builder = new SAML2ErrorResponseBuilder().destination(authSession.getRedirectUri()).issuer(getResponseIssuer(realm)).status(translateErrorToSAMLStatus(error).get());
|
return samlErrorMessage(
|
||||||
try {
|
authSession, new SamlClient(client), isPostBinding(authSession),
|
||||||
JaxrsSAML2BindingBuilder binding = new JaxrsSAML2BindingBuilder().relayState(authSession.getClientNote(GeneralConstants.RELAY_STATE));
|
authSession.getRedirectUri(), translateErrorToSAMLStatus(error), authSession.getClientNote(GeneralConstants.RELAY_STATE)
|
||||||
SamlClient samlClient = new SamlClient(client);
|
);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
new AuthenticationSessionManager(session).removeAuthenticationSession(realm, authSession, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Response samlErrorMessage(
|
||||||
|
AuthenticationSessionModel authSession, SamlClient samlClient, boolean isPostBinding,
|
||||||
|
String destination, JBossSAMLURIConstants statusDetail, String relayState) {
|
||||||
|
JaxrsSAML2BindingBuilder binding = new JaxrsSAML2BindingBuilder().relayState(relayState);
|
||||||
|
SAML2ErrorResponseBuilder builder = new SAML2ErrorResponseBuilder().destination(destination).issuer(getResponseIssuer(realm)).status(statusDetail.get());
|
||||||
KeyManager keyManager = session.keys();
|
KeyManager keyManager = session.keys();
|
||||||
if (samlClient.requiresRealmSignature()) {
|
if (samlClient.requiresRealmSignature()) {
|
||||||
KeyManager.ActiveRsaKey keys = keyManager.getActiveRsaKey(realm);
|
KeyManager.ActiveRsaKey keys = keyManager.getActiveRsaKey(realm);
|
||||||
|
@ -192,25 +203,23 @@ public class SamlProtocol implements LoginProtocol {
|
||||||
}
|
}
|
||||||
binding.signatureAlgorithm(samlClient.getSignatureAlgorithm()).signWith(keyName, keys.getPrivateKey(), keys.getPublicKey(), keys.getCertificate()).signDocument();
|
binding.signatureAlgorithm(samlClient.getSignatureAlgorithm()).signWith(keyName, keys.getPrivateKey(), keys.getPublicKey(), keys.getCertificate()).signDocument();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
// There is no support for encrypting status messages in SAML.
|
// There is no support for encrypting status messages in SAML.
|
||||||
// Only assertions, attributes, base ID and name ID can be encrypted
|
// Only assertions, attributes, base ID and name ID can be encrypted
|
||||||
// See Chapter 6 of saml-core-2.0-os.pdf
|
// See Chapter 6 of saml-core-2.0-os.pdf
|
||||||
Document document = builder.buildDocument();
|
Document document = builder.buildDocument();
|
||||||
return buildErrorResponse(authSession, binding, document);
|
return buildErrorResponse(isPostBinding, destination, binding, document);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
return ErrorPage.error(session, authSession, Response.Status.BAD_REQUEST, Messages.FAILED_TO_PROCESS_RESPONSE);
|
return ErrorPage.error(session, authSession, Response.Status.BAD_REQUEST, Messages.FAILED_TO_PROCESS_RESPONSE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} finally {
|
|
||||||
new AuthenticationSessionManager(session).removeAuthenticationSession(realm, authSession, true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected Response buildErrorResponse(AuthenticationSessionModel authSession, JaxrsSAML2BindingBuilder binding, Document document) throws ConfigurationException, ProcessingException, IOException {
|
protected Response buildErrorResponse(boolean isPostBinding, String destination, JaxrsSAML2BindingBuilder binding, Document document) throws ConfigurationException, ProcessingException, IOException {
|
||||||
if (isPostBinding(authSession)) {
|
if (isPostBinding) {
|
||||||
return binding.postBinding(document).response(authSession.getRedirectUri());
|
return binding.postBinding(document).response(destination);
|
||||||
} else {
|
} else {
|
||||||
return binding.redirectBinding(document).response(authSession.getRedirectUri());
|
return binding.redirectBinding(document).response(destination);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -302,7 +311,11 @@ public class SamlProtocol implements LoginProtocol {
|
||||||
|
|
||||||
protected String getNameId(String nameIdFormat, CommonClientSessionModel clientSession, UserSessionModel userSession) {
|
protected String getNameId(String nameIdFormat, CommonClientSessionModel clientSession, UserSessionModel userSession) {
|
||||||
if (nameIdFormat.equals(JBossSAMLURIConstants.NAMEID_FORMAT_EMAIL.get())) {
|
if (nameIdFormat.equals(JBossSAMLURIConstants.NAMEID_FORMAT_EMAIL.get())) {
|
||||||
return userSession.getUser().getEmail();
|
final String email = userSession.getUser().getEmail();
|
||||||
|
if (email == null) {
|
||||||
|
logger.debugf("E-mail of the user %s has to be set for %s NameIDFormat", userSession.getUser().getUsername(), JBossSAMLURIConstants.NAMEID_FORMAT_EMAIL.get());
|
||||||
|
}
|
||||||
|
return email;
|
||||||
} else if (nameIdFormat.equals(JBossSAMLURIConstants.NAMEID_FORMAT_TRANSIENT.get())) {
|
} else if (nameIdFormat.equals(JBossSAMLURIConstants.NAMEID_FORMAT_TRANSIENT.get())) {
|
||||||
// "G-" stands for "generated" Add this for the slight possibility of collisions.
|
// "G-" stands for "generated" Add this for the slight possibility of collisions.
|
||||||
return "G-" + UUID.randomUUID().toString();
|
return "G-" + UUID.randomUUID().toString();
|
||||||
|
@ -365,6 +378,13 @@ public class SamlProtocol implements LoginProtocol {
|
||||||
String nameIdFormat = getNameIdFormat(samlClient, clientSession);
|
String nameIdFormat = getNameIdFormat(samlClient, clientSession);
|
||||||
String nameId = getNameId(nameIdFormat, clientSession, userSession);
|
String nameId = getNameId(nameIdFormat, clientSession, userSession);
|
||||||
|
|
||||||
|
if (nameId == null) {
|
||||||
|
return samlErrorMessage(
|
||||||
|
null, samlClient, isPostBinding(clientSession),
|
||||||
|
redirectUri, JBossSAMLURIConstants.STATUS_INVALID_NAMEIDPOLICY, relayState
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// save NAME_ID and format in clientSession as they may be persistent or transient or email and not username
|
// save NAME_ID and format in clientSession as they may be persistent or transient or email and not username
|
||||||
// we'll need to send this back on a logout
|
// we'll need to send this back on a logout
|
||||||
clientSession.setNote(SAML_NAME_ID, nameId);
|
clientSession.setNote(SAML_NAME_ID, nameId);
|
||||||
|
|
|
@ -135,7 +135,7 @@ public class SamlEcpProfileService extends SamlService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Response buildErrorResponse(AuthenticationSessionModel authSession, JaxrsSAML2BindingBuilder binding, Document document) throws ConfigurationException, ProcessingException, IOException {
|
protected Response buildErrorResponse(boolean isPostBinding, String uri, JaxrsSAML2BindingBuilder binding, Document document) throws ConfigurationException, ProcessingException, IOException {
|
||||||
return Soap.createMessage().addToBody(document).build();
|
return Soap.createMessage().addToBody(document).build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
*/
|
*/
|
||||||
package org.keycloak.testsuite.updaters;
|
package org.keycloak.testsuite.updaters;
|
||||||
|
|
||||||
|
import org.keycloak.admin.client.resource.IdentityProviderResource;
|
||||||
import org.keycloak.admin.client.resource.IdentityProvidersResource;
|
import org.keycloak.admin.client.resource.IdentityProvidersResource;
|
||||||
import java.io.Closeable;
|
import java.io.Closeable;
|
||||||
import javax.ws.rs.NotFoundException;
|
import javax.ws.rs.NotFoundException;
|
||||||
|
@ -48,6 +49,10 @@ public class IdentityProviderCreator implements Closeable {
|
||||||
return this.resource;
|
return this.resource;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public IdentityProviderResource identityProvider() {
|
||||||
|
return this.resource().get(alias);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void close() throws IOException {
|
public void close() throws IOException {
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -25,6 +25,7 @@ import org.keycloak.saml.common.constants.JBossSAMLURIConstants;
|
||||||
import org.keycloak.testsuite.util.matchers.*;
|
import org.keycloak.testsuite.util.matchers.*;
|
||||||
|
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import javax.ws.rs.core.Response;
|
import javax.ws.rs.core.Response;
|
||||||
import org.apache.http.HttpResponse;
|
import org.apache.http.HttpResponse;
|
||||||
|
@ -147,10 +148,10 @@ public class Matchers {
|
||||||
* @param expectedStatusCode
|
* @param expectedStatusCode
|
||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
public static <T> Matcher<SAML2Object> isSamlStatusResponse(JBossSAMLURIConstants expectedStatus) {
|
public static <T> Matcher<SAML2Object> isSamlStatusResponse(JBossSAMLURIConstants... expectedStatus) {
|
||||||
return allOf(
|
return allOf(
|
||||||
instanceOf(StatusResponseType.class),
|
instanceOf(StatusResponseType.class),
|
||||||
new SamlStatusResponseTypeMatcher(is(expectedStatus.getUri()))
|
new SamlStatusResponseTypeMatcher(Arrays.stream(expectedStatus).map(JBossSAMLURIConstants::getUri).toArray(i -> new URI[i]))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,8 +6,11 @@
|
||||||
package org.keycloak.testsuite.util.matchers;
|
package org.keycloak.testsuite.util.matchers;
|
||||||
|
|
||||||
import org.keycloak.dom.saml.v2.SAML2Object;
|
import org.keycloak.dom.saml.v2.SAML2Object;
|
||||||
|
import org.keycloak.dom.saml.v2.protocol.StatusCodeType;
|
||||||
import org.keycloak.dom.saml.v2.protocol.StatusResponseType;
|
import org.keycloak.dom.saml.v2.protocol.StatusResponseType;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
import org.hamcrest.*;
|
import org.hamcrest.*;
|
||||||
import static org.hamcrest.Matchers.*;
|
import static org.hamcrest.Matchers.*;
|
||||||
|
|
||||||
|
@ -17,28 +20,46 @@ import static org.hamcrest.Matchers.*;
|
||||||
*/
|
*/
|
||||||
public class SamlStatusResponseTypeMatcher extends BaseMatcher<SAML2Object> {
|
public class SamlStatusResponseTypeMatcher extends BaseMatcher<SAML2Object> {
|
||||||
|
|
||||||
private final Matcher<URI> statusMatcher;
|
private final List<Matcher<URI>> statusMatchers;
|
||||||
|
|
||||||
public SamlStatusResponseTypeMatcher(URI statusMatcher) {
|
public SamlStatusResponseTypeMatcher(URI... statusMatchers) {
|
||||||
this.statusMatcher = is(statusMatcher);
|
this.statusMatchers = new ArrayList(statusMatchers.length);
|
||||||
|
for (URI uri : statusMatchers) {
|
||||||
|
this.statusMatchers.add(is(uri));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public SamlStatusResponseTypeMatcher(Matcher<URI> statusMatcher) {
|
public SamlStatusResponseTypeMatcher(List<Matcher<URI>> statusMatchers) {
|
||||||
this.statusMatcher = statusMatcher;
|
this.statusMatchers = statusMatchers;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean matches(Object item) {
|
public boolean matches(Object item) {
|
||||||
return statusMatcher.matches(((StatusResponseType) item).getStatus().getStatusCode().getValue());
|
StatusCodeType statusCode = ((StatusResponseType) item).getStatus().getStatusCode();
|
||||||
|
for (Matcher<URI> statusMatcher : statusMatchers) {
|
||||||
|
if (! statusMatcher.matches(statusCode.getValue())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
statusCode = statusCode.getStatusCode();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void describeMismatch(Object item, Description description) {
|
public void describeMismatch(Object item, Description description) {
|
||||||
description.appendText("was ").appendValue(((StatusResponseType) item).getStatus().getStatusCode().getValue());
|
StatusCodeType statusCode = ((StatusResponseType) item).getStatus().getStatusCode();
|
||||||
|
description.appendText("was ");
|
||||||
|
while (statusCode != null) {
|
||||||
|
description.appendText("/").appendValue(statusCode.getValue());
|
||||||
|
statusCode = statusCode.getStatusCode();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void describeTo(Description description) {
|
public void describeTo(Description description) {
|
||||||
description.appendText("SAML status response status matches ").appendDescriptionOf(this.statusMatcher);
|
description.appendText("SAML status response status matches ");
|
||||||
|
for (Matcher<URI> statusMatcher : statusMatchers) {
|
||||||
|
description.appendText("/").appendDescriptionOf(statusMatcher);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -40,6 +40,7 @@ public abstract class AbstractSamlTest extends AbstractAuthTest {
|
||||||
public static final String SAML_CLIENT_SALES_POST_ENC_PRIVATE_KEY = "MIICXQIBAAKBgQDb7kwJPkGdU34hicplwfp6/WmNcaLh94TSc7Jyr9Undp5pkyLgb0DE7EIE+6kSs4LsqCb8HDkB0nLD5DXbBJFd8n0WGoKstelvtg6FtVJMnwN7k7yZbfkPECWH9zF70VeOo9vbzrApNRnct8ZhH5fbflRB4JMA9L9R+LbURdoSKQIDAQABAoGBANtbZG9bruoSGp2s5zhzLzd4hczT6Jfk3o9hYjzNb5Z60ymN3Z1omXtQAdEiiNHkRdNxK+EM7TcKBfmoJqcaeTkW8cksVEAW23ip8W9/XsLqmbU2mRrJiKa+KQNDSHqJi1VGyimi4DDApcaqRZcaKDFXg2KDr/Qt5JFD/o9IIIPZAkEA+ZENdBIlpbUfkJh6Ln+bUTss/FZ1FsrcPZWu13rChRMrsmXsfzu9kZUWdUeQ2Dj5AoW2Q7L/cqdGXS7Mm5XhcwJBAOGZq9axJY5YhKrsksvYRLhQbStmGu5LG75suF+rc/44sFq+aQM7+oeRr4VY88Mvz7mk4esdfnk7ae+cCazqJvMCQQCx1L1cZw3yfRSn6S6u8XjQMjWE/WpjulujeoRiwPPY9WcesOgLZZtYIH8nRL6ehEJTnMnahbLmlPFbttxPRUanAkA11MtSIVcKzkhp2KV2ipZrPJWwI18NuVJXb+3WtjypTrGWFZVNNkSjkLnHIeCYlJIGhDd8OL9zAiBXEm6kmgLNAkBWAg0tK2hCjvzsaA505gWQb4X56uKWdb0IzN+fOLB3Qt7+fLqbVQNQoNGzqey6B4MoS1fUKAStqdGTFYPG/+9t";
|
public static final String SAML_CLIENT_SALES_POST_ENC_PRIVATE_KEY = "MIICXQIBAAKBgQDb7kwJPkGdU34hicplwfp6/WmNcaLh94TSc7Jyr9Undp5pkyLgb0DE7EIE+6kSs4LsqCb8HDkB0nLD5DXbBJFd8n0WGoKstelvtg6FtVJMnwN7k7yZbfkPECWH9zF70VeOo9vbzrApNRnct8ZhH5fbflRB4JMA9L9R+LbURdoSKQIDAQABAoGBANtbZG9bruoSGp2s5zhzLzd4hczT6Jfk3o9hYjzNb5Z60ymN3Z1omXtQAdEiiNHkRdNxK+EM7TcKBfmoJqcaeTkW8cksVEAW23ip8W9/XsLqmbU2mRrJiKa+KQNDSHqJi1VGyimi4DDApcaqRZcaKDFXg2KDr/Qt5JFD/o9IIIPZAkEA+ZENdBIlpbUfkJh6Ln+bUTss/FZ1FsrcPZWu13rChRMrsmXsfzu9kZUWdUeQ2Dj5AoW2Q7L/cqdGXS7Mm5XhcwJBAOGZq9axJY5YhKrsksvYRLhQbStmGu5LG75suF+rc/44sFq+aQM7+oeRr4VY88Mvz7mk4esdfnk7ae+cCazqJvMCQQCx1L1cZw3yfRSn6S6u8XjQMjWE/WpjulujeoRiwPPY9WcesOgLZZtYIH8nRL6ehEJTnMnahbLmlPFbttxPRUanAkA11MtSIVcKzkhp2KV2ipZrPJWwI18NuVJXb+3WtjypTrGWFZVNNkSjkLnHIeCYlJIGhDd8OL9zAiBXEm6kmgLNAkBWAg0tK2hCjvzsaA505gWQb4X56uKWdb0IzN+fOLB3Qt7+fLqbVQNQoNGzqey6B4MoS1fUKAStqdGTFYPG/+9t";
|
||||||
public static final String SAML_CLIENT_SALES_POST_ENC_PUBLIC_KEY = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDb7kwJPkGdU34hicplwfp6/WmNcaLh94TSc7Jyr9Undp5pkyLgb0DE7EIE+6kSs4LsqCb8HDkB0nLD5DXbBJFd8n0WGoKstelvtg6FtVJMnwN7k7yZbfkPECWH9zF70VeOo9vbzrApNRnct8ZhH5fbflRB4JMA9L9R+LbURdoSKQIDAQAB";
|
public static final String SAML_CLIENT_SALES_POST_ENC_PUBLIC_KEY = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDb7kwJPkGdU34hicplwfp6/WmNcaLh94TSc7Jyr9Undp5pkyLgb0DE7EIE+6kSs4LsqCb8HDkB0nLD5DXbBJFd8n0WGoKstelvtg6FtVJMnwN7k7yZbfkPECWH9zF70VeOo9vbzrApNRnct8ZhH5fbflRB4JMA9L9R+LbURdoSKQIDAQAB";
|
||||||
|
|
||||||
|
public static final String SAML_BROKER_ALIAS = "saml-broker";
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void addTestRealms(List<RealmRepresentation> testRealms) {
|
public void addTestRealms(List<RealmRepresentation> testRealms) {
|
||||||
|
@ -61,4 +62,8 @@ public abstract class AbstractSamlTest extends AbstractAuthTest {
|
||||||
.realmBaseUrl(UriBuilder.fromUri(getAuthServerRoot()))
|
.realmBaseUrl(UriBuilder.fromUri(getAuthServerRoot()))
|
||||||
.build(realm);
|
.build(realm);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected URI getSamlBrokerUrl(String realmName) {
|
||||||
|
return URI.create(getAuthServerRealmBase(realmName).toString() + "/broker/" + SAML_BROKER_ALIAS + "/endpoint");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,156 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2018 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.testsuite.saml;
|
||||||
|
|
||||||
|
import org.keycloak.admin.client.resource.ClientsResource;
|
||||||
|
import org.keycloak.admin.client.resource.RealmResource;
|
||||||
|
import org.keycloak.authentication.authenticators.broker.IdpReviewProfileAuthenticatorFactory;
|
||||||
|
import org.keycloak.broker.saml.SAMLIdentityProviderConfig;
|
||||||
|
import org.keycloak.broker.saml.SAMLIdentityProviderFactory;
|
||||||
|
import org.keycloak.dom.saml.v2.SAML2Object;
|
||||||
|
import org.keycloak.dom.saml.v2.assertion.AttributeStatementType;
|
||||||
|
import org.keycloak.dom.saml.v2.assertion.AttributeStatementType.ASTChoiceType;
|
||||||
|
import org.keycloak.dom.saml.v2.assertion.AttributeType;
|
||||||
|
import org.keycloak.dom.saml.v2.protocol.AuthnRequestType;
|
||||||
|
import org.keycloak.dom.saml.v2.protocol.NameIDPolicyType;
|
||||||
|
import org.keycloak.dom.saml.v2.protocol.ResponseType;
|
||||||
|
import org.keycloak.models.AuthenticationExecutionModel.Requirement;
|
||||||
|
import org.keycloak.representations.idm.AuthenticationExecutionInfoRepresentation;
|
||||||
|
import org.keycloak.representations.idm.IdentityProviderRepresentation;
|
||||||
|
import org.keycloak.saml.SAML2LoginResponseBuilder;
|
||||||
|
import org.keycloak.saml.common.constants.JBossSAMLURIConstants;
|
||||||
|
import org.keycloak.saml.common.exceptions.ConfigurationException;
|
||||||
|
import org.keycloak.saml.common.exceptions.ProcessingException;
|
||||||
|
import org.keycloak.saml.processing.core.saml.v2.common.SAMLDocumentHolder;
|
||||||
|
import org.keycloak.testsuite.updaters.IdentityProviderCreator;
|
||||||
|
import org.keycloak.testsuite.util.IdentityProviderBuilder;
|
||||||
|
import org.keycloak.testsuite.util.SamlClientBuilder;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.UUID;
|
||||||
|
import org.junit.Assert;
|
||||||
|
import org.junit.Test;
|
||||||
|
import static org.junit.Assert.assertThat;
|
||||||
|
import static org.keycloak.testsuite.saml.AbstractSamlTest.REALM_NAME;
|
||||||
|
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.isSamlStatusResponse;
|
||||||
|
import static org.keycloak.testsuite.util.SamlClient.Binding.POST;
|
||||||
|
import static org.keycloak.testsuite.util.SamlClient.Binding.REDIRECT;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @author hmlnarik
|
||||||
|
*/
|
||||||
|
public class BrokerTest extends AbstractSamlTest {
|
||||||
|
|
||||||
|
private IdentityProviderRepresentation addIdentityProvider() {
|
||||||
|
IdentityProviderRepresentation identityProvider = IdentityProviderBuilder.create()
|
||||||
|
.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.NAME_ID_POLICY_FORMAT, JBossSAMLURIConstants.NAMEID_FORMAT_EMAIL.get())
|
||||||
|
.setAttribute(SAMLIdentityProviderConfig.POST_BINDING_RESPONSE, "false")
|
||||||
|
.setAttribute(SAMLIdentityProviderConfig.POST_BINDING_AUTHN_REQUEST, "false")
|
||||||
|
.setAttribute(SAMLIdentityProviderConfig.BACKCHANNEL_SUPPORTED, "false")
|
||||||
|
.build();
|
||||||
|
return identityProvider;
|
||||||
|
}
|
||||||
|
|
||||||
|
private SAML2Object createAuthnResponse(SAML2Object so) {
|
||||||
|
AuthnRequestType req = (AuthnRequestType) so;
|
||||||
|
try {
|
||||||
|
final ResponseType res = new SAML2LoginResponseBuilder()
|
||||||
|
.requestID(req.getID())
|
||||||
|
.destination(req.getAssertionConsumerServiceURL().toString())
|
||||||
|
.issuer("http://saml.idp/saml")
|
||||||
|
.assertionExpiration(1000000)
|
||||||
|
.subjectExpiration(1000000)
|
||||||
|
.requestIssuer(getAuthServerRealmBase(REALM_NAME).toString())
|
||||||
|
.sessionIndex("idp:" + UUID.randomUUID())
|
||||||
|
.buildModel();
|
||||||
|
|
||||||
|
AttributeStatementType attrStatement = new AttributeStatementType();
|
||||||
|
AttributeType attribute = new AttributeType("mail");
|
||||||
|
attribute.addAttributeValue("v@w.x");
|
||||||
|
attrStatement.addAttribute(new ASTChoiceType(attribute));
|
||||||
|
|
||||||
|
res.getAssertions().get(0).getAssertion().addStatement(attrStatement);
|
||||||
|
|
||||||
|
return res;
|
||||||
|
} catch (ConfigurationException | ProcessingException ex) {
|
||||||
|
throw new RuntimeException(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testLogoutPropagatesToSamlIdentityProvider() throws IOException {
|
||||||
|
final RealmResource realm = adminClient.realm(REALM_NAME);
|
||||||
|
final ClientsResource clients = realm.clients();
|
||||||
|
|
||||||
|
AuthenticationExecutionInfoRepresentation reviewProfileAuthenticator = null;
|
||||||
|
String firstBrokerLoginFlowAlias = null;
|
||||||
|
try (IdentityProviderCreator idp = new IdentityProviderCreator(realm, addIdentityProvider())) {
|
||||||
|
IdentityProviderRepresentation idpRepresentation = idp.identityProvider().toRepresentation();
|
||||||
|
firstBrokerLoginFlowAlias = idpRepresentation.getFirstBrokerLoginFlowAlias();
|
||||||
|
List<AuthenticationExecutionInfoRepresentation> executions = realm.flows().getExecutions(firstBrokerLoginFlowAlias);
|
||||||
|
reviewProfileAuthenticator = executions.stream()
|
||||||
|
.filter(ex -> Objects.equals(ex.getProviderId(), IdpReviewProfileAuthenticatorFactory.PROVIDER_ID))
|
||||||
|
.findFirst()
|
||||||
|
.orElseGet(() -> { Assert.fail("Could not find update profile in first broker login flow"); return null; });
|
||||||
|
|
||||||
|
reviewProfileAuthenticator.setRequirement(Requirement.DISABLED.name());
|
||||||
|
realm.flows().updateExecutions(firstBrokerLoginFlowAlias, reviewProfileAuthenticator);
|
||||||
|
|
||||||
|
SAMLDocumentHolder samlResponse = new SamlClientBuilder()
|
||||||
|
.authnRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST, SAML_ASSERTION_CONSUMER_URL_SALES_POST, POST)
|
||||||
|
.transformObject(ar -> {
|
||||||
|
NameIDPolicyType nameIDPolicy = new NameIDPolicyType();
|
||||||
|
nameIDPolicy.setAllowCreate(Boolean.TRUE);
|
||||||
|
nameIDPolicy.setFormat(JBossSAMLURIConstants.NAMEID_FORMAT_EMAIL.getUri());
|
||||||
|
|
||||||
|
ar.setNameIDPolicy(nameIDPolicy);
|
||||||
|
return ar;
|
||||||
|
})
|
||||||
|
.build()
|
||||||
|
|
||||||
|
.login().idp(SAML_BROKER_ALIAS).build()
|
||||||
|
|
||||||
|
// Virtually perform login at IdP (return artificial SAML response)
|
||||||
|
.processSamlResponse(REDIRECT)
|
||||||
|
.transformObject(this::createAuthnResponse)
|
||||||
|
.targetAttributeSamlResponse()
|
||||||
|
.targetUri(getSamlBrokerUrl(REALM_NAME))
|
||||||
|
.build()
|
||||||
|
.followOneRedirect() // first-broker-login
|
||||||
|
.followOneRedirect() // after-first-broker-login
|
||||||
|
.getSamlResponse(POST);
|
||||||
|
|
||||||
|
assertThat(samlResponse.getSamlObject(), isSamlStatusResponse(
|
||||||
|
JBossSAMLURIConstants.STATUS_RESPONDER,
|
||||||
|
JBossSAMLURIConstants.STATUS_INVALID_NAMEIDPOLICY
|
||||||
|
));
|
||||||
|
} finally {
|
||||||
|
reviewProfileAuthenticator.setRequirement(Requirement.REQUIRED.name());
|
||||||
|
realm.flows().updateExecutions(firstBrokerLoginFlowAlias, reviewProfileAuthenticator);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -49,7 +49,6 @@ import org.keycloak.testsuite.util.SamlClientBuilder;
|
||||||
|
|
||||||
import java.io.Closeable;
|
import java.io.Closeable;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.URI;
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
@ -79,8 +78,6 @@ public class LogoutTest extends AbstractSamlTest {
|
||||||
private final AtomicReference<NameIDType> nameIdRef = new AtomicReference<>();
|
private final AtomicReference<NameIDType> nameIdRef = new AtomicReference<>();
|
||||||
private final AtomicReference<String> sessionIndexRef = new AtomicReference<>();
|
private final AtomicReference<String> sessionIndexRef = new AtomicReference<>();
|
||||||
|
|
||||||
private static final String SAML_BROKER_ALIAS = "saml-broker";
|
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
public void setup() {
|
public void setup() {
|
||||||
salesRep = adminClient.realm(REALM_NAME).clients().findByClientId(SAML_CLIENT_ID_SALES_POST).get(0);
|
salesRep = adminClient.realm(REALM_NAME).clients().findByClientId(SAML_CLIENT_ID_SALES_POST).get(0);
|
||||||
|
@ -414,8 +411,4 @@ public class LogoutTest extends AbstractSamlTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private URI getSamlBrokerUrl(String realmName) {
|
|
||||||
return URI.create(getAuthServerRealmBase(realmName).toString() + "/broker/" + SAML_BROKER_ALIAS + "/endpoint");
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
package org.keycloak.testsuite.saml;
|
package org.keycloak.testsuite.saml;
|
||||||
|
|
||||||
|
import org.keycloak.protocol.saml.SamlClient;
|
||||||
|
import org.keycloak.protocol.saml.SamlConfigAttributes;
|
||||||
|
import org.keycloak.protocol.saml.SamlProtocol;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.keycloak.representations.idm.ClientRepresentation;
|
import org.keycloak.representations.idm.ClientRepresentation;
|
||||||
import org.keycloak.representations.idm.RealmRepresentation;
|
import org.keycloak.representations.idm.RealmRepresentation;
|
||||||
|
@ -32,7 +35,7 @@ public class SamlConsentTest extends AbstractSamlTest {
|
||||||
public void rejectedConsentResponseTest() throws ParsingException, ConfigurationException, ProcessingException {
|
public void rejectedConsentResponseTest() throws ParsingException, ConfigurationException, ProcessingException {
|
||||||
ClientRepresentation client = adminClient.realm(REALM_NAME)
|
ClientRepresentation client = adminClient.realm(REALM_NAME)
|
||||||
.clients()
|
.clients()
|
||||||
.findByClientId(SAML_CLIENT_ID_SALES_POST_ENC)
|
.findByClientId(SAML_CLIENT_ID_SALES_POST)
|
||||||
.get(0);
|
.get(0);
|
||||||
|
|
||||||
adminClient.realm(REALM_NAME)
|
adminClient.realm(REALM_NAME)
|
||||||
|
@ -40,14 +43,14 @@ public class SamlConsentTest extends AbstractSamlTest {
|
||||||
.get(client.getId())
|
.get(client.getId())
|
||||||
.update(ClientBuilder.edit(client)
|
.update(ClientBuilder.edit(client)
|
||||||
.consentRequired(true)
|
.consentRequired(true)
|
||||||
.attribute("saml.encrypt", "false") //remove after RHSSO-797
|
.attribute(SamlProtocol.SAML_IDP_INITIATED_SSO_URL_NAME, "sales-post")
|
||||||
.attribute("saml_idp_initiated_sso_url_name", "sales-post-enc")
|
.attribute(SamlProtocol.SAML_ASSERTION_CONSUMER_URL_POST_ATTRIBUTE, SAML_ASSERTION_CONSUMER_URL_SALES_POST + "saml")
|
||||||
.attribute("saml_assertion_consumer_url_post", SAML_ASSERTION_CONSUMER_URL_SALES_POST_ENC + "saml")
|
.attribute(SamlConfigAttributes.SAML_SERVER_SIGNATURE, "true")
|
||||||
.build());
|
.build());
|
||||||
|
|
||||||
log.debug("Log in using idp initiated login");
|
log.debug("Log in using idp initiated login");
|
||||||
SAMLDocumentHolder documentHolder = new SamlClientBuilder()
|
SAMLDocumentHolder documentHolder = new SamlClientBuilder()
|
||||||
.idpInitiatedLogin(getAuthServerSamlEndpoint(REALM_NAME), "sales-post-enc").build()
|
.authnRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST, SAML_ASSERTION_CONSUMER_URL_SALES_POST, Binding.POST).build()
|
||||||
.login().user(bburkeUser).build()
|
.login().user(bburkeUser).build()
|
||||||
.consentRequired().approveConsent(false).build()
|
.consentRequired().approveConsent(false).build()
|
||||||
.getSamlResponse(Binding.POST);
|
.getSamlResponse(Binding.POST);
|
||||||
|
|
Loading…
Reference in a new issue