Merge pull request #1951 from pedroigor/KEYCLOAK-2202

[KEYCLOAK-2202] - Initial support for SAML ECP Profile.
This commit is contained in:
Bill Burke 2015-12-17 19:04:17 -05:00
commit 0197c69ac3
26 changed files with 1799 additions and 543 deletions

View file

@ -26,6 +26,7 @@
<module name="org.keycloak.keycloak-connections-http-client" services="import"/> <module name="org.keycloak.keycloak-connections-http-client" services="import"/>
<module name="javax.api"/> <module name="javax.api"/>
<module name="javax.xml.soap.api"/>
</dependencies> </dependencies>
</module> </module>

View file

@ -24,6 +24,7 @@ public class DefaultAuthenticationFlows {
public static final String DIRECT_GRANT_FLOW = "direct grant"; public static final String DIRECT_GRANT_FLOW = "direct grant";
public static final String RESET_CREDENTIALS_FLOW = "reset credentials"; public static final String RESET_CREDENTIALS_FLOW = "reset credentials";
public static final String LOGIN_FORMS_FLOW = "forms"; public static final String LOGIN_FORMS_FLOW = "forms";
public static final String SAML_ECP_FLOW = "saml ecp";
public static final String CLIENT_AUTHENTICATION_FLOW = "clients"; public static final String CLIENT_AUTHENTICATION_FLOW = "clients";
public static final String FIRST_BROKER_LOGIN_FLOW = "first broker login"; public static final String FIRST_BROKER_LOGIN_FLOW = "first broker login";
@ -39,6 +40,7 @@ public class DefaultAuthenticationFlows {
if (realm.getFlowByAlias(RESET_CREDENTIALS_FLOW) == null) resetCredentialsFlow(realm); if (realm.getFlowByAlias(RESET_CREDENTIALS_FLOW) == null) resetCredentialsFlow(realm);
if (realm.getFlowByAlias(CLIENT_AUTHENTICATION_FLOW) == null) clientAuthFlow(realm); if (realm.getFlowByAlias(CLIENT_AUTHENTICATION_FLOW) == null) clientAuthFlow(realm);
if (realm.getFlowByAlias(FIRST_BROKER_LOGIN_FLOW) == null) firstBrokerLoginFlow(realm, false); if (realm.getFlowByAlias(FIRST_BROKER_LOGIN_FLOW) == null) firstBrokerLoginFlow(realm, false);
if (realm.getFlowByAlias(SAML_ECP_FLOW) == null) samlEcpProfile(realm);
} }
public static void migrateFlows(RealmModel realm) { public static void migrateFlows(RealmModel realm) {
if (realm.getFlowByAlias(BROWSER_FLOW) == null) browserFlow(realm, true); if (realm.getFlowByAlias(BROWSER_FLOW) == null) browserFlow(realm, true);
@ -47,6 +49,7 @@ public class DefaultAuthenticationFlows {
if (realm.getFlowByAlias(RESET_CREDENTIALS_FLOW) == null) resetCredentialsFlow(realm); if (realm.getFlowByAlias(RESET_CREDENTIALS_FLOW) == null) resetCredentialsFlow(realm);
if (realm.getFlowByAlias(CLIENT_AUTHENTICATION_FLOW) == null) clientAuthFlow(realm); if (realm.getFlowByAlias(CLIENT_AUTHENTICATION_FLOW) == null) clientAuthFlow(realm);
if (realm.getFlowByAlias(FIRST_BROKER_LOGIN_FLOW) == null) firstBrokerLoginFlow(realm, true); if (realm.getFlowByAlias(FIRST_BROKER_LOGIN_FLOW) == null) firstBrokerLoginFlow(realm, true);
if (realm.getFlowByAlias(SAML_ECP_FLOW) == null) samlEcpProfile(realm);
} }
public static void registrationFlow(RealmModel realm) { public static void registrationFlow(RealmModel realm) {
@ -447,4 +450,25 @@ public class DefaultAuthenticationFlows {
execution.setAuthenticatorFlow(false); execution.setAuthenticatorFlow(false);
realm.addAuthenticatorExecution(execution); realm.addAuthenticatorExecution(execution);
} }
public static void samlEcpProfile(RealmModel realm) {
AuthenticationFlowModel ecpFlow = new AuthenticationFlowModel();
ecpFlow.setAlias(SAML_ECP_FLOW);
ecpFlow.setDescription("SAML ECP Profile Authentication Flow");
ecpFlow.setProviderId("basic-flow");
ecpFlow.setTopLevel(true);
ecpFlow.setBuiltIn(true);
ecpFlow = realm.addAuthenticationFlow(ecpFlow);
AuthenticationExecutionModel execution = new AuthenticationExecutionModel();
execution.setParentFlow(ecpFlow.getId());
execution.setRequirement(AuthenticationExecutionModel.Requirement.REQUIRED);
execution.setAuthenticator("http-basic-authenticator");
execution.setPriority(10);
execution.setAuthenticatorFlow(false);
realm.addAuthenticatorExecution(execution);
}
} }

View file

@ -7,21 +7,24 @@ import org.keycloak.saml.BaseSAML2BindingBuilder;
import org.keycloak.saml.SAML2AuthnRequestBuilder; import org.keycloak.saml.SAML2AuthnRequestBuilder;
import org.keycloak.saml.SAML2NameIDPolicyBuilder; import org.keycloak.saml.SAML2NameIDPolicyBuilder;
import org.keycloak.saml.common.constants.JBossSAMLURIConstants; import org.keycloak.saml.common.constants.JBossSAMLURIConstants;
import org.keycloak.saml.common.exceptions.ConfigurationException;
import org.keycloak.saml.common.exceptions.ProcessingException;
import org.w3c.dom.Document; import org.w3c.dom.Document;
import java.io.IOException;
import java.security.KeyPair; import java.security.KeyPair;
/** /**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a> * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $ * @version $Revision: 1 $
*/ */
public class InitiateLogin implements AuthChallenge { public abstract class AbstractInitiateLogin implements AuthChallenge {
protected static Logger log = Logger.getLogger(InitiateLogin.class); protected static Logger log = Logger.getLogger(AbstractInitiateLogin.class);
protected SamlDeployment deployment; protected SamlDeployment deployment;
protected SamlSessionStore sessionStore; protected SamlSessionStore sessionStore;
public InitiateLogin(SamlDeployment deployment, SamlSessionStore sessionStore) { public AbstractInitiateLogin(SamlDeployment deployment, SamlSessionStore sessionStore) {
this.deployment = deployment; this.deployment = deployment;
this.sessionStore = sessionStore; this.sessionStore = sessionStore;
} }
@ -35,18 +38,14 @@ public class InitiateLogin implements AuthChallenge {
public boolean challenge(HttpFacade httpFacade) { public boolean challenge(HttpFacade httpFacade) {
try { try {
String issuerURL = deployment.getEntityID(); String issuerURL = deployment.getEntityID();
String actionUrl = deployment.getIDP().getSingleSignOnService().getRequestBindingUrl();
String destinationUrl = actionUrl;
String nameIDPolicyFormat = deployment.getNameIDPolicyFormat(); String nameIDPolicyFormat = deployment.getNameIDPolicyFormat();
if (nameIDPolicyFormat == null) { if (nameIDPolicyFormat == null) {
nameIDPolicyFormat = JBossSAMLURIConstants.NAMEID_FORMAT_PERSISTENT.get(); nameIDPolicyFormat = JBossSAMLURIConstants.NAMEID_FORMAT_PERSISTENT.get();
} }
SAML2AuthnRequestBuilder authnRequestBuilder = new SAML2AuthnRequestBuilder() SAML2AuthnRequestBuilder authnRequestBuilder = new SAML2AuthnRequestBuilder()
.destination(destinationUrl) .destination(deployment.getIDP().getSingleSignOnService().getRequestBindingUrl())
.issuer(issuerURL) .issuer(issuerURL)
.forceAuthn(deployment.isForceAuthentication()).isPassive(deployment.isIsPassive()) .forceAuthn(deployment.isForceAuthentication()).isPassive(deployment.isIsPassive())
.nameIdPolicy(SAML2NameIDPolicyBuilder.format(nameIDPolicyFormat)); .nameIdPolicy(SAML2NameIDPolicyBuilder.format(nameIDPolicyFormat));
@ -79,9 +78,7 @@ public class InitiateLogin implements AuthChallenge {
} }
sessionStore.saveRequest(); sessionStore.saveRequest();
Document document = authnRequestBuilder.toDocument(); sendAuthnRequest(httpFacade, authnRequestBuilder, binding);
SamlDeployment.Binding samlBinding = deployment.getIDP().getSingleSignOnService().getRequestBinding();
SamlUtil.sendSaml(true, httpFacade, actionUrl, binding, document, samlBinding);
sessionStore.setCurrentAction(SamlSessionStore.CurrentAction.LOGGING_IN); sessionStore.setCurrentAction(SamlSessionStore.CurrentAction.LOGGING_IN);
} catch (Exception e) { } catch (Exception e) {
throw new RuntimeException("Could not create authentication request.", e); throw new RuntimeException("Could not create authentication request.", e);
@ -89,4 +86,6 @@ public class InitiateLogin implements AuthChallenge {
return true; return true;
} }
protected abstract void sendAuthnRequest(HttpFacade httpFacade, SAML2AuthnRequestBuilder authnRequestBuilder, BaseSAML2BindingBuilder binding) throws ProcessingException, ConfigurationException, IOException;
} }

View file

@ -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);
}

View file

@ -1,530 +1,49 @@
package org.keycloak.adapters.saml; package org.keycloak.adapters.saml;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.keycloak.adapters.saml.profile.SamlAuthenticationHandler;
import org.keycloak.adapters.saml.profile.ecp.EcpAuthenticationHandler;
import org.keycloak.adapters.saml.profile.webbrowsersso.WebBrowserSsoAuthenticationHandler;
import org.keycloak.adapters.spi.AuthChallenge; import org.keycloak.adapters.spi.AuthChallenge;
import org.keycloak.adapters.spi.AuthOutcome; import org.keycloak.adapters.spi.AuthOutcome;
import org.keycloak.adapters.spi.HttpFacade; import org.keycloak.adapters.spi.HttpFacade;
import org.keycloak.common.VerificationException;
import org.keycloak.common.util.KeycloakUriBuilder;
import org.keycloak.common.util.MultivaluedHashMap;
import org.keycloak.dom.saml.v2.assertion.AssertionType;
import org.keycloak.dom.saml.v2.assertion.AttributeStatementType;
import org.keycloak.dom.saml.v2.assertion.AttributeType;
import org.keycloak.dom.saml.v2.assertion.AuthnStatementType;
import org.keycloak.dom.saml.v2.assertion.NameIDType;
import org.keycloak.dom.saml.v2.assertion.StatementAbstractType;
import org.keycloak.dom.saml.v2.assertion.SubjectType;
import org.keycloak.dom.saml.v2.protocol.LogoutRequestType;
import org.keycloak.dom.saml.v2.protocol.RequestAbstractType;
import org.keycloak.dom.saml.v2.protocol.ResponseType;
import org.keycloak.dom.saml.v2.protocol.StatusCodeType;
import org.keycloak.dom.saml.v2.protocol.StatusResponseType;
import org.keycloak.dom.saml.v2.protocol.StatusType;
import org.keycloak.saml.BaseSAML2BindingBuilder;
import org.keycloak.saml.SAML2LogoutRequestBuilder;
import org.keycloak.saml.SAML2LogoutResponseBuilder;
import org.keycloak.saml.SAMLRequestParser;
import org.keycloak.saml.SignatureAlgorithm;
import org.keycloak.saml.common.constants.GeneralConstants;
import org.keycloak.saml.common.constants.JBossSAMLURIConstants;
import org.keycloak.saml.common.exceptions.ProcessingException;
import org.keycloak.saml.common.util.Base64;
import org.keycloak.saml.common.util.StringUtil;
import org.keycloak.saml.processing.api.saml.v2.sig.SAML2Signature;
import org.keycloak.saml.processing.core.saml.v2.common.SAMLDocumentHolder;
import org.keycloak.saml.processing.core.saml.v2.util.AssertionUtil;
import org.keycloak.saml.processing.web.util.PostBindingUtil;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import java.net.URI;
import java.security.PublicKey;
import java.security.Signature;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/** /**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a> * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $ * @version $Revision: 1 $
*/ */
public abstract class SamlAuthenticator { public abstract class SamlAuthenticator {
protected static Logger log = Logger.getLogger(SamlAuthenticator.class); protected static Logger log = Logger.getLogger(SamlAuthenticator.class);
protected HttpFacade facade; private final SamlAuthenticationHandler handler;
protected AuthChallenge challenge;
protected SamlDeployment deployment;
protected SamlSessionStore sessionStore;
public SamlAuthenticator(HttpFacade facade, SamlDeployment deployment, SamlSessionStore sessionStore) { public SamlAuthenticator(final HttpFacade facade, final SamlDeployment deployment, final SamlSessionStore sessionStore) {
this.facade = facade; this.handler = createAuthenticationHandler(facade, deployment, sessionStore);
this.deployment = deployment;
this.sessionStore = sessionStore;
} }
public AuthChallenge getChallenge() { public AuthChallenge getChallenge() {
return challenge; return this.handler.getChallenge();
} }
public AuthOutcome authenticate() { public AuthOutcome authenticate() {
log.debugf("SamlAuthenticator is using handler [%s]", this.handler);
return this.handler.handle(new OnSessionCreated() {
String samlRequest = facade.getRequest().getFirstParam(GeneralConstants.SAML_REQUEST_KEY); @Override
String samlResponse = facade.getRequest().getFirstParam(GeneralConstants.SAML_RESPONSE_KEY); public void onSessionCreated(SamlSession samlSession) {
String relayState = facade.getRequest().getFirstParam(GeneralConstants.RELAY_STATE); completeAuthentication(samlSession);
boolean globalLogout = "true".equals(facade.getRequest().getQueryParamValue("GLO"));
if (samlRequest != null) {
return handleSamlRequest(samlRequest, relayState);
} else if (samlResponse != null) {
return handleSamlResponse(samlResponse, relayState);
} else if (sessionStore.isLoggedIn()) {
if (globalLogout) {
return globalLogout();
} }
if (verifySSL()) return AuthOutcome.FAILED; });
log.debug("AUTHENTICATED: was cached");
return AuthOutcome.AUTHENTICATED;
}
return initiateLogin();
} }
protected AuthOutcome globalLogout() { protected abstract void completeAuthentication(SamlSession samlSession);
SamlSession account = sessionStore.getAccount();
if (account == null) { private SamlAuthenticationHandler createAuthenticationHandler(HttpFacade facade, SamlDeployment deployment, SamlSessionStore sessionStore) {
return AuthOutcome.NOT_ATTEMPTED; if (EcpAuthenticationHandler.canHandle(facade)) {
} return EcpAuthenticationHandler.create(facade, deployment, sessionStore);
SAML2LogoutRequestBuilder logoutBuilder = new SAML2LogoutRequestBuilder()
.assertionExpiration(30)
.issuer(deployment.getEntityID())
.sessionIndex(account.getSessionIndex())
.userPrincipal(account.getPrincipal().getSamlSubject(), account.getPrincipal().getNameIDFormat())
.destination(deployment.getIDP().getSingleLogoutService().getRequestBindingUrl());
BaseSAML2BindingBuilder binding = new BaseSAML2BindingBuilder();
if (deployment.getIDP().getSingleLogoutService().signRequest()) {
binding.signWith(deployment.getSigningKeyPair())
.signDocument();
} }
binding.relayState("logout"); // defaults to the web browser sso profile
return WebBrowserSsoAuthenticationHandler.create(facade, deployment, sessionStore);
try {
SamlUtil.sendSaml(true, facade, deployment.getIDP().getSingleLogoutService().getRequestBindingUrl(), binding, logoutBuilder.buildDocument(), deployment.getIDP().getSingleLogoutService().getRequestBinding());
sessionStore.setCurrentAction(SamlSessionStore.CurrentAction.LOGGING_OUT);
} catch (Exception e) {
log.error("Could not send global logout SAML request", e);
return AuthOutcome.FAILED;
}
return AuthOutcome.NOT_ATTEMPTED;
} }
protected AuthOutcome handleSamlRequest(String samlRequest, String relayState) {
SAMLDocumentHolder holder = null;
boolean postBinding = false;
String requestUri = facade.getRequest().getURI();
if (facade.getRequest().getMethod().equalsIgnoreCase("GET")) {
// strip out query params
int index = requestUri.indexOf('?');
if (index > -1) {
requestUri = requestUri.substring(0, index);
}
holder = SAMLRequestParser.parseRequestRedirectBinding(samlRequest);
} else {
postBinding = true;
holder = SAMLRequestParser.parseRequestPostBinding(samlRequest);
}
RequestAbstractType requestAbstractType = (RequestAbstractType) holder.getSamlObject();
if (!requestUri.equals(requestAbstractType.getDestination().toString())) {
log.error("expected destination '" + requestUri + "' got '" + requestAbstractType.getDestination() + "'");
return AuthOutcome.FAILED;
}
if (requestAbstractType instanceof LogoutRequestType) {
if (deployment.getIDP().getSingleLogoutService().validateRequestSignature()) {
try {
validateSamlSignature(holder, postBinding, GeneralConstants.SAML_REQUEST_KEY);
} catch (VerificationException e) {
log.error("Failed to verify saml request signature", e);
return AuthOutcome.FAILED;
}
}
LogoutRequestType logout = (LogoutRequestType) requestAbstractType;
return logoutRequest(logout, relayState);
} else {
log.error("unknown SAML request type");
return AuthOutcome.FAILED;
}
}
protected AuthOutcome logoutRequest(LogoutRequestType request, String relayState) {
if (request.getSessionIndex() == null || request.getSessionIndex().isEmpty()) {
sessionStore.logoutByPrincipal(request.getNameID().getValue());
} else {
sessionStore.logoutBySsoId(request.getSessionIndex());
}
String issuerURL = deployment.getEntityID();
SAML2LogoutResponseBuilder builder = new SAML2LogoutResponseBuilder();
builder.logoutRequestID(request.getID());
builder.destination(deployment.getIDP().getSingleLogoutService().getResponseBindingUrl());
builder.issuer(issuerURL);
BaseSAML2BindingBuilder binding = new BaseSAML2BindingBuilder().relayState(relayState);
if (deployment.getIDP().getSingleLogoutService().signResponse()) {
binding.signatureAlgorithm(deployment.getSignatureAlgorithm())
.signWith(deployment.getSigningKeyPair())
.signDocument();
if (deployment.getSignatureCanonicalizationMethod() != null)
binding.canonicalizationMethod(deployment.getSignatureCanonicalizationMethod());
}
try {
SamlUtil.sendSaml(false, facade, deployment.getIDP().getSingleLogoutService().getResponseBindingUrl(), binding, builder.buildDocument(),
deployment.getIDP().getSingleLogoutService().getResponseBinding());
} catch (Exception e) {
log.error("Could not send logout response SAML request", e);
return AuthOutcome.FAILED;
}
return AuthOutcome.NOT_ATTEMPTED;
}
protected AuthOutcome handleSamlResponse(String samlResponse, String relayState) {
SAMLDocumentHolder holder = null;
boolean postBinding = false;
String requestUri = facade.getRequest().getURI();
if (facade.getRequest().getMethod().equalsIgnoreCase("GET")) {
int index = requestUri.indexOf('?');
if (index > -1) {
requestUri = requestUri.substring(0, index);
}
holder = extractRedirectBindingResponse(samlResponse);
} else {
postBinding = true;
holder = extractPostBindingResponse(samlResponse);
}
final StatusResponseType statusResponse = (StatusResponseType) holder.getSamlObject();
// validate destination
if (!requestUri.equals(statusResponse.getDestination())) {
log.error("Request URI does not match SAML request destination");
return AuthOutcome.FAILED;
}
if (statusResponse instanceof ResponseType) {
try {
if (deployment.getIDP().getSingleSignOnService().validateResponseSignature()) {
try {
validateSamlSignature(holder, postBinding, GeneralConstants.SAML_RESPONSE_KEY);
} catch (VerificationException e) {
log.error("Failed to verify saml response signature", e);
challenge = new AuthChallenge() {
@Override
public boolean challenge(HttpFacade exchange) {
SamlAuthenticationError error = new SamlAuthenticationError(SamlAuthenticationError.Reason.INVALID_SIGNATURE);
exchange.getRequest().setError(error);
exchange.getResponse().sendError(403);
return true;
}
@Override
public int getResponseCode() {
return 403;
}
};
return AuthOutcome.FAILED;
}
}
return handleLoginResponse((ResponseType) statusResponse);
} finally {
sessionStore.setCurrentAction(SamlSessionStore.CurrentAction.NONE);
}
} else {
if (sessionStore.isLoggingOut()) {
try {
if (deployment.getIDP().getSingleLogoutService().validateResponseSignature()) {
try {
validateSamlSignature(holder, postBinding, GeneralConstants.SAML_RESPONSE_KEY);
} catch (VerificationException e) {
log.error("Failed to verify saml response signature", e);
return AuthOutcome.FAILED;
}
}
return handleLogoutResponse(holder, statusResponse, relayState);
} finally {
sessionStore.setCurrentAction(SamlSessionStore.CurrentAction.NONE);
}
} else if (sessionStore.isLoggingIn()) {
try {
// KEYCLOAK-2107 - handle user not authenticated due passive mode. Return special outcome so different authentication mechanisms can behave accordingly.
StatusType status = statusResponse.getStatus();
if(checkStatusCodeValue(status.getStatusCode(), JBossSAMLURIConstants.STATUS_RESPONDER.get()) && checkStatusCodeValue(status.getStatusCode().getStatusCode(), JBossSAMLURIConstants.STATUS_NO_PASSIVE.get())){
log.debug("Not authenticated due passive mode Status found in SAML response: " + status.toString());
return AuthOutcome.NOT_AUTHENTICATED;
}
challenge = new AuthChallenge() {
@Override
public boolean challenge(HttpFacade exchange) {
SamlAuthenticationError error = new SamlAuthenticationError(SamlAuthenticationError.Reason.ERROR_STATUS, statusResponse);
exchange.getRequest().setError(error);
exchange.getResponse().sendError(403);
return true;
}
@Override
public int getResponseCode() {
return 403;
}
};
return AuthOutcome.FAILED;
} finally {
sessionStore.setCurrentAction(SamlSessionStore.CurrentAction.NONE);
}
}
return AuthOutcome.NOT_ATTEMPTED;
}
}
private void validateSamlSignature(SAMLDocumentHolder holder, boolean postBinding, String paramKey) throws VerificationException {
if (postBinding) {
verifyPostBindingSignature(holder.getSamlDocument(), deployment.getIDP().getSignatureValidationKey());
} else {
verifyRedirectBindingSignature(deployment.getIDP().getSignatureValidationKey(), paramKey);
}
}
private boolean checkStatusCodeValue(StatusCodeType statusCode, String expectedValue){
if(statusCode != null && statusCode.getValue()!=null){
String v = statusCode.getValue().toString();
return expectedValue.equals(v);
}
return false;
}
protected AuthOutcome handleLoginResponse(ResponseType responseType) {
AssertionType assertion = null;
try {
assertion = AssertionUtil.getAssertion(responseType, deployment.getDecryptionKey());
if (AssertionUtil.hasExpired(assertion)) {
return initiateLogin();
}
} catch (Exception e) {
log.error("Error extracting SAML assertion: " + e.getMessage());
challenge = new AuthChallenge() {
@Override
public boolean challenge(HttpFacade exchange) {
SamlAuthenticationError error = new SamlAuthenticationError(SamlAuthenticationError.Reason.EXTRACTION_FAILURE);
exchange.getRequest().setError(error);
exchange.getResponse().sendError(403);
return true;
}
@Override
public int getResponseCode() {
return 403;
}
};
}
SubjectType subject = assertion.getSubject();
SubjectType.STSubType subType = subject.getSubType();
NameIDType subjectNameID = (NameIDType) subType.getBaseID();
String principalName = subjectNameID.getValue();
final Set<String> roles = new HashSet<>();
MultivaluedHashMap<String, String> attributes = new MultivaluedHashMap<>();
MultivaluedHashMap<String, String> friendlyAttributes = new MultivaluedHashMap<>();
Set<StatementAbstractType> statements = assertion.getStatements();
for (StatementAbstractType statement : statements) {
if (statement instanceof AttributeStatementType) {
AttributeStatementType attributeStatement = (AttributeStatementType) statement;
List<AttributeStatementType.ASTChoiceType> attList = attributeStatement.getAttributes();
for (AttributeStatementType.ASTChoiceType obj : attList) {
AttributeType attr = obj.getAttribute();
if (isRole(attr)) {
List<Object> attributeValues = attr.getAttributeValue();
if (attributeValues != null) {
for (Object attrValue : attributeValues) {
String role = getAttributeValue(attrValue);
log.debugv("Add role: {0}", role);
roles.add(role);
}
}
} else {
List<Object> attributeValues = attr.getAttributeValue();
if (attributeValues != null) {
for (Object attrValue : attributeValues) {
String value = getAttributeValue(attrValue);
if (attr.getName() != null) {
attributes.add(attr.getName(), value);
}
if (attr.getFriendlyName() != null) {
friendlyAttributes.add(attr.getFriendlyName(), value);
}
}
}
}
}
}
}
if (deployment.getPrincipalNamePolicy() == SamlDeployment.PrincipalNamePolicy.FROM_ATTRIBUTE) {
if (deployment.getPrincipalAttributeName() != null) {
String attribute = attributes.getFirst(deployment.getPrincipalAttributeName());
if (attribute != null) principalName = attribute;
else {
attribute = friendlyAttributes.getFirst(deployment.getPrincipalAttributeName());
if (attribute != null) principalName = attribute;
}
}
}
AuthnStatementType authn = null;
for (Object statement : assertion.getStatements()) {
if (statement instanceof AuthnStatementType) {
authn = (AuthnStatementType) statement;
break;
}
}
URI nameFormat = subjectNameID.getFormat();
String nameFormatString = nameFormat == null ? JBossSAMLURIConstants.NAMEID_FORMAT_UNSPECIFIED.get() : nameFormat.toString();
final SamlPrincipal principal = new SamlPrincipal(assertion, principalName, principalName, nameFormatString, attributes, friendlyAttributes);
String index = authn == null ? null : authn.getSessionIndex();
final String sessionIndex = index;
SamlSession account = new SamlSession(principal, roles, sessionIndex);
sessionStore.saveAccount(account);
completeAuthentication(account);
// redirect to original request, it will be restored
String redirectUri = sessionStore.getRedirectUri();
if (redirectUri != null) {
facade.getResponse().setHeader("Location", redirectUri);
facade.getResponse().setStatus(302);
facade.getResponse().end();
} else {
log.debug("IDP initiated invocation");
}
log.debug("AUTHENTICATED authn");
return AuthOutcome.AUTHENTICATED;
}
protected abstract void completeAuthentication(SamlSession account);
private String getAttributeValue(Object attrValue) {
String value = null;
if (attrValue instanceof String) {
value = (String) attrValue;
} else if (attrValue instanceof Node) {
Node roleNode = (Node) attrValue;
value = roleNode.getFirstChild().getNodeValue();
} else if (attrValue instanceof NameIDType) {
NameIDType nameIdType = (NameIDType) attrValue;
value = nameIdType.getValue();
} else {
log.warn("Unable to extract unknown SAML assertion attribute value type: " + attrValue.getClass().getName());
}
return value;
}
protected boolean isRole(AttributeType attribute) {
return (attribute.getName() != null && deployment.getRoleAttributeNames().contains(attribute.getName())) || (attribute.getFriendlyName() != null && deployment.getRoleAttributeNames().contains(attribute.getFriendlyName()));
}
protected AuthOutcome handleLogoutResponse(SAMLDocumentHolder holder, StatusResponseType responseType, String relayState) {
boolean loggedIn = sessionStore.isLoggedIn();
if (!loggedIn || !"logout".equals(relayState)) {
return AuthOutcome.NOT_ATTEMPTED;
}
sessionStore.logoutAccount();
return AuthOutcome.LOGGED_OUT;
}
protected SAMLDocumentHolder extractRedirectBindingResponse(String response) {
return SAMLRequestParser.parseRequestRedirectBinding(response);
}
protected SAMLDocumentHolder extractPostBindingResponse(String response) {
byte[] samlBytes = PostBindingUtil.base64Decode(response);
return SAMLRequestParser.parseResponseDocument(samlBytes);
}
protected AuthOutcome initiateLogin() {
challenge = new InitiateLogin(deployment, sessionStore);
return AuthOutcome.NOT_ATTEMPTED;
}
protected boolean verifySSL() {
if (!facade.getRequest().isSecure() && deployment.getSslRequired().isRequired(facade.getRequest().getRemoteAddr())) {
log.warn("SSL is required to authenticate");
return true;
}
return false;
}
public void verifyPostBindingSignature(Document document, PublicKey publicKey) throws VerificationException {
SAML2Signature saml2Signature = new SAML2Signature();
try {
if (!saml2Signature.validate(document, publicKey)) {
throw new VerificationException("Invalid signature on document");
}
} catch (ProcessingException e) {
throw new VerificationException("Error validating signature", e);
}
}
public void verifyRedirectBindingSignature(PublicKey publicKey, String paramKey) throws VerificationException {
String request = facade.getRequest().getQueryParamValue(paramKey);
String algorithm = facade.getRequest().getQueryParamValue(GeneralConstants.SAML_SIG_ALG_REQUEST_KEY);
String signature = facade.getRequest().getQueryParamValue(GeneralConstants.SAML_SIGNATURE_REQUEST_KEY);
String decodedAlgorithm = facade.getRequest().getQueryParamValue(GeneralConstants.SAML_SIG_ALG_REQUEST_KEY);
if (request == null) {
throw new VerificationException("SAML Request was null");
}
if (algorithm == null) throw new VerificationException("SigAlg was null");
if (signature == null) throw new VerificationException("Signature was null");
// Shibboleth doesn't sign the document for redirect binding.
// todo maybe a flag?
String relayState = facade.getRequest().getQueryParamValue(GeneralConstants.RELAY_STATE);
KeycloakUriBuilder builder = KeycloakUriBuilder.fromPath("/")
.queryParam(paramKey, request);
if (relayState != null) {
builder.queryParam(GeneralConstants.RELAY_STATE, relayState);
}
builder.queryParam(GeneralConstants.SAML_SIG_ALG_REQUEST_KEY, algorithm);
String rawQuery = builder.build().getRawQuery();
try {
//byte[] decodedSignature = RedirectBindingUtil.urlBase64Decode(signature);
byte[] decodedSignature = Base64.decode(signature);
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.getFromXmlMethod(decodedAlgorithm);
Signature validator = signatureAlgorithm.createSignature(); // todo plugin signature alg
validator.initVerify(publicKey);
validator.update(rawQuery.getBytes("UTF-8"));
if (!validator.verify(decodedSignature)) {
throw new VerificationException("Invalid query param signature");
}
} catch (Exception e) {
throw new VerificationException(e);
}
}
} }

View file

@ -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);
}
}
}

View file

@ -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();
}

View file

@ -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;
}
}

View file

@ -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());
}
};
}
}

View file

@ -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;
}
}

View file

@ -65,7 +65,8 @@ public enum JBossSAMLConstants {
"XACMLAuthzDecisionQuery"), XACML_AUTHZ_DECISION_QUERY_TYPE("XACMLAuthzDecisionQueryType"), XACML_AUTHZ_DECISION_STATEMENT_TYPE( "XACMLAuthzDecisionQuery"), XACML_AUTHZ_DECISION_QUERY_TYPE("XACMLAuthzDecisionQueryType"), XACML_AUTHZ_DECISION_STATEMENT_TYPE(
"XACMLAuthzDecisionStatementType"), HTTP_POST_BINDING("urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"), ONE_TIME_USE ("OneTimeUse"), "XACMLAuthzDecisionStatementType"), HTTP_POST_BINDING("urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"), ONE_TIME_USE ("OneTimeUse"),
UNSOLICITED_RESPONSE_TARGET("TARGET"), UNSOLICITED_RESPONSE_SAML_VERSION("SAML_VERSION"), UNSOLICITED_RESPONSE_SAML_BINDING("SAML_BINDING"), UNSOLICITED_RESPONSE_TARGET("TARGET"), UNSOLICITED_RESPONSE_SAML_VERSION("SAML_VERSION"), UNSOLICITED_RESPONSE_SAML_BINDING("SAML_BINDING"),
ROLE_DESCRIPTOR("RoleDescriptor"); ROLE_DESCRIPTOR("RoleDescriptor"),
REQUEST_AUTHENTICATED("RequestAuthenticated");
private String name; private String name;

View file

@ -73,12 +73,15 @@ public enum JBossSAMLURIConstants {
"urn:oasis:names:tc:SAML:2.0:nameid-format:entity"), "urn:oasis:names:tc:SAML:2.0:nameid-format:entity"),
PROTOCOL_NSURI("urn:oasis:names:tc:SAML:2.0:protocol"), PROTOCOL_NSURI("urn:oasis:names:tc:SAML:2.0:protocol"),
ECP_PROFILE("urn:oasis:names:tc:SAML:2.0:profiles:SSO:ecp"),
PAOS_BINDING("urn:liberty:paos:2003-08"),
SIGNATURE_DSA_SHA1("http://www.w3.org/2000/09/xmldsig#dsa-sha1"), SIGNATURE_RSA_SHA1( SIGNATURE_DSA_SHA1("http://www.w3.org/2000/09/xmldsig#dsa-sha1"), SIGNATURE_RSA_SHA1(
"http://www.w3.org/2000/09/xmldsig#rsa-sha1"), "http://www.w3.org/2000/09/xmldsig#rsa-sha1"),
SAML_HTTP_POST_BINDING("urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"), SAML_HTTP_REDIRECT_BINDING( SAML_HTTP_POST_BINDING("urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"),
"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"), SAML_HTTP_SOAP_BINDING("urn:oasis:names:tc:SAML:2.0:bindings:SOAP"),
SAML_HTTP_REDIRECT_BINDING("urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"),
SAML_11_NS("urn:oasis:names:tc:SAML:1.0:assertion"), SAML_11_NS("urn:oasis:names:tc:SAML:1.0:assertion"),

View file

@ -84,6 +84,7 @@ public class SamlProtocol implements LoginProtocol {
public static final String SAML_BINDING = "saml_binding"; public static final String SAML_BINDING = "saml_binding";
public static final String SAML_IDP_INITIATED_LOGIN = "saml_idp_initiated_login"; public static final String SAML_IDP_INITIATED_LOGIN = "saml_idp_initiated_login";
public static final String SAML_POST_BINDING = "post"; public static final String SAML_POST_BINDING = "post";
public static final String SAML_SOAP_BINDING = "soap";
public static final String SAML_REDIRECT_BINDING = "get"; public static final String SAML_REDIRECT_BINDING = "get";
public static final String SAML_SERVER_SIGNATURE = "saml.server.signature"; public static final String SAML_SERVER_SIGNATURE = "saml.server.signature";
public static final String SAML_ASSERTION_SIGNATURE = "saml.assertion.signature"; public static final String SAML_ASSERTION_SIGNATURE = "saml.assertion.signature";
@ -165,11 +166,7 @@ public class SamlProtocol implements LoginProtocol {
try { try {
JaxrsSAML2BindingBuilder binding = new JaxrsSAML2BindingBuilder().relayState(clientSession.getNote(GeneralConstants.RELAY_STATE)); JaxrsSAML2BindingBuilder binding = new JaxrsSAML2BindingBuilder().relayState(clientSession.getNote(GeneralConstants.RELAY_STATE));
Document document = builder.buildDocument(); Document document = builder.buildDocument();
if (isPostBinding(clientSession)) { return buildErrorResponse(clientSession, binding, document);
return binding.postBinding(document).response(clientSession.getRedirectUri());
} else {
return binding.redirectBinding(document).response(clientSession.getRedirectUri());
}
} catch (Exception e) { } catch (Exception e) {
return ErrorPage.error(session, Messages.FAILED_TO_PROCESS_RESPONSE); return ErrorPage.error(session, Messages.FAILED_TO_PROCESS_RESPONSE);
} }
@ -180,6 +177,14 @@ public class SamlProtocol implements LoginProtocol {
} }
} }
protected Response buildErrorResponse(ClientSessionModel clientSession, JaxrsSAML2BindingBuilder binding, Document document) throws ConfigurationException, ProcessingException, IOException {
if (isPostBinding(clientSession)) {
return binding.postBinding(document).response(clientSession.getRedirectUri());
} else {
return binding.redirectBinding(document).response(clientSession.getRedirectUri());
}
}
private JBossSAMLURIConstants translateErrorToSAMLStatus(Error error) { private JBossSAMLURIConstants translateErrorToSAMLStatus(Error error) {
switch (error) { switch (error) {
case CANCELLED_BY_USER: case CANCELLED_BY_USER:
@ -390,17 +395,21 @@ public class SamlProtocol implements LoginProtocol {
bindingBuilder.encrypt(publicKey); bindingBuilder.encrypt(publicKey);
} }
try { try {
if (isPostBinding(clientSession)) { return buildAuthenticatedResponse(clientSession, redirectUri, samlDocument, bindingBuilder);
return bindingBuilder.postBinding(samlDocument).response(redirectUri);
} else {
return bindingBuilder.redirectBinding(samlDocument).response(redirectUri);
}
} catch (Exception e) { } catch (Exception e) {
logger.error("failed", e); logger.error("failed", e);
return ErrorPage.error(session, Messages.FAILED_TO_PROCESS_RESPONSE); return ErrorPage.error(session, Messages.FAILED_TO_PROCESS_RESPONSE);
} }
} }
protected Response buildAuthenticatedResponse(ClientSessionModel clientSession, String redirectUri, Document samlDocument, JaxrsSAML2BindingBuilder bindingBuilder) throws ConfigurationException, ProcessingException, IOException {
if (isPostBinding(clientSession)) {
return bindingBuilder.postBinding(samlDocument).response(redirectUri);
} else {
return bindingBuilder.redirectBinding(samlDocument).response(redirectUri);
}
}
public static boolean requiresRealmSignature(ClientModel client) { public static boolean requiresRealmSignature(ClientModel client) {
return "true".equals(client.getAttribute(SAML_SERVER_SIGNATURE)); return "true".equals(client.getAttribute(SAML_SERVER_SIGNATURE));
} }
@ -544,11 +553,7 @@ public class SamlProtocol implements LoginProtocol {
} }
try { try {
if (isLogoutPostBindingForInitiator(userSession)) { return buildLogoutResponse(userSession, logoutBindingUri, builder, binding);
return binding.postBinding(builder.buildDocument()).response(logoutBindingUri);
} else {
return binding.redirectBinding(builder.buildDocument()).response(logoutBindingUri);
}
} catch (ConfigurationException e) { } catch (ConfigurationException e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} catch (ProcessingException e) { } catch (ProcessingException e) {
@ -558,6 +563,14 @@ public class SamlProtocol implements LoginProtocol {
} }
} }
protected Response buildLogoutResponse(UserSessionModel userSession, String logoutBindingUri, SAML2LogoutResponseBuilder builder, JaxrsSAML2BindingBuilder binding) throws ConfigurationException, ProcessingException, IOException {
if (isLogoutPostBindingForInitiator(userSession)) {
return binding.postBinding(builder.buildDocument()).response(logoutBindingUri);
} else {
return binding.redirectBinding(builder.buildDocument()).response(logoutBindingUri);
}
}
@Override @Override
public void backchannelLogout(UserSessionModel userSession, ClientSessionModel clientSession) { public void backchannelLogout(UserSessionModel userSession, ClientSessionModel clientSession) {
ClientModel client = clientSession.getClient(); ClientModel client = clientSession.getClient();

View file

@ -42,7 +42,7 @@ public class SamlProtocolFactory extends AbstractLoginProtocolFactory {
@Override @Override
public String getId() { public String getId() {
return "saml"; return SamlProtocol.LOGIN_PROTOCOL;
} }
@Override @Override
@ -90,8 +90,9 @@ public class SamlProtocolFactory extends AbstractLoginProtocolFactory {
@Override @Override
protected void addDefaults(ClientModel client) { protected void addDefaults(ClientModel client) {
for (ProtocolMapperModel model : defaultBuiltins) client.addProtocolMapper(model); for (ProtocolMapperModel model : defaultBuiltins) {
model.setProtocol(getId());
client.addProtocolMapper(model);
}
} }
} }

View file

@ -16,6 +16,7 @@ import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.jboss.resteasy.spi.ResteasyProviderFactory;
import org.keycloak.common.VerificationException; import org.keycloak.common.VerificationException;
import org.keycloak.common.util.StreamUtil; import org.keycloak.common.util.StreamUtil;
import org.keycloak.dom.saml.v2.SAML2Object; import org.keycloak.dom.saml.v2.SAML2Object;
@ -34,7 +35,9 @@ import org.keycloak.models.RealmModel;
import org.keycloak.models.UserSessionModel; import org.keycloak.models.UserSessionModel;
import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.protocol.AuthorizationEndpointBase; import org.keycloak.protocol.AuthorizationEndpointBase;
import org.keycloak.protocol.LoginProtocol;
import org.keycloak.protocol.oidc.utils.RedirectUtils; import org.keycloak.protocol.oidc.utils.RedirectUtils;
import org.keycloak.protocol.saml.profile.ecp.SamlEcpProfileService;
import org.keycloak.saml.SAML2LogoutResponseBuilder; import org.keycloak.saml.SAML2LogoutResponseBuilder;
import org.keycloak.saml.SAMLRequestParser; import org.keycloak.saml.SAMLRequestParser;
import org.keycloak.saml.SignatureAlgorithm; import org.keycloak.saml.SignatureAlgorithm;
@ -221,7 +224,7 @@ public class SamlService extends AuthorizationEndpointBase {
} }
ClientSessionModel clientSession = session.sessions().createClientSession(realm, client); ClientSessionModel clientSession = session.sessions().createClientSession(realm, client);
clientSession.setAuthMethod(SamlProtocol.LOGIN_PROTOCOL); clientSession.setAuthMethod(getLoginProtocol());
clientSession.setRedirectUri(redirect); clientSession.setRedirectUri(redirect);
clientSession.setAction(ClientSessionModel.Action.AUTHENTICATE.name()); clientSession.setAction(ClientSessionModel.Action.AUTHENTICATE.name());
clientSession.setNote(ClientSessionCode.ACTION_KEY, KeycloakModelUtils.generateCodeSecret()); clientSession.setNote(ClientSessionCode.ACTION_KEY, KeycloakModelUtils.generateCodeSecret());
@ -246,7 +249,7 @@ public class SamlService extends AuthorizationEndpointBase {
return newBrowserAuthentication(clientSession, requestAbstractType.isIsPassive()); return newBrowserAuthentication(clientSession, requestAbstractType.isIsPassive());
} }
private String getBindingType(AuthnRequestType requestAbstractType) { protected String getBindingType(AuthnRequestType requestAbstractType) {
URI requestedProtocolBinding = requestAbstractType.getProtocolBinding(); URI requestedProtocolBinding = requestAbstractType.getProtocolBinding();
if (requestedProtocolBinding != null) { if (requestedProtocolBinding != null) {
@ -370,7 +373,7 @@ public class SamlService extends AuthorizationEndpointBase {
} }
} }
protected class PostBindingProtocol extends BindingProtocol { public class PostBindingProtocol extends BindingProtocol {
@Override @Override
protected void verifySignature(SAMLDocumentHolder documentHolder, ClientModel client) throws VerificationException { protected void verifySignature(SAMLDocumentHolder documentHolder, ClientModel client) throws VerificationException {
@ -443,7 +446,12 @@ public class SamlService extends AuthorizationEndpointBase {
} }
protected Response newBrowserAuthentication(ClientSessionModel clientSession, boolean isPassive) { protected Response newBrowserAuthentication(ClientSessionModel clientSession, boolean isPassive) {
return handleBrowserAuthenticationRequest(clientSession, new SamlProtocol().setEventBuilder(event).setHttpHeaders(headers).setRealm(realm).setSession(session).setUriInfo(uriInfo), isPassive); LoginProtocol protocol = session.getProvider(LoginProtocol.class, clientSession.getAuthMethod());
protocol.setRealm(realm)
.setHttpHeaders(request.getHttpHeaders())
.setUriInfo(uriInfo)
.setEventBuilder(event);
return handleBrowserAuthenticationRequest(clientSession, protocol, isPassive);
} }
/** /**
@ -463,6 +471,16 @@ public class SamlService extends AuthorizationEndpointBase {
return new PostBindingProtocol().execute(samlRequest, samlResponse, relayState); return new PostBindingProtocol().execute(samlRequest, samlResponse, relayState);
} }
@POST
@Consumes("application/soap+xml")
public Response soapBinding(InputStream inputStream) {
SamlEcpProfileService bindingService = new SamlEcpProfileService(realm, event, authManager);
ResteasyProviderFactory.getInstance().injectProperties(bindingService);
return bindingService.authenticate(inputStream);
}
@GET @GET
@Path("descriptor") @Path("descriptor")
@Produces(MediaType.APPLICATION_XML) @Produces(MediaType.APPLICATION_XML)
@ -519,7 +537,7 @@ public class SamlService extends AuthorizationEndpointBase {
} }
ClientSessionModel clientSession = session.sessions().createClientSession(realm, client); ClientSessionModel clientSession = session.sessions().createClientSession(realm, client);
clientSession.setAuthMethod(SamlProtocol.LOGIN_PROTOCOL); clientSession.setAuthMethod(getLoginProtocol());
clientSession.setAction(ClientSessionModel.Action.AUTHENTICATE.name()); clientSession.setAction(ClientSessionModel.Action.AUTHENTICATE.name());
clientSession.setNote(ClientSessionCode.ACTION_KEY, KeycloakModelUtils.generateCodeSecret()); clientSession.setNote(ClientSessionCode.ACTION_KEY, KeycloakModelUtils.generateCodeSecret());
clientSession.setNote(SamlProtocol.SAML_BINDING, SamlProtocol.SAML_POST_BINDING); clientSession.setNote(SamlProtocol.SAML_BINDING, SamlProtocol.SAML_POST_BINDING);
@ -537,4 +555,8 @@ public class SamlService extends AuthorizationEndpointBase {
} }
protected String getLoginProtocol() {
return SamlProtocol.LOGIN_PROTOCOL;
}
} }

View file

@ -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;
}
}

View file

@ -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.");
}
}

View file

@ -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;
}
}

View file

@ -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);
}
}
}

View file

@ -0,0 +1 @@
org.keycloak.protocol.saml.profile.ecp.authenticator.HttpBasicAuthenticator

View file

@ -1 +1,2 @@
org.keycloak.protocol.saml.SamlProtocolFactory org.keycloak.protocol.saml.SamlProtocolFactory
org.keycloak.protocol.saml.profile.ecp.SamlEcpProfileProtocolFactory

View file

@ -87,7 +87,7 @@ public abstract class AuthorizationEndpointBase {
} }
} }
AuthenticationFlowModel flow = realm.getBrowserFlow(); AuthenticationFlowModel flow = getAuthenticationFlow();
String flowId = flow.getId(); String flowId = flow.getId();
AuthenticationProcessor processor = createProcessor(clientSession, flowId, LoginActionsService.AUTHENTICATE_PATH); AuthenticationProcessor processor = createProcessor(clientSession, flowId, LoginActionsService.AUTHENTICATE_PATH);
@ -127,6 +127,10 @@ public abstract class AuthorizationEndpointBase {
} }
} }
protected AuthenticationFlowModel getAuthenticationFlow() {
return realm.getBrowserFlow();
}
protected Response buildRedirectToIdentityProvider(String providerId, String accessCode) { protected Response buildRedirectToIdentityProvider(String providerId, String accessCode) {
logger.debug("Automatically redirect to identity provider: " + providerId); logger.debug("Automatically redirect to identity provider: " + providerId);
return Response.temporaryRedirect( return Response.temporaryRedirect(

View file

@ -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")));
}
}

View file

@ -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>

View file

@ -0,0 +1,67 @@
{
"id": "demo",
"realm": "demo",
"enabled": true,
"sslRequired": "external",
"registrationAllowed": true,
"resetPasswordAllowed": true,
"privateKey": "MIICXAIBAAKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQABAoGAfmO8gVhyBxdqlxmIuglbz8bcjQbhXJLR2EoS8ngTXmN1bo2L90M0mUKSdc7qF10LgETBzqL8jYlQIbt+e6TH8fcEpKCjUlyq0Mf/vVbfZSNaVycY13nTzo27iPyWQHK5NLuJzn1xvxxrUeXI6A2WFpGEBLbHjwpx5WQG9A+2scECQQDvdn9NE75HPTVPxBqsEd2z10TKkl9CZxu10Qby3iQQmWLEJ9LNmy3acvKrE3gMiYNWb6xHPKiIqOR1as7L24aTAkEAtyvQOlCvr5kAjVqrEKXalj0Tzewjweuxc0pskvArTI2Oo070h65GpoIKLc9jf+UA69cRtquwP93aZKtW06U8dQJAF2Y44ks/mK5+eyDqik3koCI08qaC8HYq2wVl7G2QkJ6sbAaILtcvD92ToOvyGyeE0flvmDZxMYlvaZnaQ0lcSQJBAKZU6umJi3/xeEbkJqMfeLclD27XGEFoPeNrmdx0q10Azp4NfJAY+Z8KRyQCR2BEG+oNitBOZ+YXF9KCpH3cdmECQHEigJhYg+ykOvr1aiZUMFT72HU0jnmQe2FVekuG+LJUt2Tm7GtMjTFoGpf0JwrVuZN39fOYAlo+nTixgeW7X8Y=",
"publicKey": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB",
"requiredCredentials": [ "password" ],
"defaultRoles": [ "user" ],
"smtpServer": {
"from": "auto@keycloak.org",
"host": "localhost",
"port":"3025"
},
"users" : [
{
"username" : "pedroigor",
"enabled": true,
"email" : "psilva@redhat.com",
"credentials" : [
{ "type" : "password",
"value" : "password" }
],
"attributes" : {
"phone": "617"
},
"realmRoles": ["manager", "user"]
}
],
"applications": [
{
"name": "http://localhost:8081/ecp-sp/",
"enabled": true,
"protocol": "saml",
"fullScopeAllowed": true,
"baseUrl": "http://localhost:8081/ecp-sp",
"redirectUris": [
"http://localhost:8081/ecp-sp/*"
],
"attributes": {
"saml_assertion_consumer_url_post": "http://localhost:8081/ecp-sp/",
"saml_assertion_consumer_url_redirect": "http://localhost:8081/ecp-sp/",
"saml_single_logout_service_url_post": "http://localhost:8081/ecp-sp/",
"saml_single_logout_service_url_redirect": "http://localhost:8081/ecp-sp/",
"saml.server.signature": "true",
"saml.signature.algorithm": "RSA_SHA256",
"saml.client.signature": "true",
"saml.authnstatement": "true",
"saml.signing.certificate": "MIIB1DCCAT0CBgFJGP5dZDANBgkqhkiG9w0BAQsFADAwMS4wLAYDVQQDEyVodHRwOi8vbG9jYWxob3N0OjgwODAvc2FsZXMtcG9zdC1zaWcvMB4XDTE0MTAxNjEyNDQyM1oXDTI0MTAxNjEyNDYwM1owMDEuMCwGA1UEAxMlaHR0cDovL2xvY2FsaG9zdDo4MDgwL3NhbGVzLXBvc3Qtc2lnLzCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA1RvGu8RjemSJA23nnMksoHA37MqY1DDTxOECY4rPAd9egr7GUNIXE0y1MokaR5R2crNpN8RIRwR8phQtQDjXL82c6W+NLQISxztarQJ7rdNJIYwHY0d5ri1XRpDP8zAuxubPYiMAVYcDkIcvlbBpwh/dRM5I2eElRK+eSiaMkCUCAwEAATANBgkqhkiG9w0BAQsFAAOBgQCLms6htnPaY69k1ntm9a5jgwSn/K61cdai8R8B0ccY7zvinn9AfRD7fiROQpFyY29wKn8WCLrJ86NBXfgFUGyR5nLNHVy3FghE36N2oHy53uichieMxffE6vhkKJ4P8ChfJMMOZlmCPsQPDvjoAghHt4mriFiQgRdPgIy/zDjSNw=="
}
}
],
"roles" : {
"realm" : [
{
"name": "manager",
"description": "Have Manager privileges"
},
{
"name": "user",
"description": "Have User privileges"
}
]
}
}