parent
38a46726e4
commit
3cd413dee1
16 changed files with 611 additions and 102 deletions
|
@ -212,6 +212,28 @@ public class BaseSAML2BindingBuilder<T extends BaseSAML2BindingBuilder> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static class BaseSoapBindingBuilder {
|
||||||
|
protected Document document;
|
||||||
|
protected BaseSAML2BindingBuilder builder;
|
||||||
|
|
||||||
|
public BaseSoapBindingBuilder(BaseSAML2BindingBuilder builder, Document document) throws ProcessingException {
|
||||||
|
this.builder = builder;
|
||||||
|
this.document = document;
|
||||||
|
if (builder.signAssertions) {
|
||||||
|
builder.signAssertion(document);
|
||||||
|
}
|
||||||
|
if (builder.encrypt) builder.encryptDocument(document);
|
||||||
|
if (builder.sign) {
|
||||||
|
builder.signDocument(document);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Document getDocument() {
|
||||||
|
return document;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
public BaseRedirectBindingBuilder redirectBinding(Document document) throws ProcessingException {
|
public BaseRedirectBindingBuilder redirectBinding(Document document) throws ProcessingException {
|
||||||
return new BaseRedirectBindingBuilder(this, document);
|
return new BaseRedirectBindingBuilder(this, document);
|
||||||
|
|
||||||
|
@ -222,7 +244,9 @@ public class BaseSAML2BindingBuilder<T extends BaseSAML2BindingBuilder> {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public BaseSoapBindingBuilder soapBinding(Document document) throws ProcessingException {
|
||||||
|
return new BaseSoapBindingBuilder(this, document);
|
||||||
|
}
|
||||||
|
|
||||||
public String getSAMLNSPrefix(Document samlResponseDocument) {
|
public String getSAMLNSPrefix(Document samlResponseDocument) {
|
||||||
Node assertionElement = samlResponseDocument.getDocumentElement()
|
Node assertionElement = samlResponseDocument.getDocumentElement()
|
||||||
|
|
|
@ -66,6 +66,7 @@ import javax.xml.datatype.XMLGregorianCalendar;
|
||||||
import javax.xml.namespace.QName;
|
import javax.xml.namespace.QName;
|
||||||
import java.io.ByteArrayInputStream;
|
import java.io.ByteArrayInputStream;
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
import java.io.Writer;
|
import java.io.Writer;
|
||||||
|
@ -393,6 +394,21 @@ public class SAML2Response {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the Underlying SAML2Object from a document
|
||||||
|
* @param samlDocument a Document containing a SAML2Object
|
||||||
|
* @return a SAMLDocumentHolder
|
||||||
|
* @throws ProcessingException
|
||||||
|
* @throws ParsingException
|
||||||
|
*/
|
||||||
|
public static SAMLDocumentHolder getSAML2ObjectFromDocument(Document samlDocument) throws ProcessingException, ParsingException {
|
||||||
|
SAMLParser samlParser = SAMLParser.getInstance();
|
||||||
|
JAXPValidationUtil.checkSchemaValidation(samlDocument);
|
||||||
|
SAML2Object responseType = (SAML2Object) samlParser.parse(samlDocument);
|
||||||
|
|
||||||
|
return new SAMLDocumentHolder(responseType, samlDocument);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert an EncryptedElement into a Document
|
* Convert an EncryptedElement into a Document
|
||||||
*
|
*
|
||||||
|
@ -423,7 +439,7 @@ public class SAML2Response {
|
||||||
* @throws ConfigurationException
|
* @throws ConfigurationException
|
||||||
* @throws ProcessingException
|
* @throws ProcessingException
|
||||||
*/
|
*/
|
||||||
public Document convert(StatusResponseType responseType) throws ProcessingException, ConfigurationException,
|
public static Document convert(StatusResponseType responseType) throws ProcessingException, ConfigurationException,
|
||||||
ParsingException {
|
ParsingException {
|
||||||
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||||
|
|
||||||
|
|
|
@ -194,6 +194,8 @@ public class EntityDescriptorDescriptionConverter implements ClientDescriptionCo
|
||||||
if (logoutPost != null) attributes.put(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_POST_ATTRIBUTE, logoutPost);
|
if (logoutPost != null) attributes.put(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_POST_ATTRIBUTE, logoutPost);
|
||||||
String logoutRedirect = getLogoutLocation(spDescriptorType, JBossSAMLURIConstants.SAML_HTTP_REDIRECT_BINDING.get());
|
String logoutRedirect = getLogoutLocation(spDescriptorType, JBossSAMLURIConstants.SAML_HTTP_REDIRECT_BINDING.get());
|
||||||
if (logoutRedirect != null) attributes.put(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_REDIRECT_ATTRIBUTE, logoutRedirect);
|
if (logoutRedirect != null) attributes.put(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_REDIRECT_ATTRIBUTE, logoutRedirect);
|
||||||
|
String logoutSoap = getLogoutLocation(spDescriptorType, JBossSAMLURIConstants.SAML_SOAP_BINDING.get());
|
||||||
|
if (logoutSoap != null) attributes.put(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_SOAP_ATTRIBUTE, logoutSoap);
|
||||||
|
|
||||||
String assertionConsumerServicePostBinding = getServiceURL(spDescriptorType, JBossSAMLURIConstants.SAML_HTTP_POST_BINDING.get());
|
String assertionConsumerServicePostBinding = getServiceURL(spDescriptorType, JBossSAMLURIConstants.SAML_HTTP_POST_BINDING.get());
|
||||||
if (assertionConsumerServicePostBinding != null) {
|
if (assertionConsumerServicePostBinding != null) {
|
||||||
|
|
|
@ -75,6 +75,7 @@ public class IDPMetadataDescriptor {
|
||||||
spIDPDescriptor.addSingleLogoutService(new EndpointType(SAML_HTTP_POST_BINDING.getUri(), logoutEndpoint));
|
spIDPDescriptor.addSingleLogoutService(new EndpointType(SAML_HTTP_POST_BINDING.getUri(), logoutEndpoint));
|
||||||
spIDPDescriptor.addSingleLogoutService(new EndpointType(SAML_HTTP_REDIRECT_BINDING.getUri(), logoutEndpoint));
|
spIDPDescriptor.addSingleLogoutService(new EndpointType(SAML_HTTP_REDIRECT_BINDING.getUri(), logoutEndpoint));
|
||||||
spIDPDescriptor.addSingleLogoutService(new EndpointType(SAML_HTTP_ARTIFACT_BINDING.getUri(), logoutEndpoint));
|
spIDPDescriptor.addSingleLogoutService(new EndpointType(SAML_HTTP_ARTIFACT_BINDING.getUri(), logoutEndpoint));
|
||||||
|
spIDPDescriptor.addSingleLogoutService(new EndpointType(SAML_SOAP_BINDING.getUri(), logoutEndpoint));
|
||||||
spIDPDescriptor.addSingleSignOnService(new EndpointType(SAML_HTTP_POST_BINDING.getUri(), loginPostEndpoint));
|
spIDPDescriptor.addSingleSignOnService(new EndpointType(SAML_HTTP_POST_BINDING.getUri(), loginPostEndpoint));
|
||||||
spIDPDescriptor.addSingleSignOnService(new EndpointType(SAML_HTTP_REDIRECT_BINDING.getUri(), loginRedirectEndpoint));
|
spIDPDescriptor.addSingleSignOnService(new EndpointType(SAML_HTTP_REDIRECT_BINDING.getUri(), loginRedirectEndpoint));
|
||||||
spIDPDescriptor.addSingleSignOnService(new EndpointType(SAML_SOAP_BINDING.getUri(), loginPostEndpoint));
|
spIDPDescriptor.addSingleSignOnService(new EndpointType(SAML_SOAP_BINDING.getUri(), loginPostEndpoint));
|
||||||
|
|
|
@ -19,6 +19,7 @@ package org.keycloak.protocol.saml;
|
||||||
|
|
||||||
import org.keycloak.forms.login.LoginFormsProvider;
|
import org.keycloak.forms.login.LoginFormsProvider;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
import org.keycloak.protocol.saml.profile.util.Soap;
|
||||||
import org.keycloak.saml.BaseSAML2BindingBuilder;
|
import org.keycloak.saml.BaseSAML2BindingBuilder;
|
||||||
import org.keycloak.saml.common.constants.GeneralConstants;
|
import org.keycloak.saml.common.constants.GeneralConstants;
|
||||||
import org.keycloak.saml.common.exceptions.ConfigurationException;
|
import org.keycloak.saml.common.exceptions.ConfigurationException;
|
||||||
|
@ -95,6 +96,22 @@ public class JaxrsSAML2BindingBuilder extends BaseSAML2BindingBuilder<JaxrsSAML2
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static class SoapBindingBuilder extends BaseSoapBindingBuilder {
|
||||||
|
public SoapBindingBuilder(JaxrsSAML2BindingBuilder builder, Document document) throws ProcessingException {
|
||||||
|
super(builder, document);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Response response() throws ConfigurationException, ProcessingException, IOException {
|
||||||
|
try {
|
||||||
|
Soap.SoapMessageBuilder messageBuilder = Soap.createMessage();
|
||||||
|
messageBuilder.addToBody(document);
|
||||||
|
return messageBuilder.build();
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException("Error while creating SAML response.", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public RedirectBindingBuilder redirectBinding(Document document) throws ProcessingException {
|
public RedirectBindingBuilder redirectBinding(Document document) throws ProcessingException {
|
||||||
return new RedirectBindingBuilder(this, document);
|
return new RedirectBindingBuilder(this, document);
|
||||||
|
@ -105,7 +122,8 @@ public class JaxrsSAML2BindingBuilder extends BaseSAML2BindingBuilder<JaxrsSAML2
|
||||||
return new PostBindingBuilder(this, document);
|
return new PostBindingBuilder(this, document);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SoapBindingBuilder soapBinding(Document document) throws ProcessingException {
|
||||||
|
return new SoapBindingBuilder(this, document);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,6 +23,7 @@ import org.apache.http.client.methods.HttpPost;
|
||||||
import org.apache.http.message.BasicNameValuePair;
|
import org.apache.http.message.BasicNameValuePair;
|
||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
import org.keycloak.broker.saml.SAMLDataMarshaller;
|
import org.keycloak.broker.saml.SAMLDataMarshaller;
|
||||||
|
import org.keycloak.common.VerificationException;
|
||||||
import org.keycloak.common.util.KeycloakUriBuilder;
|
import org.keycloak.common.util.KeycloakUriBuilder;
|
||||||
import org.keycloak.connections.httpclient.HttpClientProvider;
|
import org.keycloak.connections.httpclient.HttpClientProvider;
|
||||||
import org.keycloak.crypto.Algorithm;
|
import org.keycloak.crypto.Algorithm;
|
||||||
|
@ -57,6 +58,7 @@ import org.keycloak.protocol.saml.mappers.SAMLLoginResponseMapper;
|
||||||
import org.keycloak.protocol.saml.mappers.SAMLNameIdMapper;
|
import org.keycloak.protocol.saml.mappers.SAMLNameIdMapper;
|
||||||
import org.keycloak.protocol.saml.mappers.SAMLRoleListMapper;
|
import org.keycloak.protocol.saml.mappers.SAMLRoleListMapper;
|
||||||
import org.keycloak.protocol.saml.preprocessor.SamlAuthenticationPreprocessor;
|
import org.keycloak.protocol.saml.preprocessor.SamlAuthenticationPreprocessor;
|
||||||
|
import org.keycloak.protocol.saml.profile.util.Soap;
|
||||||
import org.keycloak.saml.SAML2ErrorResponseBuilder;
|
import org.keycloak.saml.SAML2ErrorResponseBuilder;
|
||||||
import org.keycloak.saml.SAML2LoginResponseBuilder;
|
import org.keycloak.saml.SAML2LoginResponseBuilder;
|
||||||
import org.keycloak.saml.SAML2LogoutRequestBuilder;
|
import org.keycloak.saml.SAML2LogoutRequestBuilder;
|
||||||
|
@ -71,6 +73,8 @@ import org.keycloak.saml.common.exceptions.ParsingException;
|
||||||
import org.keycloak.saml.common.exceptions.ProcessingException;
|
import org.keycloak.saml.common.exceptions.ProcessingException;
|
||||||
import org.keycloak.saml.common.util.XmlKeyInfoKeyNameTransformer;
|
import org.keycloak.saml.common.util.XmlKeyInfoKeyNameTransformer;
|
||||||
import org.keycloak.saml.processing.api.saml.v2.request.SAML2Request;
|
import org.keycloak.saml.processing.api.saml.v2.request.SAML2Request;
|
||||||
|
import org.keycloak.saml.processing.api.saml.v2.response.SAML2Response;
|
||||||
|
import org.keycloak.saml.processing.core.saml.v2.common.SAMLDocumentHolder;
|
||||||
import org.keycloak.saml.processing.core.util.KeycloakKeySamlExtensionGenerator;
|
import org.keycloak.saml.processing.core.util.KeycloakKeySamlExtensionGenerator;
|
||||||
import org.keycloak.services.ErrorPage;
|
import org.keycloak.services.ErrorPage;
|
||||||
import org.keycloak.services.managers.AuthenticationSessionManager;
|
import org.keycloak.services.managers.AuthenticationSessionManager;
|
||||||
|
@ -86,6 +90,8 @@ import javax.ws.rs.core.MediaType;
|
||||||
import javax.ws.rs.core.Response;
|
import javax.ws.rs.core.Response;
|
||||||
import javax.ws.rs.core.UriBuilder;
|
import javax.ws.rs.core.UriBuilder;
|
||||||
import javax.ws.rs.core.UriInfo;
|
import javax.ws.rs.core.UriInfo;
|
||||||
|
import javax.xml.soap.SOAPException;
|
||||||
|
import javax.xml.soap.SOAPMessage;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.security.PrivateKey;
|
import java.security.PrivateKey;
|
||||||
|
@ -116,6 +122,7 @@ public class SamlProtocol implements LoginProtocol {
|
||||||
public static final String SAML_SINGLE_LOGOUT_SERVICE_URL_POST_ATTRIBUTE = "saml_single_logout_service_url_post";
|
public static final String SAML_SINGLE_LOGOUT_SERVICE_URL_POST_ATTRIBUTE = "saml_single_logout_service_url_post";
|
||||||
public static final String SAML_SINGLE_LOGOUT_SERVICE_URL_ARTIFACT_ATTRIBUTE = "saml_single_logout_service_url_artifact";
|
public static final String SAML_SINGLE_LOGOUT_SERVICE_URL_ARTIFACT_ATTRIBUTE = "saml_single_logout_service_url_artifact";
|
||||||
public static final String SAML_SINGLE_LOGOUT_SERVICE_URL_REDIRECT_ATTRIBUTE = "saml_single_logout_service_url_redirect";
|
public static final String SAML_SINGLE_LOGOUT_SERVICE_URL_REDIRECT_ATTRIBUTE = "saml_single_logout_service_url_redirect";
|
||||||
|
public static final String SAML_SINGLE_LOGOUT_SERVICE_URL_SOAP_ATTRIBUTE = "saml_single_logout_service_url_soap";
|
||||||
public static final String SAML_ARTIFACT_RESOLUTION_SERVICE_URL_ATTRIBUTE = "saml_artifact_resolution_service_url";
|
public static final String SAML_ARTIFACT_RESOLUTION_SERVICE_URL_ATTRIBUTE = "saml_artifact_resolution_service_url";
|
||||||
public static final String LOGIN_PROTOCOL = "saml";
|
public static final String LOGIN_PROTOCOL = "saml";
|
||||||
public static final String SAML_BINDING = "saml_binding";
|
public static final String SAML_BINDING = "saml_binding";
|
||||||
|
@ -612,8 +619,14 @@ public class SamlProtocol implements LoginProtocol {
|
||||||
|
|
||||||
public static String getLogoutServiceUrl(KeycloakSession session, ClientModel client, String bindingType, boolean backChannelLogout) {
|
public static String getLogoutServiceUrl(KeycloakSession session, ClientModel client, String bindingType, boolean backChannelLogout) {
|
||||||
String logoutServiceUrl = null;
|
String logoutServiceUrl = null;
|
||||||
|
|
||||||
|
if (SAML_SOAP_BINDING.equals(bindingType)) {
|
||||||
|
// standard backchannel logout; cannot do front channel with SOAP binding
|
||||||
|
// we do not allow this URL to be set through the management URL (it is a purely backend-oriented URL)
|
||||||
|
logoutServiceUrl = client.getAttribute(SAML_SINGLE_LOGOUT_SERVICE_URL_SOAP_ATTRIBUTE);
|
||||||
|
return logoutServiceUrl == null || logoutServiceUrl.trim().equals("") ? null : logoutServiceUrl;
|
||||||
|
} else if (!backChannelLogout && useArtifactForLogout(client)) {
|
||||||
// backchannel logout doesn't support sending artifacts
|
// backchannel logout doesn't support sending artifacts
|
||||||
if (!backChannelLogout && useArtifactForLogout(client)) {
|
|
||||||
logoutServiceUrl = client.getAttribute(SAML_SINGLE_LOGOUT_SERVICE_URL_ARTIFACT_ATTRIBUTE);
|
logoutServiceUrl = client.getAttribute(SAML_SINGLE_LOGOUT_SERVICE_URL_ARTIFACT_ATTRIBUTE);
|
||||||
} else if (SAML_POST_BINDING.equals(bindingType)) {
|
} else if (SAML_POST_BINDING.equals(bindingType)) {
|
||||||
logoutServiceUrl = client.getAttribute(SAML_SINGLE_LOGOUT_SERVICE_URL_POST_ATTRIBUTE);
|
logoutServiceUrl = client.getAttribute(SAML_SINGLE_LOGOUT_SERVICE_URL_POST_ATTRIBUTE);
|
||||||
|
@ -752,6 +765,32 @@ public class SamlProtocol implements LoginProtocol {
|
||||||
public Response backchannelLogout(UserSessionModel userSession, AuthenticatedClientSessionModel clientSession) {
|
public Response backchannelLogout(UserSessionModel userSession, AuthenticatedClientSessionModel clientSession) {
|
||||||
ClientModel client = clientSession.getClient();
|
ClientModel client = clientSession.getClient();
|
||||||
SamlClient samlClient = new SamlClient(client);
|
SamlClient samlClient = new SamlClient(client);
|
||||||
|
|
||||||
|
// real backchannel logout if SOAP binding is supported (#9548)
|
||||||
|
String soapLogoutUrl = getLogoutServiceUrl(session, client, SAML_SOAP_BINDING, true);
|
||||||
|
if (soapLogoutUrl != null) {
|
||||||
|
try {
|
||||||
|
LogoutRequestType logoutRequest = createLogoutRequest(soapLogoutUrl, clientSession, client);
|
||||||
|
Document samlLogoutRequest = createBindingBuilder(samlClient, false).soapBinding(SAML2Request.convert(logoutRequest)).getDocument();
|
||||||
|
SOAPMessage soapResponse = Soap.createMessage().addToBody(samlLogoutRequest).call(soapLogoutUrl);
|
||||||
|
Document logoutResponse = Soap.extractSoapMessage(soapResponse);
|
||||||
|
SAMLDocumentHolder samlDocResponse = SAML2Response.getSAML2ObjectFromDocument(logoutResponse);
|
||||||
|
if (!validateLogoutResponse(logoutRequest, samlDocResponse, client)) {
|
||||||
|
return Response.serverError().build();
|
||||||
|
}
|
||||||
|
return Response.ok().build();
|
||||||
|
} catch (SOAPException e) {
|
||||||
|
logger.warnf(e, "Logout failed for client %s", client.getClientId());
|
||||||
|
return Response.serverError().build();
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.warn("failed to execute saml soap logout", e);
|
||||||
|
return Response.serverError().build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
logger.warnf("Can't do SOAP backchannel logout. No SingleLogoutService SOAP Binding registered for client %s; fallback on legacy backchannel logout",
|
||||||
|
client.getClientId());
|
||||||
|
|
||||||
|
// legacy backchannel logout implementation (send POST / Redirect binding directly to the logout endpoint without going through the browser)
|
||||||
String logoutUrl = getLogoutServiceUrl(session, client, SAML_POST_BINDING, true);
|
String logoutUrl = getLogoutServiceUrl(session, client, SAML_POST_BINDING, true);
|
||||||
if (logoutUrl == null) {
|
if (logoutUrl == null) {
|
||||||
logger.warnf("Can't do backchannel logout. No SingleLogoutService POST Binding registered for client: %s",
|
logger.warnf("Can't do backchannel logout. No SingleLogoutService POST Binding registered for client: %s",
|
||||||
|
@ -805,6 +844,36 @@ public class SamlProtocol implements LoginProtocol {
|
||||||
return Response.ok().build();
|
return Response.ok().build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate the logout response received by the client through the backchannel
|
||||||
|
*/
|
||||||
|
private boolean validateLogoutResponse(LogoutRequestType logoutRequest, SAMLDocumentHolder holder, ClientModel client) {
|
||||||
|
if (!(holder.getSamlObject() instanceof StatusResponseType)) {
|
||||||
|
logger.warn("Logout response format is not valid");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (new SamlClient(client).requiresClientSignature()) {
|
||||||
|
try {
|
||||||
|
SamlProtocolUtils.verifyDocumentSignature(client, holder.getSamlDocument());
|
||||||
|
} catch (VerificationException ex) {
|
||||||
|
logger.warnf("Logout response from client %s contains invalid signature", client.getClientId());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
StatusResponseType statusResponse = (StatusResponseType) holder.getSamlObject();
|
||||||
|
String issuer = statusResponse.getIssuer().getValue();
|
||||||
|
if (!client.getClientId().equals(issuer)) {
|
||||||
|
logger.warn("Logout response contains wrong 'issuer' value");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// check inResponseTo field of response
|
||||||
|
if (!logoutRequest.getID().equals(statusResponse.getInResponseTo())) {
|
||||||
|
logger.warn("Logout response contains wrong 'inResponseTo' value");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
protected LogoutRequestType createLogoutRequest(String logoutUrl, AuthenticatedClientSessionModel clientSession, ClientModel client, NodeGenerator... extensions) throws ConfigurationException {
|
protected LogoutRequestType createLogoutRequest(String logoutUrl, AuthenticatedClientSessionModel clientSession, ClientModel client, NodeGenerator... extensions) throws ConfigurationException {
|
||||||
// build userPrincipal with subject used at login
|
// build userPrincipal with subject used at login
|
||||||
SAML2LogoutRequestBuilder logoutBuilder = new SAML2LogoutRequestBuilder().assertionExpiration(realm.getAccessCodeLifespan()).issuer(getResponseIssuer(realm))
|
SAML2LogoutRequestBuilder logoutBuilder = new SAML2LogoutRequestBuilder().assertionExpiration(realm.getAccessCodeLifespan()).issuer(getResponseIssuer(realm))
|
||||||
|
|
|
@ -172,23 +172,25 @@ public class SamlService extends AuthorizationEndpointBase {
|
||||||
// and we want to turn it off.
|
// and we want to turn it off.
|
||||||
protected boolean redirectToAuthentication;
|
protected boolean redirectToAuthentication;
|
||||||
|
|
||||||
|
protected abstract Response error(KeycloakSession session, AuthenticationSessionModel authenticationSession, Response.Status status, String message, Object... parameters);
|
||||||
|
|
||||||
protected Response basicChecks(String samlRequest, String samlResponse, String artifact) {
|
protected Response basicChecks(String samlRequest, String samlResponse, String artifact) {
|
||||||
logger.tracef("basicChecks(%s, %s, %s)%s", samlRequest, samlResponse, artifact, getShortStackTrace());
|
logger.tracef("basicChecks(%s, %s, %s)%s", samlRequest, samlResponse, artifact, getShortStackTrace());
|
||||||
if (!checkSsl()) {
|
if (!checkSsl()) {
|
||||||
event.event(EventType.LOGIN);
|
event.event(EventType.LOGIN);
|
||||||
event.error(Errors.SSL_REQUIRED);
|
event.error(Errors.SSL_REQUIRED);
|
||||||
return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.HTTPS_REQUIRED);
|
return error(session, null, Response.Status.BAD_REQUEST, Messages.HTTPS_REQUIRED);
|
||||||
}
|
}
|
||||||
if (!realm.isEnabled()) {
|
if (!realm.isEnabled()) {
|
||||||
event.event(EventType.LOGIN_ERROR);
|
event.event(EventType.LOGIN_ERROR);
|
||||||
event.error(Errors.REALM_DISABLED);
|
event.error(Errors.REALM_DISABLED);
|
||||||
return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.REALM_NOT_ENABLED);
|
return error(session, null, Response.Status.BAD_REQUEST, Messages.REALM_NOT_ENABLED);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (samlRequest == null && samlResponse == null && artifact == null) {
|
if (samlRequest == null && samlResponse == null && artifact == null) {
|
||||||
event.event(EventType.LOGIN);
|
event.event(EventType.LOGIN);
|
||||||
event.error(Errors.SAML_TOKEN_NOT_FOUND);
|
event.error(Errors.SAML_TOKEN_NOT_FOUND);
|
||||||
return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.INVALID_REQUEST);
|
return error(session, null, Response.Status.BAD_REQUEST, Messages.INVALID_REQUEST);
|
||||||
|
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
@ -205,7 +207,7 @@ public class SamlService extends AuthorizationEndpointBase {
|
||||||
if (! (holder.getSamlObject() instanceof StatusResponseType)) {
|
if (! (holder.getSamlObject() instanceof StatusResponseType)) {
|
||||||
event.detail(Details.REASON, Errors.INVALID_SAML_RESPONSE);
|
event.detail(Details.REASON, Errors.INVALID_SAML_RESPONSE);
|
||||||
event.error(Errors.INVALID_SAML_RESPONSE);
|
event.error(Errors.INVALID_SAML_RESPONSE);
|
||||||
return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.INVALID_REQUEST);
|
return error(session, null, Response.Status.BAD_REQUEST, Messages.INVALID_REQUEST);
|
||||||
}
|
}
|
||||||
|
|
||||||
StatusResponseType statusResponse = (StatusResponseType) holder.getSamlObject();
|
StatusResponseType statusResponse = (StatusResponseType) holder.getSamlObject();
|
||||||
|
@ -214,12 +216,12 @@ public class SamlService extends AuthorizationEndpointBase {
|
||||||
statusResponse.getDestination() == null && containsUnencryptedSignature(holder)) {
|
statusResponse.getDestination() == null && containsUnencryptedSignature(holder)) {
|
||||||
event.detail(Details.REASON, Errors.MISSING_REQUIRED_DESTINATION);
|
event.detail(Details.REASON, Errors.MISSING_REQUIRED_DESTINATION);
|
||||||
event.error(Errors.INVALID_SAML_LOGOUT_RESPONSE);
|
event.error(Errors.INVALID_SAML_LOGOUT_RESPONSE);
|
||||||
return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.INVALID_REQUEST);
|
return error(session, null, Response.Status.BAD_REQUEST, Messages.INVALID_REQUEST);
|
||||||
}
|
}
|
||||||
if (! destinationValidator.validate(this.getExpectedDestinationUri(session), statusResponse.getDestination())) {
|
if (! destinationValidator.validate(this.getExpectedDestinationUri(session), statusResponse.getDestination())) {
|
||||||
event.detail(Details.REASON, Errors.INVALID_DESTINATION);
|
event.detail(Details.REASON, Errors.INVALID_DESTINATION);
|
||||||
event.error(Errors.INVALID_SAML_LOGOUT_RESPONSE);
|
event.error(Errors.INVALID_SAML_LOGOUT_RESPONSE);
|
||||||
return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.INVALID_REQUEST);
|
return error(session, null, Response.Status.BAD_REQUEST, Messages.INVALID_REQUEST);
|
||||||
}
|
}
|
||||||
|
|
||||||
AuthenticationManager.AuthResult authResult = authManager.authenticateIdentityCookie(session, realm, false);
|
AuthenticationManager.AuthResult authResult = authManager.authenticateIdentityCookie(session, realm, false);
|
||||||
|
@ -227,7 +229,7 @@ public class SamlService extends AuthorizationEndpointBase {
|
||||||
logger.warn("Unknown saml response.");
|
logger.warn("Unknown saml response.");
|
||||||
event.event(EventType.LOGOUT);
|
event.event(EventType.LOGOUT);
|
||||||
event.error(Errors.INVALID_TOKEN);
|
event.error(Errors.INVALID_TOKEN);
|
||||||
return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.INVALID_REQUEST);
|
return error(session, null, Response.Status.BAD_REQUEST, Messages.INVALID_REQUEST);
|
||||||
}
|
}
|
||||||
// assume this is a logout response
|
// assume this is a logout response
|
||||||
UserSessionModel userSession = authResult.getSession();
|
UserSessionModel userSession = authResult.getSession();
|
||||||
|
@ -236,7 +238,7 @@ public class SamlService extends AuthorizationEndpointBase {
|
||||||
logger.warn("UserSession is not tagged as logging out.");
|
logger.warn("UserSession is not tagged as logging out.");
|
||||||
event.event(EventType.LOGOUT);
|
event.event(EventType.LOGOUT);
|
||||||
event.error(Errors.INVALID_SAML_LOGOUT_RESPONSE);
|
event.error(Errors.INVALID_SAML_LOGOUT_RESPONSE);
|
||||||
return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.INVALID_REQUEST);
|
return error(session, null, Response.Status.BAD_REQUEST, Messages.INVALID_REQUEST);
|
||||||
}
|
}
|
||||||
String issuer = statusResponse.getIssuer().getValue();
|
String issuer = statusResponse.getIssuer().getValue();
|
||||||
ClientModel client = realm.getClientByClientId(issuer);
|
ClientModel client = realm.getClientByClientId(issuer);
|
||||||
|
@ -244,13 +246,13 @@ public class SamlService extends AuthorizationEndpointBase {
|
||||||
event.event(EventType.LOGOUT);
|
event.event(EventType.LOGOUT);
|
||||||
event.client(issuer);
|
event.client(issuer);
|
||||||
event.error(Errors.CLIENT_NOT_FOUND);
|
event.error(Errors.CLIENT_NOT_FOUND);
|
||||||
return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.CLIENT_NOT_FOUND);
|
return error(session, null, Response.Status.BAD_REQUEST, Messages.CLIENT_NOT_FOUND);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isClientProtocolCorrect(client)) {
|
if (!isClientProtocolCorrect(client)) {
|
||||||
event.event(EventType.LOGOUT);
|
event.event(EventType.LOGOUT);
|
||||||
event.error(Errors.INVALID_CLIENT);
|
event.error(Errors.INVALID_CLIENT);
|
||||||
return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, "Wrong client protocol.");
|
return error(session, null, Response.Status.BAD_REQUEST, "Wrong client protocol.");
|
||||||
}
|
}
|
||||||
|
|
||||||
session.getContext().setClient(client);
|
session.getContext().setClient(client);
|
||||||
|
@ -265,7 +267,7 @@ public class SamlService extends AuthorizationEndpointBase {
|
||||||
if (documentHolder == null) {
|
if (documentHolder == null) {
|
||||||
event.event(EventType.LOGIN);
|
event.event(EventType.LOGIN);
|
||||||
event.error(Errors.INVALID_TOKEN);
|
event.error(Errors.INVALID_TOKEN);
|
||||||
return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.INVALID_REQUEST);
|
return error(session, null, Response.Status.BAD_REQUEST, Messages.INVALID_REQUEST);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -281,7 +283,7 @@ public class SamlService extends AuthorizationEndpointBase {
|
||||||
event.event(EventType.LOGIN);
|
event.event(EventType.LOGIN);
|
||||||
event.error(Errors.INVALID_TOKEN);
|
event.error(Errors.INVALID_TOKEN);
|
||||||
event.detail(Details.REASON, "Unhandled SAML document type: " + (samlObject == null ? "<null>" : samlObject.getClass().getSimpleName()));
|
event.detail(Details.REASON, "Unhandled SAML document type: " + (samlObject == null ? "<null>" : samlObject.getClass().getSimpleName()));
|
||||||
return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.INVALID_REQUEST);
|
return error(session, null, Response.Status.BAD_REQUEST, Messages.INVALID_REQUEST);
|
||||||
}
|
}
|
||||||
|
|
||||||
RequestAbstractType requestAbstractType = (RequestAbstractType) samlObject;
|
RequestAbstractType requestAbstractType = (RequestAbstractType) samlObject;
|
||||||
|
@ -304,7 +306,7 @@ public class SamlService extends AuthorizationEndpointBase {
|
||||||
} catch (VerificationException e) {
|
} catch (VerificationException e) {
|
||||||
SamlService.logger.error("request validation failed", e);
|
SamlService.logger.error("request validation failed", e);
|
||||||
event.error(Errors.INVALID_SIGNATURE);
|
event.error(Errors.INVALID_SIGNATURE);
|
||||||
return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.INVALID_REQUESTER);
|
return error(session, null, Response.Status.BAD_REQUEST, Messages.INVALID_REQUESTER);
|
||||||
}
|
}
|
||||||
logger.debug("verified request");
|
logger.debug("verified request");
|
||||||
|
|
||||||
|
@ -312,7 +314,7 @@ public class SamlService extends AuthorizationEndpointBase {
|
||||||
requestAbstractType.getDestination() == null && containsUnencryptedSignature(documentHolder)) {
|
requestAbstractType.getDestination() == null && containsUnencryptedSignature(documentHolder)) {
|
||||||
event.detail(Details.REASON, Errors.MISSING_REQUIRED_DESTINATION);
|
event.detail(Details.REASON, Errors.MISSING_REQUIRED_DESTINATION);
|
||||||
event.error(Errors.INVALID_REQUEST);
|
event.error(Errors.INVALID_REQUEST);
|
||||||
return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.INVALID_REQUEST);
|
return error(session, null, Response.Status.BAD_REQUEST, Messages.INVALID_REQUEST);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (samlObject instanceof AuthnRequestType) {
|
if (samlObject instanceof AuthnRequestType) {
|
||||||
|
@ -353,7 +355,7 @@ public class SamlService extends AuthorizationEndpointBase {
|
||||||
event.event(EventType.LOGIN);
|
event.event(EventType.LOGIN);
|
||||||
event.detail(Details.REASON, e.getMessage());
|
event.detail(Details.REASON, e.getMessage());
|
||||||
event.error(Errors.INVALID_SAML_ARTIFACT);
|
event.error(Errors.INVALID_SAML_ARTIFACT);
|
||||||
asyncResponse.resume(ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.INVALID_REQUEST));
|
asyncResponse.resume(error(session, null, Response.Status.BAD_REQUEST, Messages.INVALID_REQUEST));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -393,7 +395,7 @@ public class SamlService extends AuthorizationEndpointBase {
|
||||||
event.event(EventType.LOGIN);
|
event.event(EventType.LOGIN);
|
||||||
event.detail(Details.REASON, e.getMessage());
|
event.detail(Details.REASON, e.getMessage());
|
||||||
event.error(Errors.IDENTITY_PROVIDER_ERROR);
|
event.error(Errors.IDENTITY_PROVIDER_ERROR);
|
||||||
asyncResponse.resume(ErrorPage.error(session, null, Response.Status.INTERNAL_SERVER_ERROR, Messages.UNEXPECTED_ERROR_HANDLING_REQUEST));
|
asyncResponse.resume(error(session, null, Response.Status.INTERNAL_SERVER_ERROR, Messages.UNEXPECTED_ERROR_HANDLING_REQUEST));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -412,7 +414,7 @@ public class SamlService extends AuthorizationEndpointBase {
|
||||||
SamlClient samlClient = new SamlClient(client);
|
SamlClient samlClient = new SamlClient(client);
|
||||||
|
|
||||||
if (! validateDestination(requestAbstractType, samlClient, Errors.INVALID_SAML_AUTHN_REQUEST)) {
|
if (! validateDestination(requestAbstractType, samlClient, Errors.INVALID_SAML_AUTHN_REQUEST)) {
|
||||||
return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.INVALID_REQUEST);
|
return error(session, null, Response.Status.BAD_REQUEST, Messages.INVALID_REQUEST);
|
||||||
}
|
}
|
||||||
|
|
||||||
String bindingType = getBindingType(requestAbstractType);
|
String bindingType = getBindingType(requestAbstractType);
|
||||||
|
@ -441,7 +443,7 @@ public class SamlService extends AuthorizationEndpointBase {
|
||||||
|
|
||||||
if (redirect == null) {
|
if (redirect == null) {
|
||||||
event.error(Errors.INVALID_REDIRECT_URI);
|
event.error(Errors.INVALID_REDIRECT_URI);
|
||||||
return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.INVALID_REDIRECT_URI);
|
return error(session, null, Response.Status.BAD_REQUEST, Messages.INVALID_REDIRECT_URI);
|
||||||
}
|
}
|
||||||
|
|
||||||
AuthenticationSessionModel authSession = createAuthenticationSession(client, relayState);
|
AuthenticationSessionModel authSession = createAuthenticationSession(client, relayState);
|
||||||
|
@ -472,7 +474,7 @@ public class SamlService extends AuthorizationEndpointBase {
|
||||||
} else {
|
} else {
|
||||||
event.detail(Details.REASON, Errors.UNSUPPORTED_NAMEID_FORMAT);
|
event.detail(Details.REASON, Errors.UNSUPPORTED_NAMEID_FORMAT);
|
||||||
event.error(Errors.INVALID_SAML_AUTHN_REQUEST);
|
event.error(Errors.INVALID_SAML_AUTHN_REQUEST);
|
||||||
return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.UNSUPPORTED_NAME_ID_FORMAT);
|
return error(session, null, Response.Status.BAD_REQUEST, Messages.UNSUPPORTED_NAME_ID_FORMAT);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -532,7 +534,7 @@ public class SamlService extends AuthorizationEndpointBase {
|
||||||
protected Response logoutRequest(LogoutRequestType logoutRequest, ClientModel client, String relayState) {
|
protected Response logoutRequest(LogoutRequestType logoutRequest, ClientModel client, String relayState) {
|
||||||
SamlClient samlClient = new SamlClient(client);
|
SamlClient samlClient = new SamlClient(client);
|
||||||
if (! validateDestination(logoutRequest, samlClient, Errors.INVALID_SAML_LOGOUT_REQUEST)) {
|
if (! validateDestination(logoutRequest, samlClient, Errors.INVALID_SAML_LOGOUT_REQUEST)) {
|
||||||
return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.INVALID_REQUEST);
|
return error(session, null, Response.Status.BAD_REQUEST, Messages.INVALID_REQUEST);
|
||||||
}
|
}
|
||||||
|
|
||||||
// authenticate identity cookie, but ignore an access token timeout as we're logging out anyways.
|
// authenticate identity cookie, but ignore an access token timeout as we're logging out anyways.
|
||||||
|
@ -598,9 +600,18 @@ public class SamlService extends AuthorizationEndpointBase {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
event.event(EventType.LOGOUT)
|
||||||
|
.detail(Details.AUTH_METHOD, userSession.getAuthMethod())
|
||||||
|
.client(session.getContext().getClient())
|
||||||
|
.user(userSession.getUser())
|
||||||
|
.session(userSession)
|
||||||
|
.detail(Details.USERNAME, userSession.getLoginUsername())
|
||||||
|
.detail(Details.RESPONSE_MODE, getBindingType());
|
||||||
authManager.backchannelLogout(session, realm, userSession, session.getContext().getUri(), clientConnection, headers, true);
|
authManager.backchannelLogout(session, realm, userSession, session.getContext().getUri(), clientConnection, headers, true);
|
||||||
|
event.success();
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
logger.warn("Failure with backchannel logout", e);
|
logger.warn("Failure with backchannel logout", e);
|
||||||
|
event.error("Failure with backchannel logout");
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -628,6 +639,8 @@ public class SamlService extends AuthorizationEndpointBase {
|
||||||
try {
|
try {
|
||||||
if (postBinding) {
|
if (postBinding) {
|
||||||
return binding.postBinding(builder.buildDocument()).response(logoutBindingUri);
|
return binding.postBinding(builder.buildDocument()).response(logoutBindingUri);
|
||||||
|
} else if (SamlProtocol.SAML_SOAP_BINDING.equals(logoutBinding)) {
|
||||||
|
return binding.soapBinding(builder.buildDocument()).response();
|
||||||
} else {
|
} else {
|
||||||
return binding.redirectBinding(builder.buildDocument()).response(logoutBindingUri);
|
return binding.redirectBinding(builder.buildDocument()).response(logoutBindingUri);
|
||||||
}
|
}
|
||||||
|
@ -704,10 +717,46 @@ public class SamlService extends AuthorizationEndpointBase {
|
||||||
final URI baseUri = session.getContext().getUri().getBaseUri();
|
final URI baseUri = session.getContext().getUri().getBaseUri();
|
||||||
return Urls.samlRequestEndpoint(baseUri, realmName);
|
return Urls.samlRequestEndpoint(baseUri, realmName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Response checkClientValidity(ClientModel client) {
|
||||||
|
if (client == null) {
|
||||||
|
event.event(EventType.LOGIN);
|
||||||
|
event.detail(Details.REASON, "Cannot_match_source_hash");
|
||||||
|
event.error(Errors.CLIENT_NOT_FOUND);
|
||||||
|
return error(session, null, Response.Status.BAD_REQUEST, Messages.INVALID_REQUEST);
|
||||||
|
}
|
||||||
|
if (!client.isEnabled()) {
|
||||||
|
event.event(EventType.LOGIN);
|
||||||
|
event.error(Errors.CLIENT_DISABLED);
|
||||||
|
return error(session, null, Response.Status.BAD_REQUEST, Messages.LOGIN_REQUESTER_NOT_ENABLED);
|
||||||
|
}
|
||||||
|
if (client.isBearerOnly()) {
|
||||||
|
event.event(EventType.LOGIN);
|
||||||
|
event.error(Errors.NOT_ALLOWED);
|
||||||
|
return error(session, null, Response.Status.BAD_REQUEST, Messages.BEARER_ONLY);
|
||||||
|
}
|
||||||
|
if (!client.isStandardFlowEnabled()) {
|
||||||
|
event.event(EventType.LOGIN);
|
||||||
|
event.error(Errors.NOT_ALLOWED);
|
||||||
|
return error(session, null, Response.Status.BAD_REQUEST, Messages.STANDARD_FLOW_DISABLED);
|
||||||
|
}
|
||||||
|
if (!isClientProtocolCorrect(client)) {
|
||||||
|
event.event(EventType.LOGIN);
|
||||||
|
event.error(Errors.INVALID_CLIENT);
|
||||||
|
return error(session, null, Response.Status.BAD_REQUEST, "Wrong client protocol.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected class PostBindingProtocol extends BindingProtocol {
|
protected class PostBindingProtocol extends BindingProtocol {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Response error(KeycloakSession session, AuthenticationSessionModel authenticationSession, Response.Status status, String message, Object... parameters) {
|
||||||
|
return ErrorPage.error(session, authenticationSession, status, message, parameters);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected String encodeSamlDocument(Document samlDocument) throws ProcessingException {
|
protected String encodeSamlDocument(Document samlDocument) throws ProcessingException {
|
||||||
try {
|
try {
|
||||||
|
@ -748,6 +797,11 @@ public class SamlService extends AuthorizationEndpointBase {
|
||||||
|
|
||||||
protected class RedirectBindingProtocol extends BindingProtocol {
|
protected class RedirectBindingProtocol extends BindingProtocol {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Response error(KeycloakSession session, AuthenticationSessionModel authenticationSession, Response.Status status, String message, Object... parameters) {
|
||||||
|
return ErrorPage.error(session, authenticationSession, status, message, parameters);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected String encodeSamlDocument(Document samlDocument) throws ProcessingException {
|
protected String encodeSamlDocument(Document samlDocument) throws ProcessingException {
|
||||||
try {
|
try {
|
||||||
|
@ -883,37 +937,6 @@ public class SamlService extends AuthorizationEndpointBase {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Response checkClientValidity(ClientModel client) {
|
|
||||||
if (client == null) {
|
|
||||||
event.event(EventType.LOGIN);
|
|
||||||
event.detail(Details.REASON, "Cannot_match_source_hash");
|
|
||||||
event.error(Errors.CLIENT_NOT_FOUND);
|
|
||||||
return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.INVALID_REQUEST);
|
|
||||||
}
|
|
||||||
if (!client.isEnabled()) {
|
|
||||||
event.event(EventType.LOGIN);
|
|
||||||
event.error(Errors.CLIENT_DISABLED);
|
|
||||||
return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.LOGIN_REQUESTER_NOT_ENABLED);
|
|
||||||
}
|
|
||||||
if (client.isBearerOnly()) {
|
|
||||||
event.event(EventType.LOGIN);
|
|
||||||
event.error(Errors.NOT_ALLOWED);
|
|
||||||
return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.BEARER_ONLY);
|
|
||||||
}
|
|
||||||
if (!client.isStandardFlowEnabled()) {
|
|
||||||
event.event(EventType.LOGIN);
|
|
||||||
event.error(Errors.NOT_ALLOWED);
|
|
||||||
return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.STANDARD_FLOW_DISABLED);
|
|
||||||
}
|
|
||||||
if (!isClientProtocolCorrect(client)) {
|
|
||||||
event.event(EventType.LOGIN);
|
|
||||||
event.error(Errors.INVALID_CLIENT);
|
|
||||||
return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, "Wrong client protocol.");
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
@Path("clients/{client}")
|
@Path("clients/{client}")
|
||||||
@Produces(MediaType.TEXT_HTML_UTF_8)
|
@Produces(MediaType.TEXT_HTML_UTF_8)
|
||||||
|
|
|
@ -37,6 +37,7 @@ import org.keycloak.saml.common.constants.JBossSAMLURIConstants;
|
||||||
import org.keycloak.saml.common.exceptions.ConfigurationException;
|
import org.keycloak.saml.common.exceptions.ConfigurationException;
|
||||||
import org.keycloak.saml.common.exceptions.ProcessingException;
|
import org.keycloak.saml.common.exceptions.ProcessingException;
|
||||||
import org.keycloak.saml.validators.DestinationValidator;
|
import org.keycloak.saml.validators.DestinationValidator;
|
||||||
|
import org.keycloak.services.ErrorPage;
|
||||||
import org.keycloak.services.managers.AuthenticationManager;
|
import org.keycloak.services.managers.AuthenticationManager;
|
||||||
import org.keycloak.sessions.AuthenticationSessionModel;
|
import org.keycloak.sessions.AuthenticationSessionModel;
|
||||||
import org.w3c.dom.Document;
|
import org.w3c.dom.Document;
|
||||||
|
@ -68,11 +69,22 @@ public class SamlEcpProfileService extends SamlService {
|
||||||
public Response authenticate(Document soapMessage) {
|
public Response authenticate(Document soapMessage) {
|
||||||
try {
|
try {
|
||||||
return new PostBindingProtocol() {
|
return new PostBindingProtocol() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Response error(KeycloakSession session, AuthenticationSessionModel authenticationSession, Response.Status status, String message, Object... parameters) {
|
||||||
|
return Soap.createFault().code("error").reason(message).build();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected String getBindingType(AuthnRequestType requestAbstractType) {
|
protected String getBindingType(AuthnRequestType requestAbstractType) {
|
||||||
return SamlProtocol.SAML_SOAP_BINDING;
|
return SamlProtocol.SAML_SOAP_BINDING;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String getBindingType() {
|
||||||
|
return SamlProtocol.SAML_SOAP_BINDING;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected boolean isDestinationRequired() {
|
protected boolean isDestinationRequired() {
|
||||||
return false;
|
return false;
|
||||||
|
|
|
@ -23,6 +23,7 @@ import org.apache.http.entity.ContentType;
|
||||||
import org.keycloak.saml.processing.core.saml.v2.util.DocumentUtil;
|
import org.keycloak.saml.processing.core.saml.v2.util.DocumentUtil;
|
||||||
import org.keycloak.saml.processing.web.util.PostBindingUtil;
|
import org.keycloak.saml.processing.web.util.PostBindingUtil;
|
||||||
import org.w3c.dom.Document;
|
import org.w3c.dom.Document;
|
||||||
|
import org.w3c.dom.Element;
|
||||||
import org.w3c.dom.Node;
|
import org.w3c.dom.Node;
|
||||||
|
|
||||||
import javax.ws.rs.core.MediaType;
|
import javax.ws.rs.core.MediaType;
|
||||||
|
@ -101,7 +102,7 @@ public final class Soap {
|
||||||
public static Document extractSoapMessage(SOAPMessage soapMessage) {
|
public static Document extractSoapMessage(SOAPMessage soapMessage) {
|
||||||
try {
|
try {
|
||||||
SOAPBody soapBody = soapMessage.getSOAPBody();
|
SOAPBody soapBody = soapMessage.getSOAPBody();
|
||||||
Node authnRequestNode = soapBody.getFirstChild();
|
Node authnRequestNode = getFirstChild(soapBody);
|
||||||
Document document = DocumentUtil.createDocument();
|
Document document = DocumentUtil.createDocument();
|
||||||
document.appendChild(document.importNode(authnRequestNode, true));
|
document.appendChild(document.importNode(authnRequestNode, true));
|
||||||
return document;
|
return document;
|
||||||
|
@ -110,6 +111,20 @@ public final class Soap {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the first direct child that is an XML element.
|
||||||
|
* In case of pretty-printed XML (with newlines and spaces), this method skips non-element objects (e.g. text)
|
||||||
|
* to really fetch the next XML tag.
|
||||||
|
*/
|
||||||
|
public static Node getFirstChild(Node parent) {
|
||||||
|
Node n = parent.getFirstChild();
|
||||||
|
while (n != null && !(n instanceof Element)) {
|
||||||
|
n = n.getNextSibling();
|
||||||
|
}
|
||||||
|
if (n == null) return null;
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
public static class SoapMessageBuilder {
|
public static class SoapMessageBuilder {
|
||||||
private final SOAPMessage message;
|
private final SOAPMessage message;
|
||||||
private final SOAPBody body;
|
private final SOAPBody body;
|
||||||
|
|
|
@ -75,6 +75,7 @@ import javax.xml.soap.SOAPException;
|
||||||
import javax.xml.soap.SOAPHeader;
|
import javax.xml.soap.SOAPHeader;
|
||||||
import javax.xml.soap.SOAPHeaderElement;
|
import javax.xml.soap.SOAPHeaderElement;
|
||||||
import javax.xml.soap.SOAPMessage;
|
import javax.xml.soap.SOAPMessage;
|
||||||
|
import javax.xml.ws.soap.SOAPFaultException;
|
||||||
import java.io.ByteArrayInputStream;
|
import java.io.ByteArrayInputStream;
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
@ -348,11 +349,10 @@ public class SamlClient {
|
||||||
@Override
|
@Override
|
||||||
public SAMLDocumentHolder extractResponse(CloseableHttpResponse response, String realmPublicKey) throws IOException {
|
public SAMLDocumentHolder extractResponse(CloseableHttpResponse response, String realmPublicKey) throws IOException {
|
||||||
|
|
||||||
assertThat(response, statusCodeIsHC(200));
|
|
||||||
|
|
||||||
MessageFactory messageFactory = null;
|
|
||||||
try {
|
try {
|
||||||
messageFactory = MessageFactory.newInstance();
|
int statusCode = response.getStatusLine().getStatusCode();
|
||||||
|
if (statusCode == 200) {
|
||||||
|
MessageFactory messageFactory = MessageFactory.newInstance();
|
||||||
SOAPMessage soapMessage = messageFactory.createMessage(null, response.getEntity().getContent());
|
SOAPMessage soapMessage = messageFactory.createMessage(null, response.getEntity().getContent());
|
||||||
SOAPBody soapBody = soapMessage.getSOAPBody();
|
SOAPBody soapBody = soapMessage.getSOAPBody();
|
||||||
Node authnRequestNode = soapBody.getFirstChild();
|
Node authnRequestNode = soapBody.getFirstChild();
|
||||||
|
@ -365,6 +365,15 @@ public class SamlClient {
|
||||||
SAML2Object responseType = (SAML2Object) samlParser.parse(document);
|
SAML2Object responseType = (SAML2Object) samlParser.parse(document);
|
||||||
|
|
||||||
return new SAMLDocumentHolder(responseType, document);
|
return new SAMLDocumentHolder(responseType, document);
|
||||||
|
|
||||||
|
} else if (statusCode == 500) {
|
||||||
|
MessageFactory messageFactory = MessageFactory.newInstance();
|
||||||
|
SOAPMessage soapMessage = messageFactory.createMessage(null, response.getEntity().getContent());
|
||||||
|
SOAPBody soapBody = soapMessage.getSOAPBody();
|
||||||
|
throw new SOAPFaultException(soapBody.getFault());
|
||||||
|
} else {
|
||||||
|
throw new RuntimeException("Unexpected response status code (" + statusCode + ")");
|
||||||
|
}
|
||||||
} catch (SOAPException | ConfigurationException | ProcessingException | ParsingException e) {
|
} catch (SOAPException | ConfigurationException | ProcessingException | ParsingException e) {
|
||||||
throw new RuntimeException(e);
|
throw new RuntimeException(e);
|
||||||
}
|
}
|
||||||
|
|
|
@ -171,9 +171,14 @@ public class SamlClientBuilder {
|
||||||
return addStepBuilder(new CreateAuthnRequestStepBuilder(authServerSamlUrl, authnRequestDocument, requestBinding, this));
|
return addStepBuilder(new CreateAuthnRequestStepBuilder(authServerSamlUrl, authnRequestDocument, requestBinding, this));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Issues the given AuthnRequest to the SAML endpoint */
|
/** Issues the given LogoutRequest to the SAML endpoint */
|
||||||
public CreateLogoutRequestStepBuilder logoutRequest(URI authServerSamlUrl, String issuer, Binding requestBinding) {
|
public CreateLogoutRequestStepBuilder logoutRequest(URI logoutServerSamlUrl, String issuer, Binding requestBinding) {
|
||||||
return addStepBuilder(new CreateLogoutRequestStepBuilder(authServerSamlUrl, issuer, requestBinding, this));
|
return addStepBuilder(new CreateLogoutRequestStepBuilder(logoutServerSamlUrl, issuer, requestBinding, this));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Issues the given LogoutRequest to the SAML endpoint */
|
||||||
|
public CreateLogoutRequestStepBuilder logoutRequest(URI logoutServerSamlUrl, String issuer, Binding requestBinding, boolean skipSignature) {
|
||||||
|
return addStepBuilder(new CreateLogoutRequestStepBuilder(logoutServerSamlUrl, issuer, requestBinding, this, skipSignature));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Issues the given SAML document to the SAML endpoint */
|
/** Issues the given SAML document to the SAML endpoint */
|
||||||
|
|
|
@ -35,7 +35,7 @@ import org.apache.http.impl.client.CloseableHttpClient;
|
||||||
*/
|
*/
|
||||||
public class CreateLogoutRequestStepBuilder extends SamlDocumentStepBuilder<LogoutRequestType, CreateLogoutRequestStepBuilder> {
|
public class CreateLogoutRequestStepBuilder extends SamlDocumentStepBuilder<LogoutRequestType, CreateLogoutRequestStepBuilder> {
|
||||||
|
|
||||||
private final URI authServerSamlUrl;
|
private final URI logoutServerSamlUrl;
|
||||||
private final String issuer;
|
private final String issuer;
|
||||||
private final Binding requestBinding;
|
private final Binding requestBinding;
|
||||||
|
|
||||||
|
@ -46,13 +46,23 @@ public class CreateLogoutRequestStepBuilder extends SamlDocumentStepBuilder<Logo
|
||||||
private String signingPrivateKeyPem;
|
private String signingPrivateKeyPem;
|
||||||
private String signingCertificate;
|
private String signingCertificate;
|
||||||
|
|
||||||
public CreateLogoutRequestStepBuilder(URI authServerSamlUrl, String issuer, Binding requestBinding, SamlClientBuilder clientBuilder) {
|
private boolean skipSignature;
|
||||||
|
|
||||||
|
public CreateLogoutRequestStepBuilder(URI logoutServerSamlUrl, String issuer, Binding requestBinding, SamlClientBuilder clientBuilder) {
|
||||||
super(clientBuilder);
|
super(clientBuilder);
|
||||||
this.authServerSamlUrl = authServerSamlUrl;
|
this.logoutServerSamlUrl = logoutServerSamlUrl;
|
||||||
this.issuer = issuer;
|
this.issuer = issuer;
|
||||||
this.requestBinding = requestBinding;
|
this.requestBinding = requestBinding;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public CreateLogoutRequestStepBuilder(URI logoutServerSamlUrl, String issuer, Binding requestBinding, SamlClientBuilder clientBuilder, boolean skipSignature) {
|
||||||
|
super(clientBuilder);
|
||||||
|
this.logoutServerSamlUrl = logoutServerSamlUrl;
|
||||||
|
this.issuer = issuer;
|
||||||
|
this.requestBinding = requestBinding;
|
||||||
|
this.skipSignature = skipSignature;
|
||||||
|
}
|
||||||
|
|
||||||
public String sessionIndex() {
|
public String sessionIndex() {
|
||||||
return sessionIndex.get();
|
return sessionIndex.get();
|
||||||
}
|
}
|
||||||
|
@ -109,7 +119,7 @@ public class CreateLogoutRequestStepBuilder extends SamlDocumentStepBuilder<Logo
|
||||||
@Override
|
@Override
|
||||||
public HttpUriRequest perform(CloseableHttpClient client, URI currentURI, CloseableHttpResponse currentResponse, HttpClientContext context) throws Exception {
|
public HttpUriRequest perform(CloseableHttpClient client, URI currentURI, CloseableHttpResponse currentResponse, HttpClientContext context) throws Exception {
|
||||||
SAML2LogoutRequestBuilder builder = new SAML2LogoutRequestBuilder()
|
SAML2LogoutRequestBuilder builder = new SAML2LogoutRequestBuilder()
|
||||||
.destination(authServerSamlUrl == null ? null : authServerSamlUrl.toString())
|
.destination(logoutServerSamlUrl == null ? null : logoutServerSamlUrl.toString())
|
||||||
.issuer(issuer)
|
.issuer(issuer)
|
||||||
.sessionIndex(sessionIndex())
|
.sessionIndex(sessionIndex())
|
||||||
.nameId(nameId());
|
.nameId(nameId());
|
||||||
|
@ -121,9 +131,9 @@ public class CreateLogoutRequestStepBuilder extends SamlDocumentStepBuilder<Logo
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.signingPrivateKeyPem == null
|
return this.signingPrivateKeyPem == null || skipSignature
|
||||||
? requestBinding.createSamlUnsignedRequest(authServerSamlUrl, relayState(), DocumentUtil.getDocument(transformed))
|
? requestBinding.createSamlUnsignedRequest(logoutServerSamlUrl, relayState(), DocumentUtil.getDocument(transformed))
|
||||||
: requestBinding.createSamlSignedRequest(authServerSamlUrl, relayState(), DocumentUtil.getDocument(transformed), signingPrivateKeyPem, signingPublicKeyPem, signingCertificate);
|
: requestBinding.createSamlSignedRequest(logoutServerSamlUrl, relayState(), DocumentUtil.getDocument(transformed), signingPrivateKeyPem, signingPublicKeyPem, signingCertificate);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,124 @@
|
||||||
|
package org.keycloak.testsuite.util.saml;
|
||||||
|
|
||||||
|
|
||||||
|
import com.sun.net.httpserver.HttpExchange;
|
||||||
|
import com.sun.net.httpserver.HttpHandler;
|
||||||
|
import com.sun.net.httpserver.HttpServer;
|
||||||
|
import org.keycloak.common.util.KeyUtils;
|
||||||
|
import org.keycloak.dom.saml.v2.protocol.LogoutRequestType;
|
||||||
|
import org.keycloak.dom.saml.v2.protocol.StatusResponseType;
|
||||||
|
import org.keycloak.protocol.saml.JaxrsSAML2BindingBuilder;
|
||||||
|
import org.keycloak.protocol.saml.SamlConfigAttributes;
|
||||||
|
import org.keycloak.protocol.saml.profile.util.Soap;
|
||||||
|
import org.keycloak.representations.idm.ClientRepresentation;
|
||||||
|
import org.keycloak.saml.SAML2LogoutResponseBuilder;
|
||||||
|
import org.keycloak.saml.SignatureAlgorithm;
|
||||||
|
import org.keycloak.saml.processing.api.saml.v2.response.SAML2Response;
|
||||||
|
import org.keycloak.saml.processing.core.saml.v2.common.SAMLDocumentHolder;
|
||||||
|
import org.w3c.dom.Document;
|
||||||
|
|
||||||
|
import javax.ws.rs.core.HttpHeaders;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.net.InetAddress;
|
||||||
|
import java.net.InetSocketAddress;
|
||||||
|
import java.security.PrivateKey;
|
||||||
|
import java.security.PublicKey;
|
||||||
|
|
||||||
|
public class SamlBackchannelLogoutReceiver implements AutoCloseable {
|
||||||
|
|
||||||
|
private final HttpServer server;
|
||||||
|
private LogoutRequestType logoutRequest;
|
||||||
|
private final String url;
|
||||||
|
private final ClientRepresentation samlClient;
|
||||||
|
private final PublicKey publicKey;
|
||||||
|
private final PrivateKey privateKey;
|
||||||
|
|
||||||
|
public SamlBackchannelLogoutReceiver(int port, ClientRepresentation samlClient, String publicKeyStr, String privateKeyStr) {
|
||||||
|
this.samlClient = samlClient;
|
||||||
|
publicKey = publicKeyStr == null ? null : org.keycloak.testsuite.util.KeyUtils.publicKeyFromString(publicKeyStr);
|
||||||
|
privateKey = privateKeyStr == null ? null : org.keycloak.testsuite.util.KeyUtils.privateKeyFromString(privateKeyStr);
|
||||||
|
try {
|
||||||
|
InetSocketAddress address = new InetSocketAddress(InetAddress.getByName("localhost"), port);
|
||||||
|
server = HttpServer.create(address, 0);
|
||||||
|
this.url = "http://" + address.getHostString() + ":" + port;
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException("Cannot create http server", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
server.createContext("/", new SamlBackchannelLogoutReceiver.SamlBackchannelLogoutHandler());
|
||||||
|
server.setExecutor(null);
|
||||||
|
server.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
public SamlBackchannelLogoutReceiver(int port, ClientRepresentation samlClient) {
|
||||||
|
this(port, samlClient, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getUrl() {
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isLogoutRequestReceived() {
|
||||||
|
return logoutRequest != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LogoutRequestType getLogoutRequest() {
|
||||||
|
return logoutRequest;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() throws Exception {
|
||||||
|
server.stop(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private class SamlBackchannelLogoutHandler implements HttpHandler {
|
||||||
|
public void handle(HttpExchange t) throws IOException {
|
||||||
|
try {
|
||||||
|
t.getResponseHeaders().add(HttpHeaders.CONTENT_TYPE, "text/xml");
|
||||||
|
t.sendResponseHeaders(200, 0);
|
||||||
|
|
||||||
|
Document request = Soap.extractSoapMessage(t.getRequestBody());
|
||||||
|
SAMLDocumentHolder samlDoc = SAML2Response.getSAML2ObjectFromDocument(request);
|
||||||
|
if (!(samlDoc.getSamlObject() instanceof LogoutRequestType)) {
|
||||||
|
throw new RuntimeException("SamlBackchannelLogoutReceiver received a message that was not LogoutRequestType");
|
||||||
|
}
|
||||||
|
logoutRequest = (LogoutRequestType) samlDoc.getSamlObject();
|
||||||
|
StatusResponseType logoutResponse = new SAML2LogoutResponseBuilder()
|
||||||
|
.issuer(samlClient.getClientId())
|
||||||
|
.logoutRequestID(logoutRequest.getID())
|
||||||
|
.buildModel();
|
||||||
|
JaxrsSAML2BindingBuilder soapBinding = new JaxrsSAML2BindingBuilder(null);
|
||||||
|
if (requiresClientSignature(samlClient)) {
|
||||||
|
soapBinding.signatureAlgorithm(getSignatureAlgorithm(samlClient))
|
||||||
|
.signWith(KeyUtils.createKeyId(privateKey), privateKey, publicKey, null)
|
||||||
|
.signDocument();
|
||||||
|
}
|
||||||
|
Document doc = soapBinding.soapBinding(SAML2Response.convert(logoutResponse)).getDocument();
|
||||||
|
|
||||||
|
// send logout response
|
||||||
|
OutputStream os = t.getResponseBody();
|
||||||
|
os.write(Soap.createMessage().addToBody(doc).getBytes());
|
||||||
|
os.close();
|
||||||
|
} catch (Exception ex) {
|
||||||
|
t.sendResponseHeaders(500, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private SignatureAlgorithm getSignatureAlgorithm(ClientRepresentation client) {
|
||||||
|
String alg = client.getAttributes().get(SamlConfigAttributes.SAML_SIGNATURE_ALGORITHM);
|
||||||
|
if (alg != null) {
|
||||||
|
SignatureAlgorithm algorithm = SignatureAlgorithm.valueOf(alg);
|
||||||
|
if (algorithm != null)
|
||||||
|
return algorithm;
|
||||||
|
}
|
||||||
|
return SignatureAlgorithm.RSA_SHA256;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean requiresClientSignature(ClientRepresentation client) {
|
||||||
|
return "true".equals(client.getAttributes().get(SamlConfigAttributes.SAML_CLIENT_SIGNATURE_ATTRIBUTE));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
@ -26,6 +26,7 @@ import org.keycloak.client.registration.ClientRegistrationException;
|
||||||
import org.keycloak.client.registration.HttpErrorException;
|
import org.keycloak.client.registration.HttpErrorException;
|
||||||
import org.keycloak.events.Errors;
|
import org.keycloak.events.Errors;
|
||||||
import org.keycloak.protocol.saml.SamlConfigAttributes;
|
import org.keycloak.protocol.saml.SamlConfigAttributes;
|
||||||
|
import org.keycloak.protocol.saml.SamlProtocol;
|
||||||
import org.keycloak.protocol.saml.mappers.AttributeStatementHelper;
|
import org.keycloak.protocol.saml.mappers.AttributeStatementHelper;
|
||||||
import org.keycloak.protocol.saml.util.ArtifactBindingUtils;
|
import org.keycloak.protocol.saml.util.ArtifactBindingUtils;
|
||||||
import org.keycloak.representations.idm.ClientInitialAccessCreatePresentation;
|
import org.keycloak.representations.idm.ClientInitialAccessCreatePresentation;
|
||||||
|
@ -107,7 +108,8 @@ public class SAMLClientRegistrationTest extends AbstractClientRegistrationTest {
|
||||||
"https://LoadBalancer-9.siroe.com:3443/federation/Consumer/metaAlias/sp/artifact"
|
"https://LoadBalancer-9.siroe.com:3443/federation/Consumer/metaAlias/sp/artifact"
|
||||||
));
|
));
|
||||||
|
|
||||||
assertThat(response.getAttributes().get("saml_single_logout_service_url_redirect"), is("https://LoadBalancer-9.siroe.com:3443/federation/SPSloRedirect/metaAlias/sp"));
|
assertThat(response.getAttributes().get(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_REDIRECT_ATTRIBUTE), is("https://LoadBalancer-9.siroe.com:3443/federation/SPSloRedirect/metaAlias/sp"));
|
||||||
|
assertThat(response.getAttributes().get(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_SOAP_ATTRIBUTE), is("https://LoadBalancer-9.siroe.com:3443/federation/SPSloSoap/metaAlias/sp"));
|
||||||
assertThat(response.getAttributes().get(SamlConfigAttributes.SAML_ARTIFACT_BINDING_IDENTIFIER), is(ArtifactBindingUtils.computeArtifactBindingIdentifierString("loadbalancer-9.siroe.com")));
|
assertThat(response.getAttributes().get(SamlConfigAttributes.SAML_ARTIFACT_BINDING_IDENTIFIER), is(ArtifactBindingUtils.computeArtifactBindingIdentifierString("loadbalancer-9.siroe.com")));
|
||||||
|
|
||||||
Assert.assertNotNull(response.getProtocolMappers());
|
Assert.assertNotNull(response.getProtocolMappers());
|
||||||
|
|
|
@ -20,8 +20,6 @@ import org.keycloak.admin.client.resource.RealmResource;
|
||||||
import org.keycloak.broker.saml.SAMLIdentityProviderConfig;
|
import org.keycloak.broker.saml.SAMLIdentityProviderConfig;
|
||||||
import org.keycloak.broker.saml.SAMLIdentityProviderFactory;
|
import org.keycloak.broker.saml.SAMLIdentityProviderFactory;
|
||||||
import org.keycloak.dom.saml.v2.SAML2Object;
|
import org.keycloak.dom.saml.v2.SAML2Object;
|
||||||
import org.keycloak.dom.saml.v2.assertion.AssertionType;
|
|
||||||
import org.keycloak.dom.saml.v2.assertion.AuthnStatementType;
|
|
||||||
import org.keycloak.dom.saml.v2.assertion.NameIDType;
|
import org.keycloak.dom.saml.v2.assertion.NameIDType;
|
||||||
import org.keycloak.dom.saml.v2.protocol.AuthnRequestType;
|
import org.keycloak.dom.saml.v2.protocol.AuthnRequestType;
|
||||||
import org.keycloak.dom.saml.v2.protocol.LogoutRequestType;
|
import org.keycloak.dom.saml.v2.protocol.LogoutRequestType;
|
||||||
|
@ -29,6 +27,7 @@ import org.keycloak.dom.saml.v2.protocol.ResponseType;
|
||||||
import org.keycloak.dom.saml.v2.protocol.StatusResponseType;
|
import org.keycloak.dom.saml.v2.protocol.StatusResponseType;
|
||||||
import org.keycloak.events.Details;
|
import org.keycloak.events.Details;
|
||||||
import org.keycloak.events.EventType;
|
import org.keycloak.events.EventType;
|
||||||
|
import org.keycloak.protocol.saml.SamlConfigAttributes;
|
||||||
import org.keycloak.protocol.saml.SamlProtocol;
|
import org.keycloak.protocol.saml.SamlProtocol;
|
||||||
import org.keycloak.representations.idm.ClientRepresentation;
|
import org.keycloak.representations.idm.ClientRepresentation;
|
||||||
import org.keycloak.representations.idm.EventRepresentation;
|
import org.keycloak.representations.idm.EventRepresentation;
|
||||||
|
@ -54,22 +53,25 @@ import java.io.IOException;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.concurrent.atomic.AtomicReference;
|
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
import javax.ws.rs.core.Response.Status;
|
import javax.ws.rs.core.Response.Status;
|
||||||
import javax.ws.rs.core.UriBuilderException;
|
import javax.ws.rs.core.UriBuilderException;
|
||||||
import javax.xml.transform.dom.DOMSource;
|
import javax.xml.transform.dom.DOMSource;
|
||||||
|
import javax.xml.ws.soap.SOAPFaultException;
|
||||||
|
|
||||||
import org.apache.http.HttpResponse;
|
import org.apache.http.HttpResponse;
|
||||||
import org.apache.http.client.methods.CloseableHttpResponse;
|
import org.apache.http.client.methods.CloseableHttpResponse;
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
import org.keycloak.testsuite.util.saml.SamlBackchannelLogoutReceiver;
|
||||||
|
|
||||||
import static org.hamcrest.CoreMatchers.instanceOf;
|
|
||||||
import static org.hamcrest.Matchers.is;
|
import static org.hamcrest.Matchers.is;
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
import static org.junit.Assert.assertFalse;
|
import static org.junit.Assert.assertFalse;
|
||||||
import static org.junit.Assert.assertNotNull;
|
import static org.junit.Assert.assertNotNull;
|
||||||
import static org.junit.Assert.assertThat;
|
import static org.junit.Assert.assertThat;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
import static org.junit.Assert.fail;
|
||||||
import static org.keycloak.testsuite.util.Matchers.*;
|
import static org.keycloak.testsuite.util.Matchers.*;
|
||||||
import static org.keycloak.testsuite.util.SamlClient.Binding.*;
|
import static org.keycloak.testsuite.util.SamlClient.Binding.*;
|
||||||
|
|
||||||
|
@ -89,11 +91,13 @@ public class LogoutTest extends AbstractSamlTest {
|
||||||
|
|
||||||
private ClientRepresentation salesRep;
|
private ClientRepresentation salesRep;
|
||||||
private ClientRepresentation sales2Rep;
|
private ClientRepresentation sales2Rep;
|
||||||
|
private ClientRepresentation salesSigRep;
|
||||||
|
|
||||||
@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);
|
||||||
sales2Rep = adminClient.realm(REALM_NAME).clients().findByClientId(SAML_CLIENT_ID_SALES_POST2).get(0);
|
sales2Rep = adminClient.realm(REALM_NAME).clients().findByClientId(SAML_CLIENT_ID_SALES_POST2).get(0);
|
||||||
|
salesSigRep = adminClient.realm(REALM_NAME).clients().findByClientId(SAML_CLIENT_ID_SALES_POST_SIG).get(0);
|
||||||
|
|
||||||
adminClient.realm(REALM_NAME)
|
adminClient.realm(REALM_NAME)
|
||||||
.clients().get(salesRep.getId())
|
.clients().get(salesRep.getId())
|
||||||
|
@ -148,6 +152,22 @@ public class LogoutTest extends AbstractSamlTest {
|
||||||
}).build();
|
}).build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private SamlClientBuilder prepareLogIntoTwoAppsSig() {
|
||||||
|
return new SamlClientBuilder()
|
||||||
|
.authnRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST, SAML_ASSERTION_CONSUMER_URL_SALES_POST, POST).build()
|
||||||
|
.login().user(bburkeUser).build()
|
||||||
|
.processSamlResponse(POST)
|
||||||
|
.transformObject(this::extractNameIdAndSessionIndexAndTerminate)
|
||||||
|
.build()
|
||||||
|
.authnRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST_SIG, SAML_ASSERTION_CONSUMER_URL_SALES_POST_SIG, POST)
|
||||||
|
.signWith(SAML_CLIENT_SALES_POST_SIG_PRIVATE_KEY, SAML_CLIENT_SALES_POST_SIG_PUBLIC_KEY).build()
|
||||||
|
.login().sso(true).build() // This is a formal step
|
||||||
|
.processSamlResponse(POST).transformObject(so -> {
|
||||||
|
assertThat(so, isSamlResponse(JBossSAMLURIConstants.STATUS_SUCCESS));
|
||||||
|
return null; // Do not follow the redirect to the app from the returned response
|
||||||
|
}).build();
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testLogoutDifferentBrowser() {
|
public void testLogoutDifferentBrowser() {
|
||||||
// This is in fact the same as admin logging out a session from admin console.
|
// This is in fact the same as admin logging out a session from admin console.
|
||||||
|
@ -195,6 +215,139 @@ public class LogoutTest extends AbstractSamlTest {
|
||||||
assertLogoutEvent(SAML_CLIENT_ID_SALES_POST);
|
assertLogoutEvent(SAML_CLIENT_ID_SALES_POST);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logout triggered with POST binding, with 2 clients to logout in the SLO process.
|
||||||
|
* One of the client is configured with backchannel logout + SOAP logout URL
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void testSoapBackchannelLogout() {
|
||||||
|
try (SamlBackchannelLogoutReceiver backchannelLogoutReceiver = new SamlBackchannelLogoutReceiver(8082, sales2Rep);
|
||||||
|
Closeable sales2 = ClientAttributeUpdater.forClient(adminClient, REALM_NAME, SAML_CLIENT_ID_SALES_POST2)
|
||||||
|
.setFrontchannelLogout(false)
|
||||||
|
.setAttribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_SOAP_ATTRIBUTE, backchannelLogoutReceiver.getUrl())
|
||||||
|
.setAttribute(SamlConfigAttributes.SAML_SERVER_SIGNATURE, "true") // sign logout requests
|
||||||
|
.setAttribute(SamlConfigAttributes.SAML_FORCE_NAME_ID_FORMAT_ATTRIBUTE, "true") // Force NameID to username
|
||||||
|
.setAttribute(SamlConfigAttributes.SAML_NAME_ID_FORMAT_ATTRIBUTE, "username") // Force NameID to username
|
||||||
|
.update();
|
||||||
|
) {
|
||||||
|
|
||||||
|
SAMLDocumentHolder samlResponse = prepareLogIntoTwoApps()
|
||||||
|
.logoutRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST, POST)
|
||||||
|
.nameId(nameIdRef::get)
|
||||||
|
.sessionIndex(sessionIndexRef::get)
|
||||||
|
.build()
|
||||||
|
.getSamlResponse(POST);
|
||||||
|
|
||||||
|
assertThat(samlResponse.getSamlObject(), isSamlStatusResponse(JBossSAMLURIConstants.STATUS_SUCCESS));
|
||||||
|
assertLogoutEvent(SAML_CLIENT_ID_SALES_POST);
|
||||||
|
|
||||||
|
// check that the logout request sent to the client is compliant and signed
|
||||||
|
assertTrue(backchannelLogoutReceiver.isLogoutRequestReceived());
|
||||||
|
LogoutRequestType logoutRequest = backchannelLogoutReceiver.getLogoutRequest();
|
||||||
|
assertNotNull(backchannelLogoutReceiver.getLogoutRequest().getSignature());
|
||||||
|
// check nameID
|
||||||
|
assertEquals(logoutRequest.getNameID().getValue(), bburkeUser.getUsername());
|
||||||
|
} catch (Exception ex) {
|
||||||
|
fail("unexpected error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logout triggered with POST binding, with 2 clients to logout in the SLO process.
|
||||||
|
* One of the client is configured with backchannel logout + SOAP logout URL
|
||||||
|
* This client is also configured with "client signature required" --> a signature is expected on the logout response
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void testSoapBackchannelLogoutSignedResponseFromClient() {
|
||||||
|
try (SamlBackchannelLogoutReceiver backchannelLogoutReceiver = new SamlBackchannelLogoutReceiver(8082, salesSigRep, SAML_CLIENT_SALES_POST_SIG_PUBLIC_KEY, SAML_CLIENT_SALES_POST_SIG_PRIVATE_KEY);
|
||||||
|
Closeable salesSig = ClientAttributeUpdater.forClient(adminClient, REALM_NAME, SAML_CLIENT_ID_SALES_POST_SIG)
|
||||||
|
.setFrontchannelLogout(false)
|
||||||
|
.setAttribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_SOAP_ATTRIBUTE, backchannelLogoutReceiver.getUrl())
|
||||||
|
.setAttribute(SamlConfigAttributes.SAML_SERVER_SIGNATURE, "true") // sign logout requests
|
||||||
|
.setAttribute(SamlConfigAttributes.SAML_FORCE_NAME_ID_FORMAT_ATTRIBUTE, "true") // Force NameID to username
|
||||||
|
.setAttribute(SamlConfigAttributes.SAML_NAME_ID_FORMAT_ATTRIBUTE, "username") // Force NameID to username
|
||||||
|
.update();
|
||||||
|
) {
|
||||||
|
|
||||||
|
SAMLDocumentHolder samlResponse = prepareLogIntoTwoAppsSig()
|
||||||
|
.logoutRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST, POST)
|
||||||
|
.nameId(nameIdRef::get)
|
||||||
|
.sessionIndex(sessionIndexRef::get)
|
||||||
|
.signWith(SAML_CLIENT_SALES_POST_SIG_PRIVATE_KEY, SAML_CLIENT_SALES_POST_SIG_PUBLIC_KEY)
|
||||||
|
.build()
|
||||||
|
.getSamlResponse(POST);
|
||||||
|
|
||||||
|
assertThat(samlResponse.getSamlObject(), isSamlStatusResponse(JBossSAMLURIConstants.STATUS_SUCCESS));
|
||||||
|
assertLogoutEvent(SAML_CLIENT_ID_SALES_POST);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
fail("unexpected error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Logout triggered with SOAP binding, request is properly signed */
|
||||||
|
@Test
|
||||||
|
public void testSoapBackchannelLogoutFromSamlClient() {
|
||||||
|
try (
|
||||||
|
Closeable sales = ClientAttributeUpdater.forClient(adminClient, REALM_NAME, SAML_CLIENT_ID_SALES_POST_SIG)
|
||||||
|
.setFrontchannelLogout(false)
|
||||||
|
.setAttribute(SamlConfigAttributes.SAML_FORCE_NAME_ID_FORMAT_ATTRIBUTE, "true") // Force NameID to username
|
||||||
|
.setAttribute(SamlConfigAttributes.SAML_NAME_ID_FORMAT_ATTRIBUTE, "username") // Force NameID to username
|
||||||
|
.update();
|
||||||
|
) {
|
||||||
|
|
||||||
|
SAMLDocumentHolder samlLogoutResponse = prepareLogIntoTwoAppsSig()
|
||||||
|
.clearCookies() // remove cookies, since SOAP calls do not embed cookie normally
|
||||||
|
.logoutRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST_SIG, SOAP)
|
||||||
|
.nameId(nameIdRef::get)
|
||||||
|
.sessionIndex(sessionIndexRef::get)
|
||||||
|
.signWith(SAML_CLIENT_SALES_POST_SIG_PRIVATE_KEY, SAML_CLIENT_SALES_POST_SIG_PUBLIC_KEY)
|
||||||
|
.build()
|
||||||
|
.getSamlResponse(SOAP);
|
||||||
|
|
||||||
|
assertThat(samlLogoutResponse.getSamlObject(), isSamlStatusResponse(JBossSAMLURIConstants.STATUS_SUCCESS));
|
||||||
|
assertSoapLogoutEvent(SAML_CLIENT_ID_SALES_POST_SIG);
|
||||||
|
|
||||||
|
} catch (Exception ex) {
|
||||||
|
ex.printStackTrace();
|
||||||
|
fail("unexpected error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Logout triggered with SOAP binding, request is wrongly not signed --> ensure an error is thrown */
|
||||||
|
@Test
|
||||||
|
public void testSoapBackchannelLogoutFromSamlClientUnsignedRequest() {
|
||||||
|
try (
|
||||||
|
Closeable sales = ClientAttributeUpdater.forClient(adminClient, REALM_NAME, SAML_CLIENT_ID_SALES_POST_SIG)
|
||||||
|
.setFrontchannelLogout(false)
|
||||||
|
.setAttribute(SamlConfigAttributes.SAML_FORCE_NAME_ID_FORMAT_ATTRIBUTE, "true") // Force NameID to username
|
||||||
|
.setAttribute(SamlConfigAttributes.SAML_NAME_ID_FORMAT_ATTRIBUTE, "username") // Force NameID to username
|
||||||
|
.update();
|
||||||
|
) {
|
||||||
|
|
||||||
|
try {
|
||||||
|
SAMLDocumentHolder samlLogoutResponse = prepareLogIntoTwoAppsSig()
|
||||||
|
.clearCookies() // remove cookies, since SOAP calls do not embed cookie normally
|
||||||
|
.logoutRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST_SIG, SOAP, true)
|
||||||
|
.nameId(nameIdRef::get)
|
||||||
|
.sessionIndex(sessionIndexRef::get)
|
||||||
|
.build()
|
||||||
|
.getSamlResponse(SOAP);
|
||||||
|
fail("should have triggered an error");
|
||||||
|
} catch (RuntimeException ex) {
|
||||||
|
// exception expected since the request is not signed
|
||||||
|
if (ex.getCause() instanceof SOAPFaultException) {
|
||||||
|
SOAPFaultException sfe = (SOAPFaultException) ex.getCause();
|
||||||
|
assertThat(sfe.getFault().getFaultString(), is("invalidRequesterMessage"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assertSoapLogoutErrorEvent(SAML_CLIENT_ID_SALES_POST_SIG);
|
||||||
|
|
||||||
|
} catch (Exception ex) {
|
||||||
|
ex.printStackTrace();
|
||||||
|
fail("unexpected error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testFrontchannelLogoutNoLogoutServiceUrlSetInSameBrowser() {
|
public void testFrontchannelLogoutNoLogoutServiceUrlSetInSameBrowser() {
|
||||||
adminClient.realm(REALM_NAME)
|
adminClient.realm(REALM_NAME)
|
||||||
|
@ -327,6 +480,28 @@ public class LogoutTest extends AbstractSamlTest {
|
||||||
assertNotNull(logoutEvent.getDetails().get(SamlProtocol.SAML_LOGOUT_REQUEST_ID));
|
assertNotNull(logoutEvent.getDetails().get(SamlProtocol.SAML_LOGOUT_REQUEST_ID));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void assertSoapLogoutEvent(String clientId) {
|
||||||
|
List<EventRepresentation> logoutEvents = adminClient.realm(REALM_NAME)
|
||||||
|
.getEvents(Arrays.asList(EventType.LOGOUT.name()), clientId, null, null, null, null, null, null);
|
||||||
|
|
||||||
|
assertFalse(logoutEvents.isEmpty());
|
||||||
|
assertEquals(1, logoutEvents.size());
|
||||||
|
|
||||||
|
EventRepresentation logoutEvent = logoutEvents.get(0);
|
||||||
|
|
||||||
|
assertEquals(bburkeUser.getUsername(), logoutEvent.getDetails().get(Details.USERNAME));
|
||||||
|
assertEquals(SamlProtocol.SAML_SOAP_BINDING, logoutEvent.getDetails().get(Details.RESPONSE_MODE));
|
||||||
|
assertEquals("saml", logoutEvent.getDetails().get(Details.AUTH_METHOD));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertSoapLogoutErrorEvent(String clientId) {
|
||||||
|
List<EventRepresentation> logoutEvents = adminClient.realm(REALM_NAME)
|
||||||
|
.getEvents(Arrays.asList(EventType.LOGOUT_ERROR.name()), null, null, null, null, null, null, null);
|
||||||
|
|
||||||
|
assertFalse(logoutEvents.isEmpty());
|
||||||
|
assertEquals(1, logoutEvents.size());
|
||||||
|
}
|
||||||
|
|
||||||
private IdentityProviderRepresentation addIdentityProvider() {
|
private IdentityProviderRepresentation addIdentityProvider() {
|
||||||
IdentityProviderRepresentation identityProvider = IdentityProviderBuilder.create()
|
IdentityProviderRepresentation identityProvider = IdentityProviderBuilder.create()
|
||||||
.providerId(SAMLIdentityProviderFactory.PROVIDER_ID)
|
.providerId(SAMLIdentityProviderFactory.PROVIDER_ID)
|
||||||
|
|
|
@ -137,12 +137,13 @@ public class SOAPBindingTest extends AbstractSamlTest {
|
||||||
.processSamlResponse(POST)
|
.processSamlResponse(POST)
|
||||||
.transformObject(this::extractNameIdAndSessionIndexAndTerminate)
|
.transformObject(this::extractNameIdAndSessionIndexAndTerminate)
|
||||||
.build()
|
.build()
|
||||||
|
.clearCookies()
|
||||||
.logoutRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_ECP_SP, SOAP)
|
.logoutRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_ECP_SP, SOAP)
|
||||||
.nameId(nameIdRef::get)
|
.nameId(nameIdRef::get)
|
||||||
.sessionIndex(sessionIndexRef::get)
|
.sessionIndex(sessionIndexRef::get)
|
||||||
.signWith(SAML_CLIENT_SALES_POST_SIG_PRIVATE_KEY, SAML_CLIENT_SALES_POST_SIG_PUBLIC_KEY)
|
.signWith(SAML_CLIENT_SALES_POST_SIG_PRIVATE_KEY, SAML_CLIENT_SALES_POST_SIG_PUBLIC_KEY)
|
||||||
.build()
|
.build()
|
||||||
.executeAndTransform(POST::extractResponse);
|
.executeAndTransform(SOAP::extractResponse);
|
||||||
|
|
||||||
|
|
||||||
assertThat(response.getSamlObject(), instanceOf(StatusResponseType.class));
|
assertThat(response.getSamlObject(), instanceOf(StatusResponseType.class));
|
||||||
|
@ -164,11 +165,12 @@ public class SOAPBindingTest extends AbstractSamlTest {
|
||||||
.processSamlResponse(POST)
|
.processSamlResponse(POST)
|
||||||
.transformObject(this::extractNameIdAndSessionIndexAndTerminate)
|
.transformObject(this::extractNameIdAndSessionIndexAndTerminate)
|
||||||
.build()
|
.build()
|
||||||
|
.clearCookies()
|
||||||
.logoutRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_ECP_SP, SOAP)
|
.logoutRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_ECP_SP, SOAP)
|
||||||
.nameId(nameIdRef::get)
|
.nameId(nameIdRef::get)
|
||||||
.sessionIndex(sessionIndexRef::get)
|
.sessionIndex(sessionIndexRef::get)
|
||||||
.build()
|
.build()
|
||||||
.executeAndTransform(POST::extractResponse);
|
.executeAndTransform(SOAP::extractResponse);
|
||||||
|
|
||||||
|
|
||||||
assertThat(response.getSamlObject(), instanceOf(StatusResponseType.class));
|
assertThat(response.getSamlObject(), instanceOf(StatusResponseType.class));
|
||||||
|
@ -184,6 +186,7 @@ public class SOAPBindingTest extends AbstractSamlTest {
|
||||||
.processSamlResponse(POST)
|
.processSamlResponse(POST)
|
||||||
.transformObject(this::extractNameIdAndSessionIndexAndTerminate)
|
.transformObject(this::extractNameIdAndSessionIndexAndTerminate)
|
||||||
.build()
|
.build()
|
||||||
|
.clearCookies()
|
||||||
.logoutRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_ECP_SP, SOAP)
|
.logoutRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_ECP_SP, SOAP)
|
||||||
.nameId(nameIdRef::get)
|
.nameId(nameIdRef::get)
|
||||||
.sessionIndex(sessionIndexRef::get)
|
.sessionIndex(sessionIndexRef::get)
|
||||||
|
@ -193,7 +196,7 @@ public class SOAPBindingTest extends AbstractSamlTest {
|
||||||
return logoutRequestType;
|
return logoutRequestType;
|
||||||
})
|
})
|
||||||
.build()
|
.build()
|
||||||
.executeAndTransform(POST::extractResponse);
|
.executeAndTransform(SOAP::extractResponse);
|
||||||
|
|
||||||
|
|
||||||
assertThat(response.getSamlObject(), instanceOf(StatusResponseType.class));
|
assertThat(response.getSamlObject(), instanceOf(StatusResponseType.class));
|
||||||
|
@ -215,6 +218,7 @@ public class SOAPBindingTest extends AbstractSamlTest {
|
||||||
.processSamlResponse(POST)
|
.processSamlResponse(POST)
|
||||||
.transformObject(this::extractNameIdAndSessionIndexAndTerminate)
|
.transformObject(this::extractNameIdAndSessionIndexAndTerminate)
|
||||||
.build()
|
.build()
|
||||||
|
.clearCookies()
|
||||||
.logoutRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_ECP_SP, SOAP)
|
.logoutRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_ECP_SP, SOAP)
|
||||||
.nameId(nameIdRef::get)
|
.nameId(nameIdRef::get)
|
||||||
.sessionIndex(sessionIndexRef::get)
|
.sessionIndex(sessionIndexRef::get)
|
||||||
|
@ -223,7 +227,7 @@ public class SOAPBindingTest extends AbstractSamlTest {
|
||||||
return logoutRequestType;
|
return logoutRequestType;
|
||||||
})
|
})
|
||||||
.build()
|
.build()
|
||||||
.executeAndTransform(POST::extractResponse);
|
.executeAndTransform(SOAP::extractResponse);
|
||||||
|
|
||||||
|
|
||||||
assertThat(response.getSamlObject(), instanceOf(StatusResponseType.class));
|
assertThat(response.getSamlObject(), instanceOf(StatusResponseType.class));
|
||||||
|
|
Loading…
Reference in a new issue