From 1b614a379bf558f5e4231427817b49ff0de594f7 Mon Sep 17 00:00:00 2001 From: Pedro Igor Date: Wed, 16 Dec 2015 18:46:52 -0200 Subject: [PATCH] [KEYCLOAK-2202] - Initial support for SAML ECP Profile. --- .../keycloak-saml-protocol/main/module.xml | 1 + .../utils/DefaultAuthenticationFlows.java | 24 + ...eLogin.java => AbstractInitiateLogin.java} | 21 +- .../adapters/saml/OnSessionCreated.java | 9 + .../adapters/saml/SamlAuthenticator.java | 525 +----------------- .../AbstractSamlAuthenticationHandler.java | 484 ++++++++++++++++ .../profile/SamlAuthenticationHandler.java | 13 + .../saml/profile/SamlInvocationContext.java | 37 ++ .../profile/ecp/EcpAuthenticationHandler.java | 146 +++++ .../WebBrowserSsoAuthenticationHandler.java | 111 ++++ .../common/constants/JBossSAMLConstants.java | 3 +- .../constants/JBossSAMLURIConstants.java | 7 +- .../keycloak/protocol/saml/SamlProtocol.java | 43 +- .../protocol/saml/SamlProtocolFactory.java | 9 +- .../keycloak/protocol/saml/SamlService.java | 32 +- .../ecp/SamlEcpProfileProtocolFactory.java | 109 ++++ .../profile/ecp/SamlEcpProfileService.java | 70 +++ .../authenticator/HttpBasicAuthenticator.java | 174 ++++++ .../protocol/saml/profile/ecp/util/Soap.java | 177 ++++++ ...ycloak.authentication.AuthenticatorFactory | 1 + ...org.keycloak.protocol.LoginProtocolFactory | 3 +- .../protocol/AuthorizationEndpointBase.java | 6 +- .../testsuite/saml/SamlEcpProfileTest.java | 230 ++++++++ .../ecp/ecp-sp/WEB-INF/keycloak-saml.xml | 40 ++ .../ecp/ecp-sp/WEB-INF/keystore.jks | Bin 0 -> 1705 bytes .../keycloak-saml/ecp/testsamlecp.json | 67 +++ 26 files changed, 1799 insertions(+), 543 deletions(-) rename saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/{InitiateLogin.java => AbstractInitiateLogin.java} (79%) create mode 100644 saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/OnSessionCreated.java create mode 100644 saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/profile/AbstractSamlAuthenticationHandler.java create mode 100644 saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/profile/SamlAuthenticationHandler.java create mode 100644 saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/profile/SamlInvocationContext.java create mode 100644 saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/profile/ecp/EcpAuthenticationHandler.java create mode 100644 saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/profile/webbrowsersso/WebBrowserSsoAuthenticationHandler.java create mode 100644 saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/profile/ecp/SamlEcpProfileProtocolFactory.java create mode 100644 saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/profile/ecp/SamlEcpProfileService.java create mode 100644 saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/profile/ecp/authenticator/HttpBasicAuthenticator.java create mode 100644 saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/profile/ecp/util/Soap.java create mode 100755 saml/saml-protocol/src/main/resources/META-INF/services/org.keycloak.authentication.AuthenticatorFactory create mode 100755 testsuite/integration/src/test/java/org/keycloak/testsuite/saml/SamlEcpProfileTest.java create mode 100755 testsuite/integration/src/test/resources/keycloak-saml/ecp/ecp-sp/WEB-INF/keycloak-saml.xml create mode 100755 testsuite/integration/src/test/resources/keycloak-saml/ecp/ecp-sp/WEB-INF/keystore.jks create mode 100755 testsuite/integration/src/test/resources/keycloak-saml/ecp/testsamlecp.json diff --git a/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/base/org/keycloak/keycloak-saml-protocol/main/module.xml b/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/base/org/keycloak/keycloak-saml-protocol/main/module.xml index fbd65fd5cb..81cd365853 100755 --- a/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/base/org/keycloak/keycloak-saml-protocol/main/module.xml +++ b/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/base/org/keycloak/keycloak-saml-protocol/main/module.xml @@ -26,6 +26,7 @@ + diff --git a/model/api/src/main/java/org/keycloak/models/utils/DefaultAuthenticationFlows.java b/model/api/src/main/java/org/keycloak/models/utils/DefaultAuthenticationFlows.java index 3a105c46e2..c49f5b6b9b 100755 --- a/model/api/src/main/java/org/keycloak/models/utils/DefaultAuthenticationFlows.java +++ b/model/api/src/main/java/org/keycloak/models/utils/DefaultAuthenticationFlows.java @@ -24,6 +24,7 @@ public class DefaultAuthenticationFlows { public static final String DIRECT_GRANT_FLOW = "direct grant"; public static final String RESET_CREDENTIALS_FLOW = "reset credentials"; 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 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(CLIENT_AUTHENTICATION_FLOW) == null) clientAuthFlow(realm); 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) { 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(CLIENT_AUTHENTICATION_FLOW) == null) clientAuthFlow(realm); 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) { @@ -447,4 +450,25 @@ public class DefaultAuthenticationFlows { execution.setAuthenticatorFlow(false); 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); + } } diff --git a/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/InitiateLogin.java b/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/AbstractInitiateLogin.java similarity index 79% rename from saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/InitiateLogin.java rename to saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/AbstractInitiateLogin.java index 4c7cbab5e0..2496b7c49f 100755 --- a/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/InitiateLogin.java +++ b/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/AbstractInitiateLogin.java @@ -7,21 +7,24 @@ import org.keycloak.saml.BaseSAML2BindingBuilder; import org.keycloak.saml.SAML2AuthnRequestBuilder; import org.keycloak.saml.SAML2NameIDPolicyBuilder; 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 java.io.IOException; import java.security.KeyPair; /** * @author Bill Burke * @version $Revision: 1 $ */ -public class InitiateLogin implements AuthChallenge { - protected static Logger log = Logger.getLogger(InitiateLogin.class); +public abstract class AbstractInitiateLogin implements AuthChallenge { + protected static Logger log = Logger.getLogger(AbstractInitiateLogin.class); protected SamlDeployment deployment; protected SamlSessionStore sessionStore; - public InitiateLogin(SamlDeployment deployment, SamlSessionStore sessionStore) { + public AbstractInitiateLogin(SamlDeployment deployment, SamlSessionStore sessionStore) { this.deployment = deployment; this.sessionStore = sessionStore; } @@ -35,18 +38,14 @@ public class InitiateLogin implements AuthChallenge { public boolean challenge(HttpFacade httpFacade) { try { String issuerURL = deployment.getEntityID(); - String actionUrl = deployment.getIDP().getSingleSignOnService().getRequestBindingUrl(); - String destinationUrl = actionUrl; String nameIDPolicyFormat = deployment.getNameIDPolicyFormat(); if (nameIDPolicyFormat == null) { nameIDPolicyFormat = JBossSAMLURIConstants.NAMEID_FORMAT_PERSISTENT.get(); } - - SAML2AuthnRequestBuilder authnRequestBuilder = new SAML2AuthnRequestBuilder() - .destination(destinationUrl) + .destination(deployment.getIDP().getSingleSignOnService().getRequestBindingUrl()) .issuer(issuerURL) .forceAuthn(deployment.isForceAuthentication()).isPassive(deployment.isIsPassive()) .nameIdPolicy(SAML2NameIDPolicyBuilder.format(nameIDPolicyFormat)); @@ -79,9 +78,7 @@ public class InitiateLogin implements AuthChallenge { } sessionStore.saveRequest(); - Document document = authnRequestBuilder.toDocument(); - SamlDeployment.Binding samlBinding = deployment.getIDP().getSingleSignOnService().getRequestBinding(); - SamlUtil.sendSaml(true, httpFacade, actionUrl, binding, document, samlBinding); + sendAuthnRequest(httpFacade, authnRequestBuilder, binding); sessionStore.setCurrentAction(SamlSessionStore.CurrentAction.LOGGING_IN); } catch (Exception e) { throw new RuntimeException("Could not create authentication request.", e); @@ -89,4 +86,6 @@ public class InitiateLogin implements AuthChallenge { return true; } + protected abstract void sendAuthnRequest(HttpFacade httpFacade, SAML2AuthnRequestBuilder authnRequestBuilder, BaseSAML2BindingBuilder binding) throws ProcessingException, ConfigurationException, IOException; + } diff --git a/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/OnSessionCreated.java b/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/OnSessionCreated.java new file mode 100644 index 0000000000..92f3b4db3e --- /dev/null +++ b/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/OnSessionCreated.java @@ -0,0 +1,9 @@ +package org.keycloak.adapters.saml; + +/** + * @author Pedro Igor + */ +public interface OnSessionCreated { + + void onSessionCreated(SamlSession samlSession); +} diff --git a/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/SamlAuthenticator.java b/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/SamlAuthenticator.java index 02097db12c..cd9affd674 100755 --- a/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/SamlAuthenticator.java +++ b/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/SamlAuthenticator.java @@ -1,530 +1,49 @@ package org.keycloak.adapters.saml; 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.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.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 Bill Burke * @version $Revision: 1 $ */ public abstract class SamlAuthenticator { + protected static Logger log = Logger.getLogger(SamlAuthenticator.class); - protected HttpFacade facade; - protected AuthChallenge challenge; - protected SamlDeployment deployment; - protected SamlSessionStore sessionStore; + private final SamlAuthenticationHandler handler; - public SamlAuthenticator(HttpFacade facade, SamlDeployment deployment, SamlSessionStore sessionStore) { - this.facade = facade; - this.deployment = deployment; - this.sessionStore = sessionStore; + public SamlAuthenticator(final HttpFacade facade, final SamlDeployment deployment, final SamlSessionStore sessionStore) { + this.handler = createAuthenticationHandler(facade, deployment, sessionStore); } public AuthChallenge getChallenge() { - return challenge; + return this.handler.getChallenge(); } public AuthOutcome authenticate() { - - - String samlRequest = facade.getRequest().getFirstParam(GeneralConstants.SAML_REQUEST_KEY); - String samlResponse = facade.getRequest().getFirstParam(GeneralConstants.SAML_RESPONSE_KEY); - String relayState = facade.getRequest().getFirstParam(GeneralConstants.RELAY_STATE); - 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(); + log.debugf("SamlAuthenticator is using handler [%s]", this.handler); + return this.handler.handle(new OnSessionCreated() { + @Override + public void onSessionCreated(SamlSession samlSession) { + completeAuthentication(samlSession); } - if (verifySSL()) return AuthOutcome.FAILED; - log.debug("AUTHENTICATED: was cached"); - return AuthOutcome.AUTHENTICATED; - } - return initiateLogin(); + }); } - protected 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(); + protected abstract void completeAuthentication(SamlSession samlSession); + + private SamlAuthenticationHandler createAuthenticationHandler(HttpFacade facade, SamlDeployment deployment, SamlSessionStore sessionStore) { + if (EcpAuthenticationHandler.canHandle(facade)) { + return EcpAuthenticationHandler.create(facade, deployment, sessionStore); } - 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; + // defaults to the web browser sso profile + return WebBrowserSsoAuthenticationHandler.create(facade, deployment, sessionStore); } - - 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 roles = new HashSet<>(); - MultivaluedHashMap attributes = new MultivaluedHashMap<>(); - MultivaluedHashMap friendlyAttributes = new MultivaluedHashMap<>(); - - Set statements = assertion.getStatements(); - for (StatementAbstractType statement : statements) { - if (statement instanceof AttributeStatementType) { - AttributeStatementType attributeStatement = (AttributeStatementType) statement; - List attList = attributeStatement.getAttributes(); - for (AttributeStatementType.ASTChoiceType obj : attList) { - AttributeType attr = obj.getAttribute(); - if (isRole(attr)) { - List 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 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); - } - } - - -} +} \ No newline at end of file diff --git a/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/profile/AbstractSamlAuthenticationHandler.java b/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/profile/AbstractSamlAuthenticationHandler.java new file mode 100644 index 0000000000..290b2d7eb4 --- /dev/null +++ b/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/profile/AbstractSamlAuthenticationHandler.java @@ -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 Bill Burke + */ +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 roles = new HashSet<>(); + MultivaluedHashMap attributes = new MultivaluedHashMap<>(); + MultivaluedHashMap friendlyAttributes = new MultivaluedHashMap<>(); + + Set statements = assertion.getStatements(); + for (StatementAbstractType statement : statements) { + if (statement instanceof AttributeStatementType) { + AttributeStatementType attributeStatement = (AttributeStatementType) statement; + List attList = attributeStatement.getAttributes(); + for (AttributeStatementType.ASTChoiceType obj : attList) { + AttributeType attr = obj.getAttribute(); + if (isRole(attr)) { + List 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 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); + } + } +} diff --git a/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/profile/SamlAuthenticationHandler.java b/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/profile/SamlAuthenticationHandler.java new file mode 100644 index 0000000000..4f499c740a --- /dev/null +++ b/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/profile/SamlAuthenticationHandler.java @@ -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 Pedro Igor + */ +public interface SamlAuthenticationHandler { + AuthOutcome handle(OnSessionCreated onCreateSession); + AuthChallenge getChallenge(); +} diff --git a/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/profile/SamlInvocationContext.java b/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/profile/SamlInvocationContext.java new file mode 100644 index 0000000000..1155b0d45e --- /dev/null +++ b/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/profile/SamlInvocationContext.java @@ -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 Pedro Igor + */ +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; + } +} diff --git a/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/profile/ecp/EcpAuthenticationHandler.java b/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/profile/ecp/EcpAuthenticationHandler.java new file mode 100644 index 0000000000..5fa99a0909 --- /dev/null +++ b/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/profile/ecp/EcpAuthenticationHandler.java @@ -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 Pedro Igor + */ +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()); + } + }; + } +} \ No newline at end of file diff --git a/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/profile/webbrowsersso/WebBrowserSsoAuthenticationHandler.java b/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/profile/webbrowsersso/WebBrowserSsoAuthenticationHandler.java new file mode 100644 index 0000000000..f3e98e5492 --- /dev/null +++ b/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/profile/webbrowsersso/WebBrowserSsoAuthenticationHandler.java @@ -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 Pedro Igor + */ +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; + } +} diff --git a/saml/saml-core/src/main/java/org/keycloak/saml/common/constants/JBossSAMLConstants.java b/saml/saml-core/src/main/java/org/keycloak/saml/common/constants/JBossSAMLConstants.java index fb90e17922..219042b24f 100755 --- a/saml/saml-core/src/main/java/org/keycloak/saml/common/constants/JBossSAMLConstants.java +++ b/saml/saml-core/src/main/java/org/keycloak/saml/common/constants/JBossSAMLConstants.java @@ -65,7 +65,8 @@ public enum JBossSAMLConstants { "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"), 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; diff --git a/saml/saml-core/src/main/java/org/keycloak/saml/common/constants/JBossSAMLURIConstants.java b/saml/saml-core/src/main/java/org/keycloak/saml/common/constants/JBossSAMLURIConstants.java index 3833c56f38..ad7bee5656 100755 --- a/saml/saml-core/src/main/java/org/keycloak/saml/common/constants/JBossSAMLURIConstants.java +++ b/saml/saml-core/src/main/java/org/keycloak/saml/common/constants/JBossSAMLURIConstants.java @@ -73,12 +73,15 @@ public enum JBossSAMLURIConstants { "urn:oasis:names:tc:SAML:2.0:nameid-format:entity"), 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( "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( - "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"), + SAML_HTTP_POST_BINDING("urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"), + 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"), diff --git a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlProtocol.java b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlProtocol.java index 0bc3edeea0..07c528ddd9 100755 --- a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlProtocol.java +++ b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlProtocol.java @@ -84,6 +84,7 @@ public class SamlProtocol implements LoginProtocol { 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_POST_BINDING = "post"; + public static final String SAML_SOAP_BINDING = "soap"; public static final String SAML_REDIRECT_BINDING = "get"; public static final String SAML_SERVER_SIGNATURE = "saml.server.signature"; public static final String SAML_ASSERTION_SIGNATURE = "saml.assertion.signature"; @@ -165,11 +166,7 @@ public class SamlProtocol implements LoginProtocol { try { JaxrsSAML2BindingBuilder binding = new JaxrsSAML2BindingBuilder().relayState(clientSession.getNote(GeneralConstants.RELAY_STATE)); Document document = builder.buildDocument(); - if (isPostBinding(clientSession)) { - return binding.postBinding(document).response(clientSession.getRedirectUri()); - } else { - return binding.redirectBinding(document).response(clientSession.getRedirectUri()); - } + return buildErrorResponse(clientSession, binding, document); } catch (Exception e) { 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) { switch (error) { case CANCELLED_BY_USER: @@ -390,17 +395,21 @@ public class SamlProtocol implements LoginProtocol { bindingBuilder.encrypt(publicKey); } try { - if (isPostBinding(clientSession)) { - return bindingBuilder.postBinding(samlDocument).response(redirectUri); - } else { - return bindingBuilder.redirectBinding(samlDocument).response(redirectUri); - } + return buildAuthenticatedResponse(clientSession, redirectUri, samlDocument, bindingBuilder); } catch (Exception e) { logger.error("failed", e); 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) { return "true".equals(client.getAttribute(SAML_SERVER_SIGNATURE)); } @@ -544,11 +553,7 @@ public class SamlProtocol implements LoginProtocol { } try { - if (isLogoutPostBindingForInitiator(userSession)) { - return binding.postBinding(builder.buildDocument()).response(logoutBindingUri); - } else { - return binding.redirectBinding(builder.buildDocument()).response(logoutBindingUri); - } + return buildLogoutResponse(userSession, logoutBindingUri, builder, binding); } catch (ConfigurationException e) { throw new RuntimeException(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 public void backchannelLogout(UserSessionModel userSession, ClientSessionModel clientSession) { ClientModel client = clientSession.getClient(); diff --git a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlProtocolFactory.java b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlProtocolFactory.java index a7a86edc3a..7dcc86695b 100755 --- a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlProtocolFactory.java +++ b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlProtocolFactory.java @@ -42,7 +42,7 @@ public class SamlProtocolFactory extends AbstractLoginProtocolFactory { @Override public String getId() { - return "saml"; + return SamlProtocol.LOGIN_PROTOCOL; } @Override @@ -90,8 +90,9 @@ public class SamlProtocolFactory extends AbstractLoginProtocolFactory { @Override protected void addDefaults(ClientModel client) { - for (ProtocolMapperModel model : defaultBuiltins) client.addProtocolMapper(model); - + for (ProtocolMapperModel model : defaultBuiltins) { + model.setProtocol(getId()); + client.addProtocolMapper(model); + } } - } diff --git a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlService.java b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlService.java index 34025933d1..f9aa30b148 100755 --- a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlService.java +++ b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlService.java @@ -16,6 +16,7 @@ import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import org.jboss.logging.Logger; +import org.jboss.resteasy.spi.ResteasyProviderFactory; import org.keycloak.common.VerificationException; import org.keycloak.common.util.StreamUtil; 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.utils.KeycloakModelUtils; import org.keycloak.protocol.AuthorizationEndpointBase; +import org.keycloak.protocol.LoginProtocol; import org.keycloak.protocol.oidc.utils.RedirectUtils; +import org.keycloak.protocol.saml.profile.ecp.SamlEcpProfileService; import org.keycloak.saml.SAML2LogoutResponseBuilder; import org.keycloak.saml.SAMLRequestParser; import org.keycloak.saml.SignatureAlgorithm; @@ -221,7 +224,7 @@ public class SamlService extends AuthorizationEndpointBase { } ClientSessionModel clientSession = session.sessions().createClientSession(realm, client); - clientSession.setAuthMethod(SamlProtocol.LOGIN_PROTOCOL); + clientSession.setAuthMethod(getLoginProtocol()); clientSession.setRedirectUri(redirect); clientSession.setAction(ClientSessionModel.Action.AUTHENTICATE.name()); clientSession.setNote(ClientSessionCode.ACTION_KEY, KeycloakModelUtils.generateCodeSecret()); @@ -246,7 +249,7 @@ public class SamlService extends AuthorizationEndpointBase { return newBrowserAuthentication(clientSession, requestAbstractType.isIsPassive()); } - private String getBindingType(AuthnRequestType requestAbstractType) { + protected String getBindingType(AuthnRequestType requestAbstractType) { URI requestedProtocolBinding = requestAbstractType.getProtocolBinding(); if (requestedProtocolBinding != null) { @@ -370,7 +373,7 @@ public class SamlService extends AuthorizationEndpointBase { } } - protected class PostBindingProtocol extends BindingProtocol { + public class PostBindingProtocol extends BindingProtocol { @Override 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) { - 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); } + @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 @Path("descriptor") @Produces(MediaType.APPLICATION_XML) @@ -519,7 +537,7 @@ public class SamlService extends AuthorizationEndpointBase { } ClientSessionModel clientSession = session.sessions().createClientSession(realm, client); - clientSession.setAuthMethod(SamlProtocol.LOGIN_PROTOCOL); + clientSession.setAuthMethod(getLoginProtocol()); clientSession.setAction(ClientSessionModel.Action.AUTHENTICATE.name()); clientSession.setNote(ClientSessionCode.ACTION_KEY, KeycloakModelUtils.generateCodeSecret()); 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; + } + } diff --git a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/profile/ecp/SamlEcpProfileProtocolFactory.java b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/profile/ecp/SamlEcpProfileProtocolFactory.java new file mode 100644 index 0000000000..dba1b295b9 --- /dev/null +++ b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/profile/ecp/SamlEcpProfileProtocolFactory.java @@ -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 Pedro Igor + */ +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; + } +} diff --git a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/profile/ecp/SamlEcpProfileService.java b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/profile/ecp/SamlEcpProfileService.java new file mode 100644 index 0000000000..c16b997757 --- /dev/null +++ b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/profile/ecp/SamlEcpProfileService.java @@ -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 Pedro Igor + */ +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."); + } +} diff --git a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/profile/ecp/authenticator/HttpBasicAuthenticator.java b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/profile/ecp/authenticator/HttpBasicAuthenticator.java new file mode 100644 index 0000000000..2e1550229b --- /dev/null +++ b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/profile/ecp/authenticator/HttpBasicAuthenticator.java @@ -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 Pedro Igor + */ +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 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 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; + } +} diff --git a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/profile/ecp/util/Soap.java b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/profile/ecp/util/Soap.java new file mode 100644 index 0000000000..4bdf76a10e --- /dev/null +++ b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/profile/ecp/util/Soap.java @@ -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 Pedro Igor + */ +public final class Soap { + + public static SoapFaultBuilder createFault() { + return new SoapFaultBuilder(); + } + + public static SoapMessageBuilder createMessage() { + return new SoapMessageBuilder(); + } + + /** + *

Returns a string encoded accordingly with the SAML HTTP POST Binding specification based on the + * given inputStream which must contain a valid SOAP message. + * + *

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); + } + } +} diff --git a/saml/saml-protocol/src/main/resources/META-INF/services/org.keycloak.authentication.AuthenticatorFactory b/saml/saml-protocol/src/main/resources/META-INF/services/org.keycloak.authentication.AuthenticatorFactory new file mode 100755 index 0000000000..9ac8020231 --- /dev/null +++ b/saml/saml-protocol/src/main/resources/META-INF/services/org.keycloak.authentication.AuthenticatorFactory @@ -0,0 +1 @@ +org.keycloak.protocol.saml.profile.ecp.authenticator.HttpBasicAuthenticator \ No newline at end of file diff --git a/saml/saml-protocol/src/main/resources/META-INF/services/org.keycloak.protocol.LoginProtocolFactory b/saml/saml-protocol/src/main/resources/META-INF/services/org.keycloak.protocol.LoginProtocolFactory index d0a2dd046f..ae434f63eb 100755 --- a/saml/saml-protocol/src/main/resources/META-INF/services/org.keycloak.protocol.LoginProtocolFactory +++ b/saml/saml-protocol/src/main/resources/META-INF/services/org.keycloak.protocol.LoginProtocolFactory @@ -1 +1,2 @@ -org.keycloak.protocol.saml.SamlProtocolFactory \ No newline at end of file +org.keycloak.protocol.saml.SamlProtocolFactory +org.keycloak.protocol.saml.profile.ecp.SamlEcpProfileProtocolFactory \ No newline at end of file diff --git a/services/src/main/java/org/keycloak/protocol/AuthorizationEndpointBase.java b/services/src/main/java/org/keycloak/protocol/AuthorizationEndpointBase.java index a1fc4a7708..9dc5548f57 100644 --- a/services/src/main/java/org/keycloak/protocol/AuthorizationEndpointBase.java +++ b/services/src/main/java/org/keycloak/protocol/AuthorizationEndpointBase.java @@ -87,7 +87,7 @@ public abstract class AuthorizationEndpointBase { } } - AuthenticationFlowModel flow = realm.getBrowserFlow(); + AuthenticationFlowModel flow = getAuthenticationFlow(); String flowId = flow.getId(); 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) { logger.debug("Automatically redirect to identity provider: " + providerId); return Response.temporaryRedirect( diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/saml/SamlEcpProfileTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/saml/SamlEcpProfileTest.java new file mode 100755 index 0000000000..c02deeb3b9 --- /dev/null +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/saml/SamlEcpProfileTest.java @@ -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 Bill Burke + * @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 it = authnRequestMessage.getSOAPHeader().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 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 it = authnRequestMessage.getSOAPHeader().getChildElements(new QName("urn:liberty:paos:2003-08", "Request")); + + it.next(); + + it = authnRequestMessage.getSOAPHeader().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"))); + } +} diff --git a/testsuite/integration/src/test/resources/keycloak-saml/ecp/ecp-sp/WEB-INF/keycloak-saml.xml b/testsuite/integration/src/test/resources/keycloak-saml/ecp/ecp-sp/WEB-INF/keycloak-saml.xml new file mode 100755 index 0000000000..df39712bf3 --- /dev/null +++ b/testsuite/integration/src/test/resources/keycloak-saml/ecp/ecp-sp/WEB-INF/keycloak-saml.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/testsuite/integration/src/test/resources/keycloak-saml/ecp/ecp-sp/WEB-INF/keystore.jks b/testsuite/integration/src/test/resources/keycloak-saml/ecp/ecp-sp/WEB-INF/keystore.jks new file mode 100755 index 0000000000000000000000000000000000000000..144830bc77683d1d0a3d29f8793471d4f395bdb0 GIT binary patch literal 1705 zcmezO_TO6u1_mZ5W@J#!C@Cqh($~+)PfpCq$S*FjvM{hP&@WERNiEhb0P=N1M z7#KYz`B=UJb?!81V%loJ$Ht}2#>m2`#U#kc$jZRd#I(>@`_HHQr3*BE{x`hDyEHld z+K~ja7y5T|r=%}Fu;`1zi=v(re>&z=o_dkol)j+zYO>t=Qy1S}v3en%vP3563G;>m zf%$6H#y=LWo_E+v&g1u@xzTso16CaPbkO_D8o@;(`MobV^Igq3BNxZ5w|jA2W>>?O zsK)o5D^C2naN_4wlZOiFZx$ZYW2t4}JhWx=_GhgLGruICtx3+%d3V!9>Dt7nXAhlc z$;{X;mJr8NzU5%3ud}q*;Tc<&7_HRg&F#_Q?wWFKHOG|)@xdBe`K>+ek+V}K-){E) zl)cDAKzFrg_KD*rPhJ#0xY%188oeS%DBgct!6JRDwVn#urq)k%GX7O$-%BXGHp?M$ zPK(%~JsS=itGtnGNeXRIJS=enoqTH}>U?=&w0Rv-!V zt0IaI7=L{C`R*=P35_m^|6)DbnI1Rom?{^itz&v>PMs${YwWF4+mV{{ zB3p#_4CDJAHQ!e4#HZJ%dA%($R{WrMXd~~o9sJ4l4<@p_UZ&<;=%+j9w)>u?FXr2x z;Qi$;R(a*~n?JwFjfqj;bam5w#iu)2cStlBuRNH-DYCa*SFTj`Ruc2O%^{1v zS^p3Un$kT>@>nGEyG?KA#5`*A-Q)TDd5mCQN4nqL|I4L=R^IMCEM*}Xua~%r;rLwD z(rqehS=sAVMB2P;SIyh9w+fi%85vk3^h^ybfvg(_O^jE7*p`Wn(Np4IY>ELd8>d#A zN85K^Mn-N{1_J{_Jp&y!=1>-9Vd650ft)z6p^2e^p_!qPiHVVMlsK;ujB93K97TqS zjq{OR1z|$TGQU@sN$9wB)LN$MwU<2= zd)SlRE7$3UyG=O%*+B2uv-3&H4597J6WaBsZpb_&f7j)l$IXYTF6-xcsr5`yWnyMz zU_^EvFwmHR?&_X(ZsE4#TgmHFu2rXfU6s0)lCKe^<7iiUsaqK$zd9h>c57vO3iO-78%EoLL<8ZmZxNJeq&U-KBw5j(T{eI-t zj}(o0{2w&pRSxr|MK;+T;phADf{DX@d*|wohzX72{tZ3*&sbbC2N!KDDXF>n@KOsN z=~ICbKGC4@Hz>{}*<-lToNvHy$ZNn2iF2?C$Wdx)Vu&22rlzLAD8`N=&Wn#AY>fF{RdOu_iDQssmI(9hPO(2Uiv*4ZQlGJH!&z=`K zw%L^5bkqb1@@gO=+rT(BxQD(6^h&8PDm@hVJA%Qxx$H92KBf6;Q! zeu+xYdBwlV_8D(8vle0u7ZYR_ZJGO3A6Ja^r!9<;Jf^6B_V*`!pI4_%{2#46Qrcoa z{d4O3!wr>pceY+y88$ipliuM2X9G4KozS*BUZS)4a=%Gc^OV}R*}*rh8m{{;m0Oa) z^Ysn?xlP?KeF94gT3Fr`zuJ}MUVH6cw8xR2&A~UG*V~pyDQR4_ymoT_iPrx=pDW$^ gV7obl)$b?cUQd>i8HM*{tk@@=IXfvlri|w}0JG8182|tP literal 0 HcmV?d00001 diff --git a/testsuite/integration/src/test/resources/keycloak-saml/ecp/testsamlecp.json b/testsuite/integration/src/test/resources/keycloak-saml/ecp/testsamlecp.json new file mode 100755 index 0000000000..981cbda169 --- /dev/null +++ b/testsuite/integration/src/test/resources/keycloak-saml/ecp/testsamlecp.json @@ -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" + } + ] + } +}