keycloak-aplcache
Changes
distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/base/org/keycloak/keycloak-saml-protocol/main/module.xml 1(+1 -0)
saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/AbstractInitiateLogin.java 21(+10 -11)
saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/profile/AbstractSamlAuthenticationHandler.java 484(+484 -0)
saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/profile/ecp/EcpAuthenticationHandler.java 146(+146 -0)
saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/profile/SamlAuthenticationHandler.java 13(+13 -0)
saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/profile/SamlInvocationContext.java 37(+37 -0)
saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/profile/webbrowsersso/WebBrowserSsoAuthenticationHandler.java 111(+111 -0)
saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/SamlAuthenticator.java 523(+21 -502)
saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/profile/ecp/authenticator/HttpBasicAuthenticator.java 174(+174 -0)
saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/profile/ecp/SamlEcpProfileProtocolFactory.java 109(+109 -0)
saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/profile/ecp/SamlEcpProfileService.java 70(+70 -0)
saml/saml-protocol/src/main/resources/META-INF/services/org.keycloak.authentication.AuthenticatorFactory 1(+1 -0)
saml/saml-protocol/src/main/resources/META-INF/services/org.keycloak.protocol.LoginProtocolFactory 3(+2 -1)
testsuite/integration/src/test/java/org/keycloak/testsuite/saml/SamlEcpProfileTest.java 230(+230 -0)
Details
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 fbd65fd..81cd365 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 @@
<module name="org.keycloak.keycloak-connections-http-client" services="import"/>
<module name="javax.api"/>
+ <module name="javax.xml.soap.api"/>
</dependencies>
</module>
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 3a105c4..c49f5b6 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/OnSessionCreated.java b/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/OnSessionCreated.java
new file mode 100644
index 0000000..92f3b4d
--- /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 <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public interface OnSessionCreated {
+
+ void onSessionCreated(SamlSession samlSession);
+}
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 0000000..290b2d7
--- /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 <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ */
+public abstract class AbstractSamlAuthenticationHandler implements SamlAuthenticationHandler {
+
+ protected static Logger log = Logger.getLogger(WebBrowserSsoAuthenticationHandler.class);
+
+ protected final HttpFacade facade;
+ protected final SamlSessionStore sessionStore;
+ protected final SamlDeployment deployment;
+ protected AuthChallenge challenge;
+
+ public AbstractSamlAuthenticationHandler(HttpFacade facade, SamlDeployment deployment, SamlSessionStore sessionStore) {
+ this.facade = facade;
+ this.deployment = deployment;
+ this.sessionStore = sessionStore;
+ }
+
+ public AuthOutcome doHandle(SamlInvocationContext context, OnSessionCreated onCreateSession) {
+ String samlRequest = context.getSamlRequest();
+ String samlResponse = context.getSamlResponse();
+ String relayState = context.getRelayState();
+ if (samlRequest != null) {
+ return handleSamlRequest(samlRequest, relayState);
+ } else if (samlResponse != null) {
+ return handleSamlResponse(samlResponse, relayState, onCreateSession);
+ } else if (sessionStore.isLoggedIn()) {
+ if (verifySSL()) return AuthOutcome.FAILED;
+ log.debug("AUTHENTICATED: was cached");
+ return handleRequest();
+ }
+ return initiateLogin();
+ }
+
+ protected AuthOutcome handleRequest() {
+ return AuthOutcome.AUTHENTICATED;
+ }
+
+ @Override
+ public AuthChallenge getChallenge() {
+ return this.challenge;
+ }
+
+ protected AuthOutcome handleSamlRequest(String samlRequest, String relayState) {
+ SAMLDocumentHolder holder = null;
+ boolean postBinding = false;
+ String requestUri = facade.getRequest().getURI();
+ if (facade.getRequest().getMethod().equalsIgnoreCase("GET")) {
+ // strip out query params
+ int index = requestUri.indexOf('?');
+ if (index > -1) {
+ requestUri = requestUri.substring(0, index);
+ }
+ holder = SAMLRequestParser.parseRequestRedirectBinding(samlRequest);
+ } else {
+ postBinding = true;
+ holder = SAMLRequestParser.parseRequestPostBinding(samlRequest);
+ }
+ RequestAbstractType requestAbstractType = (RequestAbstractType) holder.getSamlObject();
+ if (!requestUri.equals(requestAbstractType.getDestination().toString())) {
+ log.error("expected destination '" + requestUri + "' got '" + requestAbstractType.getDestination() + "'");
+ return AuthOutcome.FAILED;
+ }
+
+ if (requestAbstractType instanceof LogoutRequestType) {
+ if (deployment.getIDP().getSingleLogoutService().validateRequestSignature()) {
+ try {
+ validateSamlSignature(holder, postBinding, GeneralConstants.SAML_REQUEST_KEY);
+ } catch (VerificationException e) {
+ log.error("Failed to verify saml request signature", e);
+ return AuthOutcome.FAILED;
+ }
+ }
+ LogoutRequestType logout = (LogoutRequestType) requestAbstractType;
+ return logoutRequest(logout, relayState);
+
+ } else {
+ log.error("unknown SAML request type");
+ return AuthOutcome.FAILED;
+ }
+ }
+
+ protected abstract AuthOutcome logoutRequest(LogoutRequestType request, String relayState);
+
+ protected AuthOutcome handleSamlResponse(String samlResponse, String relayState, OnSessionCreated onCreateSession) {
+ SAMLDocumentHolder holder = null;
+ boolean postBinding = false;
+ String requestUri = facade.getRequest().getURI();
+ if (facade.getRequest().getMethod().equalsIgnoreCase("GET")) {
+ int index = requestUri.indexOf('?');
+ if (index > -1) {
+ requestUri = requestUri.substring(0, index);
+ }
+ holder = extractRedirectBindingResponse(samlResponse);
+ } else {
+ postBinding = true;
+ holder = extractPostBindingResponse(samlResponse);
+ }
+ final StatusResponseType statusResponse = (StatusResponseType) holder.getSamlObject();
+ // validate destination
+ if (!requestUri.equals(statusResponse.getDestination())) {
+ log.error("Request URI does not match SAML request destination");
+ return AuthOutcome.FAILED;
+ }
+
+ if (statusResponse instanceof ResponseType) {
+ try {
+ if (deployment.getIDP().getSingleSignOnService().validateResponseSignature()) {
+ try {
+ validateSamlSignature(holder, postBinding, GeneralConstants.SAML_RESPONSE_KEY);
+ } catch (VerificationException e) {
+ log.error("Failed to verify saml response signature", e);
+
+ challenge = new AuthChallenge() {
+ @Override
+ public boolean challenge(HttpFacade exchange) {
+ SamlAuthenticationError error = new SamlAuthenticationError(SamlAuthenticationError.Reason.INVALID_SIGNATURE);
+ exchange.getRequest().setError(error);
+ exchange.getResponse().sendError(403);
+ return true;
+ }
+
+ @Override
+ public int getResponseCode() {
+ return 403;
+ }
+ };
+ return AuthOutcome.FAILED;
+ }
+ }
+ return handleLoginResponse((ResponseType) statusResponse, onCreateSession);
+ } finally {
+ sessionStore.setCurrentAction(SamlSessionStore.CurrentAction.NONE);
+ }
+
+ } else {
+ if (sessionStore.isLoggingOut()) {
+ try {
+ if (deployment.getIDP().getSingleLogoutService().validateResponseSignature()) {
+ try {
+ validateSamlSignature(holder, postBinding, GeneralConstants.SAML_RESPONSE_KEY);
+ } catch (VerificationException e) {
+ log.error("Failed to verify saml response signature", e);
+ return AuthOutcome.FAILED;
+ }
+ }
+ return handleLogoutResponse(holder, statusResponse, relayState);
+ } finally {
+ sessionStore.setCurrentAction(SamlSessionStore.CurrentAction.NONE);
+ }
+
+ } else if (sessionStore.isLoggingIn()) {
+
+ try {
+ // KEYCLOAK-2107 - handle user not authenticated due passive mode. Return special outcome so different authentication mechanisms can behave accordingly.
+ StatusType status = statusResponse.getStatus();
+ if(checkStatusCodeValue(status.getStatusCode(), JBossSAMLURIConstants.STATUS_RESPONDER.get()) && checkStatusCodeValue(status.getStatusCode().getStatusCode(), JBossSAMLURIConstants.STATUS_NO_PASSIVE.get())){
+ log.debug("Not authenticated due passive mode Status found in SAML response: " + status.toString());
+ return AuthOutcome.NOT_AUTHENTICATED;
+ }
+
+ challenge = new AuthChallenge() {
+ @Override
+ public boolean challenge(HttpFacade exchange) {
+ SamlAuthenticationError error = new SamlAuthenticationError(SamlAuthenticationError.Reason.ERROR_STATUS, statusResponse);
+ exchange.getRequest().setError(error);
+ exchange.getResponse().sendError(403);
+ return true;
+ }
+
+ @Override
+ public int getResponseCode() {
+ return 403;
+ }
+ };
+ return AuthOutcome.FAILED;
+ } finally {
+ sessionStore.setCurrentAction(SamlSessionStore.CurrentAction.NONE);
+ }
+ }
+ return AuthOutcome.NOT_ATTEMPTED;
+ }
+
+ }
+
+ private void validateSamlSignature(SAMLDocumentHolder holder, boolean postBinding, String paramKey) throws VerificationException {
+ if (postBinding) {
+ verifyPostBindingSignature(holder.getSamlDocument(), deployment.getIDP().getSignatureValidationKey());
+ } else {
+ verifyRedirectBindingSignature(deployment.getIDP().getSignatureValidationKey(), paramKey);
+ }
+ }
+
+ private boolean checkStatusCodeValue(StatusCodeType statusCode, String expectedValue){
+ if(statusCode != null && statusCode.getValue()!=null){
+ String v = statusCode.getValue().toString();
+ return expectedValue.equals(v);
+ }
+ return false;
+ }
+
+ protected AuthOutcome handleLoginResponse(ResponseType responseType, OnSessionCreated onCreateSession) {
+
+ AssertionType assertion = null;
+ try {
+ assertion = AssertionUtil.getAssertion(responseType, deployment.getDecryptionKey());
+ if (AssertionUtil.hasExpired(assertion)) {
+ return initiateLogin();
+ }
+ } catch (Exception e) {
+ log.error("Error extracting SAML assertion: " + e.getMessage());
+ challenge = new AuthChallenge() {
+ @Override
+ public boolean challenge(HttpFacade exchange) {
+ SamlAuthenticationError error = new SamlAuthenticationError(SamlAuthenticationError.Reason.EXTRACTION_FAILURE);
+ exchange.getRequest().setError(error);
+ exchange.getResponse().sendError(403);
+ return true;
+ }
+
+ @Override
+ public int getResponseCode() {
+ return 403;
+ }
+ };
+ }
+
+ SubjectType subject = assertion.getSubject();
+ SubjectType.STSubType subType = subject.getSubType();
+ NameIDType subjectNameID = (NameIDType) subType.getBaseID();
+ String principalName = subjectNameID.getValue();
+
+ final Set<String> roles = new HashSet<>();
+ MultivaluedHashMap<String, String> attributes = new MultivaluedHashMap<>();
+ MultivaluedHashMap<String, String> friendlyAttributes = new MultivaluedHashMap<>();
+
+ Set<StatementAbstractType> statements = assertion.getStatements();
+ for (StatementAbstractType statement : statements) {
+ if (statement instanceof AttributeStatementType) {
+ AttributeStatementType attributeStatement = (AttributeStatementType) statement;
+ List<AttributeStatementType.ASTChoiceType> attList = attributeStatement.getAttributes();
+ for (AttributeStatementType.ASTChoiceType obj : attList) {
+ AttributeType attr = obj.getAttribute();
+ if (isRole(attr)) {
+ List<Object> attributeValues = attr.getAttributeValue();
+ if (attributeValues != null) {
+ for (Object attrValue : attributeValues) {
+ String role = getAttributeValue(attrValue);
+ log.debugv("Add role: {0}", role);
+ roles.add(role);
+ }
+ }
+ } else {
+ List<Object> attributeValues = attr.getAttributeValue();
+ if (attributeValues != null) {
+ for (Object attrValue : attributeValues) {
+ String value = getAttributeValue(attrValue);
+ if (attr.getName() != null) {
+ attributes.add(attr.getName(), value);
+ }
+ if (attr.getFriendlyName() != null) {
+ friendlyAttributes.add(attr.getFriendlyName(), value);
+ }
+ }
+ }
+ }
+
+ }
+ }
+ }
+ if (deployment.getPrincipalNamePolicy() == SamlDeployment.PrincipalNamePolicy.FROM_ATTRIBUTE) {
+ if (deployment.getPrincipalAttributeName() != null) {
+ String attribute = attributes.getFirst(deployment.getPrincipalAttributeName());
+ if (attribute != null) principalName = attribute;
+ else {
+ attribute = friendlyAttributes.getFirst(deployment.getPrincipalAttributeName());
+ if (attribute != null) principalName = attribute;
+ }
+ }
+ }
+
+ AuthnStatementType authn = null;
+ for (Object statement : assertion.getStatements()) {
+ if (statement instanceof AuthnStatementType) {
+ authn = (AuthnStatementType) statement;
+ break;
+ }
+ }
+
+
+ URI nameFormat = subjectNameID.getFormat();
+ String nameFormatString = nameFormat == null ? JBossSAMLURIConstants.NAMEID_FORMAT_UNSPECIFIED.get() : nameFormat.toString();
+ final SamlPrincipal principal = new SamlPrincipal(assertion, principalName, principalName, nameFormatString, attributes, friendlyAttributes);
+ String index = authn == null ? null : authn.getSessionIndex();
+ final String sessionIndex = index;
+ SamlSession account = new SamlSession(principal, roles, sessionIndex);
+ sessionStore.saveAccount(account);
+ onCreateSession.onSessionCreated(account);
+
+ // redirect to original request, it will be restored
+ String redirectUri = sessionStore.getRedirectUri();
+ if (redirectUri != null) {
+ facade.getResponse().setHeader("Location", redirectUri);
+ facade.getResponse().setStatus(302);
+ facade.getResponse().end();
+ } else {
+ log.debug("IDP initiated invocation");
+ }
+ log.debug("AUTHENTICATED authn");
+
+ return AuthOutcome.AUTHENTICATED;
+ }
+
+ private String getAttributeValue(Object attrValue) {
+ String value = null;
+ if (attrValue instanceof String) {
+ value = (String) attrValue;
+ } else if (attrValue instanceof Node) {
+ Node roleNode = (Node) attrValue;
+ value = roleNode.getFirstChild().getNodeValue();
+ } else if (attrValue instanceof NameIDType) {
+ NameIDType nameIdType = (NameIDType) attrValue;
+ value = nameIdType.getValue();
+ } else {
+ log.warn("Unable to extract unknown SAML assertion attribute value type: " + attrValue.getClass().getName());
+ }
+ return value;
+ }
+
+ protected boolean isRole(AttributeType attribute) {
+ return (attribute.getName() != null && deployment.getRoleAttributeNames().contains(attribute.getName())) || (attribute.getFriendlyName() != null && deployment.getRoleAttributeNames().contains(attribute.getFriendlyName()));
+ }
+
+ protected AuthOutcome handleLogoutResponse(SAMLDocumentHolder holder, StatusResponseType responseType, String relayState) {
+ boolean loggedIn = sessionStore.isLoggedIn();
+ if (!loggedIn || !"logout".equals(relayState)) {
+ return AuthOutcome.NOT_ATTEMPTED;
+ }
+ sessionStore.logoutAccount();
+ return AuthOutcome.LOGGED_OUT;
+ }
+
+ protected SAMLDocumentHolder extractRedirectBindingResponse(String response) {
+ return SAMLRequestParser.parseRequestRedirectBinding(response);
+ }
+
+
+ protected SAMLDocumentHolder extractPostBindingResponse(String response) {
+ byte[] samlBytes = PostBindingUtil.base64Decode(response);
+ return SAMLRequestParser.parseResponseDocument(samlBytes);
+ }
+
+
+ protected AuthOutcome initiateLogin() {
+ challenge = createChallenge();
+ return AuthOutcome.NOT_ATTEMPTED;
+ }
+
+ protected AbstractInitiateLogin createChallenge() {
+ return new AbstractInitiateLogin(deployment, sessionStore) {
+ @Override
+ protected void sendAuthnRequest(HttpFacade httpFacade, SAML2AuthnRequestBuilder authnRequestBuilder, BaseSAML2BindingBuilder binding) throws ProcessingException, ConfigurationException, IOException {
+ Document document = authnRequestBuilder.toDocument();
+ SamlDeployment.Binding samlBinding = deployment.getIDP().getSingleSignOnService().getRequestBinding();
+ SamlUtil.sendSaml(true, httpFacade, deployment.getIDP().getSingleSignOnService().getRequestBindingUrl(), binding, document, samlBinding);
+ }
+ };
+ }
+
+ protected boolean verifySSL() {
+ if (!facade.getRequest().isSecure() && deployment.getSslRequired().isRequired(facade.getRequest().getRemoteAddr())) {
+ log.warn("SSL is required to authenticate");
+ return true;
+ }
+ return false;
+ }
+
+ public void verifyPostBindingSignature(Document document, PublicKey publicKey) throws VerificationException {
+ SAML2Signature saml2Signature = new SAML2Signature();
+ try {
+ if (!saml2Signature.validate(document, publicKey)) {
+ throw new VerificationException("Invalid signature on document");
+ }
+ } catch (ProcessingException e) {
+ throw new VerificationException("Error validating signature", e);
+ }
+ }
+
+ public void verifyRedirectBindingSignature(PublicKey publicKey, String paramKey) throws VerificationException {
+ String request = facade.getRequest().getQueryParamValue(paramKey);
+ String algorithm = facade.getRequest().getQueryParamValue(GeneralConstants.SAML_SIG_ALG_REQUEST_KEY);
+ String signature = facade.getRequest().getQueryParamValue(GeneralConstants.SAML_SIGNATURE_REQUEST_KEY);
+ String decodedAlgorithm = facade.getRequest().getQueryParamValue(GeneralConstants.SAML_SIG_ALG_REQUEST_KEY);
+
+ if (request == null) {
+ throw new VerificationException("SAML Request was null");
+ }
+ if (algorithm == null) throw new VerificationException("SigAlg was null");
+ if (signature == null) throw new VerificationException("Signature was null");
+
+ // Shibboleth doesn't sign the document for redirect binding.
+ // todo maybe a flag?
+
+ String relayState = facade.getRequest().getQueryParamValue(GeneralConstants.RELAY_STATE);
+ KeycloakUriBuilder builder = KeycloakUriBuilder.fromPath("/")
+ .queryParam(paramKey, request);
+ if (relayState != null) {
+ builder.queryParam(GeneralConstants.RELAY_STATE, relayState);
+ }
+ builder.queryParam(GeneralConstants.SAML_SIG_ALG_REQUEST_KEY, algorithm);
+ String rawQuery = builder.build().getRawQuery();
+
+ try {
+ //byte[] decodedSignature = RedirectBindingUtil.urlBase64Decode(signature);
+ byte[] decodedSignature = Base64.decode(signature);
+
+ SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.getFromXmlMethod(decodedAlgorithm);
+ Signature validator = signatureAlgorithm.createSignature(); // todo plugin signature alg
+ validator.initVerify(publicKey);
+ validator.update(rawQuery.getBytes("UTF-8"));
+ if (!validator.verify(decodedSignature)) {
+ throw new VerificationException("Invalid query param signature");
+ }
+ } catch (Exception e) {
+ throw new VerificationException(e);
+ }
+ }
+}
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 0000000..5fa99a0
--- /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 <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class EcpAuthenticationHandler extends AbstractSamlAuthenticationHandler {
+
+ public static final String PAOS_HEADER = "PAOS";
+ public static final String PAOS_CONTENT_TYPE = "application/vnd.paos+xml";
+ private static final String NS_PREFIX_PROFILE_ECP = "ecp";
+ private static final String NS_PREFIX_SAML_PROTOCOL = "samlp";
+ private static final String NS_PREFIX_SAML_ASSERTION = "saml";
+ private static final String NS_PREFIX_PAOS_BINDING = "paos";
+
+ public static boolean canHandle(HttpFacade httpFacade) {
+ HttpFacade.Request request = httpFacade.getRequest();
+ String acceptHeader = request.getHeader("Accept");
+ String contentTypeHeader = request.getHeader("Content-Type");
+
+ return (acceptHeader != null && acceptHeader.contains(PAOS_CONTENT_TYPE) && request.getHeader(PAOS_HEADER) != null)
+ || (contentTypeHeader != null && contentTypeHeader.contains(PAOS_CONTENT_TYPE));
+ }
+
+ public static SamlAuthenticationHandler create(HttpFacade facade, SamlDeployment deployment, SamlSessionStore sessionStore) {
+ return new EcpAuthenticationHandler(facade, deployment, sessionStore);
+ }
+
+ private EcpAuthenticationHandler(HttpFacade facade, SamlDeployment deployment, SamlSessionStore sessionStore) {
+ super(facade, deployment, sessionStore);
+ }
+
+ @Override
+ protected AuthOutcome logoutRequest(LogoutRequestType request, String relayState) {
+ throw new RuntimeException("Not supported.");
+ }
+
+
+ @Override
+ public AuthOutcome handle(OnSessionCreated onCreateSession) {
+ String header = facade.getRequest().getHeader(PAOS_HEADER);
+
+ if (header != null) {
+ return doHandle(new SamlInvocationContext(), onCreateSession);
+ } else {
+ try {
+ MessageFactory messageFactory = MessageFactory.newInstance();
+ SOAPMessage soapMessage = messageFactory.createMessage(null, facade.getRequest().getInputStream());
+ SOAPBody soapBody = soapMessage.getSOAPBody();
+ Node authnRequestNode = soapBody.getFirstChild();
+ Document document = DocumentUtil.createDocument();
+
+ document.appendChild(document.importNode(authnRequestNode, true));
+
+ String samlResponse = PostBindingUtil.base64Encode(DocumentUtil.asString(document));
+
+ return doHandle(new SamlInvocationContext(null, samlResponse, null), onCreateSession);
+ } catch (Exception e) {
+ throw new RuntimeException("Error creating fault message.", e);
+ }
+ }
+ }
+
+ @Override
+ protected AbstractInitiateLogin createChallenge() {
+ return new AbstractInitiateLogin(deployment, sessionStore) {
+ @Override
+ protected void sendAuthnRequest(HttpFacade httpFacade, SAML2AuthnRequestBuilder authnRequestBuilder, BaseSAML2BindingBuilder binding) {
+ try {
+ MessageFactory messageFactory = MessageFactory.newInstance();
+ SOAPMessage message = messageFactory.createMessage();
+
+ SOAPEnvelope envelope = message.getSOAPPart().getEnvelope();
+
+ envelope.addNamespaceDeclaration(NS_PREFIX_SAML_ASSERTION, JBossSAMLURIConstants.ASSERTION_NSURI.get());
+ envelope.addNamespaceDeclaration(NS_PREFIX_SAML_PROTOCOL, JBossSAMLURIConstants.PROTOCOL_NSURI.get());
+ envelope.addNamespaceDeclaration(NS_PREFIX_PAOS_BINDING, JBossSAMLURIConstants.PAOS_BINDING.get());
+ envelope.addNamespaceDeclaration(NS_PREFIX_PROFILE_ECP, JBossSAMLURIConstants.ECP_PROFILE.get());
+
+ createPaosRequestHeader(envelope);
+ createEcpRequestHeader(envelope);
+
+ SOAPBody body = envelope.getBody();
+
+ body.addDocument(binding.postBinding(authnRequestBuilder.toDocument()).getDocument());
+
+ message.writeTo(httpFacade.getResponse().getOutputStream());
+ } catch (Exception e) {
+ throw new RuntimeException("Could not create AuthnRequest.", e);
+ }
+ }
+
+ private void createEcpRequestHeader(SOAPEnvelope envelope) throws SOAPException {
+ SOAPHeader headers = envelope.getHeader();
+ SOAPHeaderElement ecpRequestHeader = headers.addHeaderElement(envelope.createQName(JBossSAMLConstants.REQUEST.get(), NS_PREFIX_PROFILE_ECP));
+
+ ecpRequestHeader.setMustUnderstand(true);
+ ecpRequestHeader.setActor("http://schemas.xmlsoap.org/soap/actor/next");
+ ecpRequestHeader.addAttribute(envelope.createName("ProviderName"), deployment.getEntityID());
+ ecpRequestHeader.addAttribute(envelope.createName("IsPassive"), "0");
+ ecpRequestHeader.addChildElement(envelope.createQName("Issuer", "saml")).setValue(deployment.getEntityID());
+ ecpRequestHeader.addChildElement(envelope.createQName("IDPList", "samlp"))
+ .addChildElement(envelope.createQName("IDPEntry", "samlp"))
+ .addAttribute(envelope.createName("ProviderID"), deployment.getIDP().getEntityID())
+ .addAttribute(envelope.createName("Name"), deployment.getIDP().getEntityID())
+ .addAttribute(envelope.createName("Loc"), deployment.getIDP().getSingleSignOnService().getRequestBindingUrl());
+ }
+
+ private void createPaosRequestHeader(SOAPEnvelope envelope) throws SOAPException {
+ SOAPHeader headers = envelope.getHeader();
+ SOAPHeaderElement paosRequestHeader = headers.addHeaderElement(envelope.createQName(JBossSAMLConstants.REQUEST.get(), NS_PREFIX_PAOS_BINDING));
+
+ paosRequestHeader.setMustUnderstand(true);
+ paosRequestHeader.setActor("http://schemas.xmlsoap.org/soap/actor/next");
+ paosRequestHeader.addAttribute(envelope.createName("service"), JBossSAMLURIConstants.ECP_PROFILE.get());
+ paosRequestHeader.addAttribute(envelope.createName("responseConsumerURL"), deployment.getAssertionConsumerServiceUrl());
+ }
+ };
+ }
+}
\ No newline at end of file
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 0000000..4f499c7
--- /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 <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+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 0000000..1155b0d
--- /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 <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class SamlInvocationContext {
+
+ private String samlRequest;
+ private String samlResponse;
+ private String relayState;
+
+ public SamlInvocationContext() {
+ this(null, null, null);
+ }
+
+ public SamlInvocationContext(String samlRequest, String samlResponse, String relayState) {
+ this.samlRequest = samlRequest;
+ this.samlResponse = samlResponse;
+ this.relayState = relayState;
+ }
+
+ public String getSamlRequest() {
+ return this.samlRequest;
+ }
+
+ public String getSamlResponse() {
+ return this.samlResponse;
+ }
+
+ public String getRelayState() {
+ return this.relayState;
+ }
+}
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 0000000..f3e98e5
--- /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 <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class WebBrowserSsoAuthenticationHandler extends AbstractSamlAuthenticationHandler {
+
+ public static SamlAuthenticationHandler create(HttpFacade facade, SamlDeployment deployment, SamlSessionStore sessionStore) {
+ return new WebBrowserSsoAuthenticationHandler(facade, deployment, sessionStore);
+ }
+
+ private WebBrowserSsoAuthenticationHandler(HttpFacade facade, SamlDeployment deployment, SamlSessionStore sessionStore) {
+ super(facade, deployment, sessionStore);
+ }
+
+ @Override
+ public AuthOutcome handle(OnSessionCreated onCreateSession) {
+ return doHandle(new SamlInvocationContext(facade.getRequest().getFirstParam(GeneralConstants.SAML_REQUEST_KEY),
+ facade.getRequest().getFirstParam(GeneralConstants.SAML_RESPONSE_KEY),
+ facade.getRequest().getFirstParam(GeneralConstants.RELAY_STATE)), onCreateSession);
+ }
+
+ @Override
+ protected AuthOutcome handleRequest() {
+ boolean globalLogout = "true".equals(facade.getRequest().getQueryParamValue("GLO"));
+
+ if (globalLogout) {
+ return globalLogout();
+ }
+
+ return AuthOutcome.AUTHENTICATED;
+ }
+
+ @Override
+ protected AuthOutcome logoutRequest(LogoutRequestType request, String relayState) {
+ if (request.getSessionIndex() == null || request.getSessionIndex().isEmpty()) {
+ sessionStore.logoutByPrincipal(request.getNameID().getValue());
+ } else {
+ sessionStore.logoutBySsoId(request.getSessionIndex());
+ }
+
+ String issuerURL = deployment.getEntityID();
+ SAML2LogoutResponseBuilder builder = new SAML2LogoutResponseBuilder();
+ builder.logoutRequestID(request.getID());
+ builder.destination(deployment.getIDP().getSingleLogoutService().getResponseBindingUrl());
+ builder.issuer(issuerURL);
+ BaseSAML2BindingBuilder binding = new BaseSAML2BindingBuilder().relayState(relayState);
+ if (deployment.getIDP().getSingleLogoutService().signResponse()) {
+ binding.signatureAlgorithm(deployment.getSignatureAlgorithm())
+ .signWith(deployment.getSigningKeyPair())
+ .signDocument();
+ if (deployment.getSignatureCanonicalizationMethod() != null)
+ binding.canonicalizationMethod(deployment.getSignatureCanonicalizationMethod());
+ }
+
+
+ try {
+ SamlUtil.sendSaml(false, facade, deployment.getIDP().getSingleLogoutService().getResponseBindingUrl(), binding, builder.buildDocument(),
+ deployment.getIDP().getSingleLogoutService().getResponseBinding());
+ } catch (Exception e) {
+ log.error("Could not send logout response SAML request", e);
+ return AuthOutcome.FAILED;
+ }
+ return AuthOutcome.NOT_ATTEMPTED;
+ }
+
+ private AuthOutcome globalLogout() {
+ SamlSession account = sessionStore.getAccount();
+ if (account == null) {
+ return AuthOutcome.NOT_ATTEMPTED;
+ }
+ SAML2LogoutRequestBuilder logoutBuilder = new SAML2LogoutRequestBuilder()
+ .assertionExpiration(30)
+ .issuer(deployment.getEntityID())
+ .sessionIndex(account.getSessionIndex())
+ .userPrincipal(account.getPrincipal().getSamlSubject(), account.getPrincipal().getNameIDFormat())
+ .destination(deployment.getIDP().getSingleLogoutService().getRequestBindingUrl());
+ BaseSAML2BindingBuilder binding = new BaseSAML2BindingBuilder();
+ if (deployment.getIDP().getSingleLogoutService().signRequest()) {
+ binding.signWith(deployment.getSigningKeyPair())
+ .signDocument();
+ }
+
+ binding.relayState("logout");
+
+ try {
+ SamlUtil.sendSaml(true, facade, deployment.getIDP().getSingleLogoutService().getRequestBindingUrl(), binding, logoutBuilder.buildDocument(), deployment.getIDP().getSingleLogoutService().getRequestBinding());
+ sessionStore.setCurrentAction(SamlSessionStore.CurrentAction.LOGGING_OUT);
+ } catch (Exception e) {
+ log.error("Could not send global logout SAML request", e);
+ return AuthOutcome.FAILED;
+ }
+ return AuthOutcome.NOT_ATTEMPTED;
+ }
+}
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 02097db..cd9affd 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 <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @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();
- }
-
- binding.relayState("logout");
+ protected abstract void completeAuthentication(SamlSession samlSession);
- 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;
+ private SamlAuthenticationHandler createAuthenticationHandler(HttpFacade facade, SamlDeployment deployment, SamlSessionStore sessionStore) {
+ if (EcpAuthenticationHandler.canHandle(facade)) {
+ return EcpAuthenticationHandler.create(facade, deployment, sessionStore);
}
- return AuthOutcome.NOT_ATTEMPTED;
- }
- protected AuthOutcome handleSamlRequest(String samlRequest, String relayState) {
- SAMLDocumentHolder holder = null;
- boolean postBinding = false;
- String requestUri = facade.getRequest().getURI();
- if (facade.getRequest().getMethod().equalsIgnoreCase("GET")) {
- // strip out query params
- int index = requestUri.indexOf('?');
- if (index > -1) {
- requestUri = requestUri.substring(0, index);
- }
- holder = SAMLRequestParser.parseRequestRedirectBinding(samlRequest);
- } else {
- postBinding = true;
- holder = SAMLRequestParser.parseRequestPostBinding(samlRequest);
- }
- RequestAbstractType requestAbstractType = (RequestAbstractType) holder.getSamlObject();
- if (!requestUri.equals(requestAbstractType.getDestination().toString())) {
- log.error("expected destination '" + requestUri + "' got '" + requestAbstractType.getDestination() + "'");
- return AuthOutcome.FAILED;
- }
-
- if (requestAbstractType instanceof LogoutRequestType) {
- if (deployment.getIDP().getSingleLogoutService().validateRequestSignature()) {
- try {
- validateSamlSignature(holder, postBinding, GeneralConstants.SAML_REQUEST_KEY);
- } catch (VerificationException e) {
- log.error("Failed to verify saml request signature", e);
- return AuthOutcome.FAILED;
- }
- }
- LogoutRequestType logout = (LogoutRequestType) requestAbstractType;
- return logoutRequest(logout, relayState);
-
- } else {
- log.error("unknown SAML request type");
- return AuthOutcome.FAILED;
- }
+ // defaults to the web browser sso profile
+ return WebBrowserSsoAuthenticationHandler.create(facade, deployment, sessionStore);
}
-
- protected AuthOutcome logoutRequest(LogoutRequestType request, String relayState) {
- if (request.getSessionIndex() == null || request.getSessionIndex().isEmpty()) {
- sessionStore.logoutByPrincipal(request.getNameID().getValue());
- } else {
- sessionStore.logoutBySsoId(request.getSessionIndex());
- }
-
- String issuerURL = deployment.getEntityID();
- SAML2LogoutResponseBuilder builder = new SAML2LogoutResponseBuilder();
- builder.logoutRequestID(request.getID());
- builder.destination(deployment.getIDP().getSingleLogoutService().getResponseBindingUrl());
- builder.issuer(issuerURL);
- BaseSAML2BindingBuilder binding = new BaseSAML2BindingBuilder().relayState(relayState);
- if (deployment.getIDP().getSingleLogoutService().signResponse()) {
- binding.signatureAlgorithm(deployment.getSignatureAlgorithm())
- .signWith(deployment.getSigningKeyPair())
- .signDocument();
- if (deployment.getSignatureCanonicalizationMethod() != null)
- binding.canonicalizationMethod(deployment.getSignatureCanonicalizationMethod());
- }
-
-
- try {
- SamlUtil.sendSaml(false, facade, deployment.getIDP().getSingleLogoutService().getResponseBindingUrl(), binding, builder.buildDocument(),
- deployment.getIDP().getSingleLogoutService().getResponseBinding());
- } catch (Exception e) {
- log.error("Could not send logout response SAML request", e);
- return AuthOutcome.FAILED;
- }
- return AuthOutcome.NOT_ATTEMPTED;
-
- }
-
-
- protected AuthOutcome handleSamlResponse(String samlResponse, String relayState) {
- SAMLDocumentHolder holder = null;
- boolean postBinding = false;
- String requestUri = facade.getRequest().getURI();
- if (facade.getRequest().getMethod().equalsIgnoreCase("GET")) {
- int index = requestUri.indexOf('?');
- if (index > -1) {
- requestUri = requestUri.substring(0, index);
- }
- holder = extractRedirectBindingResponse(samlResponse);
- } else {
- postBinding = true;
- holder = extractPostBindingResponse(samlResponse);
- }
- final StatusResponseType statusResponse = (StatusResponseType) holder.getSamlObject();
- // validate destination
- if (!requestUri.equals(statusResponse.getDestination())) {
- log.error("Request URI does not match SAML request destination");
- return AuthOutcome.FAILED;
- }
-
- if (statusResponse instanceof ResponseType) {
- try {
- if (deployment.getIDP().getSingleSignOnService().validateResponseSignature()) {
- try {
- validateSamlSignature(holder, postBinding, GeneralConstants.SAML_RESPONSE_KEY);
- } catch (VerificationException e) {
- log.error("Failed to verify saml response signature", e);
-
- challenge = new AuthChallenge() {
- @Override
- public boolean challenge(HttpFacade exchange) {
- SamlAuthenticationError error = new SamlAuthenticationError(SamlAuthenticationError.Reason.INVALID_SIGNATURE);
- exchange.getRequest().setError(error);
- exchange.getResponse().sendError(403);
- return true;
- }
-
- @Override
- public int getResponseCode() {
- return 403;
- }
- };
- return AuthOutcome.FAILED;
- }
- }
- return handleLoginResponse((ResponseType) statusResponse);
- } finally {
- sessionStore.setCurrentAction(SamlSessionStore.CurrentAction.NONE);
- }
-
- } else {
- if (sessionStore.isLoggingOut()) {
- try {
- if (deployment.getIDP().getSingleLogoutService().validateResponseSignature()) {
- try {
- validateSamlSignature(holder, postBinding, GeneralConstants.SAML_RESPONSE_KEY);
- } catch (VerificationException e) {
- log.error("Failed to verify saml response signature", e);
- return AuthOutcome.FAILED;
- }
- }
- return handleLogoutResponse(holder, statusResponse, relayState);
- } finally {
- sessionStore.setCurrentAction(SamlSessionStore.CurrentAction.NONE);
- }
-
- } else if (sessionStore.isLoggingIn()) {
-
- try {
- // KEYCLOAK-2107 - handle user not authenticated due passive mode. Return special outcome so different authentication mechanisms can behave accordingly.
- StatusType status = statusResponse.getStatus();
- if(checkStatusCodeValue(status.getStatusCode(), JBossSAMLURIConstants.STATUS_RESPONDER.get()) && checkStatusCodeValue(status.getStatusCode().getStatusCode(), JBossSAMLURIConstants.STATUS_NO_PASSIVE.get())){
- log.debug("Not authenticated due passive mode Status found in SAML response: " + status.toString());
- return AuthOutcome.NOT_AUTHENTICATED;
- }
-
- challenge = new AuthChallenge() {
- @Override
- public boolean challenge(HttpFacade exchange) {
- SamlAuthenticationError error = new SamlAuthenticationError(SamlAuthenticationError.Reason.ERROR_STATUS, statusResponse);
- exchange.getRequest().setError(error);
- exchange.getResponse().sendError(403);
- return true;
- }
-
- @Override
- public int getResponseCode() {
- return 403;
- }
- };
- return AuthOutcome.FAILED;
- } finally {
- sessionStore.setCurrentAction(SamlSessionStore.CurrentAction.NONE);
- }
- }
- return AuthOutcome.NOT_ATTEMPTED;
- }
-
- }
-
- private void validateSamlSignature(SAMLDocumentHolder holder, boolean postBinding, String paramKey) throws VerificationException {
- if (postBinding) {
- verifyPostBindingSignature(holder.getSamlDocument(), deployment.getIDP().getSignatureValidationKey());
- } else {
- verifyRedirectBindingSignature(deployment.getIDP().getSignatureValidationKey(), paramKey);
- }
- }
-
- private boolean checkStatusCodeValue(StatusCodeType statusCode, String expectedValue){
- if(statusCode != null && statusCode.getValue()!=null){
- String v = statusCode.getValue().toString();
- return expectedValue.equals(v);
- }
- return false;
- }
-
- protected AuthOutcome handleLoginResponse(ResponseType responseType) {
-
- AssertionType assertion = null;
- try {
- assertion = AssertionUtil.getAssertion(responseType, deployment.getDecryptionKey());
- if (AssertionUtil.hasExpired(assertion)) {
- return initiateLogin();
- }
- } catch (Exception e) {
- log.error("Error extracting SAML assertion: " + e.getMessage());
- challenge = new AuthChallenge() {
- @Override
- public boolean challenge(HttpFacade exchange) {
- SamlAuthenticationError error = new SamlAuthenticationError(SamlAuthenticationError.Reason.EXTRACTION_FAILURE);
- exchange.getRequest().setError(error);
- exchange.getResponse().sendError(403);
- return true;
- }
-
- @Override
- public int getResponseCode() {
- return 403;
- }
- };
- }
-
- SubjectType subject = assertion.getSubject();
- SubjectType.STSubType subType = subject.getSubType();
- NameIDType subjectNameID = (NameIDType) subType.getBaseID();
- String principalName = subjectNameID.getValue();
-
- final Set<String> roles = new HashSet<>();
- MultivaluedHashMap<String, String> attributes = new MultivaluedHashMap<>();
- MultivaluedHashMap<String, String> friendlyAttributes = new MultivaluedHashMap<>();
-
- Set<StatementAbstractType> statements = assertion.getStatements();
- for (StatementAbstractType statement : statements) {
- if (statement instanceof AttributeStatementType) {
- AttributeStatementType attributeStatement = (AttributeStatementType) statement;
- List<AttributeStatementType.ASTChoiceType> attList = attributeStatement.getAttributes();
- for (AttributeStatementType.ASTChoiceType obj : attList) {
- AttributeType attr = obj.getAttribute();
- if (isRole(attr)) {
- List<Object> attributeValues = attr.getAttributeValue();
- if (attributeValues != null) {
- for (Object attrValue : attributeValues) {
- String role = getAttributeValue(attrValue);
- log.debugv("Add role: {0}", role);
- roles.add(role);
- }
- }
- } else {
- List<Object> attributeValues = attr.getAttributeValue();
- if (attributeValues != null) {
- for (Object attrValue : attributeValues) {
- String value = getAttributeValue(attrValue);
- if (attr.getName() != null) {
- attributes.add(attr.getName(), value);
- }
- if (attr.getFriendlyName() != null) {
- friendlyAttributes.add(attr.getFriendlyName(), value);
- }
- }
- }
- }
-
- }
- }
- }
- if (deployment.getPrincipalNamePolicy() == SamlDeployment.PrincipalNamePolicy.FROM_ATTRIBUTE) {
- if (deployment.getPrincipalAttributeName() != null) {
- String attribute = attributes.getFirst(deployment.getPrincipalAttributeName());
- if (attribute != null) principalName = attribute;
- else {
- attribute = friendlyAttributes.getFirst(deployment.getPrincipalAttributeName());
- if (attribute != null) principalName = attribute;
- }
- }
- }
-
- AuthnStatementType authn = null;
- for (Object statement : assertion.getStatements()) {
- if (statement instanceof AuthnStatementType) {
- authn = (AuthnStatementType) statement;
- break;
- }
- }
-
-
- URI nameFormat = subjectNameID.getFormat();
- String nameFormatString = nameFormat == null ? JBossSAMLURIConstants.NAMEID_FORMAT_UNSPECIFIED.get() : nameFormat.toString();
- final SamlPrincipal principal = new SamlPrincipal(assertion, principalName, principalName, nameFormatString, attributes, friendlyAttributes);
- String index = authn == null ? null : authn.getSessionIndex();
- final String sessionIndex = index;
- SamlSession account = new SamlSession(principal, roles, sessionIndex);
- sessionStore.saveAccount(account);
- completeAuthentication(account);
-
-
- // redirect to original request, it will be restored
- String redirectUri = sessionStore.getRedirectUri();
- if (redirectUri != null) {
- facade.getResponse().setHeader("Location", redirectUri);
- facade.getResponse().setStatus(302);
- facade.getResponse().end();
- } else {
- log.debug("IDP initiated invocation");
- }
- log.debug("AUTHENTICATED authn");
-
- return AuthOutcome.AUTHENTICATED;
- }
-
- protected abstract void completeAuthentication(SamlSession account);
-
- private String getAttributeValue(Object attrValue) {
- String value = null;
- if (attrValue instanceof String) {
- value = (String) attrValue;
- } else if (attrValue instanceof Node) {
- Node roleNode = (Node) attrValue;
- value = roleNode.getFirstChild().getNodeValue();
- } else if (attrValue instanceof NameIDType) {
- NameIDType nameIdType = (NameIDType) attrValue;
- value = nameIdType.getValue();
- } else {
- log.warn("Unable to extract unknown SAML assertion attribute value type: " + attrValue.getClass().getName());
- }
- return value;
- }
-
- protected boolean isRole(AttributeType attribute) {
- return (attribute.getName() != null && deployment.getRoleAttributeNames().contains(attribute.getName())) || (attribute.getFriendlyName() != null && deployment.getRoleAttributeNames().contains(attribute.getFriendlyName()));
- }
-
- protected AuthOutcome handleLogoutResponse(SAMLDocumentHolder holder, StatusResponseType responseType, String relayState) {
- boolean loggedIn = sessionStore.isLoggedIn();
- if (!loggedIn || !"logout".equals(relayState)) {
- return AuthOutcome.NOT_ATTEMPTED;
- }
- sessionStore.logoutAccount();
- return AuthOutcome.LOGGED_OUT;
- }
-
- protected SAMLDocumentHolder extractRedirectBindingResponse(String response) {
- return SAMLRequestParser.parseRequestRedirectBinding(response);
- }
-
-
- protected SAMLDocumentHolder extractPostBindingResponse(String response) {
- byte[] samlBytes = PostBindingUtil.base64Decode(response);
- return SAMLRequestParser.parseResponseDocument(samlBytes);
- }
-
-
- protected AuthOutcome initiateLogin() {
- challenge = new InitiateLogin(deployment, sessionStore);
- return AuthOutcome.NOT_ATTEMPTED;
- }
-
- protected boolean verifySSL() {
- if (!facade.getRequest().isSecure() && deployment.getSslRequired().isRequired(facade.getRequest().getRemoteAddr())) {
- log.warn("SSL is required to authenticate");
- return true;
- }
- return false;
- }
-
- public void verifyPostBindingSignature(Document document, PublicKey publicKey) throws VerificationException {
- SAML2Signature saml2Signature = new SAML2Signature();
- try {
- if (!saml2Signature.validate(document, publicKey)) {
- throw new VerificationException("Invalid signature on document");
- }
- } catch (ProcessingException e) {
- throw new VerificationException("Error validating signature", e);
- }
- }
-
- public void verifyRedirectBindingSignature(PublicKey publicKey, String paramKey) throws VerificationException {
- String request = facade.getRequest().getQueryParamValue(paramKey);
- String algorithm = facade.getRequest().getQueryParamValue(GeneralConstants.SAML_SIG_ALG_REQUEST_KEY);
- String signature = facade.getRequest().getQueryParamValue(GeneralConstants.SAML_SIGNATURE_REQUEST_KEY);
- String decodedAlgorithm = facade.getRequest().getQueryParamValue(GeneralConstants.SAML_SIG_ALG_REQUEST_KEY);
-
- if (request == null) {
- throw new VerificationException("SAML Request was null");
- }
- if (algorithm == null) throw new VerificationException("SigAlg was null");
- if (signature == null) throw new VerificationException("Signature was null");
-
- // Shibboleth doesn't sign the document for redirect binding.
- // todo maybe a flag?
-
- String relayState = facade.getRequest().getQueryParamValue(GeneralConstants.RELAY_STATE);
- KeycloakUriBuilder builder = KeycloakUriBuilder.fromPath("/")
- .queryParam(paramKey, request);
- if (relayState != null) {
- builder.queryParam(GeneralConstants.RELAY_STATE, relayState);
- }
- builder.queryParam(GeneralConstants.SAML_SIG_ALG_REQUEST_KEY, algorithm);
- String rawQuery = builder.build().getRawQuery();
-
- try {
- //byte[] decodedSignature = RedirectBindingUtil.urlBase64Decode(signature);
- byte[] decodedSignature = Base64.decode(signature);
-
- SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.getFromXmlMethod(decodedAlgorithm);
- Signature validator = signatureAlgorithm.createSignature(); // todo plugin signature alg
- validator.initVerify(publicKey);
- validator.update(rawQuery.getBytes("UTF-8"));
- if (!validator.verify(decodedSignature)) {
- throw new VerificationException("Invalid query param signature");
- }
- } catch (Exception e) {
- throw new VerificationException(e);
- }
- }
-
-
-}
+}
\ No newline at end of file
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 fb90e17..219042b 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 3833c56..ad7bee5 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/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 0000000..2e15502
--- /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 <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class HttpBasicAuthenticator implements AuthenticatorFactory {
+
+ public static final String PROVIDER_ID = "http-basic-authenticator";
+
+ @Override
+ public String getDisplayType() {
+ return null;
+ }
+
+ @Override
+ public String getReferenceCategory() {
+ return null;
+ }
+
+ @Override
+ public boolean isConfigurable() {
+ return false;
+ }
+
+ @Override
+ public Requirement[] getRequirementChoices() {
+ return new Requirement[0];
+ }
+
+ @Override
+ public boolean isUserSetupAllowed() {
+ return false;
+ }
+
+ @Override
+ public String getHelpText() {
+ return null;
+ }
+
+ @Override
+ public List<ProviderConfigProperty> getConfigProperties() {
+ return null;
+ }
+
+ @Override
+ public Authenticator create(KeycloakSession session) {
+ return new Authenticator() {
+
+ private static final String BASIC = "Basic";
+ private static final String BASIC_PREFIX = BASIC + " ";
+
+ @Override
+ public void authenticate(AuthenticationFlowContext context) {
+ HttpRequest httpRequest = context.getHttpRequest();
+ HttpHeaders httpHeaders = httpRequest.getHttpHeaders();
+ String[] usernameAndPassword = getUsernameAndPassword(httpHeaders);
+
+ context.attempted();
+
+ if (usernameAndPassword != null) {
+ RealmModel realm = context.getRealm();
+ UserModel user = context.getSession().users().getUserByUsername(usernameAndPassword[0], realm);
+
+ if (user != null) {
+ String password = usernameAndPassword[1];
+ boolean valid = context.getSession().users().validCredentials(context.getSession(), realm, user, UserCredentialModel.password(password));
+
+ if (valid) {
+ context.getClientSession().setAuthenticatedUser(user);
+ context.success();
+ } else {
+ context.getEvent().user(user);
+ context.getEvent().error(Errors.INVALID_USER_CREDENTIALS);
+ context.failure(AuthenticationFlowError.INVALID_USER, Response.status(Response.Status.UNAUTHORIZED)
+ .header(HttpHeaders.WWW_AUTHENTICATE, BASIC_PREFIX + "realm=\"" + realm.getName() + "\"")
+ .build());
+ }
+ }
+ }
+ }
+
+ private String[] getUsernameAndPassword(HttpHeaders httpHeaders) {
+ List<String> authHeaders = httpHeaders.getRequestHeader(HttpHeaders.AUTHORIZATION);
+
+ if (authHeaders == null || authHeaders.size() == 0) {
+ return null;
+ }
+
+ String credentials = null;
+
+ for (String authHeader : authHeaders) {
+ if (authHeader.startsWith(BASIC_PREFIX)) {
+ String[] split = authHeader.trim().split("\\s+");
+
+ if (split == null || split.length != 2) return null;
+
+ credentials = split[1];
+ }
+ }
+
+ try {
+ return new String(Base64.decode(credentials)).split(":");
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to parse credentials.", e);
+ }
+ }
+
+ @Override
+ public void action(AuthenticationFlowContext context) {
+
+ }
+
+ @Override
+ public boolean requiresUser() {
+ return false;
+ }
+
+ @Override
+ public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) {
+ return false;
+ }
+
+ @Override
+ public void setRequiredActions(KeycloakSession session, RealmModel realm, UserModel user) {
+
+ }
+
+ @Override
+ public void close() {
+
+ }
+ };
+ }
+
+ @Override
+ public void init(Config.Scope config) {
+
+ }
+
+ @Override
+ public void postInit(KeycloakSessionFactory factory) {
+
+ }
+
+ @Override
+ public void close() {
+
+ }
+
+ @Override
+ public String getId() {
+ return PROVIDER_ID;
+ }
+}
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 0000000..dba1b29
--- /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 <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class SamlEcpProfileProtocolFactory extends SamlProtocolFactory {
+
+ static final String ID = "saml-ecp-profile";
+
+ private static final String NS_PREFIX_PROFILE_ECP = "ecp";
+ private static final String NS_PREFIX_SAML_PROTOCOL = "samlp";
+ private static final String NS_PREFIX_SAML_ASSERTION = "saml";
+
+ @Override
+ public Object createProtocolEndpoint(RealmModel realm, EventBuilder event, AuthenticationManager authManager) {
+ return new SamlEcpProfileService(realm, event, authManager);
+ }
+
+ @Override
+ public LoginProtocol create(KeycloakSession session) {
+ return new SamlProtocol() {
+ // method created to send a SOAP Binding response instead of a HTTP POST response
+ @Override
+ protected Response buildAuthenticatedResponse(ClientSessionModel clientSession, String redirectUri, Document samlDocument, JaxrsSAML2BindingBuilder bindingBuilder) throws ConfigurationException, ProcessingException, IOException {
+ Document document = bindingBuilder.postBinding(samlDocument).getDocument();
+
+ try {
+ SoapMessageBuilder messageBuilder = Soap.createMessage()
+ .addNamespace(NS_PREFIX_SAML_ASSERTION, JBossSAMLURIConstants.ASSERTION_NSURI.get())
+ .addNamespace(NS_PREFIX_SAML_PROTOCOL, JBossSAMLURIConstants.PROTOCOL_NSURI.get())
+ .addNamespace(NS_PREFIX_PROFILE_ECP, JBossSAMLURIConstants.ECP_PROFILE.get());
+
+ createEcpResponseHeader(redirectUri, messageBuilder);
+ createRequestAuthenticatedHeader(clientSession, messageBuilder);
+
+ messageBuilder.addToBody(document);
+
+ return messageBuilder.build();
+ } catch (Exception e) {
+ throw new RuntimeException("Error while creating SAML response.", e);
+ }
+ }
+
+ private void createRequestAuthenticatedHeader(ClientSessionModel clientSession, SoapMessageBuilder messageBuilder) {
+ ClientModel client = clientSession.getClient();
+
+ if ("true".equals(client.getAttribute(SamlProtocol.SAML_CLIENT_SIGNATURE_ATTRIBUTE))) {
+ SOAPHeaderElement ecpRequestAuthenticated = messageBuilder.addHeader(JBossSAMLConstants.REQUEST_AUTHENTICATED.get(), NS_PREFIX_PROFILE_ECP);
+
+ ecpRequestAuthenticated.setMustUnderstand(true);
+ ecpRequestAuthenticated.setActor("http://schemas.xmlsoap.org/soap/actor/next");
+ }
+ }
+
+ private void createEcpResponseHeader(String redirectUri, SoapMessageBuilder messageBuilder) throws SOAPException {
+ SOAPHeaderElement ecpResponseHeader = messageBuilder.addHeader(JBossSAMLConstants.RESPONSE.get(), NS_PREFIX_PROFILE_ECP);
+
+ ecpResponseHeader.setMustUnderstand(true);
+ ecpResponseHeader.setActor("http://schemas.xmlsoap.org/soap/actor/next");
+ ecpResponseHeader.addAttribute(messageBuilder.createName(JBossSAMLConstants.ASSERTION_CONSUMER_SERVICE_URL.get()), redirectUri);
+ }
+
+ @Override
+ protected Response buildErrorResponse(ClientSessionModel clientSession, JaxrsSAML2BindingBuilder binding, Document document) throws ConfigurationException, ProcessingException, IOException {
+ return Soap.createMessage().addToBody(document).build();
+ }
+
+ @Override
+ protected Response buildLogoutResponse(UserSessionModel userSession, String logoutBindingUri, SAML2LogoutResponseBuilder builder, JaxrsSAML2BindingBuilder binding) throws ConfigurationException, ProcessingException, IOException {
+ return Soap.createFault().reason("Logout not supported.").build();
+ }
+ }.setSession(session);
+ }
+
+ @Override
+ public void init(Config.Scope config) {
+ }
+
+ @Override
+ public String getId() {
+ return ID;
+ }
+}
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 0000000..c16b997
--- /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 <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class SamlEcpProfileService extends SamlService {
+
+ public SamlEcpProfileService(RealmModel realm, EventBuilder event, AuthenticationManager authManager) {
+ super(realm, event, authManager);
+ }
+
+ public Response authenticate(InputStream inputStream) {
+ try {
+ return new PostBindingProtocol() {
+ @Override
+ protected String getBindingType(AuthnRequestType requestAbstractType) {
+ return SamlProtocol.SAML_SOAP_BINDING;
+ }
+
+ @Override
+ protected Response loginRequest(String relayState, AuthnRequestType requestAbstractType, ClientModel client) {
+ // force passive authentication when executing this profile
+ requestAbstractType.setIsPassive(true);
+ requestAbstractType.setDestination(uriInfo.getAbsolutePath());
+ return super.loginRequest(relayState, requestAbstractType, client);
+ }
+ }.execute(Soap.toSamlHttpPostMessage(inputStream), null, null);
+ } catch (Exception e) {
+ String reason = "Some error occurred while processing the AuthnRequest.";
+ String detail = e.getMessage();
+
+ if (detail == null) {
+ detail = reason;
+ }
+
+ return Soap.createFault().reason(reason).detail(detail).build();
+ }
+ }
+
+ @Override
+ protected String getLoginProtocol() {
+ return SamlEcpProfileProtocolFactory.ID;
+ }
+
+ @Override
+ protected AuthenticationFlowModel getAuthenticationFlow() {
+ for (AuthenticationFlowModel flowModel : realm.getAuthenticationFlows()) {
+ if (flowModel.getAlias().equals(DefaultAuthenticationFlows.SAML_ECP_FLOW)) {
+ return flowModel;
+ }
+ }
+
+ throw new RuntimeException("Could not resolve authentication flow for SAML ECP Profile.");
+ }
+}
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 0000000..4bdf76a
--- /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 <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public final class Soap {
+
+ public static SoapFaultBuilder createFault() {
+ return new SoapFaultBuilder();
+ }
+
+ public static SoapMessageBuilder createMessage() {
+ return new SoapMessageBuilder();
+ }
+
+ /**
+ * <p>Returns a string encoded accordingly with the SAML HTTP POST Binding specification based on the
+ * given <code>inputStream</code> which must contain a valid SOAP message.
+ *
+ * <p>The resulting string is based on the Body of the SOAP message, which should map to a valid SAML message.
+ *
+ * @param inputStream the input stream containing a valid SOAP message with a Body that contains a SAML message
+ *
+ * @return a string encoded accordingly with the SAML HTTP POST Binding specification
+ */
+ public static String toSamlHttpPostMessage(InputStream inputStream) {
+ try {
+ MessageFactory messageFactory = MessageFactory.newInstance();
+ SOAPMessage soapMessage = messageFactory.createMessage(null, inputStream);
+ SOAPBody soapBody = soapMessage.getSOAPBody();
+ Node authnRequestNode = soapBody.getFirstChild();
+ Document document = DocumentUtil.createDocument();
+
+ document.appendChild(document.importNode(authnRequestNode, true));
+
+ return PostBindingUtil.base64Encode(DocumentUtil.asString(document));
+ } catch (Exception e) {
+ throw new RuntimeException("Error creating fault message.", e);
+ }
+ }
+
+ public static class SoapMessageBuilder {
+ private final SOAPMessage message;
+ private final SOAPBody body;
+ private final SOAPEnvelope envelope;
+
+ private SoapMessageBuilder() {
+ try {
+ this.message = MessageFactory.newInstance().createMessage();
+ this.envelope = message.getSOAPPart().getEnvelope();
+ this.body = message.getSOAPBody();
+ } catch (Exception e) {
+ throw new RuntimeException("Error creating fault message.", e);
+ }
+ }
+
+ public SoapMessageBuilder addToBody(Document document) {
+ try {
+ this.body.addDocument(document);
+ } catch (SOAPException e) {
+ throw new RuntimeException("Could not add document to SOAP body.", e);
+ }
+ return this;
+ }
+
+ public SoapMessageBuilder addNamespace(String prefix, String ns) {
+ try {
+ envelope.addNamespaceDeclaration(prefix, ns);
+ } catch (SOAPException e) {
+ throw new RuntimeException("Could not add namespace to SOAP Envelope.", e);
+ }
+ return this;
+ }
+
+ public SOAPHeaderElement addHeader(String name, String prefix) {
+ try {
+ return this.envelope.getHeader().addHeaderElement(envelope.createQName(name, prefix));
+ } catch (SOAPException e) {
+ throw new RuntimeException("Could not add SOAP Header.", e);
+ }
+ }
+
+ public Name createName(String name) {
+ try {
+ return this.envelope.createName(name);
+ } catch (SOAPException e) {
+ throw new RuntimeException("Could not create Name.", e);
+ }
+ }
+
+ public Response build() {
+ return build(Status.OK);
+ }
+
+ Response build(Status status) {
+ ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+
+ try {
+ this.message.writeTo(outputStream);
+ } catch (Exception e) {
+ throw new RuntimeException("Error while building SOAP Fault.", e);
+ }
+
+ return Response.status(status).entity(outputStream.toByteArray()).build();
+ }
+
+ SOAPMessage getMessage() {
+ return this.message;
+ }
+ }
+
+ public static class SoapFaultBuilder {
+
+ private final SOAPFault fault;
+ private final SoapMessageBuilder messageBuilder;
+
+ private SoapFaultBuilder() {
+ this.messageBuilder = createMessage();
+ try {
+ this.fault = messageBuilder.getMessage().getSOAPBody().addFault();
+ } catch (SOAPException e) {
+ throw new RuntimeException("Could not create SOAP Fault.", e);
+ }
+ }
+
+ public SoapFaultBuilder detail(String detail) {
+ try {
+ this.fault.addDetail().setValue(detail);
+ } catch (SOAPException e) {
+ throw new RuntimeException("Error creating fault message.", e);
+ }
+ return this;
+ }
+
+ public SoapFaultBuilder reason(String reason) {
+ try {
+ this.fault.setFaultString(reason);
+ } catch (SOAPException e) {
+ throw new RuntimeException("Error creating fault message.", e);
+ }
+ return this;
+ }
+
+ public SoapFaultBuilder code(String code) {
+ try {
+ this.fault.setFaultCode(code);
+ } catch (SOAPException e) {
+ throw new RuntimeException("Error creating fault message.", e);
+ }
+ return this;
+ }
+
+ public Response build() {
+ return this.messageBuilder.build(Status.INTERNAL_SERVER_ERROR);
+ }
+ }
+}
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 0bc3ede..07c528d 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 a7a86ed..7dcc866 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 3402593..f9aa30b 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/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 0000000..9ac8020
--- /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 d0a2dd0..ae434f6 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 a1fc4a7..9dc5548 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 0000000..c02deeb
--- /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 <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public class SamlEcpProfileTest {
+
+ protected String APP_SERVER_BASE_URL = "http://localhost:8081";
+
+ @ClassRule
+ public static org.keycloak.testsuite.samlfilter.SamlKeycloakRule keycloakRule = new org.keycloak.testsuite.samlfilter.SamlKeycloakRule() {
+ @Override
+ public void initWars() {
+ ClassLoader classLoader = SamlAdapterTest.class.getClassLoader();
+
+ initializeSamlSecuredWar("/keycloak-saml/ecp/ecp-sp", "/ecp-sp", "ecp-sp.war", classLoader);
+ }
+
+ @Override
+ public String getRealmJson() {
+ return "/keycloak-saml/ecp/testsamlecp.json";
+ }
+ };
+
+ @Test
+ public void testSuccessfulEcpFlow() throws Exception {
+ Response authnRequestResponse = ClientBuilder.newClient().target(APP_SERVER_BASE_URL + "/ecp-sp/").request()
+ .header("Accept", "text/html; application/vnd.paos+xml")
+ .header("PAOS", "ver='urn:liberty:paos:2003-08' ;'urn:oasis:names:tc:SAML:2.0:profiles:SSO:ecp'")
+ .get();
+
+ SOAPMessage authnRequestMessage = MessageFactory.newInstance().createMessage(null, new ByteArrayInputStream(authnRequestResponse.readEntity(byte[].class)));
+
+ printDocument(authnRequestMessage.getSOAPPart().getContent(), System.out);
+
+ Iterator<SOAPHeaderElement> it = authnRequestMessage.getSOAPHeader().<SOAPHeaderElement>getChildElements(new QName("urn:oasis:names:tc:SAML:2.0:profiles:SSO:ecp", "Request"));
+ SOAPHeaderElement ecpRequestHeader = it.next();
+ NodeList idpList = ecpRequestHeader.getElementsByTagNameNS("urn:oasis:names:tc:SAML:2.0:protocol", "IDPList");
+
+ assertEquals("No IDPList returned from Service Provider", 1, idpList.getLength());
+
+ NodeList idpEntries = idpList.item(0).getChildNodes();
+
+ assertEquals("No IDPEntry returned from Service Provider", 1, idpEntries.getLength());
+
+ String singleSignOnService = null;
+
+ for (int i = 0; i < idpEntries.getLength(); i++) {
+ Node item = idpEntries.item(i);
+ NamedNodeMap attributes = item.getAttributes();
+ Node location = attributes.getNamedItem("Loc");
+
+ singleSignOnService = location.getNodeValue();
+ }
+
+ assertNotNull("Could not obtain SSO Service URL", singleSignOnService);
+
+ Document authenticationRequest = authnRequestMessage.getSOAPBody().getFirstChild().getOwnerDocument();
+ String username = "pedroigor";
+ String password = "password";
+ String pair = username + ":" + password;
+ String authHeader = "Basic " + new String(Base64.encodeBytes(pair.getBytes()));
+
+ Response authenticationResponse = ClientBuilder.newClient().target(singleSignOnService).request()
+ .header(HttpHeaders.AUTHORIZATION, authHeader)
+ .post(Entity.entity(DocumentUtil.asString(authenticationRequest), "application/soap+xml"));
+
+ assertEquals(OK.getStatusCode(), authenticationResponse.getStatus());
+
+ SOAPMessage responseMessage = MessageFactory.newInstance().createMessage(null, new ByteArrayInputStream(authenticationResponse.readEntity(byte[].class)));
+
+ printDocument(responseMessage.getSOAPPart().getContent(), System.out);
+
+ SOAPHeader responseMessageHeaders = responseMessage.getSOAPHeader();
+
+ NodeList ecpResponse = responseMessageHeaders.getElementsByTagNameNS(JBossSAMLURIConstants.ECP_PROFILE.get(), JBossSAMLConstants.RESPONSE.get());
+
+ assertEquals("No ECP Response", 1, ecpResponse.getLength());
+
+ Node samlResponse = responseMessage.getSOAPBody().getFirstChild();
+
+ assertNotNull(samlResponse);
+
+ ResponseType responseType = (ResponseType) new SAMLParser().parse(DocumentUtil.getNodeAsStream(samlResponse));
+ StatusCodeType statusCode = responseType.getStatus().getStatusCode();
+
+ assertEquals(statusCode.getValue().toString(), JBossSAMLURIConstants.STATUS_SUCCESS.get());
+ assertEquals("http://localhost:8081/ecp-sp/", responseType.getDestination());
+ assertNotNull(responseType.getSignature());
+ assertEquals(1, responseType.getAssertions().size());
+
+ SOAPMessage samlResponseRequest = MessageFactory.newInstance().createMessage();
+
+ samlResponseRequest.getSOAPBody().addDocument(responseMessage.getSOAPBody().extractContentAsDocument());
+
+ ByteArrayOutputStream os = new ByteArrayOutputStream();
+
+ samlResponseRequest.writeTo(os);
+
+ Response serviceProviderFinalResponse = ClientBuilder.newClient().target(responseType.getDestination()).request()
+ .post(Entity.entity(os.toByteArray(), "application/vnd.paos+xml"));
+
+ Map<String, NewCookie> cookies = serviceProviderFinalResponse.getCookies();
+
+ Builder resourceRequest = ClientBuilder.newClient().target(responseType.getDestination() + "/index.html").request();
+
+ for (NewCookie cookie : cookies.values()) {
+ resourceRequest.cookie(cookie);
+ }
+
+ Response resourceResponse = resourceRequest.get();
+
+ assertTrue(resourceResponse.readEntity(String.class).contains("pedroigor"));
+ }
+
+ @Test
+ public void testInvalidCredentials() throws Exception {
+ Response authnRequestResponse = ClientBuilder.newClient().target(APP_SERVER_BASE_URL + "/ecp-sp/").request()
+ .header("Accept", "text/html; application/vnd.paos+xml")
+ .header("PAOS", "ver='urn:liberty:paos:2003-08' ;'urn:oasis:names:tc:SAML:2.0:profiles:SSO:ecp'")
+ .get();
+
+ SOAPMessage authnRequestMessage = MessageFactory.newInstance().createMessage(null, new ByteArrayInputStream(authnRequestResponse.readEntity(byte[].class)));
+ Iterator<SOAPHeaderElement> it = authnRequestMessage.getSOAPHeader().<SOAPHeaderElement>getChildElements(new QName("urn:liberty:paos:2003-08", "Request"));
+
+ it.next();
+
+ it = authnRequestMessage.getSOAPHeader().<SOAPHeaderElement>getChildElements(new QName("urn:oasis:names:tc:SAML:2.0:profiles:SSO:ecp", "Request"));
+ SOAPHeaderElement ecpRequestHeader = it.next();
+ NodeList idpList = ecpRequestHeader.getElementsByTagNameNS("urn:oasis:names:tc:SAML:2.0:protocol", "IDPList");
+
+ assertEquals("No IDPList returned from Service Provider", 1, idpList.getLength());
+
+ NodeList idpEntries = idpList.item(0).getChildNodes();
+
+ assertEquals("No IDPEntry returned from Service Provider", 1, idpEntries.getLength());
+
+ String singleSignOnService = null;
+
+ for (int i = 0; i < idpEntries.getLength(); i++) {
+ Node item = idpEntries.item(i);
+ NamedNodeMap attributes = item.getAttributes();
+ Node location = attributes.getNamedItem("Loc");
+
+ singleSignOnService = location.getNodeValue();
+ }
+
+ assertNotNull("Could not obtain SSO Service URL", singleSignOnService);
+
+ Document authenticationRequest = authnRequestMessage.getSOAPBody().getFirstChild().getOwnerDocument();
+ String username = "pedroigor";
+ String password = "baspassword";
+ String pair = username + ":" + password;
+ String authHeader = "Basic " + new String(Base64.encodeBytes(pair.getBytes()));
+
+ Response authenticationResponse = ClientBuilder.newClient().target(singleSignOnService).request()
+ .header(HttpHeaders.AUTHORIZATION, authHeader)
+ .post(Entity.entity(DocumentUtil.asString(authenticationRequest), "application/soap+xml"));
+
+ assertEquals(OK.getStatusCode(), authenticationResponse.getStatus());
+
+ SOAPMessage responseMessage = MessageFactory.newInstance().createMessage(null, new ByteArrayInputStream(authenticationResponse.readEntity(byte[].class)));
+ Node samlResponse = responseMessage.getSOAPBody().getFirstChild();
+
+ assertNotNull(samlResponse);
+
+ StatusResponseType responseType = (StatusResponseType) new SAMLParser().parse(DocumentUtil.getNodeAsStream(samlResponse));
+ StatusCodeType statusCode = responseType.getStatus().getStatusCode();
+
+ assertNotEquals(statusCode.getStatusCode().getValue().toString(), JBossSAMLURIConstants.STATUS_SUCCESS.get());
+ }
+
+ public static void printDocument(Source doc, OutputStream out) throws IOException, TransformerException {
+ TransformerFactory tf = TransformerFactory.newInstance();
+ Transformer transformer = tf.newTransformer();
+ transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "no");
+ transformer.setOutputProperty(OutputKeys.METHOD, "xml");
+ transformer.setOutputProperty(OutputKeys.INDENT, "yes");
+ transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
+ transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "4");
+
+ transformer.transform(doc,
+ new StreamResult(new OutputStreamWriter(out, "UTF-8")));
+ }
+}
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 0000000..df39712
--- /dev/null
+++ b/testsuite/integration/src/test/resources/keycloak-saml/ecp/ecp-sp/WEB-INF/keycloak-saml.xml
@@ -0,0 +1,40 @@
+<keycloak-saml-adapter>
+ <SP entityID="http://localhost:8081/ecp-sp/"
+ sslPolicy="EXTERNAL"
+ nameIDPolicyFormat="urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified"
+ logoutPage="/logout.jsp"
+ forceAuthentication="false">
+ <Keys>
+ <Key signing="true" >
+ <KeyStore resource="/WEB-INF/keystore.jks" password="store123">
+ <PrivateKey alias="http://localhost:8080/sales-post-sig/" password="test123"/>
+ <Certificate alias="http://localhost:8080/sales-post-sig/"/>
+ </KeyStore>
+ </Key>
+ </Keys>
+ <PrincipalNameMapping policy="FROM_NAME_ID"/>
+ <RoleIdentifiers>
+ <Attribute name="Role"/>
+ </RoleIdentifiers>
+ <IDP entityID="idp"
+ signaturesRequired="true">
+ <SingleSignOnService requestBinding="POST"
+ bindingUrl="http://localhost:8081/auth/realms/demo/protocol/saml"
+ />
+
+ <SingleLogoutService
+ requestBinding="POST"
+ responseBinding="POST"
+ postBindingUrl="http://localhost:8081/auth/realms/demo/protocol/saml"
+ redirectBindingUrl="http://localhost:8081/auth/realms/demo/protocol/saml"
+ />
+ <Keys>
+ <Key signing="true">
+ <KeyStore resource="/WEB-INF/keystore.jks" password="store123">
+ <Certificate alias="demo"/>
+ </KeyStore>
+ </Key>
+ </Keys>
+ </IDP>
+ </SP>
+</keycloak-saml-adapter>
\ 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 0000000..144830b
Binary files /dev/null and b/testsuite/integration/src/test/resources/keycloak-saml/ecp/ecp-sp/WEB-INF/keystore.jks differ
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 0000000..981cbda
--- /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"
+ }
+ ]
+ }
+}