Merge pull request #1951 from pedroigor/KEYCLOAK-2202
[KEYCLOAK-2202] - Initial support for SAML ECP Profile.
This commit is contained in:
commit
0197c69ac3
26 changed files with 1799 additions and 543 deletions
|
@ -26,6 +26,7 @@
|
||||||
<module name="org.keycloak.keycloak-connections-http-client" services="import"/>
|
<module name="org.keycloak.keycloak-connections-http-client" services="import"/>
|
||||||
|
|
||||||
<module name="javax.api"/>
|
<module name="javax.api"/>
|
||||||
|
<module name="javax.xml.soap.api"/>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
</module>
|
</module>
|
||||||
|
|
|
@ -24,6 +24,7 @@ public class DefaultAuthenticationFlows {
|
||||||
public static final String DIRECT_GRANT_FLOW = "direct grant";
|
public static final String DIRECT_GRANT_FLOW = "direct grant";
|
||||||
public static final String RESET_CREDENTIALS_FLOW = "reset credentials";
|
public static final String RESET_CREDENTIALS_FLOW = "reset credentials";
|
||||||
public static final String LOGIN_FORMS_FLOW = "forms";
|
public static final String LOGIN_FORMS_FLOW = "forms";
|
||||||
|
public static final String SAML_ECP_FLOW = "saml ecp";
|
||||||
|
|
||||||
public static final String CLIENT_AUTHENTICATION_FLOW = "clients";
|
public static final String CLIENT_AUTHENTICATION_FLOW = "clients";
|
||||||
public static final String FIRST_BROKER_LOGIN_FLOW = "first broker login";
|
public static final String FIRST_BROKER_LOGIN_FLOW = "first broker login";
|
||||||
|
@ -39,6 +40,7 @@ public class DefaultAuthenticationFlows {
|
||||||
if (realm.getFlowByAlias(RESET_CREDENTIALS_FLOW) == null) resetCredentialsFlow(realm);
|
if (realm.getFlowByAlias(RESET_CREDENTIALS_FLOW) == null) resetCredentialsFlow(realm);
|
||||||
if (realm.getFlowByAlias(CLIENT_AUTHENTICATION_FLOW) == null) clientAuthFlow(realm);
|
if (realm.getFlowByAlias(CLIENT_AUTHENTICATION_FLOW) == null) clientAuthFlow(realm);
|
||||||
if (realm.getFlowByAlias(FIRST_BROKER_LOGIN_FLOW) == null) firstBrokerLoginFlow(realm, false);
|
if (realm.getFlowByAlias(FIRST_BROKER_LOGIN_FLOW) == null) firstBrokerLoginFlow(realm, false);
|
||||||
|
if (realm.getFlowByAlias(SAML_ECP_FLOW) == null) samlEcpProfile(realm);
|
||||||
}
|
}
|
||||||
public static void migrateFlows(RealmModel realm) {
|
public static void migrateFlows(RealmModel realm) {
|
||||||
if (realm.getFlowByAlias(BROWSER_FLOW) == null) browserFlow(realm, true);
|
if (realm.getFlowByAlias(BROWSER_FLOW) == null) browserFlow(realm, true);
|
||||||
|
@ -47,6 +49,7 @@ public class DefaultAuthenticationFlows {
|
||||||
if (realm.getFlowByAlias(RESET_CREDENTIALS_FLOW) == null) resetCredentialsFlow(realm);
|
if (realm.getFlowByAlias(RESET_CREDENTIALS_FLOW) == null) resetCredentialsFlow(realm);
|
||||||
if (realm.getFlowByAlias(CLIENT_AUTHENTICATION_FLOW) == null) clientAuthFlow(realm);
|
if (realm.getFlowByAlias(CLIENT_AUTHENTICATION_FLOW) == null) clientAuthFlow(realm);
|
||||||
if (realm.getFlowByAlias(FIRST_BROKER_LOGIN_FLOW) == null) firstBrokerLoginFlow(realm, true);
|
if (realm.getFlowByAlias(FIRST_BROKER_LOGIN_FLOW) == null) firstBrokerLoginFlow(realm, true);
|
||||||
|
if (realm.getFlowByAlias(SAML_ECP_FLOW) == null) samlEcpProfile(realm);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void registrationFlow(RealmModel realm) {
|
public static void registrationFlow(RealmModel realm) {
|
||||||
|
@ -447,4 +450,25 @@ public class DefaultAuthenticationFlows {
|
||||||
execution.setAuthenticatorFlow(false);
|
execution.setAuthenticatorFlow(false);
|
||||||
realm.addAuthenticatorExecution(execution);
|
realm.addAuthenticatorExecution(execution);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void samlEcpProfile(RealmModel realm) {
|
||||||
|
AuthenticationFlowModel ecpFlow = new AuthenticationFlowModel();
|
||||||
|
|
||||||
|
ecpFlow.setAlias(SAML_ECP_FLOW);
|
||||||
|
ecpFlow.setDescription("SAML ECP Profile Authentication Flow");
|
||||||
|
ecpFlow.setProviderId("basic-flow");
|
||||||
|
ecpFlow.setTopLevel(true);
|
||||||
|
ecpFlow.setBuiltIn(true);
|
||||||
|
ecpFlow = realm.addAuthenticationFlow(ecpFlow);
|
||||||
|
|
||||||
|
AuthenticationExecutionModel execution = new AuthenticationExecutionModel();
|
||||||
|
|
||||||
|
execution.setParentFlow(ecpFlow.getId());
|
||||||
|
execution.setRequirement(AuthenticationExecutionModel.Requirement.REQUIRED);
|
||||||
|
execution.setAuthenticator("http-basic-authenticator");
|
||||||
|
execution.setPriority(10);
|
||||||
|
execution.setAuthenticatorFlow(false);
|
||||||
|
|
||||||
|
realm.addAuthenticatorExecution(execution);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,21 +7,24 @@ import org.keycloak.saml.BaseSAML2BindingBuilder;
|
||||||
import org.keycloak.saml.SAML2AuthnRequestBuilder;
|
import org.keycloak.saml.SAML2AuthnRequestBuilder;
|
||||||
import org.keycloak.saml.SAML2NameIDPolicyBuilder;
|
import org.keycloak.saml.SAML2NameIDPolicyBuilder;
|
||||||
import org.keycloak.saml.common.constants.JBossSAMLURIConstants;
|
import org.keycloak.saml.common.constants.JBossSAMLURIConstants;
|
||||||
|
import org.keycloak.saml.common.exceptions.ConfigurationException;
|
||||||
|
import org.keycloak.saml.common.exceptions.ProcessingException;
|
||||||
import org.w3c.dom.Document;
|
import org.w3c.dom.Document;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
import java.security.KeyPair;
|
import java.security.KeyPair;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||||
* @version $Revision: 1 $
|
* @version $Revision: 1 $
|
||||||
*/
|
*/
|
||||||
public class InitiateLogin implements AuthChallenge {
|
public abstract class AbstractInitiateLogin implements AuthChallenge {
|
||||||
protected static Logger log = Logger.getLogger(InitiateLogin.class);
|
protected static Logger log = Logger.getLogger(AbstractInitiateLogin.class);
|
||||||
|
|
||||||
protected SamlDeployment deployment;
|
protected SamlDeployment deployment;
|
||||||
protected SamlSessionStore sessionStore;
|
protected SamlSessionStore sessionStore;
|
||||||
|
|
||||||
public InitiateLogin(SamlDeployment deployment, SamlSessionStore sessionStore) {
|
public AbstractInitiateLogin(SamlDeployment deployment, SamlSessionStore sessionStore) {
|
||||||
this.deployment = deployment;
|
this.deployment = deployment;
|
||||||
this.sessionStore = sessionStore;
|
this.sessionStore = sessionStore;
|
||||||
}
|
}
|
||||||
|
@ -35,18 +38,14 @@ public class InitiateLogin implements AuthChallenge {
|
||||||
public boolean challenge(HttpFacade httpFacade) {
|
public boolean challenge(HttpFacade httpFacade) {
|
||||||
try {
|
try {
|
||||||
String issuerURL = deployment.getEntityID();
|
String issuerURL = deployment.getEntityID();
|
||||||
String actionUrl = deployment.getIDP().getSingleSignOnService().getRequestBindingUrl();
|
|
||||||
String destinationUrl = actionUrl;
|
|
||||||
String nameIDPolicyFormat = deployment.getNameIDPolicyFormat();
|
String nameIDPolicyFormat = deployment.getNameIDPolicyFormat();
|
||||||
|
|
||||||
if (nameIDPolicyFormat == null) {
|
if (nameIDPolicyFormat == null) {
|
||||||
nameIDPolicyFormat = JBossSAMLURIConstants.NAMEID_FORMAT_PERSISTENT.get();
|
nameIDPolicyFormat = JBossSAMLURIConstants.NAMEID_FORMAT_PERSISTENT.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
SAML2AuthnRequestBuilder authnRequestBuilder = new SAML2AuthnRequestBuilder()
|
SAML2AuthnRequestBuilder authnRequestBuilder = new SAML2AuthnRequestBuilder()
|
||||||
.destination(destinationUrl)
|
.destination(deployment.getIDP().getSingleSignOnService().getRequestBindingUrl())
|
||||||
.issuer(issuerURL)
|
.issuer(issuerURL)
|
||||||
.forceAuthn(deployment.isForceAuthentication()).isPassive(deployment.isIsPassive())
|
.forceAuthn(deployment.isForceAuthentication()).isPassive(deployment.isIsPassive())
|
||||||
.nameIdPolicy(SAML2NameIDPolicyBuilder.format(nameIDPolicyFormat));
|
.nameIdPolicy(SAML2NameIDPolicyBuilder.format(nameIDPolicyFormat));
|
||||||
|
@ -79,9 +78,7 @@ public class InitiateLogin implements AuthChallenge {
|
||||||
}
|
}
|
||||||
sessionStore.saveRequest();
|
sessionStore.saveRequest();
|
||||||
|
|
||||||
Document document = authnRequestBuilder.toDocument();
|
sendAuthnRequest(httpFacade, authnRequestBuilder, binding);
|
||||||
SamlDeployment.Binding samlBinding = deployment.getIDP().getSingleSignOnService().getRequestBinding();
|
|
||||||
SamlUtil.sendSaml(true, httpFacade, actionUrl, binding, document, samlBinding);
|
|
||||||
sessionStore.setCurrentAction(SamlSessionStore.CurrentAction.LOGGING_IN);
|
sessionStore.setCurrentAction(SamlSessionStore.CurrentAction.LOGGING_IN);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
throw new RuntimeException("Could not create authentication request.", e);
|
throw new RuntimeException("Could not create authentication request.", e);
|
||||||
|
@ -89,4 +86,6 @@ public class InitiateLogin implements AuthChallenge {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected abstract void sendAuthnRequest(HttpFacade httpFacade, SAML2AuthnRequestBuilder authnRequestBuilder, BaseSAML2BindingBuilder binding) throws ProcessingException, ConfigurationException, IOException;
|
||||||
|
|
||||||
}
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
package org.keycloak.adapters.saml;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
||||||
|
*/
|
||||||
|
public interface OnSessionCreated {
|
||||||
|
|
||||||
|
void onSessionCreated(SamlSession samlSession);
|
||||||
|
}
|
|
@ -1,530 +1,49 @@
|
||||||
package org.keycloak.adapters.saml;
|
package org.keycloak.adapters.saml;
|
||||||
|
|
||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
|
import org.keycloak.adapters.saml.profile.SamlAuthenticationHandler;
|
||||||
|
import org.keycloak.adapters.saml.profile.ecp.EcpAuthenticationHandler;
|
||||||
|
import org.keycloak.adapters.saml.profile.webbrowsersso.WebBrowserSsoAuthenticationHandler;
|
||||||
import org.keycloak.adapters.spi.AuthChallenge;
|
import org.keycloak.adapters.spi.AuthChallenge;
|
||||||
import org.keycloak.adapters.spi.AuthOutcome;
|
import org.keycloak.adapters.spi.AuthOutcome;
|
||||||
import org.keycloak.adapters.spi.HttpFacade;
|
import org.keycloak.adapters.spi.HttpFacade;
|
||||||
import org.keycloak.common.VerificationException;
|
|
||||||
import org.keycloak.common.util.KeycloakUriBuilder;
|
|
||||||
import org.keycloak.common.util.MultivaluedHashMap;
|
|
||||||
import org.keycloak.dom.saml.v2.assertion.AssertionType;
|
|
||||||
import org.keycloak.dom.saml.v2.assertion.AttributeStatementType;
|
|
||||||
import org.keycloak.dom.saml.v2.assertion.AttributeType;
|
|
||||||
import org.keycloak.dom.saml.v2.assertion.AuthnStatementType;
|
|
||||||
import org.keycloak.dom.saml.v2.assertion.NameIDType;
|
|
||||||
import org.keycloak.dom.saml.v2.assertion.StatementAbstractType;
|
|
||||||
import org.keycloak.dom.saml.v2.assertion.SubjectType;
|
|
||||||
import org.keycloak.dom.saml.v2.protocol.LogoutRequestType;
|
|
||||||
import org.keycloak.dom.saml.v2.protocol.RequestAbstractType;
|
|
||||||
import org.keycloak.dom.saml.v2.protocol.ResponseType;
|
|
||||||
import org.keycloak.dom.saml.v2.protocol.StatusCodeType;
|
|
||||||
import org.keycloak.dom.saml.v2.protocol.StatusResponseType;
|
|
||||||
import org.keycloak.dom.saml.v2.protocol.StatusType;
|
|
||||||
import org.keycloak.saml.BaseSAML2BindingBuilder;
|
|
||||||
import org.keycloak.saml.SAML2LogoutRequestBuilder;
|
|
||||||
import org.keycloak.saml.SAML2LogoutResponseBuilder;
|
|
||||||
import org.keycloak.saml.SAMLRequestParser;
|
|
||||||
import org.keycloak.saml.SignatureAlgorithm;
|
|
||||||
import org.keycloak.saml.common.constants.GeneralConstants;
|
|
||||||
import org.keycloak.saml.common.constants.JBossSAMLURIConstants;
|
|
||||||
import org.keycloak.saml.common.exceptions.ProcessingException;
|
|
||||||
import org.keycloak.saml.common.util.Base64;
|
|
||||||
import org.keycloak.saml.common.util.StringUtil;
|
|
||||||
import org.keycloak.saml.processing.api.saml.v2.sig.SAML2Signature;
|
|
||||||
import org.keycloak.saml.processing.core.saml.v2.common.SAMLDocumentHolder;
|
|
||||||
import org.keycloak.saml.processing.core.saml.v2.util.AssertionUtil;
|
|
||||||
import org.keycloak.saml.processing.web.util.PostBindingUtil;
|
|
||||||
import org.w3c.dom.Document;
|
|
||||||
import org.w3c.dom.Node;
|
|
||||||
|
|
||||||
import java.net.URI;
|
|
||||||
import java.security.PublicKey;
|
|
||||||
import java.security.Signature;
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||||
* @version $Revision: 1 $
|
* @version $Revision: 1 $
|
||||||
*/
|
*/
|
||||||
public abstract class SamlAuthenticator {
|
public abstract class SamlAuthenticator {
|
||||||
|
|
||||||
protected static Logger log = Logger.getLogger(SamlAuthenticator.class);
|
protected static Logger log = Logger.getLogger(SamlAuthenticator.class);
|
||||||
|
|
||||||
protected HttpFacade facade;
|
private final SamlAuthenticationHandler handler;
|
||||||
protected AuthChallenge challenge;
|
|
||||||
protected SamlDeployment deployment;
|
|
||||||
protected SamlSessionStore sessionStore;
|
|
||||||
|
|
||||||
public SamlAuthenticator(HttpFacade facade, SamlDeployment deployment, SamlSessionStore sessionStore) {
|
public SamlAuthenticator(final HttpFacade facade, final SamlDeployment deployment, final SamlSessionStore sessionStore) {
|
||||||
this.facade = facade;
|
this.handler = createAuthenticationHandler(facade, deployment, sessionStore);
|
||||||
this.deployment = deployment;
|
|
||||||
this.sessionStore = sessionStore;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public AuthChallenge getChallenge() {
|
public AuthChallenge getChallenge() {
|
||||||
return challenge;
|
return this.handler.getChallenge();
|
||||||
}
|
}
|
||||||
|
|
||||||
public AuthOutcome authenticate() {
|
public AuthOutcome authenticate() {
|
||||||
|
log.debugf("SamlAuthenticator is using handler [%s]", this.handler);
|
||||||
|
return this.handler.handle(new OnSessionCreated() {
|
||||||
String samlRequest = facade.getRequest().getFirstParam(GeneralConstants.SAML_REQUEST_KEY);
|
@Override
|
||||||
String samlResponse = facade.getRequest().getFirstParam(GeneralConstants.SAML_RESPONSE_KEY);
|
public void onSessionCreated(SamlSession samlSession) {
|
||||||
String relayState = facade.getRequest().getFirstParam(GeneralConstants.RELAY_STATE);
|
completeAuthentication(samlSession);
|
||||||
boolean globalLogout = "true".equals(facade.getRequest().getQueryParamValue("GLO"));
|
|
||||||
if (samlRequest != null) {
|
|
||||||
return handleSamlRequest(samlRequest, relayState);
|
|
||||||
} else if (samlResponse != null) {
|
|
||||||
return handleSamlResponse(samlResponse, relayState);
|
|
||||||
} else if (sessionStore.isLoggedIn()) {
|
|
||||||
if (globalLogout) {
|
|
||||||
return globalLogout();
|
|
||||||
}
|
}
|
||||||
if (verifySSL()) return AuthOutcome.FAILED;
|
});
|
||||||
log.debug("AUTHENTICATED: was cached");
|
|
||||||
return AuthOutcome.AUTHENTICATED;
|
|
||||||
}
|
|
||||||
return initiateLogin();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected AuthOutcome globalLogout() {
|
protected abstract void completeAuthentication(SamlSession samlSession);
|
||||||
SamlSession account = sessionStore.getAccount();
|
|
||||||
if (account == null) {
|
private SamlAuthenticationHandler createAuthenticationHandler(HttpFacade facade, SamlDeployment deployment, SamlSessionStore sessionStore) {
|
||||||
return AuthOutcome.NOT_ATTEMPTED;
|
if (EcpAuthenticationHandler.canHandle(facade)) {
|
||||||
}
|
return EcpAuthenticationHandler.create(facade, deployment, sessionStore);
|
||||||
SAML2LogoutRequestBuilder logoutBuilder = new SAML2LogoutRequestBuilder()
|
|
||||||
.assertionExpiration(30)
|
|
||||||
.issuer(deployment.getEntityID())
|
|
||||||
.sessionIndex(account.getSessionIndex())
|
|
||||||
.userPrincipal(account.getPrincipal().getSamlSubject(), account.getPrincipal().getNameIDFormat())
|
|
||||||
.destination(deployment.getIDP().getSingleLogoutService().getRequestBindingUrl());
|
|
||||||
BaseSAML2BindingBuilder binding = new BaseSAML2BindingBuilder();
|
|
||||||
if (deployment.getIDP().getSingleLogoutService().signRequest()) {
|
|
||||||
binding.signWith(deployment.getSigningKeyPair())
|
|
||||||
.signDocument();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.relayState("logout");
|
// defaults to the web browser sso profile
|
||||||
|
return WebBrowserSsoAuthenticationHandler.create(facade, deployment, sessionStore);
|
||||||
try {
|
|
||||||
SamlUtil.sendSaml(true, facade, deployment.getIDP().getSingleLogoutService().getRequestBindingUrl(), binding, logoutBuilder.buildDocument(), deployment.getIDP().getSingleLogoutService().getRequestBinding());
|
|
||||||
sessionStore.setCurrentAction(SamlSessionStore.CurrentAction.LOGGING_OUT);
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("Could not send global logout SAML request", e);
|
|
||||||
return AuthOutcome.FAILED;
|
|
||||||
}
|
|
||||||
return AuthOutcome.NOT_ATTEMPTED;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected AuthOutcome handleSamlRequest(String samlRequest, String relayState) {
|
|
||||||
SAMLDocumentHolder holder = null;
|
|
||||||
boolean postBinding = false;
|
|
||||||
String requestUri = facade.getRequest().getURI();
|
|
||||||
if (facade.getRequest().getMethod().equalsIgnoreCase("GET")) {
|
|
||||||
// strip out query params
|
|
||||||
int index = requestUri.indexOf('?');
|
|
||||||
if (index > -1) {
|
|
||||||
requestUri = requestUri.substring(0, index);
|
|
||||||
}
|
|
||||||
holder = SAMLRequestParser.parseRequestRedirectBinding(samlRequest);
|
|
||||||
} else {
|
|
||||||
postBinding = true;
|
|
||||||
holder = SAMLRequestParser.parseRequestPostBinding(samlRequest);
|
|
||||||
}
|
|
||||||
RequestAbstractType requestAbstractType = (RequestAbstractType) holder.getSamlObject();
|
|
||||||
if (!requestUri.equals(requestAbstractType.getDestination().toString())) {
|
|
||||||
log.error("expected destination '" + requestUri + "' got '" + requestAbstractType.getDestination() + "'");
|
|
||||||
return AuthOutcome.FAILED;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (requestAbstractType instanceof LogoutRequestType) {
|
|
||||||
if (deployment.getIDP().getSingleLogoutService().validateRequestSignature()) {
|
|
||||||
try {
|
|
||||||
validateSamlSignature(holder, postBinding, GeneralConstants.SAML_REQUEST_KEY);
|
|
||||||
} catch (VerificationException e) {
|
|
||||||
log.error("Failed to verify saml request signature", e);
|
|
||||||
return AuthOutcome.FAILED;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
LogoutRequestType logout = (LogoutRequestType) requestAbstractType;
|
|
||||||
return logoutRequest(logout, relayState);
|
|
||||||
|
|
||||||
} else {
|
|
||||||
log.error("unknown SAML request type");
|
|
||||||
return AuthOutcome.FAILED;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected AuthOutcome logoutRequest(LogoutRequestType request, String relayState) {
|
|
||||||
if (request.getSessionIndex() == null || request.getSessionIndex().isEmpty()) {
|
|
||||||
sessionStore.logoutByPrincipal(request.getNameID().getValue());
|
|
||||||
} else {
|
|
||||||
sessionStore.logoutBySsoId(request.getSessionIndex());
|
|
||||||
}
|
|
||||||
|
|
||||||
String issuerURL = deployment.getEntityID();
|
|
||||||
SAML2LogoutResponseBuilder builder = new SAML2LogoutResponseBuilder();
|
|
||||||
builder.logoutRequestID(request.getID());
|
|
||||||
builder.destination(deployment.getIDP().getSingleLogoutService().getResponseBindingUrl());
|
|
||||||
builder.issuer(issuerURL);
|
|
||||||
BaseSAML2BindingBuilder binding = new BaseSAML2BindingBuilder().relayState(relayState);
|
|
||||||
if (deployment.getIDP().getSingleLogoutService().signResponse()) {
|
|
||||||
binding.signatureAlgorithm(deployment.getSignatureAlgorithm())
|
|
||||||
.signWith(deployment.getSigningKeyPair())
|
|
||||||
.signDocument();
|
|
||||||
if (deployment.getSignatureCanonicalizationMethod() != null)
|
|
||||||
binding.canonicalizationMethod(deployment.getSignatureCanonicalizationMethod());
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
try {
|
|
||||||
SamlUtil.sendSaml(false, facade, deployment.getIDP().getSingleLogoutService().getResponseBindingUrl(), binding, builder.buildDocument(),
|
|
||||||
deployment.getIDP().getSingleLogoutService().getResponseBinding());
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("Could not send logout response SAML request", e);
|
|
||||||
return AuthOutcome.FAILED;
|
|
||||||
}
|
|
||||||
return AuthOutcome.NOT_ATTEMPTED;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
protected AuthOutcome handleSamlResponse(String samlResponse, String relayState) {
|
|
||||||
SAMLDocumentHolder holder = null;
|
|
||||||
boolean postBinding = false;
|
|
||||||
String requestUri = facade.getRequest().getURI();
|
|
||||||
if (facade.getRequest().getMethod().equalsIgnoreCase("GET")) {
|
|
||||||
int index = requestUri.indexOf('?');
|
|
||||||
if (index > -1) {
|
|
||||||
requestUri = requestUri.substring(0, index);
|
|
||||||
}
|
|
||||||
holder = extractRedirectBindingResponse(samlResponse);
|
|
||||||
} else {
|
|
||||||
postBinding = true;
|
|
||||||
holder = extractPostBindingResponse(samlResponse);
|
|
||||||
}
|
|
||||||
final StatusResponseType statusResponse = (StatusResponseType) holder.getSamlObject();
|
|
||||||
// validate destination
|
|
||||||
if (!requestUri.equals(statusResponse.getDestination())) {
|
|
||||||
log.error("Request URI does not match SAML request destination");
|
|
||||||
return AuthOutcome.FAILED;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (statusResponse instanceof ResponseType) {
|
|
||||||
try {
|
|
||||||
if (deployment.getIDP().getSingleSignOnService().validateResponseSignature()) {
|
|
||||||
try {
|
|
||||||
validateSamlSignature(holder, postBinding, GeneralConstants.SAML_RESPONSE_KEY);
|
|
||||||
} catch (VerificationException e) {
|
|
||||||
log.error("Failed to verify saml response signature", e);
|
|
||||||
|
|
||||||
challenge = new AuthChallenge() {
|
|
||||||
@Override
|
|
||||||
public boolean challenge(HttpFacade exchange) {
|
|
||||||
SamlAuthenticationError error = new SamlAuthenticationError(SamlAuthenticationError.Reason.INVALID_SIGNATURE);
|
|
||||||
exchange.getRequest().setError(error);
|
|
||||||
exchange.getResponse().sendError(403);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getResponseCode() {
|
|
||||||
return 403;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
return AuthOutcome.FAILED;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return handleLoginResponse((ResponseType) statusResponse);
|
|
||||||
} finally {
|
|
||||||
sessionStore.setCurrentAction(SamlSessionStore.CurrentAction.NONE);
|
|
||||||
}
|
|
||||||
|
|
||||||
} else {
|
|
||||||
if (sessionStore.isLoggingOut()) {
|
|
||||||
try {
|
|
||||||
if (deployment.getIDP().getSingleLogoutService().validateResponseSignature()) {
|
|
||||||
try {
|
|
||||||
validateSamlSignature(holder, postBinding, GeneralConstants.SAML_RESPONSE_KEY);
|
|
||||||
} catch (VerificationException e) {
|
|
||||||
log.error("Failed to verify saml response signature", e);
|
|
||||||
return AuthOutcome.FAILED;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return handleLogoutResponse(holder, statusResponse, relayState);
|
|
||||||
} finally {
|
|
||||||
sessionStore.setCurrentAction(SamlSessionStore.CurrentAction.NONE);
|
|
||||||
}
|
|
||||||
|
|
||||||
} else if (sessionStore.isLoggingIn()) {
|
|
||||||
|
|
||||||
try {
|
|
||||||
// KEYCLOAK-2107 - handle user not authenticated due passive mode. Return special outcome so different authentication mechanisms can behave accordingly.
|
|
||||||
StatusType status = statusResponse.getStatus();
|
|
||||||
if(checkStatusCodeValue(status.getStatusCode(), JBossSAMLURIConstants.STATUS_RESPONDER.get()) && checkStatusCodeValue(status.getStatusCode().getStatusCode(), JBossSAMLURIConstants.STATUS_NO_PASSIVE.get())){
|
|
||||||
log.debug("Not authenticated due passive mode Status found in SAML response: " + status.toString());
|
|
||||||
return AuthOutcome.NOT_AUTHENTICATED;
|
|
||||||
}
|
|
||||||
|
|
||||||
challenge = new AuthChallenge() {
|
|
||||||
@Override
|
|
||||||
public boolean challenge(HttpFacade exchange) {
|
|
||||||
SamlAuthenticationError error = new SamlAuthenticationError(SamlAuthenticationError.Reason.ERROR_STATUS, statusResponse);
|
|
||||||
exchange.getRequest().setError(error);
|
|
||||||
exchange.getResponse().sendError(403);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getResponseCode() {
|
|
||||||
return 403;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
return AuthOutcome.FAILED;
|
|
||||||
} finally {
|
|
||||||
sessionStore.setCurrentAction(SamlSessionStore.CurrentAction.NONE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return AuthOutcome.NOT_ATTEMPTED;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private void validateSamlSignature(SAMLDocumentHolder holder, boolean postBinding, String paramKey) throws VerificationException {
|
|
||||||
if (postBinding) {
|
|
||||||
verifyPostBindingSignature(holder.getSamlDocument(), deployment.getIDP().getSignatureValidationKey());
|
|
||||||
} else {
|
|
||||||
verifyRedirectBindingSignature(deployment.getIDP().getSignatureValidationKey(), paramKey);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean checkStatusCodeValue(StatusCodeType statusCode, String expectedValue){
|
|
||||||
if(statusCode != null && statusCode.getValue()!=null){
|
|
||||||
String v = statusCode.getValue().toString();
|
|
||||||
return expectedValue.equals(v);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected AuthOutcome handleLoginResponse(ResponseType responseType) {
|
|
||||||
|
|
||||||
AssertionType assertion = null;
|
|
||||||
try {
|
|
||||||
assertion = AssertionUtil.getAssertion(responseType, deployment.getDecryptionKey());
|
|
||||||
if (AssertionUtil.hasExpired(assertion)) {
|
|
||||||
return initiateLogin();
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("Error extracting SAML assertion: " + e.getMessage());
|
|
||||||
challenge = new AuthChallenge() {
|
|
||||||
@Override
|
|
||||||
public boolean challenge(HttpFacade exchange) {
|
|
||||||
SamlAuthenticationError error = new SamlAuthenticationError(SamlAuthenticationError.Reason.EXTRACTION_FAILURE);
|
|
||||||
exchange.getRequest().setError(error);
|
|
||||||
exchange.getResponse().sendError(403);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getResponseCode() {
|
|
||||||
return 403;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
SubjectType subject = assertion.getSubject();
|
|
||||||
SubjectType.STSubType subType = subject.getSubType();
|
|
||||||
NameIDType subjectNameID = (NameIDType) subType.getBaseID();
|
|
||||||
String principalName = subjectNameID.getValue();
|
|
||||||
|
|
||||||
final Set<String> roles = new HashSet<>();
|
|
||||||
MultivaluedHashMap<String, String> attributes = new MultivaluedHashMap<>();
|
|
||||||
MultivaluedHashMap<String, String> friendlyAttributes = new MultivaluedHashMap<>();
|
|
||||||
|
|
||||||
Set<StatementAbstractType> statements = assertion.getStatements();
|
|
||||||
for (StatementAbstractType statement : statements) {
|
|
||||||
if (statement instanceof AttributeStatementType) {
|
|
||||||
AttributeStatementType attributeStatement = (AttributeStatementType) statement;
|
|
||||||
List<AttributeStatementType.ASTChoiceType> attList = attributeStatement.getAttributes();
|
|
||||||
for (AttributeStatementType.ASTChoiceType obj : attList) {
|
|
||||||
AttributeType attr = obj.getAttribute();
|
|
||||||
if (isRole(attr)) {
|
|
||||||
List<Object> attributeValues = attr.getAttributeValue();
|
|
||||||
if (attributeValues != null) {
|
|
||||||
for (Object attrValue : attributeValues) {
|
|
||||||
String role = getAttributeValue(attrValue);
|
|
||||||
log.debugv("Add role: {0}", role);
|
|
||||||
roles.add(role);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
List<Object> attributeValues = attr.getAttributeValue();
|
|
||||||
if (attributeValues != null) {
|
|
||||||
for (Object attrValue : attributeValues) {
|
|
||||||
String value = getAttributeValue(attrValue);
|
|
||||||
if (attr.getName() != null) {
|
|
||||||
attributes.add(attr.getName(), value);
|
|
||||||
}
|
|
||||||
if (attr.getFriendlyName() != null) {
|
|
||||||
friendlyAttributes.add(attr.getFriendlyName(), value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (deployment.getPrincipalNamePolicy() == SamlDeployment.PrincipalNamePolicy.FROM_ATTRIBUTE) {
|
|
||||||
if (deployment.getPrincipalAttributeName() != null) {
|
|
||||||
String attribute = attributes.getFirst(deployment.getPrincipalAttributeName());
|
|
||||||
if (attribute != null) principalName = attribute;
|
|
||||||
else {
|
|
||||||
attribute = friendlyAttributes.getFirst(deployment.getPrincipalAttributeName());
|
|
||||||
if (attribute != null) principalName = attribute;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
AuthnStatementType authn = null;
|
|
||||||
for (Object statement : assertion.getStatements()) {
|
|
||||||
if (statement instanceof AuthnStatementType) {
|
|
||||||
authn = (AuthnStatementType) statement;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
URI nameFormat = subjectNameID.getFormat();
|
|
||||||
String nameFormatString = nameFormat == null ? JBossSAMLURIConstants.NAMEID_FORMAT_UNSPECIFIED.get() : nameFormat.toString();
|
|
||||||
final SamlPrincipal principal = new SamlPrincipal(assertion, principalName, principalName, nameFormatString, attributes, friendlyAttributes);
|
|
||||||
String index = authn == null ? null : authn.getSessionIndex();
|
|
||||||
final String sessionIndex = index;
|
|
||||||
SamlSession account = new SamlSession(principal, roles, sessionIndex);
|
|
||||||
sessionStore.saveAccount(account);
|
|
||||||
completeAuthentication(account);
|
|
||||||
|
|
||||||
|
|
||||||
// redirect to original request, it will be restored
|
|
||||||
String redirectUri = sessionStore.getRedirectUri();
|
|
||||||
if (redirectUri != null) {
|
|
||||||
facade.getResponse().setHeader("Location", redirectUri);
|
|
||||||
facade.getResponse().setStatus(302);
|
|
||||||
facade.getResponse().end();
|
|
||||||
} else {
|
|
||||||
log.debug("IDP initiated invocation");
|
|
||||||
}
|
|
||||||
log.debug("AUTHENTICATED authn");
|
|
||||||
|
|
||||||
return AuthOutcome.AUTHENTICATED;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected abstract void completeAuthentication(SamlSession account);
|
|
||||||
|
|
||||||
private String getAttributeValue(Object attrValue) {
|
|
||||||
String value = null;
|
|
||||||
if (attrValue instanceof String) {
|
|
||||||
value = (String) attrValue;
|
|
||||||
} else if (attrValue instanceof Node) {
|
|
||||||
Node roleNode = (Node) attrValue;
|
|
||||||
value = roleNode.getFirstChild().getNodeValue();
|
|
||||||
} else if (attrValue instanceof NameIDType) {
|
|
||||||
NameIDType nameIdType = (NameIDType) attrValue;
|
|
||||||
value = nameIdType.getValue();
|
|
||||||
} else {
|
|
||||||
log.warn("Unable to extract unknown SAML assertion attribute value type: " + attrValue.getClass().getName());
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected boolean isRole(AttributeType attribute) {
|
|
||||||
return (attribute.getName() != null && deployment.getRoleAttributeNames().contains(attribute.getName())) || (attribute.getFriendlyName() != null && deployment.getRoleAttributeNames().contains(attribute.getFriendlyName()));
|
|
||||||
}
|
|
||||||
|
|
||||||
protected AuthOutcome handleLogoutResponse(SAMLDocumentHolder holder, StatusResponseType responseType, String relayState) {
|
|
||||||
boolean loggedIn = sessionStore.isLoggedIn();
|
|
||||||
if (!loggedIn || !"logout".equals(relayState)) {
|
|
||||||
return AuthOutcome.NOT_ATTEMPTED;
|
|
||||||
}
|
|
||||||
sessionStore.logoutAccount();
|
|
||||||
return AuthOutcome.LOGGED_OUT;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected SAMLDocumentHolder extractRedirectBindingResponse(String response) {
|
|
||||||
return SAMLRequestParser.parseRequestRedirectBinding(response);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
protected SAMLDocumentHolder extractPostBindingResponse(String response) {
|
|
||||||
byte[] samlBytes = PostBindingUtil.base64Decode(response);
|
|
||||||
return SAMLRequestParser.parseResponseDocument(samlBytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
protected AuthOutcome initiateLogin() {
|
|
||||||
challenge = new InitiateLogin(deployment, sessionStore);
|
|
||||||
return AuthOutcome.NOT_ATTEMPTED;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected boolean verifySSL() {
|
|
||||||
if (!facade.getRequest().isSecure() && deployment.getSslRequired().isRequired(facade.getRequest().getRemoteAddr())) {
|
|
||||||
log.warn("SSL is required to authenticate");
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void verifyPostBindingSignature(Document document, PublicKey publicKey) throws VerificationException {
|
|
||||||
SAML2Signature saml2Signature = new SAML2Signature();
|
|
||||||
try {
|
|
||||||
if (!saml2Signature.validate(document, publicKey)) {
|
|
||||||
throw new VerificationException("Invalid signature on document");
|
|
||||||
}
|
|
||||||
} catch (ProcessingException e) {
|
|
||||||
throw new VerificationException("Error validating signature", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void verifyRedirectBindingSignature(PublicKey publicKey, String paramKey) throws VerificationException {
|
|
||||||
String request = facade.getRequest().getQueryParamValue(paramKey);
|
|
||||||
String algorithm = facade.getRequest().getQueryParamValue(GeneralConstants.SAML_SIG_ALG_REQUEST_KEY);
|
|
||||||
String signature = facade.getRequest().getQueryParamValue(GeneralConstants.SAML_SIGNATURE_REQUEST_KEY);
|
|
||||||
String decodedAlgorithm = facade.getRequest().getQueryParamValue(GeneralConstants.SAML_SIG_ALG_REQUEST_KEY);
|
|
||||||
|
|
||||||
if (request == null) {
|
|
||||||
throw new VerificationException("SAML Request was null");
|
|
||||||
}
|
|
||||||
if (algorithm == null) throw new VerificationException("SigAlg was null");
|
|
||||||
if (signature == null) throw new VerificationException("Signature was null");
|
|
||||||
|
|
||||||
// Shibboleth doesn't sign the document for redirect binding.
|
|
||||||
// todo maybe a flag?
|
|
||||||
|
|
||||||
String relayState = facade.getRequest().getQueryParamValue(GeneralConstants.RELAY_STATE);
|
|
||||||
KeycloakUriBuilder builder = KeycloakUriBuilder.fromPath("/")
|
|
||||||
.queryParam(paramKey, request);
|
|
||||||
if (relayState != null) {
|
|
||||||
builder.queryParam(GeneralConstants.RELAY_STATE, relayState);
|
|
||||||
}
|
|
||||||
builder.queryParam(GeneralConstants.SAML_SIG_ALG_REQUEST_KEY, algorithm);
|
|
||||||
String rawQuery = builder.build().getRawQuery();
|
|
||||||
|
|
||||||
try {
|
|
||||||
//byte[] decodedSignature = RedirectBindingUtil.urlBase64Decode(signature);
|
|
||||||
byte[] decodedSignature = Base64.decode(signature);
|
|
||||||
|
|
||||||
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.getFromXmlMethod(decodedAlgorithm);
|
|
||||||
Signature validator = signatureAlgorithm.createSignature(); // todo plugin signature alg
|
|
||||||
validator.initVerify(publicKey);
|
|
||||||
validator.update(rawQuery.getBytes("UTF-8"));
|
|
||||||
if (!validator.verify(decodedSignature)) {
|
|
||||||
throw new VerificationException("Invalid query param signature");
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
throw new VerificationException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
|
@ -0,0 +1,484 @@
|
||||||
|
package org.keycloak.adapters.saml.profile;
|
||||||
|
|
||||||
|
import org.jboss.logging.Logger;
|
||||||
|
import org.keycloak.adapters.saml.AbstractInitiateLogin;
|
||||||
|
import org.keycloak.adapters.saml.OnSessionCreated;
|
||||||
|
import org.keycloak.adapters.saml.SamlAuthenticationError;
|
||||||
|
import org.keycloak.adapters.saml.SamlDeployment;
|
||||||
|
import org.keycloak.adapters.saml.SamlPrincipal;
|
||||||
|
import org.keycloak.adapters.saml.SamlSession;
|
||||||
|
import org.keycloak.adapters.saml.SamlSessionStore;
|
||||||
|
import org.keycloak.adapters.saml.SamlUtil;
|
||||||
|
import org.keycloak.adapters.saml.profile.webbrowsersso.WebBrowserSsoAuthenticationHandler;
|
||||||
|
import org.keycloak.adapters.spi.AuthChallenge;
|
||||||
|
import org.keycloak.adapters.spi.AuthOutcome;
|
||||||
|
import org.keycloak.adapters.spi.HttpFacade;
|
||||||
|
import org.keycloak.common.VerificationException;
|
||||||
|
import org.keycloak.common.util.KeycloakUriBuilder;
|
||||||
|
import org.keycloak.common.util.MultivaluedHashMap;
|
||||||
|
import org.keycloak.dom.saml.v2.assertion.AssertionType;
|
||||||
|
import org.keycloak.dom.saml.v2.assertion.AttributeStatementType;
|
||||||
|
import org.keycloak.dom.saml.v2.assertion.AttributeType;
|
||||||
|
import org.keycloak.dom.saml.v2.assertion.AuthnStatementType;
|
||||||
|
import org.keycloak.dom.saml.v2.assertion.NameIDType;
|
||||||
|
import org.keycloak.dom.saml.v2.assertion.StatementAbstractType;
|
||||||
|
import org.keycloak.dom.saml.v2.assertion.SubjectType;
|
||||||
|
import org.keycloak.dom.saml.v2.protocol.LogoutRequestType;
|
||||||
|
import org.keycloak.dom.saml.v2.protocol.RequestAbstractType;
|
||||||
|
import org.keycloak.dom.saml.v2.protocol.ResponseType;
|
||||||
|
import org.keycloak.dom.saml.v2.protocol.StatusCodeType;
|
||||||
|
import org.keycloak.dom.saml.v2.protocol.StatusResponseType;
|
||||||
|
import org.keycloak.dom.saml.v2.protocol.StatusType;
|
||||||
|
import org.keycloak.saml.BaseSAML2BindingBuilder;
|
||||||
|
import org.keycloak.saml.SAML2AuthnRequestBuilder;
|
||||||
|
import org.keycloak.saml.SAMLRequestParser;
|
||||||
|
import org.keycloak.saml.SignatureAlgorithm;
|
||||||
|
import org.keycloak.saml.common.constants.GeneralConstants;
|
||||||
|
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.common.util.Base64;
|
||||||
|
import org.keycloak.saml.processing.api.saml.v2.sig.SAML2Signature;
|
||||||
|
import org.keycloak.saml.processing.core.saml.v2.common.SAMLDocumentHolder;
|
||||||
|
import org.keycloak.saml.processing.core.saml.v2.util.AssertionUtil;
|
||||||
|
import org.keycloak.saml.processing.web.util.PostBindingUtil;
|
||||||
|
import org.w3c.dom.Document;
|
||||||
|
import org.w3c.dom.Node;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.security.PublicKey;
|
||||||
|
import java.security.Signature;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||||
|
*/
|
||||||
|
public abstract class AbstractSamlAuthenticationHandler implements SamlAuthenticationHandler {
|
||||||
|
|
||||||
|
protected static Logger log = Logger.getLogger(WebBrowserSsoAuthenticationHandler.class);
|
||||||
|
|
||||||
|
protected final HttpFacade facade;
|
||||||
|
protected final SamlSessionStore sessionStore;
|
||||||
|
protected final SamlDeployment deployment;
|
||||||
|
protected AuthChallenge challenge;
|
||||||
|
|
||||||
|
public AbstractSamlAuthenticationHandler(HttpFacade facade, SamlDeployment deployment, SamlSessionStore sessionStore) {
|
||||||
|
this.facade = facade;
|
||||||
|
this.deployment = deployment;
|
||||||
|
this.sessionStore = sessionStore;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AuthOutcome doHandle(SamlInvocationContext context, OnSessionCreated onCreateSession) {
|
||||||
|
String samlRequest = context.getSamlRequest();
|
||||||
|
String samlResponse = context.getSamlResponse();
|
||||||
|
String relayState = context.getRelayState();
|
||||||
|
if (samlRequest != null) {
|
||||||
|
return handleSamlRequest(samlRequest, relayState);
|
||||||
|
} else if (samlResponse != null) {
|
||||||
|
return handleSamlResponse(samlResponse, relayState, onCreateSession);
|
||||||
|
} else if (sessionStore.isLoggedIn()) {
|
||||||
|
if (verifySSL()) return AuthOutcome.FAILED;
|
||||||
|
log.debug("AUTHENTICATED: was cached");
|
||||||
|
return handleRequest();
|
||||||
|
}
|
||||||
|
return initiateLogin();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected AuthOutcome handleRequest() {
|
||||||
|
return AuthOutcome.AUTHENTICATED;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AuthChallenge getChallenge() {
|
||||||
|
return this.challenge;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected AuthOutcome handleSamlRequest(String samlRequest, String relayState) {
|
||||||
|
SAMLDocumentHolder holder = null;
|
||||||
|
boolean postBinding = false;
|
||||||
|
String requestUri = facade.getRequest().getURI();
|
||||||
|
if (facade.getRequest().getMethod().equalsIgnoreCase("GET")) {
|
||||||
|
// strip out query params
|
||||||
|
int index = requestUri.indexOf('?');
|
||||||
|
if (index > -1) {
|
||||||
|
requestUri = requestUri.substring(0, index);
|
||||||
|
}
|
||||||
|
holder = SAMLRequestParser.parseRequestRedirectBinding(samlRequest);
|
||||||
|
} else {
|
||||||
|
postBinding = true;
|
||||||
|
holder = SAMLRequestParser.parseRequestPostBinding(samlRequest);
|
||||||
|
}
|
||||||
|
RequestAbstractType requestAbstractType = (RequestAbstractType) holder.getSamlObject();
|
||||||
|
if (!requestUri.equals(requestAbstractType.getDestination().toString())) {
|
||||||
|
log.error("expected destination '" + requestUri + "' got '" + requestAbstractType.getDestination() + "'");
|
||||||
|
return AuthOutcome.FAILED;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (requestAbstractType instanceof LogoutRequestType) {
|
||||||
|
if (deployment.getIDP().getSingleLogoutService().validateRequestSignature()) {
|
||||||
|
try {
|
||||||
|
validateSamlSignature(holder, postBinding, GeneralConstants.SAML_REQUEST_KEY);
|
||||||
|
} catch (VerificationException e) {
|
||||||
|
log.error("Failed to verify saml request signature", e);
|
||||||
|
return AuthOutcome.FAILED;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LogoutRequestType logout = (LogoutRequestType) requestAbstractType;
|
||||||
|
return logoutRequest(logout, relayState);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
log.error("unknown SAML request type");
|
||||||
|
return AuthOutcome.FAILED;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract AuthOutcome logoutRequest(LogoutRequestType request, String relayState);
|
||||||
|
|
||||||
|
protected AuthOutcome handleSamlResponse(String samlResponse, String relayState, OnSessionCreated onCreateSession) {
|
||||||
|
SAMLDocumentHolder holder = null;
|
||||||
|
boolean postBinding = false;
|
||||||
|
String requestUri = facade.getRequest().getURI();
|
||||||
|
if (facade.getRequest().getMethod().equalsIgnoreCase("GET")) {
|
||||||
|
int index = requestUri.indexOf('?');
|
||||||
|
if (index > -1) {
|
||||||
|
requestUri = requestUri.substring(0, index);
|
||||||
|
}
|
||||||
|
holder = extractRedirectBindingResponse(samlResponse);
|
||||||
|
} else {
|
||||||
|
postBinding = true;
|
||||||
|
holder = extractPostBindingResponse(samlResponse);
|
||||||
|
}
|
||||||
|
final StatusResponseType statusResponse = (StatusResponseType) holder.getSamlObject();
|
||||||
|
// validate destination
|
||||||
|
if (!requestUri.equals(statusResponse.getDestination())) {
|
||||||
|
log.error("Request URI does not match SAML request destination");
|
||||||
|
return AuthOutcome.FAILED;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (statusResponse instanceof ResponseType) {
|
||||||
|
try {
|
||||||
|
if (deployment.getIDP().getSingleSignOnService().validateResponseSignature()) {
|
||||||
|
try {
|
||||||
|
validateSamlSignature(holder, postBinding, GeneralConstants.SAML_RESPONSE_KEY);
|
||||||
|
} catch (VerificationException e) {
|
||||||
|
log.error("Failed to verify saml response signature", e);
|
||||||
|
|
||||||
|
challenge = new AuthChallenge() {
|
||||||
|
@Override
|
||||||
|
public boolean challenge(HttpFacade exchange) {
|
||||||
|
SamlAuthenticationError error = new SamlAuthenticationError(SamlAuthenticationError.Reason.INVALID_SIGNATURE);
|
||||||
|
exchange.getRequest().setError(error);
|
||||||
|
exchange.getResponse().sendError(403);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getResponseCode() {
|
||||||
|
return 403;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return AuthOutcome.FAILED;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return handleLoginResponse((ResponseType) statusResponse, onCreateSession);
|
||||||
|
} finally {
|
||||||
|
sessionStore.setCurrentAction(SamlSessionStore.CurrentAction.NONE);
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
if (sessionStore.isLoggingOut()) {
|
||||||
|
try {
|
||||||
|
if (deployment.getIDP().getSingleLogoutService().validateResponseSignature()) {
|
||||||
|
try {
|
||||||
|
validateSamlSignature(holder, postBinding, GeneralConstants.SAML_RESPONSE_KEY);
|
||||||
|
} catch (VerificationException e) {
|
||||||
|
log.error("Failed to verify saml response signature", e);
|
||||||
|
return AuthOutcome.FAILED;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return handleLogoutResponse(holder, statusResponse, relayState);
|
||||||
|
} finally {
|
||||||
|
sessionStore.setCurrentAction(SamlSessionStore.CurrentAction.NONE);
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if (sessionStore.isLoggingIn()) {
|
||||||
|
|
||||||
|
try {
|
||||||
|
// KEYCLOAK-2107 - handle user not authenticated due passive mode. Return special outcome so different authentication mechanisms can behave accordingly.
|
||||||
|
StatusType status = statusResponse.getStatus();
|
||||||
|
if(checkStatusCodeValue(status.getStatusCode(), JBossSAMLURIConstants.STATUS_RESPONDER.get()) && checkStatusCodeValue(status.getStatusCode().getStatusCode(), JBossSAMLURIConstants.STATUS_NO_PASSIVE.get())){
|
||||||
|
log.debug("Not authenticated due passive mode Status found in SAML response: " + status.toString());
|
||||||
|
return AuthOutcome.NOT_AUTHENTICATED;
|
||||||
|
}
|
||||||
|
|
||||||
|
challenge = new AuthChallenge() {
|
||||||
|
@Override
|
||||||
|
public boolean challenge(HttpFacade exchange) {
|
||||||
|
SamlAuthenticationError error = new SamlAuthenticationError(SamlAuthenticationError.Reason.ERROR_STATUS, statusResponse);
|
||||||
|
exchange.getRequest().setError(error);
|
||||||
|
exchange.getResponse().sendError(403);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getResponseCode() {
|
||||||
|
return 403;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return AuthOutcome.FAILED;
|
||||||
|
} finally {
|
||||||
|
sessionStore.setCurrentAction(SamlSessionStore.CurrentAction.NONE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return AuthOutcome.NOT_ATTEMPTED;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateSamlSignature(SAMLDocumentHolder holder, boolean postBinding, String paramKey) throws VerificationException {
|
||||||
|
if (postBinding) {
|
||||||
|
verifyPostBindingSignature(holder.getSamlDocument(), deployment.getIDP().getSignatureValidationKey());
|
||||||
|
} else {
|
||||||
|
verifyRedirectBindingSignature(deployment.getIDP().getSignatureValidationKey(), paramKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean checkStatusCodeValue(StatusCodeType statusCode, String expectedValue){
|
||||||
|
if(statusCode != null && statusCode.getValue()!=null){
|
||||||
|
String v = statusCode.getValue().toString();
|
||||||
|
return expectedValue.equals(v);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected AuthOutcome handleLoginResponse(ResponseType responseType, OnSessionCreated onCreateSession) {
|
||||||
|
|
||||||
|
AssertionType assertion = null;
|
||||||
|
try {
|
||||||
|
assertion = AssertionUtil.getAssertion(responseType, deployment.getDecryptionKey());
|
||||||
|
if (AssertionUtil.hasExpired(assertion)) {
|
||||||
|
return initiateLogin();
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Error extracting SAML assertion: " + e.getMessage());
|
||||||
|
challenge = new AuthChallenge() {
|
||||||
|
@Override
|
||||||
|
public boolean challenge(HttpFacade exchange) {
|
||||||
|
SamlAuthenticationError error = new SamlAuthenticationError(SamlAuthenticationError.Reason.EXTRACTION_FAILURE);
|
||||||
|
exchange.getRequest().setError(error);
|
||||||
|
exchange.getResponse().sendError(403);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getResponseCode() {
|
||||||
|
return 403;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
SubjectType subject = assertion.getSubject();
|
||||||
|
SubjectType.STSubType subType = subject.getSubType();
|
||||||
|
NameIDType subjectNameID = (NameIDType) subType.getBaseID();
|
||||||
|
String principalName = subjectNameID.getValue();
|
||||||
|
|
||||||
|
final Set<String> roles = new HashSet<>();
|
||||||
|
MultivaluedHashMap<String, String> attributes = new MultivaluedHashMap<>();
|
||||||
|
MultivaluedHashMap<String, String> friendlyAttributes = new MultivaluedHashMap<>();
|
||||||
|
|
||||||
|
Set<StatementAbstractType> statements = assertion.getStatements();
|
||||||
|
for (StatementAbstractType statement : statements) {
|
||||||
|
if (statement instanceof AttributeStatementType) {
|
||||||
|
AttributeStatementType attributeStatement = (AttributeStatementType) statement;
|
||||||
|
List<AttributeStatementType.ASTChoiceType> attList = attributeStatement.getAttributes();
|
||||||
|
for (AttributeStatementType.ASTChoiceType obj : attList) {
|
||||||
|
AttributeType attr = obj.getAttribute();
|
||||||
|
if (isRole(attr)) {
|
||||||
|
List<Object> attributeValues = attr.getAttributeValue();
|
||||||
|
if (attributeValues != null) {
|
||||||
|
for (Object attrValue : attributeValues) {
|
||||||
|
String role = getAttributeValue(attrValue);
|
||||||
|
log.debugv("Add role: {0}", role);
|
||||||
|
roles.add(role);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
List<Object> attributeValues = attr.getAttributeValue();
|
||||||
|
if (attributeValues != null) {
|
||||||
|
for (Object attrValue : attributeValues) {
|
||||||
|
String value = getAttributeValue(attrValue);
|
||||||
|
if (attr.getName() != null) {
|
||||||
|
attributes.add(attr.getName(), value);
|
||||||
|
}
|
||||||
|
if (attr.getFriendlyName() != null) {
|
||||||
|
friendlyAttributes.add(attr.getFriendlyName(), value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (deployment.getPrincipalNamePolicy() == SamlDeployment.PrincipalNamePolicy.FROM_ATTRIBUTE) {
|
||||||
|
if (deployment.getPrincipalAttributeName() != null) {
|
||||||
|
String attribute = attributes.getFirst(deployment.getPrincipalAttributeName());
|
||||||
|
if (attribute != null) principalName = attribute;
|
||||||
|
else {
|
||||||
|
attribute = friendlyAttributes.getFirst(deployment.getPrincipalAttributeName());
|
||||||
|
if (attribute != null) principalName = attribute;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AuthnStatementType authn = null;
|
||||||
|
for (Object statement : assertion.getStatements()) {
|
||||||
|
if (statement instanceof AuthnStatementType) {
|
||||||
|
authn = (AuthnStatementType) statement;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
URI nameFormat = subjectNameID.getFormat();
|
||||||
|
String nameFormatString = nameFormat == null ? JBossSAMLURIConstants.NAMEID_FORMAT_UNSPECIFIED.get() : nameFormat.toString();
|
||||||
|
final SamlPrincipal principal = new SamlPrincipal(assertion, principalName, principalName, nameFormatString, attributes, friendlyAttributes);
|
||||||
|
String index = authn == null ? null : authn.getSessionIndex();
|
||||||
|
final String sessionIndex = index;
|
||||||
|
SamlSession account = new SamlSession(principal, roles, sessionIndex);
|
||||||
|
sessionStore.saveAccount(account);
|
||||||
|
onCreateSession.onSessionCreated(account);
|
||||||
|
|
||||||
|
// redirect to original request, it will be restored
|
||||||
|
String redirectUri = sessionStore.getRedirectUri();
|
||||||
|
if (redirectUri != null) {
|
||||||
|
facade.getResponse().setHeader("Location", redirectUri);
|
||||||
|
facade.getResponse().setStatus(302);
|
||||||
|
facade.getResponse().end();
|
||||||
|
} else {
|
||||||
|
log.debug("IDP initiated invocation");
|
||||||
|
}
|
||||||
|
log.debug("AUTHENTICATED authn");
|
||||||
|
|
||||||
|
return AuthOutcome.AUTHENTICATED;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getAttributeValue(Object attrValue) {
|
||||||
|
String value = null;
|
||||||
|
if (attrValue instanceof String) {
|
||||||
|
value = (String) attrValue;
|
||||||
|
} else if (attrValue instanceof Node) {
|
||||||
|
Node roleNode = (Node) attrValue;
|
||||||
|
value = roleNode.getFirstChild().getNodeValue();
|
||||||
|
} else if (attrValue instanceof NameIDType) {
|
||||||
|
NameIDType nameIdType = (NameIDType) attrValue;
|
||||||
|
value = nameIdType.getValue();
|
||||||
|
} else {
|
||||||
|
log.warn("Unable to extract unknown SAML assertion attribute value type: " + attrValue.getClass().getName());
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected boolean isRole(AttributeType attribute) {
|
||||||
|
return (attribute.getName() != null && deployment.getRoleAttributeNames().contains(attribute.getName())) || (attribute.getFriendlyName() != null && deployment.getRoleAttributeNames().contains(attribute.getFriendlyName()));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected AuthOutcome handleLogoutResponse(SAMLDocumentHolder holder, StatusResponseType responseType, String relayState) {
|
||||||
|
boolean loggedIn = sessionStore.isLoggedIn();
|
||||||
|
if (!loggedIn || !"logout".equals(relayState)) {
|
||||||
|
return AuthOutcome.NOT_ATTEMPTED;
|
||||||
|
}
|
||||||
|
sessionStore.logoutAccount();
|
||||||
|
return AuthOutcome.LOGGED_OUT;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected SAMLDocumentHolder extractRedirectBindingResponse(String response) {
|
||||||
|
return SAMLRequestParser.parseRequestRedirectBinding(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
protected SAMLDocumentHolder extractPostBindingResponse(String response) {
|
||||||
|
byte[] samlBytes = PostBindingUtil.base64Decode(response);
|
||||||
|
return SAMLRequestParser.parseResponseDocument(samlBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
protected AuthOutcome initiateLogin() {
|
||||||
|
challenge = createChallenge();
|
||||||
|
return AuthOutcome.NOT_ATTEMPTED;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected AbstractInitiateLogin createChallenge() {
|
||||||
|
return new AbstractInitiateLogin(deployment, sessionStore) {
|
||||||
|
@Override
|
||||||
|
protected void sendAuthnRequest(HttpFacade httpFacade, SAML2AuthnRequestBuilder authnRequestBuilder, BaseSAML2BindingBuilder binding) throws ProcessingException, ConfigurationException, IOException {
|
||||||
|
Document document = authnRequestBuilder.toDocument();
|
||||||
|
SamlDeployment.Binding samlBinding = deployment.getIDP().getSingleSignOnService().getRequestBinding();
|
||||||
|
SamlUtil.sendSaml(true, httpFacade, deployment.getIDP().getSingleSignOnService().getRequestBindingUrl(), binding, document, samlBinding);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
protected boolean verifySSL() {
|
||||||
|
if (!facade.getRequest().isSecure() && deployment.getSslRequired().isRequired(facade.getRequest().getRemoteAddr())) {
|
||||||
|
log.warn("SSL is required to authenticate");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void verifyPostBindingSignature(Document document, PublicKey publicKey) throws VerificationException {
|
||||||
|
SAML2Signature saml2Signature = new SAML2Signature();
|
||||||
|
try {
|
||||||
|
if (!saml2Signature.validate(document, publicKey)) {
|
||||||
|
throw new VerificationException("Invalid signature on document");
|
||||||
|
}
|
||||||
|
} catch (ProcessingException e) {
|
||||||
|
throw new VerificationException("Error validating signature", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void verifyRedirectBindingSignature(PublicKey publicKey, String paramKey) throws VerificationException {
|
||||||
|
String request = facade.getRequest().getQueryParamValue(paramKey);
|
||||||
|
String algorithm = facade.getRequest().getQueryParamValue(GeneralConstants.SAML_SIG_ALG_REQUEST_KEY);
|
||||||
|
String signature = facade.getRequest().getQueryParamValue(GeneralConstants.SAML_SIGNATURE_REQUEST_KEY);
|
||||||
|
String decodedAlgorithm = facade.getRequest().getQueryParamValue(GeneralConstants.SAML_SIG_ALG_REQUEST_KEY);
|
||||||
|
|
||||||
|
if (request == null) {
|
||||||
|
throw new VerificationException("SAML Request was null");
|
||||||
|
}
|
||||||
|
if (algorithm == null) throw new VerificationException("SigAlg was null");
|
||||||
|
if (signature == null) throw new VerificationException("Signature was null");
|
||||||
|
|
||||||
|
// Shibboleth doesn't sign the document for redirect binding.
|
||||||
|
// todo maybe a flag?
|
||||||
|
|
||||||
|
String relayState = facade.getRequest().getQueryParamValue(GeneralConstants.RELAY_STATE);
|
||||||
|
KeycloakUriBuilder builder = KeycloakUriBuilder.fromPath("/")
|
||||||
|
.queryParam(paramKey, request);
|
||||||
|
if (relayState != null) {
|
||||||
|
builder.queryParam(GeneralConstants.RELAY_STATE, relayState);
|
||||||
|
}
|
||||||
|
builder.queryParam(GeneralConstants.SAML_SIG_ALG_REQUEST_KEY, algorithm);
|
||||||
|
String rawQuery = builder.build().getRawQuery();
|
||||||
|
|
||||||
|
try {
|
||||||
|
//byte[] decodedSignature = RedirectBindingUtil.urlBase64Decode(signature);
|
||||||
|
byte[] decodedSignature = Base64.decode(signature);
|
||||||
|
|
||||||
|
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.getFromXmlMethod(decodedAlgorithm);
|
||||||
|
Signature validator = signatureAlgorithm.createSignature(); // todo plugin signature alg
|
||||||
|
validator.initVerify(publicKey);
|
||||||
|
validator.update(rawQuery.getBytes("UTF-8"));
|
||||||
|
if (!validator.verify(decodedSignature)) {
|
||||||
|
throw new VerificationException("Invalid query param signature");
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new VerificationException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
package org.keycloak.adapters.saml.profile;
|
||||||
|
|
||||||
|
import org.keycloak.adapters.saml.OnSessionCreated;
|
||||||
|
import org.keycloak.adapters.spi.AuthChallenge;
|
||||||
|
import org.keycloak.adapters.spi.AuthOutcome;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
||||||
|
*/
|
||||||
|
public interface SamlAuthenticationHandler {
|
||||||
|
AuthOutcome handle(OnSessionCreated onCreateSession);
|
||||||
|
AuthChallenge getChallenge();
|
||||||
|
}
|
|
@ -0,0 +1,37 @@
|
||||||
|
package org.keycloak.adapters.saml.profile;
|
||||||
|
|
||||||
|
import org.keycloak.adapters.saml.SamlDeployment;
|
||||||
|
import org.keycloak.adapters.saml.SamlSessionStore;
|
||||||
|
import org.keycloak.adapters.spi.HttpFacade;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
||||||
|
*/
|
||||||
|
public class SamlInvocationContext {
|
||||||
|
|
||||||
|
private String samlRequest;
|
||||||
|
private String samlResponse;
|
||||||
|
private String relayState;
|
||||||
|
|
||||||
|
public SamlInvocationContext() {
|
||||||
|
this(null, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public SamlInvocationContext(String samlRequest, String samlResponse, String relayState) {
|
||||||
|
this.samlRequest = samlRequest;
|
||||||
|
this.samlResponse = samlResponse;
|
||||||
|
this.relayState = relayState;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getSamlRequest() {
|
||||||
|
return this.samlRequest;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getSamlResponse() {
|
||||||
|
return this.samlResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getRelayState() {
|
||||||
|
return this.relayState;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,146 @@
|
||||||
|
package org.keycloak.adapters.saml.profile.ecp;
|
||||||
|
|
||||||
|
import org.keycloak.adapters.saml.AbstractInitiateLogin;
|
||||||
|
import org.keycloak.adapters.saml.OnSessionCreated;
|
||||||
|
import org.keycloak.adapters.saml.SamlDeployment;
|
||||||
|
import org.keycloak.adapters.saml.SamlSessionStore;
|
||||||
|
import org.keycloak.adapters.saml.profile.AbstractSamlAuthenticationHandler;
|
||||||
|
import org.keycloak.adapters.saml.profile.SamlAuthenticationHandler;
|
||||||
|
import org.keycloak.adapters.saml.profile.SamlInvocationContext;
|
||||||
|
import org.keycloak.adapters.spi.AuthOutcome;
|
||||||
|
import org.keycloak.adapters.spi.HttpFacade;
|
||||||
|
import org.keycloak.dom.saml.v2.protocol.LogoutRequestType;
|
||||||
|
import org.keycloak.saml.BaseSAML2BindingBuilder;
|
||||||
|
import org.keycloak.saml.SAML2AuthnRequestBuilder;
|
||||||
|
import org.keycloak.saml.common.constants.JBossSAMLConstants;
|
||||||
|
import org.keycloak.saml.common.constants.JBossSAMLURIConstants;
|
||||||
|
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.Node;
|
||||||
|
|
||||||
|
import javax.xml.soap.MessageFactory;
|
||||||
|
import javax.xml.soap.SOAPBody;
|
||||||
|
import javax.xml.soap.SOAPEnvelope;
|
||||||
|
import javax.xml.soap.SOAPException;
|
||||||
|
import javax.xml.soap.SOAPHeader;
|
||||||
|
import javax.xml.soap.SOAPHeaderElement;
|
||||||
|
import javax.xml.soap.SOAPMessage;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
||||||
|
*/
|
||||||
|
public class EcpAuthenticationHandler extends AbstractSamlAuthenticationHandler {
|
||||||
|
|
||||||
|
public static final String PAOS_HEADER = "PAOS";
|
||||||
|
public static final String PAOS_CONTENT_TYPE = "application/vnd.paos+xml";
|
||||||
|
private static final String NS_PREFIX_PROFILE_ECP = "ecp";
|
||||||
|
private static final String NS_PREFIX_SAML_PROTOCOL = "samlp";
|
||||||
|
private static final String NS_PREFIX_SAML_ASSERTION = "saml";
|
||||||
|
private static final String NS_PREFIX_PAOS_BINDING = "paos";
|
||||||
|
|
||||||
|
public static boolean canHandle(HttpFacade httpFacade) {
|
||||||
|
HttpFacade.Request request = httpFacade.getRequest();
|
||||||
|
String acceptHeader = request.getHeader("Accept");
|
||||||
|
String contentTypeHeader = request.getHeader("Content-Type");
|
||||||
|
|
||||||
|
return (acceptHeader != null && acceptHeader.contains(PAOS_CONTENT_TYPE) && request.getHeader(PAOS_HEADER) != null)
|
||||||
|
|| (contentTypeHeader != null && contentTypeHeader.contains(PAOS_CONTENT_TYPE));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static SamlAuthenticationHandler create(HttpFacade facade, SamlDeployment deployment, SamlSessionStore sessionStore) {
|
||||||
|
return new EcpAuthenticationHandler(facade, deployment, sessionStore);
|
||||||
|
}
|
||||||
|
|
||||||
|
private EcpAuthenticationHandler(HttpFacade facade, SamlDeployment deployment, SamlSessionStore sessionStore) {
|
||||||
|
super(facade, deployment, sessionStore);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected AuthOutcome logoutRequest(LogoutRequestType request, String relayState) {
|
||||||
|
throw new RuntimeException("Not supported.");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AuthOutcome handle(OnSessionCreated onCreateSession) {
|
||||||
|
String header = facade.getRequest().getHeader(PAOS_HEADER);
|
||||||
|
|
||||||
|
if (header != null) {
|
||||||
|
return doHandle(new SamlInvocationContext(), onCreateSession);
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
MessageFactory messageFactory = MessageFactory.newInstance();
|
||||||
|
SOAPMessage soapMessage = messageFactory.createMessage(null, facade.getRequest().getInputStream());
|
||||||
|
SOAPBody soapBody = soapMessage.getSOAPBody();
|
||||||
|
Node authnRequestNode = soapBody.getFirstChild();
|
||||||
|
Document document = DocumentUtil.createDocument();
|
||||||
|
|
||||||
|
document.appendChild(document.importNode(authnRequestNode, true));
|
||||||
|
|
||||||
|
String samlResponse = PostBindingUtil.base64Encode(DocumentUtil.asString(document));
|
||||||
|
|
||||||
|
return doHandle(new SamlInvocationContext(null, samlResponse, null), onCreateSession);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException("Error creating fault message.", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected AbstractInitiateLogin createChallenge() {
|
||||||
|
return new AbstractInitiateLogin(deployment, sessionStore) {
|
||||||
|
@Override
|
||||||
|
protected void sendAuthnRequest(HttpFacade httpFacade, SAML2AuthnRequestBuilder authnRequestBuilder, BaseSAML2BindingBuilder binding) {
|
||||||
|
try {
|
||||||
|
MessageFactory messageFactory = MessageFactory.newInstance();
|
||||||
|
SOAPMessage message = messageFactory.createMessage();
|
||||||
|
|
||||||
|
SOAPEnvelope envelope = message.getSOAPPart().getEnvelope();
|
||||||
|
|
||||||
|
envelope.addNamespaceDeclaration(NS_PREFIX_SAML_ASSERTION, JBossSAMLURIConstants.ASSERTION_NSURI.get());
|
||||||
|
envelope.addNamespaceDeclaration(NS_PREFIX_SAML_PROTOCOL, JBossSAMLURIConstants.PROTOCOL_NSURI.get());
|
||||||
|
envelope.addNamespaceDeclaration(NS_PREFIX_PAOS_BINDING, JBossSAMLURIConstants.PAOS_BINDING.get());
|
||||||
|
envelope.addNamespaceDeclaration(NS_PREFIX_PROFILE_ECP, JBossSAMLURIConstants.ECP_PROFILE.get());
|
||||||
|
|
||||||
|
createPaosRequestHeader(envelope);
|
||||||
|
createEcpRequestHeader(envelope);
|
||||||
|
|
||||||
|
SOAPBody body = envelope.getBody();
|
||||||
|
|
||||||
|
body.addDocument(binding.postBinding(authnRequestBuilder.toDocument()).getDocument());
|
||||||
|
|
||||||
|
message.writeTo(httpFacade.getResponse().getOutputStream());
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException("Could not create AuthnRequest.", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createEcpRequestHeader(SOAPEnvelope envelope) throws SOAPException {
|
||||||
|
SOAPHeader headers = envelope.getHeader();
|
||||||
|
SOAPHeaderElement ecpRequestHeader = headers.addHeaderElement(envelope.createQName(JBossSAMLConstants.REQUEST.get(), NS_PREFIX_PROFILE_ECP));
|
||||||
|
|
||||||
|
ecpRequestHeader.setMustUnderstand(true);
|
||||||
|
ecpRequestHeader.setActor("http://schemas.xmlsoap.org/soap/actor/next");
|
||||||
|
ecpRequestHeader.addAttribute(envelope.createName("ProviderName"), deployment.getEntityID());
|
||||||
|
ecpRequestHeader.addAttribute(envelope.createName("IsPassive"), "0");
|
||||||
|
ecpRequestHeader.addChildElement(envelope.createQName("Issuer", "saml")).setValue(deployment.getEntityID());
|
||||||
|
ecpRequestHeader.addChildElement(envelope.createQName("IDPList", "samlp"))
|
||||||
|
.addChildElement(envelope.createQName("IDPEntry", "samlp"))
|
||||||
|
.addAttribute(envelope.createName("ProviderID"), deployment.getIDP().getEntityID())
|
||||||
|
.addAttribute(envelope.createName("Name"), deployment.getIDP().getEntityID())
|
||||||
|
.addAttribute(envelope.createName("Loc"), deployment.getIDP().getSingleSignOnService().getRequestBindingUrl());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createPaosRequestHeader(SOAPEnvelope envelope) throws SOAPException {
|
||||||
|
SOAPHeader headers = envelope.getHeader();
|
||||||
|
SOAPHeaderElement paosRequestHeader = headers.addHeaderElement(envelope.createQName(JBossSAMLConstants.REQUEST.get(), NS_PREFIX_PAOS_BINDING));
|
||||||
|
|
||||||
|
paosRequestHeader.setMustUnderstand(true);
|
||||||
|
paosRequestHeader.setActor("http://schemas.xmlsoap.org/soap/actor/next");
|
||||||
|
paosRequestHeader.addAttribute(envelope.createName("service"), JBossSAMLURIConstants.ECP_PROFILE.get());
|
||||||
|
paosRequestHeader.addAttribute(envelope.createName("responseConsumerURL"), deployment.getAssertionConsumerServiceUrl());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,111 @@
|
||||||
|
package org.keycloak.adapters.saml.profile.webbrowsersso;
|
||||||
|
|
||||||
|
import org.keycloak.adapters.saml.OnSessionCreated;
|
||||||
|
import org.keycloak.adapters.saml.SamlDeployment;
|
||||||
|
import org.keycloak.adapters.saml.SamlSession;
|
||||||
|
import org.keycloak.adapters.saml.SamlSessionStore;
|
||||||
|
import org.keycloak.adapters.saml.SamlUtil;
|
||||||
|
import org.keycloak.adapters.saml.profile.AbstractSamlAuthenticationHandler;
|
||||||
|
import org.keycloak.adapters.saml.profile.SamlAuthenticationHandler;
|
||||||
|
import org.keycloak.adapters.saml.profile.SamlInvocationContext;
|
||||||
|
import org.keycloak.adapters.spi.AuthOutcome;
|
||||||
|
import org.keycloak.adapters.spi.HttpFacade;
|
||||||
|
import org.keycloak.dom.saml.v2.protocol.LogoutRequestType;
|
||||||
|
import org.keycloak.saml.BaseSAML2BindingBuilder;
|
||||||
|
import org.keycloak.saml.SAML2LogoutRequestBuilder;
|
||||||
|
import org.keycloak.saml.SAML2LogoutResponseBuilder;
|
||||||
|
import org.keycloak.saml.common.constants.GeneralConstants;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
||||||
|
*/
|
||||||
|
public class WebBrowserSsoAuthenticationHandler extends AbstractSamlAuthenticationHandler {
|
||||||
|
|
||||||
|
public static SamlAuthenticationHandler create(HttpFacade facade, SamlDeployment deployment, SamlSessionStore sessionStore) {
|
||||||
|
return new WebBrowserSsoAuthenticationHandler(facade, deployment, sessionStore);
|
||||||
|
}
|
||||||
|
|
||||||
|
private WebBrowserSsoAuthenticationHandler(HttpFacade facade, SamlDeployment deployment, SamlSessionStore sessionStore) {
|
||||||
|
super(facade, deployment, sessionStore);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AuthOutcome handle(OnSessionCreated onCreateSession) {
|
||||||
|
return doHandle(new SamlInvocationContext(facade.getRequest().getFirstParam(GeneralConstants.SAML_REQUEST_KEY),
|
||||||
|
facade.getRequest().getFirstParam(GeneralConstants.SAML_RESPONSE_KEY),
|
||||||
|
facade.getRequest().getFirstParam(GeneralConstants.RELAY_STATE)), onCreateSession);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected AuthOutcome handleRequest() {
|
||||||
|
boolean globalLogout = "true".equals(facade.getRequest().getQueryParamValue("GLO"));
|
||||||
|
|
||||||
|
if (globalLogout) {
|
||||||
|
return globalLogout();
|
||||||
|
}
|
||||||
|
|
||||||
|
return AuthOutcome.AUTHENTICATED;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected AuthOutcome logoutRequest(LogoutRequestType request, String relayState) {
|
||||||
|
if (request.getSessionIndex() == null || request.getSessionIndex().isEmpty()) {
|
||||||
|
sessionStore.logoutByPrincipal(request.getNameID().getValue());
|
||||||
|
} else {
|
||||||
|
sessionStore.logoutBySsoId(request.getSessionIndex());
|
||||||
|
}
|
||||||
|
|
||||||
|
String issuerURL = deployment.getEntityID();
|
||||||
|
SAML2LogoutResponseBuilder builder = new SAML2LogoutResponseBuilder();
|
||||||
|
builder.logoutRequestID(request.getID());
|
||||||
|
builder.destination(deployment.getIDP().getSingleLogoutService().getResponseBindingUrl());
|
||||||
|
builder.issuer(issuerURL);
|
||||||
|
BaseSAML2BindingBuilder binding = new BaseSAML2BindingBuilder().relayState(relayState);
|
||||||
|
if (deployment.getIDP().getSingleLogoutService().signResponse()) {
|
||||||
|
binding.signatureAlgorithm(deployment.getSignatureAlgorithm())
|
||||||
|
.signWith(deployment.getSigningKeyPair())
|
||||||
|
.signDocument();
|
||||||
|
if (deployment.getSignatureCanonicalizationMethod() != null)
|
||||||
|
binding.canonicalizationMethod(deployment.getSignatureCanonicalizationMethod());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
try {
|
||||||
|
SamlUtil.sendSaml(false, facade, deployment.getIDP().getSingleLogoutService().getResponseBindingUrl(), binding, builder.buildDocument(),
|
||||||
|
deployment.getIDP().getSingleLogoutService().getResponseBinding());
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Could not send logout response SAML request", e);
|
||||||
|
return AuthOutcome.FAILED;
|
||||||
|
}
|
||||||
|
return AuthOutcome.NOT_ATTEMPTED;
|
||||||
|
}
|
||||||
|
|
||||||
|
private AuthOutcome globalLogout() {
|
||||||
|
SamlSession account = sessionStore.getAccount();
|
||||||
|
if (account == null) {
|
||||||
|
return AuthOutcome.NOT_ATTEMPTED;
|
||||||
|
}
|
||||||
|
SAML2LogoutRequestBuilder logoutBuilder = new SAML2LogoutRequestBuilder()
|
||||||
|
.assertionExpiration(30)
|
||||||
|
.issuer(deployment.getEntityID())
|
||||||
|
.sessionIndex(account.getSessionIndex())
|
||||||
|
.userPrincipal(account.getPrincipal().getSamlSubject(), account.getPrincipal().getNameIDFormat())
|
||||||
|
.destination(deployment.getIDP().getSingleLogoutService().getRequestBindingUrl());
|
||||||
|
BaseSAML2BindingBuilder binding = new BaseSAML2BindingBuilder();
|
||||||
|
if (deployment.getIDP().getSingleLogoutService().signRequest()) {
|
||||||
|
binding.signWith(deployment.getSigningKeyPair())
|
||||||
|
.signDocument();
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.relayState("logout");
|
||||||
|
|
||||||
|
try {
|
||||||
|
SamlUtil.sendSaml(true, facade, deployment.getIDP().getSingleLogoutService().getRequestBindingUrl(), binding, logoutBuilder.buildDocument(), deployment.getIDP().getSingleLogoutService().getRequestBinding());
|
||||||
|
sessionStore.setCurrentAction(SamlSessionStore.CurrentAction.LOGGING_OUT);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Could not send global logout SAML request", e);
|
||||||
|
return AuthOutcome.FAILED;
|
||||||
|
}
|
||||||
|
return AuthOutcome.NOT_ATTEMPTED;
|
||||||
|
}
|
||||||
|
}
|
|
@ -65,7 +65,8 @@ public enum JBossSAMLConstants {
|
||||||
"XACMLAuthzDecisionQuery"), XACML_AUTHZ_DECISION_QUERY_TYPE("XACMLAuthzDecisionQueryType"), XACML_AUTHZ_DECISION_STATEMENT_TYPE(
|
"XACMLAuthzDecisionQuery"), XACML_AUTHZ_DECISION_QUERY_TYPE("XACMLAuthzDecisionQueryType"), XACML_AUTHZ_DECISION_STATEMENT_TYPE(
|
||||||
"XACMLAuthzDecisionStatementType"), HTTP_POST_BINDING("urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"), ONE_TIME_USE ("OneTimeUse"),
|
"XACMLAuthzDecisionStatementType"), HTTP_POST_BINDING("urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"), ONE_TIME_USE ("OneTimeUse"),
|
||||||
UNSOLICITED_RESPONSE_TARGET("TARGET"), UNSOLICITED_RESPONSE_SAML_VERSION("SAML_VERSION"), UNSOLICITED_RESPONSE_SAML_BINDING("SAML_BINDING"),
|
UNSOLICITED_RESPONSE_TARGET("TARGET"), UNSOLICITED_RESPONSE_SAML_VERSION("SAML_VERSION"), UNSOLICITED_RESPONSE_SAML_BINDING("SAML_BINDING"),
|
||||||
ROLE_DESCRIPTOR("RoleDescriptor");
|
ROLE_DESCRIPTOR("RoleDescriptor"),
|
||||||
|
REQUEST_AUTHENTICATED("RequestAuthenticated");
|
||||||
|
|
||||||
private String name;
|
private String name;
|
||||||
|
|
||||||
|
|
|
@ -73,12 +73,15 @@ public enum JBossSAMLURIConstants {
|
||||||
"urn:oasis:names:tc:SAML:2.0:nameid-format:entity"),
|
"urn:oasis:names:tc:SAML:2.0:nameid-format:entity"),
|
||||||
|
|
||||||
PROTOCOL_NSURI("urn:oasis:names:tc:SAML:2.0:protocol"),
|
PROTOCOL_NSURI("urn:oasis:names:tc:SAML:2.0:protocol"),
|
||||||
|
ECP_PROFILE("urn:oasis:names:tc:SAML:2.0:profiles:SSO:ecp"),
|
||||||
|
PAOS_BINDING("urn:liberty:paos:2003-08"),
|
||||||
|
|
||||||
SIGNATURE_DSA_SHA1("http://www.w3.org/2000/09/xmldsig#dsa-sha1"), SIGNATURE_RSA_SHA1(
|
SIGNATURE_DSA_SHA1("http://www.w3.org/2000/09/xmldsig#dsa-sha1"), SIGNATURE_RSA_SHA1(
|
||||||
"http://www.w3.org/2000/09/xmldsig#rsa-sha1"),
|
"http://www.w3.org/2000/09/xmldsig#rsa-sha1"),
|
||||||
|
|
||||||
SAML_HTTP_POST_BINDING("urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"), SAML_HTTP_REDIRECT_BINDING(
|
SAML_HTTP_POST_BINDING("urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"),
|
||||||
"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"),
|
SAML_HTTP_SOAP_BINDING("urn:oasis:names:tc:SAML:2.0:bindings:SOAP"),
|
||||||
|
SAML_HTTP_REDIRECT_BINDING("urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"),
|
||||||
|
|
||||||
SAML_11_NS("urn:oasis:names:tc:SAML:1.0:assertion"),
|
SAML_11_NS("urn:oasis:names:tc:SAML:1.0:assertion"),
|
||||||
|
|
||||||
|
|
|
@ -84,6 +84,7 @@ public class SamlProtocol implements LoginProtocol {
|
||||||
public static final String SAML_BINDING = "saml_binding";
|
public static final String SAML_BINDING = "saml_binding";
|
||||||
public static final String SAML_IDP_INITIATED_LOGIN = "saml_idp_initiated_login";
|
public static final String SAML_IDP_INITIATED_LOGIN = "saml_idp_initiated_login";
|
||||||
public static final String SAML_POST_BINDING = "post";
|
public static final String SAML_POST_BINDING = "post";
|
||||||
|
public static final String SAML_SOAP_BINDING = "soap";
|
||||||
public static final String SAML_REDIRECT_BINDING = "get";
|
public static final String SAML_REDIRECT_BINDING = "get";
|
||||||
public static final String SAML_SERVER_SIGNATURE = "saml.server.signature";
|
public static final String SAML_SERVER_SIGNATURE = "saml.server.signature";
|
||||||
public static final String SAML_ASSERTION_SIGNATURE = "saml.assertion.signature";
|
public static final String SAML_ASSERTION_SIGNATURE = "saml.assertion.signature";
|
||||||
|
@ -165,11 +166,7 @@ public class SamlProtocol implements LoginProtocol {
|
||||||
try {
|
try {
|
||||||
JaxrsSAML2BindingBuilder binding = new JaxrsSAML2BindingBuilder().relayState(clientSession.getNote(GeneralConstants.RELAY_STATE));
|
JaxrsSAML2BindingBuilder binding = new JaxrsSAML2BindingBuilder().relayState(clientSession.getNote(GeneralConstants.RELAY_STATE));
|
||||||
Document document = builder.buildDocument();
|
Document document = builder.buildDocument();
|
||||||
if (isPostBinding(clientSession)) {
|
return buildErrorResponse(clientSession, binding, document);
|
||||||
return binding.postBinding(document).response(clientSession.getRedirectUri());
|
|
||||||
} else {
|
|
||||||
return binding.redirectBinding(document).response(clientSession.getRedirectUri());
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
return ErrorPage.error(session, Messages.FAILED_TO_PROCESS_RESPONSE);
|
return ErrorPage.error(session, Messages.FAILED_TO_PROCESS_RESPONSE);
|
||||||
}
|
}
|
||||||
|
@ -180,6 +177,14 @@ public class SamlProtocol implements LoginProtocol {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected Response buildErrorResponse(ClientSessionModel clientSession, JaxrsSAML2BindingBuilder binding, Document document) throws ConfigurationException, ProcessingException, IOException {
|
||||||
|
if (isPostBinding(clientSession)) {
|
||||||
|
return binding.postBinding(document).response(clientSession.getRedirectUri());
|
||||||
|
} else {
|
||||||
|
return binding.redirectBinding(document).response(clientSession.getRedirectUri());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private JBossSAMLURIConstants translateErrorToSAMLStatus(Error error) {
|
private JBossSAMLURIConstants translateErrorToSAMLStatus(Error error) {
|
||||||
switch (error) {
|
switch (error) {
|
||||||
case CANCELLED_BY_USER:
|
case CANCELLED_BY_USER:
|
||||||
|
@ -390,17 +395,21 @@ public class SamlProtocol implements LoginProtocol {
|
||||||
bindingBuilder.encrypt(publicKey);
|
bindingBuilder.encrypt(publicKey);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
if (isPostBinding(clientSession)) {
|
return buildAuthenticatedResponse(clientSession, redirectUri, samlDocument, bindingBuilder);
|
||||||
return bindingBuilder.postBinding(samlDocument).response(redirectUri);
|
|
||||||
} else {
|
|
||||||
return bindingBuilder.redirectBinding(samlDocument).response(redirectUri);
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
logger.error("failed", e);
|
logger.error("failed", e);
|
||||||
return ErrorPage.error(session, Messages.FAILED_TO_PROCESS_RESPONSE);
|
return ErrorPage.error(session, Messages.FAILED_TO_PROCESS_RESPONSE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected Response buildAuthenticatedResponse(ClientSessionModel clientSession, String redirectUri, Document samlDocument, JaxrsSAML2BindingBuilder bindingBuilder) throws ConfigurationException, ProcessingException, IOException {
|
||||||
|
if (isPostBinding(clientSession)) {
|
||||||
|
return bindingBuilder.postBinding(samlDocument).response(redirectUri);
|
||||||
|
} else {
|
||||||
|
return bindingBuilder.redirectBinding(samlDocument).response(redirectUri);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static boolean requiresRealmSignature(ClientModel client) {
|
public static boolean requiresRealmSignature(ClientModel client) {
|
||||||
return "true".equals(client.getAttribute(SAML_SERVER_SIGNATURE));
|
return "true".equals(client.getAttribute(SAML_SERVER_SIGNATURE));
|
||||||
}
|
}
|
||||||
|
@ -544,11 +553,7 @@ public class SamlProtocol implements LoginProtocol {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (isLogoutPostBindingForInitiator(userSession)) {
|
return buildLogoutResponse(userSession, logoutBindingUri, builder, binding);
|
||||||
return binding.postBinding(builder.buildDocument()).response(logoutBindingUri);
|
|
||||||
} else {
|
|
||||||
return binding.redirectBinding(builder.buildDocument()).response(logoutBindingUri);
|
|
||||||
}
|
|
||||||
} catch (ConfigurationException e) {
|
} catch (ConfigurationException e) {
|
||||||
throw new RuntimeException(e);
|
throw new RuntimeException(e);
|
||||||
} catch (ProcessingException e) {
|
} catch (ProcessingException e) {
|
||||||
|
@ -558,6 +563,14 @@ public class SamlProtocol implements LoginProtocol {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected Response buildLogoutResponse(UserSessionModel userSession, String logoutBindingUri, SAML2LogoutResponseBuilder builder, JaxrsSAML2BindingBuilder binding) throws ConfigurationException, ProcessingException, IOException {
|
||||||
|
if (isLogoutPostBindingForInitiator(userSession)) {
|
||||||
|
return binding.postBinding(builder.buildDocument()).response(logoutBindingUri);
|
||||||
|
} else {
|
||||||
|
return binding.redirectBinding(builder.buildDocument()).response(logoutBindingUri);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void backchannelLogout(UserSessionModel userSession, ClientSessionModel clientSession) {
|
public void backchannelLogout(UserSessionModel userSession, ClientSessionModel clientSession) {
|
||||||
ClientModel client = clientSession.getClient();
|
ClientModel client = clientSession.getClient();
|
||||||
|
|
|
@ -42,7 +42,7 @@ public class SamlProtocolFactory extends AbstractLoginProtocolFactory {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getId() {
|
public String getId() {
|
||||||
return "saml";
|
return SamlProtocol.LOGIN_PROTOCOL;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -90,8 +90,9 @@ public class SamlProtocolFactory extends AbstractLoginProtocolFactory {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void addDefaults(ClientModel client) {
|
protected void addDefaults(ClientModel client) {
|
||||||
for (ProtocolMapperModel model : defaultBuiltins) client.addProtocolMapper(model);
|
for (ProtocolMapperModel model : defaultBuiltins) {
|
||||||
|
model.setProtocol(getId());
|
||||||
|
client.addProtocolMapper(model);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,6 +16,7 @@ import javax.ws.rs.core.MediaType;
|
||||||
import javax.ws.rs.core.Response;
|
import javax.ws.rs.core.Response;
|
||||||
|
|
||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
|
import org.jboss.resteasy.spi.ResteasyProviderFactory;
|
||||||
import org.keycloak.common.VerificationException;
|
import org.keycloak.common.VerificationException;
|
||||||
import org.keycloak.common.util.StreamUtil;
|
import org.keycloak.common.util.StreamUtil;
|
||||||
import org.keycloak.dom.saml.v2.SAML2Object;
|
import org.keycloak.dom.saml.v2.SAML2Object;
|
||||||
|
@ -34,7 +35,9 @@ import org.keycloak.models.RealmModel;
|
||||||
import org.keycloak.models.UserSessionModel;
|
import org.keycloak.models.UserSessionModel;
|
||||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||||
import org.keycloak.protocol.AuthorizationEndpointBase;
|
import org.keycloak.protocol.AuthorizationEndpointBase;
|
||||||
|
import org.keycloak.protocol.LoginProtocol;
|
||||||
import org.keycloak.protocol.oidc.utils.RedirectUtils;
|
import org.keycloak.protocol.oidc.utils.RedirectUtils;
|
||||||
|
import org.keycloak.protocol.saml.profile.ecp.SamlEcpProfileService;
|
||||||
import org.keycloak.saml.SAML2LogoutResponseBuilder;
|
import org.keycloak.saml.SAML2LogoutResponseBuilder;
|
||||||
import org.keycloak.saml.SAMLRequestParser;
|
import org.keycloak.saml.SAMLRequestParser;
|
||||||
import org.keycloak.saml.SignatureAlgorithm;
|
import org.keycloak.saml.SignatureAlgorithm;
|
||||||
|
@ -221,7 +224,7 @@ public class SamlService extends AuthorizationEndpointBase {
|
||||||
}
|
}
|
||||||
|
|
||||||
ClientSessionModel clientSession = session.sessions().createClientSession(realm, client);
|
ClientSessionModel clientSession = session.sessions().createClientSession(realm, client);
|
||||||
clientSession.setAuthMethod(SamlProtocol.LOGIN_PROTOCOL);
|
clientSession.setAuthMethod(getLoginProtocol());
|
||||||
clientSession.setRedirectUri(redirect);
|
clientSession.setRedirectUri(redirect);
|
||||||
clientSession.setAction(ClientSessionModel.Action.AUTHENTICATE.name());
|
clientSession.setAction(ClientSessionModel.Action.AUTHENTICATE.name());
|
||||||
clientSession.setNote(ClientSessionCode.ACTION_KEY, KeycloakModelUtils.generateCodeSecret());
|
clientSession.setNote(ClientSessionCode.ACTION_KEY, KeycloakModelUtils.generateCodeSecret());
|
||||||
|
@ -246,7 +249,7 @@ public class SamlService extends AuthorizationEndpointBase {
|
||||||
return newBrowserAuthentication(clientSession, requestAbstractType.isIsPassive());
|
return newBrowserAuthentication(clientSession, requestAbstractType.isIsPassive());
|
||||||
}
|
}
|
||||||
|
|
||||||
private String getBindingType(AuthnRequestType requestAbstractType) {
|
protected String getBindingType(AuthnRequestType requestAbstractType) {
|
||||||
URI requestedProtocolBinding = requestAbstractType.getProtocolBinding();
|
URI requestedProtocolBinding = requestAbstractType.getProtocolBinding();
|
||||||
|
|
||||||
if (requestedProtocolBinding != null) {
|
if (requestedProtocolBinding != null) {
|
||||||
|
@ -370,7 +373,7 @@ public class SamlService extends AuthorizationEndpointBase {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected class PostBindingProtocol extends BindingProtocol {
|
public class PostBindingProtocol extends BindingProtocol {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void verifySignature(SAMLDocumentHolder documentHolder, ClientModel client) throws VerificationException {
|
protected void verifySignature(SAMLDocumentHolder documentHolder, ClientModel client) throws VerificationException {
|
||||||
|
@ -443,7 +446,12 @@ public class SamlService extends AuthorizationEndpointBase {
|
||||||
}
|
}
|
||||||
|
|
||||||
protected Response newBrowserAuthentication(ClientSessionModel clientSession, boolean isPassive) {
|
protected Response newBrowserAuthentication(ClientSessionModel clientSession, boolean isPassive) {
|
||||||
return handleBrowserAuthenticationRequest(clientSession, new SamlProtocol().setEventBuilder(event).setHttpHeaders(headers).setRealm(realm).setSession(session).setUriInfo(uriInfo), isPassive);
|
LoginProtocol protocol = session.getProvider(LoginProtocol.class, clientSession.getAuthMethod());
|
||||||
|
protocol.setRealm(realm)
|
||||||
|
.setHttpHeaders(request.getHttpHeaders())
|
||||||
|
.setUriInfo(uriInfo)
|
||||||
|
.setEventBuilder(event);
|
||||||
|
return handleBrowserAuthenticationRequest(clientSession, protocol, isPassive);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -463,6 +471,16 @@ public class SamlService extends AuthorizationEndpointBase {
|
||||||
return new PostBindingProtocol().execute(samlRequest, samlResponse, relayState);
|
return new PostBindingProtocol().execute(samlRequest, samlResponse, relayState);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Consumes("application/soap+xml")
|
||||||
|
public Response soapBinding(InputStream inputStream) {
|
||||||
|
SamlEcpProfileService bindingService = new SamlEcpProfileService(realm, event, authManager);
|
||||||
|
|
||||||
|
ResteasyProviderFactory.getInstance().injectProperties(bindingService);
|
||||||
|
|
||||||
|
return bindingService.authenticate(inputStream);
|
||||||
|
}
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
@Path("descriptor")
|
@Path("descriptor")
|
||||||
@Produces(MediaType.APPLICATION_XML)
|
@Produces(MediaType.APPLICATION_XML)
|
||||||
|
@ -519,7 +537,7 @@ public class SamlService extends AuthorizationEndpointBase {
|
||||||
}
|
}
|
||||||
|
|
||||||
ClientSessionModel clientSession = session.sessions().createClientSession(realm, client);
|
ClientSessionModel clientSession = session.sessions().createClientSession(realm, client);
|
||||||
clientSession.setAuthMethod(SamlProtocol.LOGIN_PROTOCOL);
|
clientSession.setAuthMethod(getLoginProtocol());
|
||||||
clientSession.setAction(ClientSessionModel.Action.AUTHENTICATE.name());
|
clientSession.setAction(ClientSessionModel.Action.AUTHENTICATE.name());
|
||||||
clientSession.setNote(ClientSessionCode.ACTION_KEY, KeycloakModelUtils.generateCodeSecret());
|
clientSession.setNote(ClientSessionCode.ACTION_KEY, KeycloakModelUtils.generateCodeSecret());
|
||||||
clientSession.setNote(SamlProtocol.SAML_BINDING, SamlProtocol.SAML_POST_BINDING);
|
clientSession.setNote(SamlProtocol.SAML_BINDING, SamlProtocol.SAML_POST_BINDING);
|
||||||
|
@ -537,4 +555,8 @@ public class SamlService extends AuthorizationEndpointBase {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected String getLoginProtocol() {
|
||||||
|
return SamlProtocol.LOGIN_PROTOCOL;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,109 @@
|
||||||
|
package org.keycloak.protocol.saml.profile.ecp;
|
||||||
|
|
||||||
|
import org.keycloak.Config;
|
||||||
|
import org.keycloak.events.EventBuilder;
|
||||||
|
import org.keycloak.models.ClientModel;
|
||||||
|
import org.keycloak.models.ClientSessionModel;
|
||||||
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
import org.keycloak.models.RealmModel;
|
||||||
|
import org.keycloak.models.UserSessionModel;
|
||||||
|
import org.keycloak.protocol.LoginProtocol;
|
||||||
|
import org.keycloak.protocol.saml.JaxrsSAML2BindingBuilder;
|
||||||
|
import org.keycloak.protocol.saml.SamlProtocol;
|
||||||
|
import org.keycloak.protocol.saml.SamlProtocolFactory;
|
||||||
|
import org.keycloak.protocol.saml.profile.ecp.util.Soap;
|
||||||
|
import org.keycloak.protocol.saml.profile.ecp.util.Soap.SoapMessageBuilder;
|
||||||
|
import org.keycloak.saml.SAML2LogoutResponseBuilder;
|
||||||
|
import org.keycloak.saml.common.constants.JBossSAMLConstants;
|
||||||
|
import org.keycloak.saml.common.constants.JBossSAMLURIConstants;
|
||||||
|
import org.keycloak.saml.common.exceptions.ConfigurationException;
|
||||||
|
import org.keycloak.saml.common.exceptions.ProcessingException;
|
||||||
|
import org.keycloak.services.managers.AuthenticationManager;
|
||||||
|
import org.w3c.dom.Document;
|
||||||
|
|
||||||
|
import javax.ws.rs.core.Response;
|
||||||
|
import javax.xml.soap.SOAPException;
|
||||||
|
import javax.xml.soap.SOAPHeaderElement;
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
||||||
|
*/
|
||||||
|
public class SamlEcpProfileProtocolFactory extends SamlProtocolFactory {
|
||||||
|
|
||||||
|
static final String ID = "saml-ecp-profile";
|
||||||
|
|
||||||
|
private static final String NS_PREFIX_PROFILE_ECP = "ecp";
|
||||||
|
private static final String NS_PREFIX_SAML_PROTOCOL = "samlp";
|
||||||
|
private static final String NS_PREFIX_SAML_ASSERTION = "saml";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object createProtocolEndpoint(RealmModel realm, EventBuilder event, AuthenticationManager authManager) {
|
||||||
|
return new SamlEcpProfileService(realm, event, authManager);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public LoginProtocol create(KeycloakSession session) {
|
||||||
|
return new SamlProtocol() {
|
||||||
|
// method created to send a SOAP Binding response instead of a HTTP POST response
|
||||||
|
@Override
|
||||||
|
protected Response buildAuthenticatedResponse(ClientSessionModel clientSession, String redirectUri, Document samlDocument, JaxrsSAML2BindingBuilder bindingBuilder) throws ConfigurationException, ProcessingException, IOException {
|
||||||
|
Document document = bindingBuilder.postBinding(samlDocument).getDocument();
|
||||||
|
|
||||||
|
try {
|
||||||
|
SoapMessageBuilder messageBuilder = Soap.createMessage()
|
||||||
|
.addNamespace(NS_PREFIX_SAML_ASSERTION, JBossSAMLURIConstants.ASSERTION_NSURI.get())
|
||||||
|
.addNamespace(NS_PREFIX_SAML_PROTOCOL, JBossSAMLURIConstants.PROTOCOL_NSURI.get())
|
||||||
|
.addNamespace(NS_PREFIX_PROFILE_ECP, JBossSAMLURIConstants.ECP_PROFILE.get());
|
||||||
|
|
||||||
|
createEcpResponseHeader(redirectUri, messageBuilder);
|
||||||
|
createRequestAuthenticatedHeader(clientSession, messageBuilder);
|
||||||
|
|
||||||
|
messageBuilder.addToBody(document);
|
||||||
|
|
||||||
|
return messageBuilder.build();
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException("Error while creating SAML response.", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createRequestAuthenticatedHeader(ClientSessionModel clientSession, SoapMessageBuilder messageBuilder) {
|
||||||
|
ClientModel client = clientSession.getClient();
|
||||||
|
|
||||||
|
if ("true".equals(client.getAttribute(SamlProtocol.SAML_CLIENT_SIGNATURE_ATTRIBUTE))) {
|
||||||
|
SOAPHeaderElement ecpRequestAuthenticated = messageBuilder.addHeader(JBossSAMLConstants.REQUEST_AUTHENTICATED.get(), NS_PREFIX_PROFILE_ECP);
|
||||||
|
|
||||||
|
ecpRequestAuthenticated.setMustUnderstand(true);
|
||||||
|
ecpRequestAuthenticated.setActor("http://schemas.xmlsoap.org/soap/actor/next");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createEcpResponseHeader(String redirectUri, SoapMessageBuilder messageBuilder) throws SOAPException {
|
||||||
|
SOAPHeaderElement ecpResponseHeader = messageBuilder.addHeader(JBossSAMLConstants.RESPONSE.get(), NS_PREFIX_PROFILE_ECP);
|
||||||
|
|
||||||
|
ecpResponseHeader.setMustUnderstand(true);
|
||||||
|
ecpResponseHeader.setActor("http://schemas.xmlsoap.org/soap/actor/next");
|
||||||
|
ecpResponseHeader.addAttribute(messageBuilder.createName(JBossSAMLConstants.ASSERTION_CONSUMER_SERVICE_URL.get()), redirectUri);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Response buildErrorResponse(ClientSessionModel clientSession, JaxrsSAML2BindingBuilder binding, Document document) throws ConfigurationException, ProcessingException, IOException {
|
||||||
|
return Soap.createMessage().addToBody(document).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Response buildLogoutResponse(UserSessionModel userSession, String logoutBindingUri, SAML2LogoutResponseBuilder builder, JaxrsSAML2BindingBuilder binding) throws ConfigurationException, ProcessingException, IOException {
|
||||||
|
return Soap.createFault().reason("Logout not supported.").build();
|
||||||
|
}
|
||||||
|
}.setSession(session);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void init(Config.Scope config) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getId() {
|
||||||
|
return ID;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,70 @@
|
||||||
|
package org.keycloak.protocol.saml.profile.ecp;
|
||||||
|
|
||||||
|
import org.keycloak.dom.saml.v2.protocol.AuthnRequestType;
|
||||||
|
import org.keycloak.events.EventBuilder;
|
||||||
|
import org.keycloak.models.AuthenticationFlowModel;
|
||||||
|
import org.keycloak.models.ClientModel;
|
||||||
|
import org.keycloak.models.ClientSessionModel;
|
||||||
|
import org.keycloak.models.RealmModel;
|
||||||
|
import org.keycloak.models.utils.DefaultAuthenticationFlows;
|
||||||
|
import org.keycloak.protocol.saml.SamlProtocol;
|
||||||
|
import org.keycloak.protocol.saml.SamlService;
|
||||||
|
import org.keycloak.protocol.saml.profile.ecp.util.Soap;
|
||||||
|
import org.keycloak.services.managers.AuthenticationManager;
|
||||||
|
|
||||||
|
import javax.ws.rs.core.Response;
|
||||||
|
import java.io.InputStream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
||||||
|
*/
|
||||||
|
public class SamlEcpProfileService extends SamlService {
|
||||||
|
|
||||||
|
public SamlEcpProfileService(RealmModel realm, EventBuilder event, AuthenticationManager authManager) {
|
||||||
|
super(realm, event, authManager);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Response authenticate(InputStream inputStream) {
|
||||||
|
try {
|
||||||
|
return new PostBindingProtocol() {
|
||||||
|
@Override
|
||||||
|
protected String getBindingType(AuthnRequestType requestAbstractType) {
|
||||||
|
return SamlProtocol.SAML_SOAP_BINDING;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Response loginRequest(String relayState, AuthnRequestType requestAbstractType, ClientModel client) {
|
||||||
|
// force passive authentication when executing this profile
|
||||||
|
requestAbstractType.setIsPassive(true);
|
||||||
|
requestAbstractType.setDestination(uriInfo.getAbsolutePath());
|
||||||
|
return super.loginRequest(relayState, requestAbstractType, client);
|
||||||
|
}
|
||||||
|
}.execute(Soap.toSamlHttpPostMessage(inputStream), null, null);
|
||||||
|
} catch (Exception e) {
|
||||||
|
String reason = "Some error occurred while processing the AuthnRequest.";
|
||||||
|
String detail = e.getMessage();
|
||||||
|
|
||||||
|
if (detail == null) {
|
||||||
|
detail = reason;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Soap.createFault().reason(reason).detail(detail).build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String getLoginProtocol() {
|
||||||
|
return SamlEcpProfileProtocolFactory.ID;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected AuthenticationFlowModel getAuthenticationFlow() {
|
||||||
|
for (AuthenticationFlowModel flowModel : realm.getAuthenticationFlows()) {
|
||||||
|
if (flowModel.getAlias().equals(DefaultAuthenticationFlows.SAML_ECP_FLOW)) {
|
||||||
|
return flowModel;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new RuntimeException("Could not resolve authentication flow for SAML ECP Profile.");
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,174 @@
|
||||||
|
package org.keycloak.protocol.saml.profile.ecp.authenticator;
|
||||||
|
|
||||||
|
import org.jboss.resteasy.spi.HttpRequest;
|
||||||
|
import org.keycloak.Config;
|
||||||
|
import org.keycloak.authentication.AuthenticationFlowContext;
|
||||||
|
import org.keycloak.authentication.AuthenticationFlowError;
|
||||||
|
import org.keycloak.authentication.Authenticator;
|
||||||
|
import org.keycloak.authentication.AuthenticatorFactory;
|
||||||
|
import org.keycloak.common.util.Base64;
|
||||||
|
import org.keycloak.events.Errors;
|
||||||
|
import org.keycloak.models.*;
|
||||||
|
import org.keycloak.models.AuthenticationExecutionModel.Requirement;
|
||||||
|
import org.keycloak.provider.ProviderConfigProperty;
|
||||||
|
import org.keycloak.representations.AccessTokenResponse;
|
||||||
|
import org.keycloak.representations.idm.CredentialRepresentation;
|
||||||
|
|
||||||
|
import javax.ws.rs.core.HttpHeaders;
|
||||||
|
import javax.ws.rs.core.Response;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
||||||
|
*/
|
||||||
|
public class HttpBasicAuthenticator implements AuthenticatorFactory {
|
||||||
|
|
||||||
|
public static final String PROVIDER_ID = "http-basic-authenticator";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getDisplayType() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getReferenceCategory() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isConfigurable() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Requirement[] getRequirementChoices() {
|
||||||
|
return new Requirement[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isUserSetupAllowed() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getHelpText() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<ProviderConfigProperty> getConfigProperties() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Authenticator create(KeycloakSession session) {
|
||||||
|
return new Authenticator() {
|
||||||
|
|
||||||
|
private static final String BASIC = "Basic";
|
||||||
|
private static final String BASIC_PREFIX = BASIC + " ";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void authenticate(AuthenticationFlowContext context) {
|
||||||
|
HttpRequest httpRequest = context.getHttpRequest();
|
||||||
|
HttpHeaders httpHeaders = httpRequest.getHttpHeaders();
|
||||||
|
String[] usernameAndPassword = getUsernameAndPassword(httpHeaders);
|
||||||
|
|
||||||
|
context.attempted();
|
||||||
|
|
||||||
|
if (usernameAndPassword != null) {
|
||||||
|
RealmModel realm = context.getRealm();
|
||||||
|
UserModel user = context.getSession().users().getUserByUsername(usernameAndPassword[0], realm);
|
||||||
|
|
||||||
|
if (user != null) {
|
||||||
|
String password = usernameAndPassword[1];
|
||||||
|
boolean valid = context.getSession().users().validCredentials(context.getSession(), realm, user, UserCredentialModel.password(password));
|
||||||
|
|
||||||
|
if (valid) {
|
||||||
|
context.getClientSession().setAuthenticatedUser(user);
|
||||||
|
context.success();
|
||||||
|
} else {
|
||||||
|
context.getEvent().user(user);
|
||||||
|
context.getEvent().error(Errors.INVALID_USER_CREDENTIALS);
|
||||||
|
context.failure(AuthenticationFlowError.INVALID_USER, Response.status(Response.Status.UNAUTHORIZED)
|
||||||
|
.header(HttpHeaders.WWW_AUTHENTICATE, BASIC_PREFIX + "realm=\"" + realm.getName() + "\"")
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String[] getUsernameAndPassword(HttpHeaders httpHeaders) {
|
||||||
|
List<String> authHeaders = httpHeaders.getRequestHeader(HttpHeaders.AUTHORIZATION);
|
||||||
|
|
||||||
|
if (authHeaders == null || authHeaders.size() == 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String credentials = null;
|
||||||
|
|
||||||
|
for (String authHeader : authHeaders) {
|
||||||
|
if (authHeader.startsWith(BASIC_PREFIX)) {
|
||||||
|
String[] split = authHeader.trim().split("\\s+");
|
||||||
|
|
||||||
|
if (split == null || split.length != 2) return null;
|
||||||
|
|
||||||
|
credentials = split[1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return new String(Base64.decode(credentials)).split(":");
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException("Failed to parse credentials.", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void action(AuthenticationFlowContext context) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean requiresUser() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setRequiredActions(KeycloakSession session, RealmModel realm, UserModel user) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void init(Config.Scope config) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void postInit(KeycloakSessionFactory factory) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getId() {
|
||||||
|
return PROVIDER_ID;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,177 @@
|
||||||
|
package org.keycloak.protocol.saml.profile.ecp.util;
|
||||||
|
|
||||||
|
import org.keycloak.saml.common.constants.JBossSAMLConstants;
|
||||||
|
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.Node;
|
||||||
|
|
||||||
|
import javax.ws.rs.core.Response;
|
||||||
|
import javax.ws.rs.core.Response.Status;
|
||||||
|
import javax.xml.soap.MessageFactory;
|
||||||
|
import javax.xml.soap.Name;
|
||||||
|
import javax.xml.soap.SOAPBody;
|
||||||
|
import javax.xml.soap.SOAPEnvelope;
|
||||||
|
import javax.xml.soap.SOAPException;
|
||||||
|
import javax.xml.soap.SOAPFault;
|
||||||
|
import javax.xml.soap.SOAPHeaderElement;
|
||||||
|
import javax.xml.soap.SOAPMessage;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
||||||
|
*/
|
||||||
|
public final class Soap {
|
||||||
|
|
||||||
|
public static SoapFaultBuilder createFault() {
|
||||||
|
return new SoapFaultBuilder();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static SoapMessageBuilder createMessage() {
|
||||||
|
return new SoapMessageBuilder();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>Returns a string encoded accordingly with the SAML HTTP POST Binding specification based on the
|
||||||
|
* given <code>inputStream</code> which must contain a valid SOAP message.
|
||||||
|
*
|
||||||
|
* <p>The resulting string is based on the Body of the SOAP message, which should map to a valid SAML message.
|
||||||
|
*
|
||||||
|
* @param inputStream the input stream containing a valid SOAP message with a Body that contains a SAML message
|
||||||
|
*
|
||||||
|
* @return a string encoded accordingly with the SAML HTTP POST Binding specification
|
||||||
|
*/
|
||||||
|
public static String toSamlHttpPostMessage(InputStream inputStream) {
|
||||||
|
try {
|
||||||
|
MessageFactory messageFactory = MessageFactory.newInstance();
|
||||||
|
SOAPMessage soapMessage = messageFactory.createMessage(null, inputStream);
|
||||||
|
SOAPBody soapBody = soapMessage.getSOAPBody();
|
||||||
|
Node authnRequestNode = soapBody.getFirstChild();
|
||||||
|
Document document = DocumentUtil.createDocument();
|
||||||
|
|
||||||
|
document.appendChild(document.importNode(authnRequestNode, true));
|
||||||
|
|
||||||
|
return PostBindingUtil.base64Encode(DocumentUtil.asString(document));
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException("Error creating fault message.", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class SoapMessageBuilder {
|
||||||
|
private final SOAPMessage message;
|
||||||
|
private final SOAPBody body;
|
||||||
|
private final SOAPEnvelope envelope;
|
||||||
|
|
||||||
|
private SoapMessageBuilder() {
|
||||||
|
try {
|
||||||
|
this.message = MessageFactory.newInstance().createMessage();
|
||||||
|
this.envelope = message.getSOAPPart().getEnvelope();
|
||||||
|
this.body = message.getSOAPBody();
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException("Error creating fault message.", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public SoapMessageBuilder addToBody(Document document) {
|
||||||
|
try {
|
||||||
|
this.body.addDocument(document);
|
||||||
|
} catch (SOAPException e) {
|
||||||
|
throw new RuntimeException("Could not add document to SOAP body.", e);
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SoapMessageBuilder addNamespace(String prefix, String ns) {
|
||||||
|
try {
|
||||||
|
envelope.addNamespaceDeclaration(prefix, ns);
|
||||||
|
} catch (SOAPException e) {
|
||||||
|
throw new RuntimeException("Could not add namespace to SOAP Envelope.", e);
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SOAPHeaderElement addHeader(String name, String prefix) {
|
||||||
|
try {
|
||||||
|
return this.envelope.getHeader().addHeaderElement(envelope.createQName(name, prefix));
|
||||||
|
} catch (SOAPException e) {
|
||||||
|
throw new RuntimeException("Could not add SOAP Header.", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Name createName(String name) {
|
||||||
|
try {
|
||||||
|
return this.envelope.createName(name);
|
||||||
|
} catch (SOAPException e) {
|
||||||
|
throw new RuntimeException("Could not create Name.", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Response build() {
|
||||||
|
return build(Status.OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
Response build(Status status) {
|
||||||
|
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.message.writeTo(outputStream);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException("Error while building SOAP Fault.", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Response.status(status).entity(outputStream.toByteArray()).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
SOAPMessage getMessage() {
|
||||||
|
return this.message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class SoapFaultBuilder {
|
||||||
|
|
||||||
|
private final SOAPFault fault;
|
||||||
|
private final SoapMessageBuilder messageBuilder;
|
||||||
|
|
||||||
|
private SoapFaultBuilder() {
|
||||||
|
this.messageBuilder = createMessage();
|
||||||
|
try {
|
||||||
|
this.fault = messageBuilder.getMessage().getSOAPBody().addFault();
|
||||||
|
} catch (SOAPException e) {
|
||||||
|
throw new RuntimeException("Could not create SOAP Fault.", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public SoapFaultBuilder detail(String detail) {
|
||||||
|
try {
|
||||||
|
this.fault.addDetail().setValue(detail);
|
||||||
|
} catch (SOAPException e) {
|
||||||
|
throw new RuntimeException("Error creating fault message.", e);
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SoapFaultBuilder reason(String reason) {
|
||||||
|
try {
|
||||||
|
this.fault.setFaultString(reason);
|
||||||
|
} catch (SOAPException e) {
|
||||||
|
throw new RuntimeException("Error creating fault message.", e);
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SoapFaultBuilder code(String code) {
|
||||||
|
try {
|
||||||
|
this.fault.setFaultCode(code);
|
||||||
|
} catch (SOAPException e) {
|
||||||
|
throw new RuntimeException("Error creating fault message.", e);
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Response build() {
|
||||||
|
return this.messageBuilder.build(Status.INTERNAL_SERVER_ERROR);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
org.keycloak.protocol.saml.profile.ecp.authenticator.HttpBasicAuthenticator
|
|
@ -1 +1,2 @@
|
||||||
org.keycloak.protocol.saml.SamlProtocolFactory
|
org.keycloak.protocol.saml.SamlProtocolFactory
|
||||||
|
org.keycloak.protocol.saml.profile.ecp.SamlEcpProfileProtocolFactory
|
|
@ -87,7 +87,7 @@ public abstract class AuthorizationEndpointBase {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
AuthenticationFlowModel flow = realm.getBrowserFlow();
|
AuthenticationFlowModel flow = getAuthenticationFlow();
|
||||||
String flowId = flow.getId();
|
String flowId = flow.getId();
|
||||||
AuthenticationProcessor processor = createProcessor(clientSession, flowId, LoginActionsService.AUTHENTICATE_PATH);
|
AuthenticationProcessor processor = createProcessor(clientSession, flowId, LoginActionsService.AUTHENTICATE_PATH);
|
||||||
|
|
||||||
|
@ -127,6 +127,10 @@ public abstract class AuthorizationEndpointBase {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected AuthenticationFlowModel getAuthenticationFlow() {
|
||||||
|
return realm.getBrowserFlow();
|
||||||
|
}
|
||||||
|
|
||||||
protected Response buildRedirectToIdentityProvider(String providerId, String accessCode) {
|
protected Response buildRedirectToIdentityProvider(String providerId, String accessCode) {
|
||||||
logger.debug("Automatically redirect to identity provider: " + providerId);
|
logger.debug("Automatically redirect to identity provider: " + providerId);
|
||||||
return Response.temporaryRedirect(
|
return Response.temporaryRedirect(
|
||||||
|
|
|
@ -0,0 +1,230 @@
|
||||||
|
package org.keycloak.testsuite.saml;
|
||||||
|
|
||||||
|
import org.jboss.resteasy.util.Base64;
|
||||||
|
import org.junit.ClassRule;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.keycloak.dom.saml.v2.protocol.ResponseType;
|
||||||
|
import org.keycloak.dom.saml.v2.protocol.StatusCodeType;
|
||||||
|
import org.keycloak.dom.saml.v2.protocol.StatusResponseType;
|
||||||
|
import org.keycloak.saml.common.constants.JBossSAMLConstants;
|
||||||
|
import org.keycloak.saml.common.constants.JBossSAMLURIConstants;
|
||||||
|
import org.keycloak.saml.common.util.DocumentUtil;
|
||||||
|
import org.keycloak.saml.processing.core.parsers.saml.SAMLParser;
|
||||||
|
import org.keycloak.testsuite.samlfilter.SamlAdapterTest;
|
||||||
|
import org.w3c.dom.Document;
|
||||||
|
import org.w3c.dom.NamedNodeMap;
|
||||||
|
import org.w3c.dom.Node;
|
||||||
|
import org.w3c.dom.NodeList;
|
||||||
|
|
||||||
|
import javax.ws.rs.client.ClientBuilder;
|
||||||
|
import javax.ws.rs.client.Entity;
|
||||||
|
import javax.ws.rs.client.Invocation.Builder;
|
||||||
|
import javax.ws.rs.core.HttpHeaders;
|
||||||
|
import javax.ws.rs.core.NewCookie;
|
||||||
|
import javax.ws.rs.core.Response;
|
||||||
|
import javax.xml.namespace.QName;
|
||||||
|
import javax.xml.soap.MessageFactory;
|
||||||
|
import javax.xml.soap.SOAPHeader;
|
||||||
|
import javax.xml.soap.SOAPHeaderElement;
|
||||||
|
import javax.xml.soap.SOAPMessage;
|
||||||
|
import javax.xml.transform.OutputKeys;
|
||||||
|
import javax.xml.transform.Source;
|
||||||
|
import javax.xml.transform.Transformer;
|
||||||
|
import javax.xml.transform.TransformerException;
|
||||||
|
import javax.xml.transform.TransformerFactory;
|
||||||
|
import javax.xml.transform.stream.StreamResult;
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.io.OutputStreamWriter;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import static javax.ws.rs.core.Response.Status.OK;
|
||||||
|
import static org.junit.Assert.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||||
|
* @version $Revision: 1 $
|
||||||
|
*/
|
||||||
|
public class SamlEcpProfileTest {
|
||||||
|
|
||||||
|
protected String APP_SERVER_BASE_URL = "http://localhost:8081";
|
||||||
|
|
||||||
|
@ClassRule
|
||||||
|
public static org.keycloak.testsuite.samlfilter.SamlKeycloakRule keycloakRule = new org.keycloak.testsuite.samlfilter.SamlKeycloakRule() {
|
||||||
|
@Override
|
||||||
|
public void initWars() {
|
||||||
|
ClassLoader classLoader = SamlAdapterTest.class.getClassLoader();
|
||||||
|
|
||||||
|
initializeSamlSecuredWar("/keycloak-saml/ecp/ecp-sp", "/ecp-sp", "ecp-sp.war", classLoader);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getRealmJson() {
|
||||||
|
return "/keycloak-saml/ecp/testsamlecp.json";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSuccessfulEcpFlow() throws Exception {
|
||||||
|
Response authnRequestResponse = ClientBuilder.newClient().target(APP_SERVER_BASE_URL + "/ecp-sp/").request()
|
||||||
|
.header("Accept", "text/html; application/vnd.paos+xml")
|
||||||
|
.header("PAOS", "ver='urn:liberty:paos:2003-08' ;'urn:oasis:names:tc:SAML:2.0:profiles:SSO:ecp'")
|
||||||
|
.get();
|
||||||
|
|
||||||
|
SOAPMessage authnRequestMessage = MessageFactory.newInstance().createMessage(null, new ByteArrayInputStream(authnRequestResponse.readEntity(byte[].class)));
|
||||||
|
|
||||||
|
printDocument(authnRequestMessage.getSOAPPart().getContent(), System.out);
|
||||||
|
|
||||||
|
Iterator<SOAPHeaderElement> it = authnRequestMessage.getSOAPHeader().<SOAPHeaderElement>getChildElements(new QName("urn:oasis:names:tc:SAML:2.0:profiles:SSO:ecp", "Request"));
|
||||||
|
SOAPHeaderElement ecpRequestHeader = it.next();
|
||||||
|
NodeList idpList = ecpRequestHeader.getElementsByTagNameNS("urn:oasis:names:tc:SAML:2.0:protocol", "IDPList");
|
||||||
|
|
||||||
|
assertEquals("No IDPList returned from Service Provider", 1, idpList.getLength());
|
||||||
|
|
||||||
|
NodeList idpEntries = idpList.item(0).getChildNodes();
|
||||||
|
|
||||||
|
assertEquals("No IDPEntry returned from Service Provider", 1, idpEntries.getLength());
|
||||||
|
|
||||||
|
String singleSignOnService = null;
|
||||||
|
|
||||||
|
for (int i = 0; i < idpEntries.getLength(); i++) {
|
||||||
|
Node item = idpEntries.item(i);
|
||||||
|
NamedNodeMap attributes = item.getAttributes();
|
||||||
|
Node location = attributes.getNamedItem("Loc");
|
||||||
|
|
||||||
|
singleSignOnService = location.getNodeValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
assertNotNull("Could not obtain SSO Service URL", singleSignOnService);
|
||||||
|
|
||||||
|
Document authenticationRequest = authnRequestMessage.getSOAPBody().getFirstChild().getOwnerDocument();
|
||||||
|
String username = "pedroigor";
|
||||||
|
String password = "password";
|
||||||
|
String pair = username + ":" + password;
|
||||||
|
String authHeader = "Basic " + new String(Base64.encodeBytes(pair.getBytes()));
|
||||||
|
|
||||||
|
Response authenticationResponse = ClientBuilder.newClient().target(singleSignOnService).request()
|
||||||
|
.header(HttpHeaders.AUTHORIZATION, authHeader)
|
||||||
|
.post(Entity.entity(DocumentUtil.asString(authenticationRequest), "application/soap+xml"));
|
||||||
|
|
||||||
|
assertEquals(OK.getStatusCode(), authenticationResponse.getStatus());
|
||||||
|
|
||||||
|
SOAPMessage responseMessage = MessageFactory.newInstance().createMessage(null, new ByteArrayInputStream(authenticationResponse.readEntity(byte[].class)));
|
||||||
|
|
||||||
|
printDocument(responseMessage.getSOAPPart().getContent(), System.out);
|
||||||
|
|
||||||
|
SOAPHeader responseMessageHeaders = responseMessage.getSOAPHeader();
|
||||||
|
|
||||||
|
NodeList ecpResponse = responseMessageHeaders.getElementsByTagNameNS(JBossSAMLURIConstants.ECP_PROFILE.get(), JBossSAMLConstants.RESPONSE.get());
|
||||||
|
|
||||||
|
assertEquals("No ECP Response", 1, ecpResponse.getLength());
|
||||||
|
|
||||||
|
Node samlResponse = responseMessage.getSOAPBody().getFirstChild();
|
||||||
|
|
||||||
|
assertNotNull(samlResponse);
|
||||||
|
|
||||||
|
ResponseType responseType = (ResponseType) new SAMLParser().parse(DocumentUtil.getNodeAsStream(samlResponse));
|
||||||
|
StatusCodeType statusCode = responseType.getStatus().getStatusCode();
|
||||||
|
|
||||||
|
assertEquals(statusCode.getValue().toString(), JBossSAMLURIConstants.STATUS_SUCCESS.get());
|
||||||
|
assertEquals("http://localhost:8081/ecp-sp/", responseType.getDestination());
|
||||||
|
assertNotNull(responseType.getSignature());
|
||||||
|
assertEquals(1, responseType.getAssertions().size());
|
||||||
|
|
||||||
|
SOAPMessage samlResponseRequest = MessageFactory.newInstance().createMessage();
|
||||||
|
|
||||||
|
samlResponseRequest.getSOAPBody().addDocument(responseMessage.getSOAPBody().extractContentAsDocument());
|
||||||
|
|
||||||
|
ByteArrayOutputStream os = new ByteArrayOutputStream();
|
||||||
|
|
||||||
|
samlResponseRequest.writeTo(os);
|
||||||
|
|
||||||
|
Response serviceProviderFinalResponse = ClientBuilder.newClient().target(responseType.getDestination()).request()
|
||||||
|
.post(Entity.entity(os.toByteArray(), "application/vnd.paos+xml"));
|
||||||
|
|
||||||
|
Map<String, NewCookie> cookies = serviceProviderFinalResponse.getCookies();
|
||||||
|
|
||||||
|
Builder resourceRequest = ClientBuilder.newClient().target(responseType.getDestination() + "/index.html").request();
|
||||||
|
|
||||||
|
for (NewCookie cookie : cookies.values()) {
|
||||||
|
resourceRequest.cookie(cookie);
|
||||||
|
}
|
||||||
|
|
||||||
|
Response resourceResponse = resourceRequest.get();
|
||||||
|
|
||||||
|
assertTrue(resourceResponse.readEntity(String.class).contains("pedroigor"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testInvalidCredentials() throws Exception {
|
||||||
|
Response authnRequestResponse = ClientBuilder.newClient().target(APP_SERVER_BASE_URL + "/ecp-sp/").request()
|
||||||
|
.header("Accept", "text/html; application/vnd.paos+xml")
|
||||||
|
.header("PAOS", "ver='urn:liberty:paos:2003-08' ;'urn:oasis:names:tc:SAML:2.0:profiles:SSO:ecp'")
|
||||||
|
.get();
|
||||||
|
|
||||||
|
SOAPMessage authnRequestMessage = MessageFactory.newInstance().createMessage(null, new ByteArrayInputStream(authnRequestResponse.readEntity(byte[].class)));
|
||||||
|
Iterator<SOAPHeaderElement> it = authnRequestMessage.getSOAPHeader().<SOAPHeaderElement>getChildElements(new QName("urn:liberty:paos:2003-08", "Request"));
|
||||||
|
|
||||||
|
it.next();
|
||||||
|
|
||||||
|
it = authnRequestMessage.getSOAPHeader().<SOAPHeaderElement>getChildElements(new QName("urn:oasis:names:tc:SAML:2.0:profiles:SSO:ecp", "Request"));
|
||||||
|
SOAPHeaderElement ecpRequestHeader = it.next();
|
||||||
|
NodeList idpList = ecpRequestHeader.getElementsByTagNameNS("urn:oasis:names:tc:SAML:2.0:protocol", "IDPList");
|
||||||
|
|
||||||
|
assertEquals("No IDPList returned from Service Provider", 1, idpList.getLength());
|
||||||
|
|
||||||
|
NodeList idpEntries = idpList.item(0).getChildNodes();
|
||||||
|
|
||||||
|
assertEquals("No IDPEntry returned from Service Provider", 1, idpEntries.getLength());
|
||||||
|
|
||||||
|
String singleSignOnService = null;
|
||||||
|
|
||||||
|
for (int i = 0; i < idpEntries.getLength(); i++) {
|
||||||
|
Node item = idpEntries.item(i);
|
||||||
|
NamedNodeMap attributes = item.getAttributes();
|
||||||
|
Node location = attributes.getNamedItem("Loc");
|
||||||
|
|
||||||
|
singleSignOnService = location.getNodeValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
assertNotNull("Could not obtain SSO Service URL", singleSignOnService);
|
||||||
|
|
||||||
|
Document authenticationRequest = authnRequestMessage.getSOAPBody().getFirstChild().getOwnerDocument();
|
||||||
|
String username = "pedroigor";
|
||||||
|
String password = "baspassword";
|
||||||
|
String pair = username + ":" + password;
|
||||||
|
String authHeader = "Basic " + new String(Base64.encodeBytes(pair.getBytes()));
|
||||||
|
|
||||||
|
Response authenticationResponse = ClientBuilder.newClient().target(singleSignOnService).request()
|
||||||
|
.header(HttpHeaders.AUTHORIZATION, authHeader)
|
||||||
|
.post(Entity.entity(DocumentUtil.asString(authenticationRequest), "application/soap+xml"));
|
||||||
|
|
||||||
|
assertEquals(OK.getStatusCode(), authenticationResponse.getStatus());
|
||||||
|
|
||||||
|
SOAPMessage responseMessage = MessageFactory.newInstance().createMessage(null, new ByteArrayInputStream(authenticationResponse.readEntity(byte[].class)));
|
||||||
|
Node samlResponse = responseMessage.getSOAPBody().getFirstChild();
|
||||||
|
|
||||||
|
assertNotNull(samlResponse);
|
||||||
|
|
||||||
|
StatusResponseType responseType = (StatusResponseType) new SAMLParser().parse(DocumentUtil.getNodeAsStream(samlResponse));
|
||||||
|
StatusCodeType statusCode = responseType.getStatus().getStatusCode();
|
||||||
|
|
||||||
|
assertNotEquals(statusCode.getStatusCode().getValue().toString(), JBossSAMLURIConstants.STATUS_SUCCESS.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void printDocument(Source doc, OutputStream out) throws IOException, TransformerException {
|
||||||
|
TransformerFactory tf = TransformerFactory.newInstance();
|
||||||
|
Transformer transformer = tf.newTransformer();
|
||||||
|
transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "no");
|
||||||
|
transformer.setOutputProperty(OutputKeys.METHOD, "xml");
|
||||||
|
transformer.setOutputProperty(OutputKeys.INDENT, "yes");
|
||||||
|
transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
|
||||||
|
transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "4");
|
||||||
|
|
||||||
|
transformer.transform(doc,
|
||||||
|
new StreamResult(new OutputStreamWriter(out, "UTF-8")));
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,40 @@
|
||||||
|
<keycloak-saml-adapter>
|
||||||
|
<SP entityID="http://localhost:8081/ecp-sp/"
|
||||||
|
sslPolicy="EXTERNAL"
|
||||||
|
nameIDPolicyFormat="urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified"
|
||||||
|
logoutPage="/logout.jsp"
|
||||||
|
forceAuthentication="false">
|
||||||
|
<Keys>
|
||||||
|
<Key signing="true" >
|
||||||
|
<KeyStore resource="/WEB-INF/keystore.jks" password="store123">
|
||||||
|
<PrivateKey alias="http://localhost:8080/sales-post-sig/" password="test123"/>
|
||||||
|
<Certificate alias="http://localhost:8080/sales-post-sig/"/>
|
||||||
|
</KeyStore>
|
||||||
|
</Key>
|
||||||
|
</Keys>
|
||||||
|
<PrincipalNameMapping policy="FROM_NAME_ID"/>
|
||||||
|
<RoleIdentifiers>
|
||||||
|
<Attribute name="Role"/>
|
||||||
|
</RoleIdentifiers>
|
||||||
|
<IDP entityID="idp"
|
||||||
|
signaturesRequired="true">
|
||||||
|
<SingleSignOnService requestBinding="POST"
|
||||||
|
bindingUrl="http://localhost:8081/auth/realms/demo/protocol/saml"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SingleLogoutService
|
||||||
|
requestBinding="POST"
|
||||||
|
responseBinding="POST"
|
||||||
|
postBindingUrl="http://localhost:8081/auth/realms/demo/protocol/saml"
|
||||||
|
redirectBindingUrl="http://localhost:8081/auth/realms/demo/protocol/saml"
|
||||||
|
/>
|
||||||
|
<Keys>
|
||||||
|
<Key signing="true">
|
||||||
|
<KeyStore resource="/WEB-INF/keystore.jks" password="store123">
|
||||||
|
<Certificate alias="demo"/>
|
||||||
|
</KeyStore>
|
||||||
|
</Key>
|
||||||
|
</Keys>
|
||||||
|
</IDP>
|
||||||
|
</SP>
|
||||||
|
</keycloak-saml-adapter>
|
Binary file not shown.
67
testsuite/integration/src/test/resources/keycloak-saml/ecp/testsamlecp.json
Executable file
67
testsuite/integration/src/test/resources/keycloak-saml/ecp/testsamlecp.json
Executable file
|
@ -0,0 +1,67 @@
|
||||||
|
{
|
||||||
|
"id": "demo",
|
||||||
|
"realm": "demo",
|
||||||
|
"enabled": true,
|
||||||
|
"sslRequired": "external",
|
||||||
|
"registrationAllowed": true,
|
||||||
|
"resetPasswordAllowed": true,
|
||||||
|
"privateKey": "MIICXAIBAAKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQABAoGAfmO8gVhyBxdqlxmIuglbz8bcjQbhXJLR2EoS8ngTXmN1bo2L90M0mUKSdc7qF10LgETBzqL8jYlQIbt+e6TH8fcEpKCjUlyq0Mf/vVbfZSNaVycY13nTzo27iPyWQHK5NLuJzn1xvxxrUeXI6A2WFpGEBLbHjwpx5WQG9A+2scECQQDvdn9NE75HPTVPxBqsEd2z10TKkl9CZxu10Qby3iQQmWLEJ9LNmy3acvKrE3gMiYNWb6xHPKiIqOR1as7L24aTAkEAtyvQOlCvr5kAjVqrEKXalj0Tzewjweuxc0pskvArTI2Oo070h65GpoIKLc9jf+UA69cRtquwP93aZKtW06U8dQJAF2Y44ks/mK5+eyDqik3koCI08qaC8HYq2wVl7G2QkJ6sbAaILtcvD92ToOvyGyeE0flvmDZxMYlvaZnaQ0lcSQJBAKZU6umJi3/xeEbkJqMfeLclD27XGEFoPeNrmdx0q10Azp4NfJAY+Z8KRyQCR2BEG+oNitBOZ+YXF9KCpH3cdmECQHEigJhYg+ykOvr1aiZUMFT72HU0jnmQe2FVekuG+LJUt2Tm7GtMjTFoGpf0JwrVuZN39fOYAlo+nTixgeW7X8Y=",
|
||||||
|
"publicKey": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB",
|
||||||
|
"requiredCredentials": [ "password" ],
|
||||||
|
"defaultRoles": [ "user" ],
|
||||||
|
"smtpServer": {
|
||||||
|
"from": "auto@keycloak.org",
|
||||||
|
"host": "localhost",
|
||||||
|
"port":"3025"
|
||||||
|
},
|
||||||
|
"users" : [
|
||||||
|
{
|
||||||
|
"username" : "pedroigor",
|
||||||
|
"enabled": true,
|
||||||
|
"email" : "psilva@redhat.com",
|
||||||
|
"credentials" : [
|
||||||
|
{ "type" : "password",
|
||||||
|
"value" : "password" }
|
||||||
|
],
|
||||||
|
"attributes" : {
|
||||||
|
"phone": "617"
|
||||||
|
},
|
||||||
|
"realmRoles": ["manager", "user"]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"applications": [
|
||||||
|
{
|
||||||
|
"name": "http://localhost:8081/ecp-sp/",
|
||||||
|
"enabled": true,
|
||||||
|
"protocol": "saml",
|
||||||
|
"fullScopeAllowed": true,
|
||||||
|
"baseUrl": "http://localhost:8081/ecp-sp",
|
||||||
|
"redirectUris": [
|
||||||
|
"http://localhost:8081/ecp-sp/*"
|
||||||
|
],
|
||||||
|
"attributes": {
|
||||||
|
"saml_assertion_consumer_url_post": "http://localhost:8081/ecp-sp/",
|
||||||
|
"saml_assertion_consumer_url_redirect": "http://localhost:8081/ecp-sp/",
|
||||||
|
"saml_single_logout_service_url_post": "http://localhost:8081/ecp-sp/",
|
||||||
|
"saml_single_logout_service_url_redirect": "http://localhost:8081/ecp-sp/",
|
||||||
|
"saml.server.signature": "true",
|
||||||
|
"saml.signature.algorithm": "RSA_SHA256",
|
||||||
|
"saml.client.signature": "true",
|
||||||
|
"saml.authnstatement": "true",
|
||||||
|
"saml.signing.certificate": "MIIB1DCCAT0CBgFJGP5dZDANBgkqhkiG9w0BAQsFADAwMS4wLAYDVQQDEyVodHRwOi8vbG9jYWxob3N0OjgwODAvc2FsZXMtcG9zdC1zaWcvMB4XDTE0MTAxNjEyNDQyM1oXDTI0MTAxNjEyNDYwM1owMDEuMCwGA1UEAxMlaHR0cDovL2xvY2FsaG9zdDo4MDgwL3NhbGVzLXBvc3Qtc2lnLzCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA1RvGu8RjemSJA23nnMksoHA37MqY1DDTxOECY4rPAd9egr7GUNIXE0y1MokaR5R2crNpN8RIRwR8phQtQDjXL82c6W+NLQISxztarQJ7rdNJIYwHY0d5ri1XRpDP8zAuxubPYiMAVYcDkIcvlbBpwh/dRM5I2eElRK+eSiaMkCUCAwEAATANBgkqhkiG9w0BAQsFAAOBgQCLms6htnPaY69k1ntm9a5jgwSn/K61cdai8R8B0ccY7zvinn9AfRD7fiROQpFyY29wKn8WCLrJ86NBXfgFUGyR5nLNHVy3FghE36N2oHy53uichieMxffE6vhkKJ4P8ChfJMMOZlmCPsQPDvjoAghHt4mriFiQgRdPgIy/zDjSNw=="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"roles" : {
|
||||||
|
"realm" : [
|
||||||
|
{
|
||||||
|
"name": "manager",
|
||||||
|
"description": "Have Manager privileges"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "user",
|
||||||
|
"description": "Have User privileges"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue