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 {
|
||||
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) {
|
||||
Node assertionElement = samlResponseDocument.getDocumentElement()
|
||||
|
|
|
@ -66,6 +66,7 @@ import javax.xml.datatype.XMLGregorianCalendar;
|
|||
import javax.xml.namespace.QName;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
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
|
||||
*
|
||||
|
@ -423,7 +439,7 @@ public class SAML2Response {
|
|||
* @throws ConfigurationException
|
||||
* @throws ProcessingException
|
||||
*/
|
||||
public Document convert(StatusResponseType responseType) throws ProcessingException, ConfigurationException,
|
||||
public static Document convert(StatusResponseType responseType) throws ProcessingException, ConfigurationException,
|
||||
ParsingException {
|
||||
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);
|
||||
String logoutRedirect = getLogoutLocation(spDescriptorType, JBossSAMLURIConstants.SAML_HTTP_REDIRECT_BINDING.get());
|
||||
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());
|
||||
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_REDIRECT_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_REDIRECT_BINDING.getUri(), loginRedirectEndpoint));
|
||||
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.models.KeycloakSession;
|
||||
import org.keycloak.protocol.saml.profile.util.Soap;
|
||||
import org.keycloak.saml.BaseSAML2BindingBuilder;
|
||||
import org.keycloak.saml.common.constants.GeneralConstants;
|
||||
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
|
||||
public RedirectBindingBuilder redirectBinding(Document document) throws ProcessingException {
|
||||
return new RedirectBindingBuilder(this, document);
|
||||
|
@ -105,7 +122,8 @@ public class JaxrsSAML2BindingBuilder extends BaseSAML2BindingBuilder<JaxrsSAML2
|
|||
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.jboss.logging.Logger;
|
||||
import org.keycloak.broker.saml.SAMLDataMarshaller;
|
||||
import org.keycloak.common.VerificationException;
|
||||
import org.keycloak.common.util.KeycloakUriBuilder;
|
||||
import org.keycloak.connections.httpclient.HttpClientProvider;
|
||||
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.SAMLRoleListMapper;
|
||||
import org.keycloak.protocol.saml.preprocessor.SamlAuthenticationPreprocessor;
|
||||
import org.keycloak.protocol.saml.profile.util.Soap;
|
||||
import org.keycloak.saml.SAML2ErrorResponseBuilder;
|
||||
import org.keycloak.saml.SAML2LoginResponseBuilder;
|
||||
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.util.XmlKeyInfoKeyNameTransformer;
|
||||
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.services.ErrorPage;
|
||||
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.UriBuilder;
|
||||
import javax.ws.rs.core.UriInfo;
|
||||
import javax.xml.soap.SOAPException;
|
||||
import javax.xml.soap.SOAPMessage;
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
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_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_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 LOGIN_PROTOCOL = "saml";
|
||||
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) {
|
||||
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
|
||||
if (!backChannelLogout && useArtifactForLogout(client)) {
|
||||
logoutServiceUrl = client.getAttribute(SAML_SINGLE_LOGOUT_SERVICE_URL_ARTIFACT_ATTRIBUTE);
|
||||
} else if (SAML_POST_BINDING.equals(bindingType)) {
|
||||
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) {
|
||||
ClientModel client = clientSession.getClient();
|
||||
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);
|
||||
if (logoutUrl == null) {
|
||||
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();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
// build userPrincipal with subject used at login
|
||||
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.
|
||||
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) {
|
||||
logger.tracef("basicChecks(%s, %s, %s)%s", samlRequest, samlResponse, artifact, getShortStackTrace());
|
||||
if (!checkSsl()) {
|
||||
event.event(EventType.LOGIN);
|
||||
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()) {
|
||||
event.event(EventType.LOGIN_ERROR);
|
||||
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) {
|
||||
event.event(EventType.LOGIN);
|
||||
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;
|
||||
|
@ -205,7 +207,7 @@ public class SamlService extends AuthorizationEndpointBase {
|
|||
if (! (holder.getSamlObject() instanceof StatusResponseType)) {
|
||||
event.detail(Details.REASON, 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();
|
||||
|
@ -214,12 +216,12 @@ public class SamlService extends AuthorizationEndpointBase {
|
|||
statusResponse.getDestination() == null && containsUnencryptedSignature(holder)) {
|
||||
event.detail(Details.REASON, Errors.MISSING_REQUIRED_DESTINATION);
|
||||
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())) {
|
||||
event.detail(Details.REASON, Errors.INVALID_DESTINATION);
|
||||
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);
|
||||
|
@ -227,7 +229,7 @@ public class SamlService extends AuthorizationEndpointBase {
|
|||
logger.warn("Unknown saml response.");
|
||||
event.event(EventType.LOGOUT);
|
||||
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
|
||||
UserSessionModel userSession = authResult.getSession();
|
||||
|
@ -236,7 +238,7 @@ public class SamlService extends AuthorizationEndpointBase {
|
|||
logger.warn("UserSession is not tagged as logging out.");
|
||||
event.event(EventType.LOGOUT);
|
||||
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();
|
||||
ClientModel client = realm.getClientByClientId(issuer);
|
||||
|
@ -244,13 +246,13 @@ public class SamlService extends AuthorizationEndpointBase {
|
|||
event.event(EventType.LOGOUT);
|
||||
event.client(issuer);
|
||||
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)) {
|
||||
event.event(EventType.LOGOUT);
|
||||
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);
|
||||
|
@ -265,7 +267,7 @@ public class SamlService extends AuthorizationEndpointBase {
|
|||
if (documentHolder == null) {
|
||||
event.event(EventType.LOGIN);
|
||||
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.error(Errors.INVALID_TOKEN);
|
||||
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;
|
||||
|
@ -304,7 +306,7 @@ public class SamlService extends AuthorizationEndpointBase {
|
|||
} catch (VerificationException e) {
|
||||
SamlService.logger.error("request validation failed", e);
|
||||
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");
|
||||
|
||||
|
@ -312,7 +314,7 @@ public class SamlService extends AuthorizationEndpointBase {
|
|||
requestAbstractType.getDestination() == null && containsUnencryptedSignature(documentHolder)) {
|
||||
event.detail(Details.REASON, Errors.MISSING_REQUIRED_DESTINATION);
|
||||
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) {
|
||||
|
@ -353,7 +355,7 @@ public class SamlService extends AuthorizationEndpointBase {
|
|||
event.event(EventType.LOGIN);
|
||||
event.detail(Details.REASON, e.getMessage());
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -393,7 +395,7 @@ public class SamlService extends AuthorizationEndpointBase {
|
|||
event.event(EventType.LOGIN);
|
||||
event.detail(Details.REASON, e.getMessage());
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
@ -412,7 +414,7 @@ public class SamlService extends AuthorizationEndpointBase {
|
|||
SamlClient samlClient = new SamlClient(client);
|
||||
|
||||
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);
|
||||
|
@ -441,7 +443,7 @@ public class SamlService extends AuthorizationEndpointBase {
|
|||
|
||||
if (redirect == null) {
|
||||
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);
|
||||
|
@ -472,7 +474,7 @@ public class SamlService extends AuthorizationEndpointBase {
|
|||
} else {
|
||||
event.detail(Details.REASON, Errors.UNSUPPORTED_NAMEID_FORMAT);
|
||||
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) {
|
||||
SamlClient samlClient = new SamlClient(client);
|
||||
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.
|
||||
|
@ -598,9 +600,18 @@ public class SamlService extends AuthorizationEndpointBase {
|
|||
}
|
||||
|
||||
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);
|
||||
event.success();
|
||||
} catch (Exception e) {
|
||||
logger.warn("Failure with backchannel logout", e);
|
||||
event.error("Failure with backchannel logout");
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -628,6 +639,8 @@ public class SamlService extends AuthorizationEndpointBase {
|
|||
try {
|
||||
if (postBinding) {
|
||||
return binding.postBinding(builder.buildDocument()).response(logoutBindingUri);
|
||||
} else if (SamlProtocol.SAML_SOAP_BINDING.equals(logoutBinding)) {
|
||||
return binding.soapBinding(builder.buildDocument()).response();
|
||||
} else {
|
||||
return binding.redirectBinding(builder.buildDocument()).response(logoutBindingUri);
|
||||
}
|
||||
|
@ -704,10 +717,46 @@ public class SamlService extends AuthorizationEndpointBase {
|
|||
final URI baseUri = session.getContext().getUri().getBaseUri();
|
||||
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 {
|
||||
|
||||
@Override
|
||||
protected Response error(KeycloakSession session, AuthenticationSessionModel authenticationSession, Response.Status status, String message, Object... parameters) {
|
||||
return ErrorPage.error(session, authenticationSession, status, message, parameters);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String encodeSamlDocument(Document samlDocument) throws ProcessingException {
|
||||
try {
|
||||
|
@ -748,6 +797,11 @@ public class SamlService extends AuthorizationEndpointBase {
|
|||
|
||||
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
|
||||
protected String encodeSamlDocument(Document samlDocument) throws ProcessingException {
|
||||
try {
|
||||
|
@ -883,37 +937,6 @@ public class SamlService extends AuthorizationEndpointBase {
|
|||
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
|
||||
@Path("clients/{client}")
|
||||
@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.ProcessingException;
|
||||
import org.keycloak.saml.validators.DestinationValidator;
|
||||
import org.keycloak.services.ErrorPage;
|
||||
import org.keycloak.services.managers.AuthenticationManager;
|
||||
import org.keycloak.sessions.AuthenticationSessionModel;
|
||||
import org.w3c.dom.Document;
|
||||
|
@ -68,11 +69,22 @@ public class SamlEcpProfileService extends SamlService {
|
|||
public Response authenticate(Document soapMessage) {
|
||||
try {
|
||||
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
|
||||
protected String getBindingType(AuthnRequestType requestAbstractType) {
|
||||
return SamlProtocol.SAML_SOAP_BINDING;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getBindingType() {
|
||||
return SamlProtocol.SAML_SOAP_BINDING;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean isDestinationRequired() {
|
||||
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.web.util.PostBindingUtil;
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Element;
|
||||
import org.w3c.dom.Node;
|
||||
|
||||
import javax.ws.rs.core.MediaType;
|
||||
|
@ -101,7 +102,7 @@ public final class Soap {
|
|||
public static Document extractSoapMessage(SOAPMessage soapMessage) {
|
||||
try {
|
||||
SOAPBody soapBody = soapMessage.getSOAPBody();
|
||||
Node authnRequestNode = soapBody.getFirstChild();
|
||||
Node authnRequestNode = getFirstChild(soapBody);
|
||||
Document document = DocumentUtil.createDocument();
|
||||
document.appendChild(document.importNode(authnRequestNode, true));
|
||||
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 {
|
||||
private final SOAPMessage message;
|
||||
private final SOAPBody body;
|
||||
|
|
|
@ -75,6 +75,7 @@ import javax.xml.soap.SOAPException;
|
|||
import javax.xml.soap.SOAPHeader;
|
||||
import javax.xml.soap.SOAPHeaderElement;
|
||||
import javax.xml.soap.SOAPMessage;
|
||||
import javax.xml.ws.soap.SOAPFaultException;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
|
@ -348,11 +349,10 @@ public class SamlClient {
|
|||
@Override
|
||||
public SAMLDocumentHolder extractResponse(CloseableHttpResponse response, String realmPublicKey) throws IOException {
|
||||
|
||||
assertThat(response, statusCodeIsHC(200));
|
||||
|
||||
MessageFactory messageFactory = null;
|
||||
try {
|
||||
messageFactory = MessageFactory.newInstance();
|
||||
int statusCode = response.getStatusLine().getStatusCode();
|
||||
if (statusCode == 200) {
|
||||
MessageFactory messageFactory = MessageFactory.newInstance();
|
||||
SOAPMessage soapMessage = messageFactory.createMessage(null, response.getEntity().getContent());
|
||||
SOAPBody soapBody = soapMessage.getSOAPBody();
|
||||
Node authnRequestNode = soapBody.getFirstChild();
|
||||
|
@ -365,6 +365,15 @@ public class SamlClient {
|
|||
SAML2Object responseType = (SAML2Object) samlParser.parse(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) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
|
|
@ -171,9 +171,14 @@ public class SamlClientBuilder {
|
|||
return addStepBuilder(new CreateAuthnRequestStepBuilder(authServerSamlUrl, authnRequestDocument, requestBinding, this));
|
||||
}
|
||||
|
||||
/** Issues the given AuthnRequest to the SAML endpoint */
|
||||
public CreateLogoutRequestStepBuilder logoutRequest(URI authServerSamlUrl, String issuer, Binding requestBinding) {
|
||||
return addStepBuilder(new CreateLogoutRequestStepBuilder(authServerSamlUrl, issuer, requestBinding, this));
|
||||
/** Issues the given LogoutRequest to the SAML endpoint */
|
||||
public CreateLogoutRequestStepBuilder logoutRequest(URI logoutServerSamlUrl, String issuer, Binding requestBinding) {
|
||||
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 */
|
||||
|
|
|
@ -35,7 +35,7 @@ import org.apache.http.impl.client.CloseableHttpClient;
|
|||
*/
|
||||
public class CreateLogoutRequestStepBuilder extends SamlDocumentStepBuilder<LogoutRequestType, CreateLogoutRequestStepBuilder> {
|
||||
|
||||
private final URI authServerSamlUrl;
|
||||
private final URI logoutServerSamlUrl;
|
||||
private final String issuer;
|
||||
private final Binding requestBinding;
|
||||
|
||||
|
@ -46,13 +46,23 @@ public class CreateLogoutRequestStepBuilder extends SamlDocumentStepBuilder<Logo
|
|||
private String signingPrivateKeyPem;
|
||||
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);
|
||||
this.authServerSamlUrl = authServerSamlUrl;
|
||||
this.logoutServerSamlUrl = logoutServerSamlUrl;
|
||||
this.issuer = issuer;
|
||||
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() {
|
||||
return sessionIndex.get();
|
||||
}
|
||||
|
@ -109,7 +119,7 @@ public class CreateLogoutRequestStepBuilder extends SamlDocumentStepBuilder<Logo
|
|||
@Override
|
||||
public HttpUriRequest perform(CloseableHttpClient client, URI currentURI, CloseableHttpResponse currentResponse, HttpClientContext context) throws Exception {
|
||||
SAML2LogoutRequestBuilder builder = new SAML2LogoutRequestBuilder()
|
||||
.destination(authServerSamlUrl == null ? null : authServerSamlUrl.toString())
|
||||
.destination(logoutServerSamlUrl == null ? null : logoutServerSamlUrl.toString())
|
||||
.issuer(issuer)
|
||||
.sessionIndex(sessionIndex())
|
||||
.nameId(nameId());
|
||||
|
@ -121,9 +131,9 @@ public class CreateLogoutRequestStepBuilder extends SamlDocumentStepBuilder<Logo
|
|||
return null;
|
||||
}
|
||||
|
||||
return this.signingPrivateKeyPem == null
|
||||
? requestBinding.createSamlUnsignedRequest(authServerSamlUrl, relayState(), DocumentUtil.getDocument(transformed))
|
||||
: requestBinding.createSamlSignedRequest(authServerSamlUrl, relayState(), DocumentUtil.getDocument(transformed), signingPrivateKeyPem, signingPublicKeyPem, signingCertificate);
|
||||
return this.signingPrivateKeyPem == null || skipSignature
|
||||
? requestBinding.createSamlUnsignedRequest(logoutServerSamlUrl, relayState(), DocumentUtil.getDocument(transformed))
|
||||
: 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.events.Errors;
|
||||
import org.keycloak.protocol.saml.SamlConfigAttributes;
|
||||
import org.keycloak.protocol.saml.SamlProtocol;
|
||||
import org.keycloak.protocol.saml.mappers.AttributeStatementHelper;
|
||||
import org.keycloak.protocol.saml.util.ArtifactBindingUtils;
|
||||
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"
|
||||
));
|
||||
|
||||
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")));
|
||||
|
||||
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.SAMLIdentityProviderFactory;
|
||||
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.protocol.AuthnRequestType;
|
||||
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.events.Details;
|
||||
import org.keycloak.events.EventType;
|
||||
import org.keycloak.protocol.saml.SamlConfigAttributes;
|
||||
import org.keycloak.protocol.saml.SamlProtocol;
|
||||
import org.keycloak.representations.idm.ClientRepresentation;
|
||||
import org.keycloak.representations.idm.EventRepresentation;
|
||||
|
@ -54,22 +53,25 @@ import java.io.IOException;
|
|||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.function.Consumer;
|
||||
import javax.ws.rs.core.Response.Status;
|
||||
import javax.ws.rs.core.UriBuilderException;
|
||||
import javax.xml.transform.dom.DOMSource;
|
||||
import javax.xml.ws.soap.SOAPFaultException;
|
||||
|
||||
import org.apache.http.HttpResponse;
|
||||
import org.apache.http.client.methods.CloseableHttpResponse;
|
||||
import org.junit.Before;
|
||||
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.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
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.SamlClient.Binding.*;
|
||||
|
||||
|
@ -89,11 +91,13 @@ public class LogoutTest extends AbstractSamlTest {
|
|||
|
||||
private ClientRepresentation salesRep;
|
||||
private ClientRepresentation sales2Rep;
|
||||
private ClientRepresentation salesSigRep;
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
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);
|
||||
salesSigRep = adminClient.realm(REALM_NAME).clients().findByClientId(SAML_CLIENT_ID_SALES_POST_SIG).get(0);
|
||||
|
||||
adminClient.realm(REALM_NAME)
|
||||
.clients().get(salesRep.getId())
|
||||
|
@ -148,6 +152,22 @@ public class LogoutTest extends AbstractSamlTest {
|
|||
}).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
|
||||
public void testLogoutDifferentBrowser() {
|
||||
// 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
public void testFrontchannelLogoutNoLogoutServiceUrlSetInSameBrowser() {
|
||||
adminClient.realm(REALM_NAME)
|
||||
|
@ -327,6 +480,28 @@ public class LogoutTest extends AbstractSamlTest {
|
|||
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() {
|
||||
IdentityProviderRepresentation identityProvider = IdentityProviderBuilder.create()
|
||||
.providerId(SAMLIdentityProviderFactory.PROVIDER_ID)
|
||||
|
|
|
@ -137,12 +137,13 @@ public class SOAPBindingTest extends AbstractSamlTest {
|
|||
.processSamlResponse(POST)
|
||||
.transformObject(this::extractNameIdAndSessionIndexAndTerminate)
|
||||
.build()
|
||||
.clearCookies()
|
||||
.logoutRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_ECP_SP, SOAP)
|
||||
.nameId(nameIdRef::get)
|
||||
.sessionIndex(sessionIndexRef::get)
|
||||
.signWith(SAML_CLIENT_SALES_POST_SIG_PRIVATE_KEY, SAML_CLIENT_SALES_POST_SIG_PUBLIC_KEY)
|
||||
.build()
|
||||
.executeAndTransform(POST::extractResponse);
|
||||
.executeAndTransform(SOAP::extractResponse);
|
||||
|
||||
|
||||
assertThat(response.getSamlObject(), instanceOf(StatusResponseType.class));
|
||||
|
@ -164,11 +165,12 @@ public class SOAPBindingTest extends AbstractSamlTest {
|
|||
.processSamlResponse(POST)
|
||||
.transformObject(this::extractNameIdAndSessionIndexAndTerminate)
|
||||
.build()
|
||||
.clearCookies()
|
||||
.logoutRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_ECP_SP, SOAP)
|
||||
.nameId(nameIdRef::get)
|
||||
.sessionIndex(sessionIndexRef::get)
|
||||
.build()
|
||||
.executeAndTransform(POST::extractResponse);
|
||||
.executeAndTransform(SOAP::extractResponse);
|
||||
|
||||
|
||||
assertThat(response.getSamlObject(), instanceOf(StatusResponseType.class));
|
||||
|
@ -184,6 +186,7 @@ public class SOAPBindingTest extends AbstractSamlTest {
|
|||
.processSamlResponse(POST)
|
||||
.transformObject(this::extractNameIdAndSessionIndexAndTerminate)
|
||||
.build()
|
||||
.clearCookies()
|
||||
.logoutRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_ECP_SP, SOAP)
|
||||
.nameId(nameIdRef::get)
|
||||
.sessionIndex(sessionIndexRef::get)
|
||||
|
@ -193,7 +196,7 @@ public class SOAPBindingTest extends AbstractSamlTest {
|
|||
return logoutRequestType;
|
||||
})
|
||||
.build()
|
||||
.executeAndTransform(POST::extractResponse);
|
||||
.executeAndTransform(SOAP::extractResponse);
|
||||
|
||||
|
||||
assertThat(response.getSamlObject(), instanceOf(StatusResponseType.class));
|
||||
|
@ -215,6 +218,7 @@ public class SOAPBindingTest extends AbstractSamlTest {
|
|||
.processSamlResponse(POST)
|
||||
.transformObject(this::extractNameIdAndSessionIndexAndTerminate)
|
||||
.build()
|
||||
.clearCookies()
|
||||
.logoutRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_ECP_SP, SOAP)
|
||||
.nameId(nameIdRef::get)
|
||||
.sessionIndex(sessionIndexRef::get)
|
||||
|
@ -223,7 +227,7 @@ public class SOAPBindingTest extends AbstractSamlTest {
|
|||
return logoutRequestType;
|
||||
})
|
||||
.build()
|
||||
.executeAndTransform(POST::extractResponse);
|
||||
.executeAndTransform(SOAP::extractResponse);
|
||||
|
||||
|
||||
assertThat(response.getSamlObject(), instanceOf(StatusResponseType.class));
|
||||
|
|
Loading…
Reference in a new issue