This commit is contained in:
Bill Burke 2015-09-17 14:00:57 -04:00
parent be0c359160
commit cb8ca619ae
54 changed files with 1495 additions and 140 deletions

View file

@ -10,7 +10,6 @@ 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.EncryptedAssertionType;
import org.keycloak.dom.saml.v2.assertion.NameIDType;
import org.keycloak.dom.saml.v2.assertion.SubjectType;
import org.keycloak.dom.saml.v2.protocol.LogoutRequestType;
@ -30,26 +29,17 @@ import org.keycloak.saml.SAMLRequestParser;
import org.keycloak.protocol.saml.SamlProtocol;
import org.keycloak.protocol.saml.SamlProtocolUtils;
import org.keycloak.saml.common.constants.GeneralConstants;
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.saml.common.util.DocumentUtil;
import org.keycloak.saml.common.util.StaxParserUtil;
import org.keycloak.saml.processing.api.saml.v2.response.SAML2Response;
import org.keycloak.saml.processing.core.parsers.saml.SAMLParser;
import org.keycloak.saml.processing.core.saml.v2.common.SAMLDocumentHolder;
import org.keycloak.saml.processing.core.saml.v2.constants.X500SAMLProfileConstants;
import org.keycloak.saml.processing.core.util.JAXPValidationUtil;
import org.keycloak.saml.processing.core.util.XMLEncryptionUtil;
import org.keycloak.saml.processing.core.saml.v2.util.AssertionUtil;
import org.keycloak.saml.processing.core.util.XMLSignatureUtil;
import org.keycloak.saml.processing.web.util.PostBindingUtil;
import org.keycloak.services.ErrorPage;
import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.messages.Messages;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import javax.ws.rs.Consumes;
import javax.ws.rs.FormParam;
@ -62,9 +52,7 @@ import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriBuilder;
import javax.ws.rs.core.UriInfo;
import javax.xml.namespace.QName;
import java.io.IOException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.cert.X509Certificate;
import java.util.List;
@ -156,7 +144,7 @@ public class SAMLEndpoint {
}
protected abstract String getBindingType();
protected abstract void verifySignature(SAMLDocumentHolder documentHolder) throws VerificationException;
protected abstract void verifySignature(String key, SAMLDocumentHolder documentHolder) throws VerificationException;
protected abstract SAMLDocumentHolder extractRequestDocument(String samlRequest);
protected abstract SAMLDocumentHolder extractResponseDocument(String response);
protected PublicKey getIDPKey() {
@ -189,7 +177,7 @@ public class SAMLEndpoint {
}
if (config.isValidateSignature()) {
try {
verifySignature(holder);
verifySignature(GeneralConstants.SAML_REQUEST_KEY, holder);
} catch (VerificationException e) {
logger.error("validation failed", e);
event.event(EventType.IDENTITY_PROVIDER_RESPONSE);
@ -276,7 +264,7 @@ public class SAMLEndpoint {
protected Response handleLoginResponse(String samlResponse, SAMLDocumentHolder holder, ResponseType responseType, String relayState) {
try {
AssertionType assertion = getAssertion(responseType);
AssertionType assertion = AssertionUtil.getAssertion(responseType, realm.getPrivateKey());
SubjectType subject = assertion.getSubject();
SubjectType.STSubType subType = subject.getSubType();
NameIDType subjectNameID = (NameIDType) subType.getBaseID();
@ -336,22 +324,6 @@ public class SAMLEndpoint {
private AssertionType getAssertion(ResponseType responseType) throws ProcessingException {
List<ResponseType.RTChoiceType> assertions = responseType.getAssertions();
if (assertions.isEmpty()) {
throw new IdentityBrokerException("No assertion from response.");
}
ResponseType.RTChoiceType rtChoiceType = assertions.get(0);
EncryptedAssertionType encryptedAssertion = rtChoiceType.getEncryptedAssertion();
if (encryptedAssertion != null) {
decryptAssertion(responseType, realm.getPrivateKey());
}
return responseType.getAssertions().get(0).getAssertion();
}
public Response handleSamlResponse(String samlResponse, String relayState) {
SAMLDocumentHolder holder = extractResponseDocument(samlResponse);
@ -365,7 +337,7 @@ public class SAMLEndpoint {
}
if (config.isValidateSignature()) {
try {
verifySignature(holder);
verifySignature(GeneralConstants.SAML_RESPONSE_KEY, holder);
} catch (VerificationException e) {
logger.error("validation failed", e);
event.event(EventType.IDENTITY_PROVIDER_RESPONSE);
@ -408,43 +380,14 @@ public class SAMLEndpoint {
}
protected ResponseType decryptAssertion(ResponseType responseType, PrivateKey privateKey) throws ProcessingException {
SAML2Response saml2Response = new SAML2Response();
try {
Document doc = saml2Response.convert(responseType);
Element enc = DocumentUtil.getElement(doc, new QName(JBossSAMLConstants.ENCRYPTED_ASSERTION.get()));
if (enc == null) {
throw new IdentityBrokerException("No encrypted assertion found.");
}
String oldID = enc.getAttribute(JBossSAMLConstants.ID.get());
Document newDoc = DocumentUtil.createDocument();
Node importedNode = newDoc.importNode(enc, true);
newDoc.appendChild(importedNode);
Element decryptedDocumentElement = XMLEncryptionUtil.decryptElementInDocument(newDoc, privateKey);
SAMLParser parser = new SAMLParser();
JAXPValidationUtil.checkSchemaValidation(decryptedDocumentElement);
AssertionType assertion = (AssertionType) parser.parse(StaxParserUtil.getXMLEventReader(DocumentUtil
.getNodeAsStream(decryptedDocumentElement)));
responseType.replaceAssertion(oldID, new ResponseType.RTChoiceType(assertion));
return responseType;
} catch (Exception e) {
throw new IdentityBrokerException("Could not decrypt assertion.", e);
}
}
}
protected class PostBinding extends Binding {
@Override
protected void verifySignature(SAMLDocumentHolder documentHolder) throws VerificationException {
protected void verifySignature(String key, SAMLDocumentHolder documentHolder) throws VerificationException {
SamlProtocolUtils.verifyDocumentSignature(documentHolder.getSamlDocument(), getIDPKey());
}
@ -467,9 +410,9 @@ public class SAMLEndpoint {
protected class RedirectBinding extends Binding {
@Override
protected void verifySignature(SAMLDocumentHolder documentHolder) throws VerificationException {
protected void verifySignature(String key, SAMLDocumentHolder documentHolder) throws VerificationException {
PublicKey publicKey = getIDPKey();
SamlProtocolUtils.verifyRedirectSignature(publicKey, uriInfo);
SamlProtocolUtils.verifyRedirectSignature(publicKey, uriInfo, key);
}

View file

@ -5,7 +5,7 @@ package org.keycloak.adapters;
*
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public interface AdapterTokenStore {
public interface AdapterTokenStore extends AdapterSessionStore {
/**
* Impl can validate if current token exists and perform refreshing if it exists and is expired
@ -39,6 +39,4 @@ public interface AdapterTokenStore {
*/
void refreshCallback(RefreshableKeycloakSecurityContext securityContext);
void saveRequest();
boolean restoreRequest();
}

View file

@ -26,7 +26,7 @@ public class OAuthRequestAuthenticator {
protected KeycloakDeployment deployment;
protected RequestAuthenticator reqAuthenticator;
protected int sslRedirectPort;
protected AdapterTokenStore tokenStore;
protected AdapterSessionStore tokenStore;
protected String tokenString;
protected String idTokenString;
protected IDToken idToken;
@ -36,7 +36,7 @@ public class OAuthRequestAuthenticator {
protected String refreshToken;
protected String strippedOauthParametersRequestUri;
public OAuthRequestAuthenticator(RequestAuthenticator requestAuthenticator, HttpFacade facade, KeycloakDeployment deployment, int sslRedirectPort, AdapterTokenStore tokenStore) {
public OAuthRequestAuthenticator(RequestAuthenticator requestAuthenticator, HttpFacade facade, KeycloakDeployment deployment, int sslRedirectPort, AdapterSessionStore tokenStore) {
this.reqAuthenticator = requestAuthenticator;
this.facade = facade;
this.deployment = deployment;

View file

@ -0,0 +1,10 @@
package org.keycloak.adapters;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public interface AdapterSessionStore {
void saveRequest();
boolean restoreRequest();
}

View file

@ -7,5 +7,6 @@ package org.keycloak.adapters;
public enum AuthOutcome {
NOT_ATTEMPTED,
FAILED,
AUTHENTICATED
AUTHENTICATED,
LOGGED_OUT
}

View file

@ -33,6 +33,13 @@ public interface HttpFacade {
*/
boolean isSecure();
/**
* Get first query or form param
*
* @param param
* @return
*/
String getFirstParam(String param);
String getQueryParamValue(String param);
Cookie getCookie(String cookieName);
String getHeader(String name);

View file

@ -0,0 +1,67 @@
package org.keycloak.adapters;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
/**
* Maps external principal and SSO id to internal local http session id
*
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class InMemorySessionIdMapper implements SessionIdMaper {
ConcurrentHashMap<String, String> ssoToSession = new ConcurrentHashMap<>();
ConcurrentHashMap<String, String> sessionToSso = new ConcurrentHashMap<>();
ConcurrentHashMap<String, Set<String>> principalToSession = new ConcurrentHashMap<>();
ConcurrentHashMap<String, String> sessionToPrincipal = new ConcurrentHashMap<>();
@Override
public Set<String> getUserSessions(String principal) {
Set<String> lookup = principalToSession.get(principal);
if (lookup == null) return null;
Set<String> copy = new HashSet<>();
copy.addAll(lookup);
return copy;
}
@Override
public String getSessionFromSSO(String sso) {
return ssoToSession.get(sso);
}
@Override
public void map(String sso, String principal, String session) {
ssoToSession.put(sso, session);
sessionToSso.put(session, sso);
Set<String> userSessions = principalToSession.get(principal);
if (userSessions == null) {
final Set<String> tmp = Collections.synchronizedSet(new HashSet<String>());
userSessions = principalToSession.putIfAbsent(principal, tmp);
if (userSessions == null) {
userSessions = tmp;
}
}
userSessions.add(session);
sessionToPrincipal.put(session, principal);
}
@Override
public void removeSession(String session) {
String sso = sessionToSso.remove(session);
if (sso != null) {
ssoToSession.remove(sso);
}
String principal = sessionToPrincipal.remove(session);
if (principal != null) {
Set<String> sessions = principalToSession.get(principal);
sessions.remove(session);
if (sessions.isEmpty()) {
principalToSession.remove(principal, sessions);
}
}
}
}

View file

@ -0,0 +1,17 @@
package org.keycloak.adapters;
import java.util.Set;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public interface SessionIdMaper {
Set<String> getUserSessions(String principal);
String getSessionFromSSO(String sso);
void map(String sso, String principal, String session);
void removeSession(String session);
}

View file

@ -33,6 +33,11 @@ public class JaxrsHttpFacade implements OIDCHttpFacade {
protected class RequestFacade implements OIDCHttpFacade.Request {
@Override
public String getFirstParam(String param) {
throw new RuntimeException("NOT IMPLEMENTED");
}
@Override
public String getMethod() {
return requestContext.getMethod();

View file

@ -18,6 +18,7 @@ import java.util.List;
* @version $Revision: 1 $
*/
public class JettyHttpFacade implements HttpFacade {
public final static String __J_METHOD = "org.eclipse.jetty.security.HTTP_METHOD";
protected org.eclipse.jetty.server.Request request;
protected HttpServletResponse response;
protected RequestFacade requestFacade = new RequestFacade();
@ -58,6 +59,11 @@ public class JettyHttpFacade implements HttpFacade {
return buf.toString();
}
@Override
public String getFirstParam(String param) {
return request.getParameter(param);
}
@Override
public boolean isSecure() {
return request.isSecure();

View file

@ -4,6 +4,7 @@ import org.eclipse.jetty.server.Request;
import org.jboss.logging.Logger;
import org.keycloak.KeycloakPrincipal;
import org.keycloak.KeycloakSecurityContext;
import org.keycloak.adapters.AdapterSessionStore;
import org.keycloak.adapters.AdapterTokenStore;
import org.keycloak.adapters.AdapterUtils;
import org.keycloak.adapters.KeycloakAccount;
@ -18,17 +19,18 @@ import javax.servlet.http.HttpSession;
*
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public abstract class AbstractJettySessionTokenStore implements AdapterTokenStore {
public final static String __J_METHOD = "org.eclipse.jetty.security.HTTP_METHOD";
public class JettySessionTokenStore implements AdapterTokenStore {
private static final Logger log = Logger.getLogger(AbstractJettySessionTokenStore.class);
private static final Logger log = Logger.getLogger(JettySessionTokenStore.class);
private Request request;
protected KeycloakDeployment deployment;
protected AdapterSessionStore sessionStore;
public AbstractJettySessionTokenStore(Request request, KeycloakDeployment deployment) {
public JettySessionTokenStore(Request request, KeycloakDeployment deployment, AdapterSessionStore sessionStore) {
this.request = request;
this.deployment = deployment;
this.sessionStore = sessionStore;
}
@Override
@ -93,4 +95,14 @@ public abstract class AbstractJettySessionTokenStore implements AdapterTokenStor
// no-op
}
@Override
public void saveRequest() {
sessionStore.saveRequest();
}
@Override
public boolean restoreRequest() {
return sessionStore.restoreRequest();
}
}

View file

@ -3,8 +3,8 @@ package org.keycloak.adapters.jetty;
import org.eclipse.jetty.security.authentication.FormAuthenticator;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.util.MultiMap;
import org.keycloak.adapters.KeycloakDeployment;
import org.keycloak.adapters.jetty.core.AbstractJettySessionTokenStore;
import org.keycloak.adapters.AdapterSessionStore;
import org.keycloak.adapters.jetty.core.JettyHttpFacade;
import org.keycloak.util.MultivaluedHashMap;
import javax.servlet.http.HttpSession;
@ -13,12 +13,11 @@ import javax.servlet.http.HttpSession;
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class JettySessionTokenStore extends AbstractJettySessionTokenStore {
public class JettyAdapterSessionStore implements AdapterSessionStore {
public static final String CACHED_FORM_PARAMETERS = "__CACHED_FORM_PARAMETERS";
protected Request myRequest;
public JettySessionTokenStore(Request request, KeycloakDeployment deployment) {
super(request, deployment);
public JettyAdapterSessionStore(Request request) {
this.myRequest = request; // for IDE/compilation purposes
}
@ -43,7 +42,7 @@ public class JettySessionTokenStore extends AbstractJettySessionTokenStore {
if (myRequest.getQueryString() != null)
buf.append("?").append(myRequest.getQueryString());
if (j_uri.equals(buf.toString())) {
String method = (String)session.getAttribute(__J_METHOD);
String method = (String)session.getAttribute(JettyHttpFacade.__J_METHOD);
myRequest.setMethod(method);
MultivaluedHashMap<String, String> j_post = (MultivaluedHashMap<String, String>) session.getAttribute(CACHED_FORM_PARAMETERS);
if (j_post != null) {
@ -57,7 +56,7 @@ public class JettySessionTokenStore extends AbstractJettySessionTokenStore {
restoreFormParameters(map, myRequest);
}
session.removeAttribute(FormAuthenticator.__J_URI);
session.removeAttribute(__J_METHOD);
session.removeAttribute(JettyHttpFacade.__J_METHOD);
session.removeAttribute(FormAuthenticator.__J_POST);
}
return true;
@ -76,7 +75,7 @@ public class JettySessionTokenStore extends AbstractJettySessionTokenStore {
if (myRequest.getQueryString() != null)
buf.append("?").append(myRequest.getQueryString());
session.setAttribute(FormAuthenticator.__J_URI, buf.toString());
session.setAttribute(__J_METHOD, myRequest.getMethod());
session.setAttribute(JettyHttpFacade.__J_METHOD, myRequest.getMethod());
if ("application/x-www-form-urlencoded".equals(myRequest.getContentType()) && "POST".equalsIgnoreCase(myRequest.getMethod())) {
MultiMap<String> formParameters = extractFormParameters(myRequest);

View file

@ -7,6 +7,7 @@ import org.eclipse.jetty.server.UserIdentity;
import org.keycloak.adapters.AdapterTokenStore;
import org.keycloak.adapters.KeycloakDeployment;
import org.keycloak.adapters.jetty.core.AbstractKeycloakJettyAuthenticator;
import org.keycloak.adapters.jetty.core.JettySessionTokenStore;
import javax.servlet.ServletRequest;
@ -23,7 +24,7 @@ public class KeycloakJettyAuthenticator extends AbstractKeycloakJettyAuthenticat
@Override
public AdapterTokenStore createSessionTokenStore(Request request, KeycloakDeployment resolvedDeployment) {
return new JettySessionTokenStore(request, resolvedDeployment);
return new JettySessionTokenStore(request, resolvedDeployment, new JettyAdapterSessionStore(request));
}
@Override

View file

@ -4,8 +4,8 @@ import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.security.authentication.FormAuthenticator;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.util.MultiMap;
import org.keycloak.adapters.KeycloakDeployment;
import org.keycloak.adapters.jetty.core.AbstractJettySessionTokenStore;
import org.keycloak.adapters.AdapterSessionStore;
import org.keycloak.adapters.jetty.core.JettyHttpFacade;
import org.keycloak.util.MultivaluedHashMap;
import javax.servlet.http.HttpSession;
@ -14,17 +14,15 @@ import javax.servlet.http.HttpSession;
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class JettySessionTokenStore extends AbstractJettySessionTokenStore {
public class JettyAdapterSessionStore implements AdapterSessionStore {
public static final String CACHED_FORM_PARAMETERS = "__CACHED_FORM_PARAMETERS";
protected Request myRequest;
public JettySessionTokenStore(Request request, KeycloakDeployment deployment) {
super(request, deployment);
public JettyAdapterSessionStore(Request request) {
this.myRequest = request; // for IDE/compilation purposes
}
protected MultiMap<String> extractFormParameters(Request base_request) {
MultiMap<String> formParameters = new MultiMap<String>();
base_request.extractParameters();
return base_request.getParameters();
}
@ -44,7 +42,7 @@ public class JettySessionTokenStore extends AbstractJettySessionTokenStore {
if (myRequest.getQueryString() != null)
buf.append("?").append(myRequest.getQueryString());
if (j_uri.equals(buf.toString())) {
String method = (String)session.getAttribute(__J_METHOD);
String method = (String)session.getAttribute(JettyHttpFacade.__J_METHOD);
myRequest.setMethod(HttpMethod.valueOf(method.toUpperCase()), method);
MultivaluedHashMap<String, String> j_post = (MultivaluedHashMap<String, String>) session.getAttribute(CACHED_FORM_PARAMETERS);
if (j_post != null) {
@ -58,7 +56,7 @@ public class JettySessionTokenStore extends AbstractJettySessionTokenStore {
restoreFormParameters(map, myRequest);
}
session.removeAttribute(FormAuthenticator.__J_URI);
session.removeAttribute(__J_METHOD);
session.removeAttribute(JettyHttpFacade.__J_METHOD);
session.removeAttribute(FormAuthenticator.__J_POST);
}
return true;
@ -77,7 +75,7 @@ public class JettySessionTokenStore extends AbstractJettySessionTokenStore {
if (myRequest.getQueryString() != null)
buf.append("?").append(myRequest.getQueryString());
session.setAttribute(FormAuthenticator.__J_URI, buf.toString());
session.setAttribute(__J_METHOD, myRequest.getMethod());
session.setAttribute(JettyHttpFacade.__J_METHOD, myRequest.getMethod());
if ("application/x-www-form-urlencoded".equals(myRequest.getContentType()) && "POST".equalsIgnoreCase(myRequest.getMethod())) {
MultiMap<String> formParameters = extractFormParameters(myRequest);

View file

@ -7,6 +7,7 @@ import org.eclipse.jetty.server.UserIdentity;
import org.keycloak.adapters.AdapterTokenStore;
import org.keycloak.adapters.KeycloakDeployment;
import org.keycloak.adapters.jetty.core.AbstractKeycloakJettyAuthenticator;
import org.keycloak.adapters.jetty.core.JettySessionTokenStore;
import javax.servlet.ServletRequest;
@ -23,7 +24,7 @@ public class KeycloakJettyAuthenticator extends AbstractKeycloakJettyAuthenticat
@Override
public AdapterTokenStore createSessionTokenStore(Request request, KeycloakDeployment resolvedDeployment) {
return new JettySessionTokenStore(request, resolvedDeployment);
return new JettySessionTokenStore(request, resolvedDeployment, new JettyAdapterSessionStore(request));
}
@Override

View file

@ -4,8 +4,8 @@ import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.security.authentication.FormAuthenticator;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.util.MultiMap;
import org.keycloak.adapters.KeycloakDeployment;
import org.keycloak.adapters.jetty.core.AbstractJettySessionTokenStore;
import org.keycloak.adapters.AdapterSessionStore;
import org.keycloak.adapters.jetty.core.JettyHttpFacade;
import org.keycloak.util.MultivaluedHashMap;
import javax.servlet.http.HttpSession;
@ -14,12 +14,11 @@ import javax.servlet.http.HttpSession;
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class JettySessionTokenStore extends AbstractJettySessionTokenStore {
public class JettyAdapterSessionStore implements AdapterSessionStore {
public static final String CACHED_FORM_PARAMETERS = "__CACHED_FORM_PARAMETERS";
protected Request myRequest;
public JettySessionTokenStore(Request request, KeycloakDeployment deployment) {
super(request, deployment);
public JettyAdapterSessionStore(Request request) {
this.myRequest = request; // for IDE/compilation purposes
}
@ -44,7 +43,7 @@ public class JettySessionTokenStore extends AbstractJettySessionTokenStore {
if (myRequest.getQueryString() != null)
buf.append("?").append(myRequest.getQueryString());
if (j_uri.equals(buf.toString())) {
String method = (String)session.getAttribute(__J_METHOD);
String method = (String)session.getAttribute(JettyHttpFacade.__J_METHOD);
myRequest.setMethod(HttpMethod.valueOf(method.toUpperCase()), method);
MultivaluedHashMap<String, String> j_post = (MultivaluedHashMap<String, String>) session.getAttribute(CACHED_FORM_PARAMETERS);
if (j_post != null) {
@ -58,7 +57,7 @@ public class JettySessionTokenStore extends AbstractJettySessionTokenStore {
restoreFormParameters(map, myRequest);
}
session.removeAttribute(FormAuthenticator.__J_URI);
session.removeAttribute(__J_METHOD);
session.removeAttribute(JettyHttpFacade.__J_METHOD);
session.removeAttribute(FormAuthenticator.__J_POST);
}
return true;
@ -77,7 +76,7 @@ public class JettySessionTokenStore extends AbstractJettySessionTokenStore {
if (myRequest.getQueryString() != null)
buf.append("?").append(myRequest.getQueryString());
session.setAttribute(FormAuthenticator.__J_URI, buf.toString());
session.setAttribute(__J_METHOD, myRequest.getMethod());
session.setAttribute(JettyHttpFacade.__J_METHOD, myRequest.getMethod());
if ("application/x-www-form-urlencoded".equals(myRequest.getContentType()) && "POST".equalsIgnoreCase(myRequest.getMethod())) {
MultiMap<String> formParameters = extractFormParameters(myRequest);

View file

@ -7,6 +7,7 @@ import org.eclipse.jetty.server.UserIdentity;
import org.keycloak.adapters.AdapterTokenStore;
import org.keycloak.adapters.KeycloakDeployment;
import org.keycloak.adapters.jetty.core.AbstractKeycloakJettyAuthenticator;
import org.keycloak.adapters.jetty.core.JettySessionTokenStore;
import javax.servlet.ServletRequest;
@ -38,6 +39,6 @@ public class KeycloakJettyAuthenticator extends AbstractKeycloakJettyAuthenticat
@Override
public AdapterTokenStore createSessionTokenStore(Request request, KeycloakDeployment resolvedDeployment) {
return new JettySessionTokenStore(request, resolvedDeployment);
return new JettySessionTokenStore(request, resolvedDeployment, new JettyAdapterSessionStore(request));
}
}

View file

@ -182,6 +182,11 @@ public class ServletOAuthClient extends KeycloakDeploymentDelegateOAuthClient {
public Request getRequest() {
return new Request() {
@Override
public String getFirstParam(String param) {
return servletRequest.getParameter(param);
}
@Override
public String getMethod() {
return servletRequest.getMethod();

View file

@ -32,6 +32,11 @@ class WrappedHttpServletRequest implements Request {
this.request = request;
}
@Override
public String getFirstParam(String param) {
return request.getParameter(param);
}
@Override
public String getMethod() {
return request.getMethod();

View file

@ -2,10 +2,9 @@ package org.keycloak.adapters.springsecurity.token;
import org.junit.Before;
import org.junit.Test;
import org.keycloak.adapters.AdapterTokenStore;
import org.keycloak.adapters.AdapterSessionStore;
import org.keycloak.adapters.KeycloakDeployment;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import javax.servlet.http.HttpServletRequest;
@ -32,7 +31,7 @@ public class SpringSecurityAdapterTokenStoreFactoryTest {
@Test
public void testCreateAdapterTokenStore() throws Exception {
AdapterTokenStore store = factory.createAdapterTokenStore(deployment, request);
AdapterSessionStore store = factory.createAdapterTokenStore(deployment, request);
assertNotNull(store);
assertTrue(store instanceof SpringSecurityTokenStore);
}

View file

@ -64,6 +64,11 @@ public class CatalinaHttpFacade implements HttpFacade {
return request.isSecure();
}
@Override
public String getFirstParam(String param) {
return request.getParameter(param);
}
@Override
public String getQueryParamValue(String paramName) {
if (queryParameters == null) {

View file

@ -0,0 +1,32 @@
package org.keycloak.adapters.tomcat;
import org.apache.catalina.connector.Request;
import org.keycloak.adapters.AdapterSessionStore;
import java.io.IOException;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class CatalinaAdapterSessionStore implements AdapterSessionStore {
protected Request request;
protected AbstractKeycloakAuthenticatorValve valve;
public CatalinaAdapterSessionStore(Request request, AbstractKeycloakAuthenticatorValve valve) {
this.request = request;
this.valve = valve;
}
public void saveRequest() {
try {
valve.keycloakSaveRequest(request);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public boolean restoreRequest() {
return valve.keycloakRestoreRequest(request);
}
}

View file

@ -19,26 +19,23 @@ import java.util.logging.Logger;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class CatalinaSessionTokenStore implements AdapterTokenStore {
public class CatalinaSessionTokenStore extends CatalinaAdapterSessionStore implements AdapterTokenStore {
private static final Logger log = Logger.getLogger("" + CatalinaSessionTokenStore.class);
private Request request;
private KeycloakDeployment deployment;
private CatalinaUserSessionManagement sessionManagement;
protected GenericPrincipalFactory principalFactory;
protected AbstractKeycloakAuthenticatorValve valve;
public CatalinaSessionTokenStore(Request request, KeycloakDeployment deployment,
CatalinaUserSessionManagement sessionManagement,
GenericPrincipalFactory principalFactory,
AbstractKeycloakAuthenticatorValve valve) {
this.request = request;
super(request, valve);
this.deployment = deployment;
this.sessionManagement = sessionManagement;
this.principalFactory = principalFactory;
this.valve = valve;
}
@Override
@ -169,17 +166,4 @@ public class CatalinaSessionTokenStore implements AdapterTokenStore {
// no-op
}
@Override
public void saveRequest() {
try {
valve.keycloakSaveRequest(request);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Override
public boolean restoreRequest() {
return valve.keycloakRestoreRequest(request);
}
}

View file

@ -0,0 +1,39 @@
package org.keycloak.adapters.undertow;
import io.undertow.server.HttpServerExchange;
import io.undertow.servlet.handlers.ServletRequestContext;
import org.keycloak.adapters.HttpFacade;
import javax.security.cert.X509Certificate;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.InputStream;
import java.util.List;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class ServletHttpFacade extends UndertowHttpFacade {
protected HttpServletRequest request;
protected HttpServletResponse response;
public ServletHttpFacade(HttpServerExchange exchange) {
super(exchange);
final ServletRequestContext servletRequestContext = exchange.getAttachment(ServletRequestContext.ATTACHMENT_KEY);
request = (HttpServletRequest)servletRequestContext.getServletRequest();
}
protected class RequestFacade extends UndertowHttpFacade.RequestFacade {
@Override
public String getFirstParam(String param) {
return request.getParameter(param);
}
}
@Override
public Request getRequest() {
return new RequestFacade();
}
}

View file

@ -21,7 +21,7 @@ import java.util.Map;
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public abstract class UndertowHttpFacade implements HttpFacade {
public class UndertowHttpFacade implements HttpFacade {
protected HttpServerExchange exchange;
protected RequestFacade requestFacade = new RequestFacade();
protected ResponseFacade responseFacade = new ResponseFacade();
@ -66,6 +66,11 @@ public abstract class UndertowHttpFacade implements HttpFacade {
return protocol.equalsIgnoreCase("https");
}
@Override
public String getFirstParam(String param) {
throw new RuntimeException("Not implemented yet");
}
@Override
public String getQueryParamValue(String param) {
Map<String,Deque<String>> queryParameters = exchange.getQueryParameters();

View file

@ -60,7 +60,7 @@ public abstract class AbstractUndertowKeycloakAuthMech implements Authentication
Integer code = servePage(exchange, errorPage);
return new ChallengeResult(true, code);
}
UndertowHttpFacade facade = new OIDCUndertowHttpFacade(exchange);
UndertowHttpFacade facade = new UndertowHttpFacade(exchange);
if (challenge.challenge(facade)) {
return new ChallengeResult(true, exchange.getResponseCode());
}

View file

@ -985,6 +985,11 @@
<artifactId>keycloak-saml-protocol</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-saml-adapter-core</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-services</artifactId>

View file

@ -0,0 +1,86 @@
package org.keycloak.adapters.saml;
import org.jboss.logging.Logger;
import org.keycloak.adapters.AuthChallenge;
import org.keycloak.adapters.HttpFacade;
import org.keycloak.saml.BaseSAML2BindingBuilder;
import org.keycloak.saml.SAML2AuthnRequestBuilder;
import org.keycloak.saml.SAML2NameIDPolicyBuilder;
import org.keycloak.saml.common.constants.JBossSAMLURIConstants;
import org.w3c.dom.Document;
import java.security.KeyPair;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class InitiateLogin implements AuthChallenge {
protected static Logger log = Logger.getLogger(InitiateLogin.class);
protected SamlDeployment deployment;
protected SamlSessionStore sessionStore;
public InitiateLogin(SamlDeployment deployment, SamlSessionStore sessionStore) {
this.deployment = deployment;
this.sessionStore = sessionStore;
}
@Override
public boolean errorPage() {
return true;
}
@Override
public boolean challenge(HttpFacade httpFacade) {
try {
String issuerURL = deployment.getIssuer();
String actionUrl = deployment.getSingleSignOnServiceUrl();
String destinationUrl = actionUrl;
String nameIDPolicyFormat = deployment.getNameIDPolicyFormat();
if (nameIDPolicyFormat == null) {
nameIDPolicyFormat = JBossSAMLURIConstants.NAMEID_FORMAT_PERSISTENT.get();
}
String protocolBinding = JBossSAMLURIConstants.SAML_HTTP_REDIRECT_BINDING.get();
if (deployment.getResponseBinding() == SamlDeployment.Binding.POST) {
protocolBinding = JBossSAMLURIConstants.SAML_HTTP_POST_BINDING.get();
}
SAML2AuthnRequestBuilder authnRequestBuilder = new SAML2AuthnRequestBuilder()
.assertionConsumerUrl(deployment.getAssertionConsumerServiceUrl())
.destination(destinationUrl)
.issuer(issuerURL)
.forceAuthn(deployment.isForceAuthentication())
.protocolBinding(protocolBinding)
.nameIdPolicy(SAML2NameIDPolicyBuilder.format(nameIDPolicyFormat));
BaseSAML2BindingBuilder binding = new BaseSAML2BindingBuilder();
if (deployment.isRequestsSigned()) {
KeyPair keypair = deployment.getSigningKeyPair();
if (keypair == null) {
throw new RuntimeException("Signing keys not configured");
}
if (deployment.getSignatureCanonicalizationMethod() != null) {
binding.canonicalizationMethod(deployment.getSignatureCanonicalizationMethod());
}
binding.signWith(keypair);
binding.signDocument();
}
sessionStore.saveRequest();
Document document = authnRequestBuilder.toDocument();
SamlDeployment.Binding samlBinding = deployment.getRequestBinding();
SamlUtil.sendSaml(httpFacade, actionUrl, binding, document, samlBinding);
} catch (Exception e) {
throw new RuntimeException("Could not create authentication request.", e);
}
return true;
}
}

View file

@ -1,25 +1,430 @@
package org.keycloak.adapters.saml;
import org.jboss.logging.Logger;
import org.keycloak.VerificationException;
import org.keycloak.adapters.AuthChallenge;
import org.keycloak.adapters.AuthOutcome;
import org.keycloak.adapters.HttpFacade;
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.StatusResponseType;
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.exceptions.ConfigurationException;
import org.keycloak.saml.common.exceptions.ParsingException;
import org.keycloak.saml.common.exceptions.ProcessingException;
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.keycloak.saml.processing.web.util.RedirectBindingUtil;
import org.keycloak.util.KeycloakUriBuilder;
import org.keycloak.util.MultivaluedHashMap;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import java.io.IOException;
import java.security.PublicKey;
import java.security.Signature;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class SamlAuthenticator {
public abstract class SamlAuthenticator {
protected static Logger log = Logger.getLogger(SamlAuthenticator.class);
protected HttpFacade facade;
protected AuthChallenge challenge;
protected SamlDeployment deployment;
protected SamlSessionStore sessionStore;
public SamlAuthenticator(HttpFacade facade, SamlDeployment deployment, SamlSessionStore sessionStore) {
this.facade = facade;
this.deployment = deployment;
this.sessionStore = sessionStore;
}
public AuthChallenge getChallenge() {
return challenge;
}
public AuthOutcome authenticate() {
return null;
String samlRequest = facade.getRequest().getFirstParam(GeneralConstants.SAML_REQUEST_KEY);
String samlResponse = facade.getRequest().getFirstParam(GeneralConstants.SAML_RESPONSE_KEY);
String relayState = facade.getRequest().getFirstParam(GeneralConstants.RELAY_STATE);
boolean globalLogout = "true".equals(facade.getRequest().getQueryParamValue("GLO"));
if (samlRequest != null) {
return handleSamlRequest(samlRequest, relayState);
} else if (samlResponse != null) {
return handleSamlResponse(samlResponse, relayState);
} else if (sessionStore.isLoggedIn()) {
if (globalLogout) {
return globalLogout();
}
if (verifySSL()) return AuthOutcome.FAILED;
log.debug("AUTHENTICATED: was cached");
return AuthOutcome.AUTHENTICATED;
}
return initiateLogin();
}
protected AuthOutcome globalLogout() {
SamlSession account = sessionStore.getAccount();
SAML2LogoutRequestBuilder logoutBuilder = new SAML2LogoutRequestBuilder()
.assertionExpiration(30)
.issuer(deployment.getIssuer())
.sessionIndex(account.getSessionIndex())
.userPrincipal(account.getPrincipal().getSamlSubject(), account.getPrincipal().getNameIDFormat())
.destination(deployment.getSingleLogoutServiceUrl());
BaseSAML2BindingBuilder binding = new BaseSAML2BindingBuilder();
if (deployment.isRequestsSigned()) {
binding.signWith(deployment.getSigningKeyPair())
.signDocument();
}
binding.relayState("logout");
try {
SamlUtil.sendSaml(facade, deployment.getSingleLogoutServiceUrl(), binding, logoutBuilder.buildDocument(), deployment.getRequestBinding());
} catch (ProcessingException e) {
throw new RuntimeException(e);
} catch (ConfigurationException e) {
throw new RuntimeException(e);
} catch (IOException e) {
throw new RuntimeException(e);
} catch (ParsingException e) {
throw new RuntimeException(e);
}
return AuthOutcome.NOT_ATTEMPTED;
}
protected AuthOutcome handleSamlRequest(String samlRequest, String relayState) {
SAMLDocumentHolder holder = null;
boolean postBinding = false;
if (facade.getRequest().getMethod().equalsIgnoreCase("GET")) {
holder = SAMLRequestParser.parseRequestRedirectBinding(samlRequest);
} else {
postBinding = true;
holder = SAMLRequestParser.parseRequestPostBinding(samlRequest);
}
RequestAbstractType requestAbstractType = (RequestAbstractType) holder.getSamlObject();
if (!facade.getRequest().getURI().toString().equals(requestAbstractType.getDestination())) {
throw new RuntimeException("destination not equal to request");
}
validateSamlSignature(holder, postBinding, GeneralConstants.SAML_REQUEST_KEY);
if (requestAbstractType instanceof LogoutRequestType) {
LogoutRequestType logout = (LogoutRequestType) requestAbstractType;
return logoutRequest(logout, relayState);
} else {
throw new RuntimeException("unknown SAML request type");
}
}
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.getIssuer();
SAML2LogoutResponseBuilder builder = new SAML2LogoutResponseBuilder();
builder.logoutRequestID(request.getID());
builder.destination(deployment.getSingleLogoutServiceUrl());
builder.issuer(issuerURL);
BaseSAML2BindingBuilder binding = new BaseSAML2BindingBuilder().relayState(relayState);
if (deployment.isRequestsSigned()) {
binding.signWith(deployment.getSigningKeyPair())
.signDocument();
}
try {
SamlUtil.sendSaml(facade, deployment.getSingleLogoutServiceUrl(), binding, builder.buildDocument(),
deployment.getResponseBinding());
} catch (ConfigurationException e) {
throw new RuntimeException(e);
} catch (ProcessingException e) {
throw new RuntimeException(e);
} catch (IOException e) {
throw new RuntimeException(e);
}
return AuthOutcome.NOT_ATTEMPTED;
}
protected AuthOutcome handleSamlResponse(String samlResponse, String relayState) {
SAMLDocumentHolder holder = null;
boolean postBinding = false;
if (facade.getRequest().getMethod().equalsIgnoreCase("GET")) {
holder = extractRedirectBindingResponse(samlResponse);
} else {
postBinding = true;
holder = extractPostBindingResponse(samlResponse);
}
StatusResponseType statusResponse = (StatusResponseType)holder.getSamlObject();
// validate destination
if (!facade.getRequest().getURI().toString().equals(statusResponse.getDestination())) {
throw new RuntimeException("destination not equal to request");
}
validateSamlSignature(holder, postBinding, GeneralConstants.SAML_RESPONSE_KEY);
if (statusResponse instanceof ResponseType) {
return handleLoginResponse((ResponseType)statusResponse);
} else {
// todo need to check that it is actually a LogoutResponse
return handleLogoutResponse(holder, statusResponse, relayState);
}
}
private void validateSamlSignature(SAMLDocumentHolder holder, boolean postBinding, String paramKey) {
if (deployment.isValidateSignatures()) {
try {
if (postBinding) {
verifyPostBindingSignature(holder.getSamlDocument(), deployment.getSignatureValidationKey());
} else {
verifyRedirectBindingSignature(deployment.getSignatureValidationKey(), paramKey);
}
} catch (VerificationException e) {
log.error("validation failed", e);
throw new RuntimeException("invalid document signature");
}
}
}
protected AuthOutcome handleLoginResponse(ResponseType responseType) {
AssertionType assertion = null;
try {
assertion = AssertionUtil.getAssertion(responseType, deployment.getAssertionDecryptionKey());
if (AssertionUtil.hasExpired(assertion)) {
return initiateLogin();
}
} catch (ParsingException e) {
throw new RuntimeException(e);
} catch (ProcessingException e) {
throw new RuntimeException(e);
} catch (ConfigurationException e) {
throw new RuntimeException(e);
}
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);
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) {
attributes.add(attr.getFriendlyName(), value);
}
}
}
}
}
}
}
if (deployment.getPrincipalNamePolicy() == SamlDeployment.PrincipalNamePolicy.FROM_ATTRIBUTE_NAME) {
if (deployment.getPrincipalAttributeName() != null) {
String attribute = attributes.getFirst(deployment.getPrincipalAttributeName());
if (attribute != null) principalName = attribute;
}
} else if (deployment.getPrincipalNamePolicy() == SamlDeployment.PrincipalNamePolicy.FROM_FRIENDLY_ATTRIBUTE_NAME) {
if (deployment.getPrincipalAttributeName() != null) {
String 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;
}
}
final SamlPrincipal principal = new SamlPrincipal(principalName, principalName, subjectNameID.getFormat().toString(), attributes, friendlyAttributes);
String index = authn == null ? null : authn.getSessionIndex();
final String sessionIndex = index;
SamlSession account = new SamlSession() {
@Override
public SamlPrincipal getPrincipal() {
return principal;
}
@Override
public Set<String> getRoles() {
return roles;
}
@Override
public String getSessionIndex() {
return sessionIndex;
}
};
sessionStore.saveAccount(account);
completeAuthentication(account);
// redirect to original request, it will be restored
facade.getResponse().setHeader("Location", sessionStore.getRedirectUri());
facade.getResponse().setStatus(302);
facade.getResponse().end();
return AuthOutcome.AUTHENTICATED;
}
protected abstract void completeAuthentication(SamlSession account);
private String getAttributeValue(Object attrValue) {
String value;
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
throw new RuntimeException("Unknown attribute value type: " + attrValue.getClass().getName());
return value;
}
protected boolean isRole(AttributeType attribute) {
return deployment.getRoleAttributeNames().contains(attribute.getName()) || deployment.getRoleAttributeFriendlyNames().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);
String xml = new String(samlBytes);
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("SAM 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);
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,50 @@
package org.keycloak.adapters.saml;
import org.keycloak.enums.SslRequired;
import java.security.KeyPair;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.util.Set;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public interface SamlDeployment {
enum Binding {
POST,
REDIRECT
}
public boolean isConfigured();
SslRequired getSslRequired();
String getSingleSignOnServiceUrl();
String getSingleLogoutServiceUrl();
String getIssuer();
String getNameIDPolicyFormat();
String getAssertionConsumerServiceUrl();
Binding getRequestBinding();
Binding getResponseBinding();
KeyPair getSigningKeyPair();
String getSignatureCanonicalizationMethod();
boolean isForceAuthentication();
boolean isRequestsSigned();
boolean isValidateSignatures();
PublicKey getSignatureValidationKey();
PrivateKey getAssertionDecryptionKey();
Set<String> getRoleAttributeNames();
Set<String> getRoleAttributeFriendlyNames();
enum PrincipalNamePolicy {
FROM_NAME_ID,
FROM_ATTRIBUTE_NAME,
FROM_FRIENDLY_ATTRIBUTE_NAME
}
PrincipalNamePolicy getPrincipalNamePolicy();
String getPrincipalAttributeName();
}

View file

@ -0,0 +1,13 @@
package org.keycloak.adapters.saml;
import org.keycloak.adapters.HttpFacade;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class SamlDeploymentContext {
public SamlDeployment resolveDeployment(HttpFacade facade) {
return null;
}
}

View file

@ -0,0 +1,84 @@
package org.keycloak.adapters.saml;
import org.keycloak.util.MultivaluedHashMap;
import java.io.Serializable;
import java.security.Principal;
import java.util.Collections;
import java.util.List;
import java.util.Set;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class SamlPrincipal implements Serializable, Principal {
private MultivaluedHashMap<String, String> attributes = new MultivaluedHashMap<>();
private MultivaluedHashMap<String, String> friendlyAttributes = new MultivaluedHashMap<>();
private String name;
private String samlSubject;
private String nameIDFormat;
public SamlPrincipal(String name, String samlSubject, String nameIDFormat, MultivaluedHashMap<String, String> attributes, MultivaluedHashMap<String, String> friendlyAttributes) {
this.name = name;
this.attributes = attributes;
this.friendlyAttributes = friendlyAttributes;
this.samlSubject = samlSubject;
this.nameIDFormat = nameIDFormat;
}
public SamlPrincipal() {
}
public String getSamlSubject() {
return samlSubject;
}
public String getNameIDFormat() {
return nameIDFormat;
}
@Override
public String getName() {
return null;
}
public List<String> getAttributes(String name) {
List<String> list = attributes.get(name);
if (list != null) {
return Collections.unmodifiableList(list);
} else {
return Collections.emptyList();
}
}
public List<String> getFriendlyAttributes(String friendlyName) {
List<String> list = friendlyAttributes.get(name);
if (list != null) {
return Collections.unmodifiableList(list);
} else {
return Collections.emptyList();
}
}
public String getAttribute(String name) {
return attributes.getFirst(name);
}
public String getFriendlyAttribute(String friendlyName) {
return friendlyAttributes.getFirst(friendlyName);
}
public Set<String> getAttributeNames() {
return Collections.unmodifiableSet(attributes.keySet());
}
public Set<String> getFriendlyNames() {
return Collections.unmodifiableSet(friendlyAttributes.keySet());
}
}

View file

@ -0,0 +1,15 @@
package org.keycloak.adapters.saml;
import java.io.Serializable;
import java.security.Principal;
import java.util.Set;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public interface SamlSession extends Serializable {
SamlPrincipal getPrincipal();
Set<String> getRoles();
String getSessionIndex();
}

View file

@ -0,0 +1,20 @@
package org.keycloak.adapters.saml;
import org.keycloak.adapters.AdapterSessionStore;
import java.util.List;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public interface SamlSessionStore extends AdapterSessionStore {
boolean isLoggedIn();
SamlSession getAccount();
void saveAccount(SamlSession account);
String getRedirectUri();
void logoutAccount();
void logoutByPrincipal(String principal);
void logoutBySsoId(List<String> ssoIds);
}

View file

@ -0,0 +1,31 @@
package org.keycloak.adapters.saml;
import org.keycloak.adapters.HttpFacade;
import org.keycloak.saml.BaseSAML2BindingBuilder;
import org.keycloak.saml.common.exceptions.ConfigurationException;
import org.keycloak.saml.common.exceptions.ProcessingException;
import org.w3c.dom.Document;
import java.io.IOException;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class SamlUtil {
public static void sendSaml(HttpFacade httpFacade, String actionUrl, BaseSAML2BindingBuilder binding, Document document, SamlDeployment.Binding samlBinding) throws ProcessingException, ConfigurationException, IOException {
if (samlBinding == SamlDeployment.Binding.POST) {
String html = binding.postBinding(document).getHtmlRequest(actionUrl);
httpFacade.getResponse().setStatus(200);
httpFacade.getResponse().setHeader("Content-Type", "text/html");
httpFacade.getResponse().setHeader("Pragma", "no-cache");
httpFacade.getResponse().setHeader("Cache-Control", "no-cache, no-store");
httpFacade.getResponse().getOutputStream().write(html.getBytes());
httpFacade.getResponse().end();
} else {
String uri = binding.redirectBinding(document).requestURI(actionUrl).toString();
httpFacade.getResponse().setStatus(302);
httpFacade.getResponse().setHeader("Location", uri);
}
}
}

View file

@ -15,5 +15,6 @@
<modules>
<module>core</module>
<module>undertow</module>
</modules>
</project>

View file

@ -0,0 +1,87 @@
<?xml version="1.0"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
<version>1.5.0.Final-SNAPSHOT</version>
<relativePath>../../../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>keycloak-undertow-saml-adapter</artifactId>
<name>Keycloak Undertow SAML Adapter</name>
<description/>
<dependencies>
<dependency>
<groupId>org.jboss.logging</groupId>
<artifactId>jboss-logging</artifactId>
<version>${jboss.logging.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-saml-core</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-adapter-spi</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-core</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-saml-adapter-core</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-undertow-adapter-spi</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.jboss.logging</groupId>
<artifactId>jboss-logging</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.jboss.spec.javax.servlet</groupId>
<artifactId>jboss-servlet-api_3.0_spec</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>io.undertow</groupId>
<artifactId>undertow-servlet</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>io.undertow</groupId>
<artifactId>undertow-core</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>${maven.compiler.source}</source>
<target>${maven.compiler.target}</target>
</configuration>
</plugin>
</plugins>
</build>
</project>

View file

@ -0,0 +1,142 @@
/*
* Copyright 2014 Red Hat Inc. and/or its affiliates and other contributors
* as indicated by the @author tags. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package org.keycloak.adapters.saml.undertow;
import io.undertow.security.api.AuthenticationMechanism;
import io.undertow.security.api.NotificationReceiver;
import io.undertow.security.api.SecurityContext;
import io.undertow.security.api.SecurityNotification;
import io.undertow.server.HttpServerExchange;
import io.undertow.util.AttachmentKey;
import io.undertow.util.Headers;
import io.undertow.util.StatusCodes;
import org.keycloak.adapters.AuthChallenge;
import org.keycloak.adapters.AuthOutcome;
import org.keycloak.adapters.HttpFacade;
import org.keycloak.adapters.saml.SamlDeployment;
import org.keycloak.adapters.saml.SamlDeploymentContext;
import org.keycloak.adapters.saml.SamlSessionStore;
import org.keycloak.adapters.undertow.UndertowHttpFacade;
import org.keycloak.adapters.undertow.UndertowUserSessionManagement;
/**
* Abstract base class for a Keycloak-enabled Undertow AuthenticationMechanism.
*
* @author Stan Silvert ssilvert@redhat.com (C) 2014 Red Hat Inc.
*/
public abstract class AbstractSamlAuthMech implements AuthenticationMechanism {
public static final AttachmentKey<AuthChallenge> KEYCLOAK_CHALLENGE_ATTACHMENT_KEY = AttachmentKey.create(AuthChallenge.class);
protected SamlDeploymentContext deploymentContext;
protected UndertowUserSessionManagement sessionManagement;
protected String logoutPage;
protected String errorPage;
public AbstractSamlAuthMech(SamlDeploymentContext deploymentContext, UndertowUserSessionManagement sessionManagement,
String logoutPage,
String errorPage) {
this.deploymentContext = deploymentContext;
this.sessionManagement = sessionManagement;
this.errorPage = errorPage;
this.logoutPage = logoutPage;
}
@Override
public ChallengeResult sendChallenge(HttpServerExchange exchange, SecurityContext securityContext) {
AuthChallenge challenge = exchange.getAttachment(KEYCLOAK_CHALLENGE_ATTACHMENT_KEY);
if (challenge != null) {
if (challenge.errorPage() && errorPage != null) {
Integer code = servePage(exchange, errorPage);
return new ChallengeResult(true, code);
}
UndertowHttpFacade facade = new UndertowHttpFacade(exchange);
if (challenge.challenge(facade)) {
return new ChallengeResult(true, exchange.getResponseCode());
}
}
return new ChallengeResult(false);
}
protected Integer servePage(final HttpServerExchange exchange, final String location) {
sendRedirect(exchange, location);
return StatusCodes.TEMPORARY_REDIRECT;
}
static void sendRedirect(final HttpServerExchange exchange, final String location) {
// TODO - String concatenation to construct URLS is extremely error prone - switch to a URI which will better handle this.
String loc = exchange.getRequestScheme() + "://" + exchange.getHostAndPort() + location;
exchange.getResponseHeaders().put(Headers.LOCATION, loc);
}
protected void registerNotifications(final SecurityContext securityContext) {
final NotificationReceiver logoutReceiver = new NotificationReceiver() {
@Override
public void handleNotification(SecurityNotification notification) {
if (notification.getEventType() != SecurityNotification.EventType.LOGGED_OUT) return;
HttpServerExchange exchange = notification.getExchange();
UndertowHttpFacade facade = new UndertowHttpFacade(exchange);
SamlDeployment deployment = deploymentContext.resolveDeployment(facade);
SamlSessionStore sessionStore = getTokenStore(exchange, facade, deployment, securityContext);
sessionStore.logoutAccount();
}
};
securityContext.registerNotificationReceiver(logoutReceiver);
}
/**
* Call this inside your authenticate method.
*/
public AuthenticationMechanismOutcome authenticate(HttpServerExchange exchange, SecurityContext securityContext) {
UndertowHttpFacade facade = new UndertowHttpFacade(exchange);
SamlDeployment deployment = deploymentContext.resolveDeployment(facade);
if (!deployment.isConfigured()) {
return AuthenticationMechanismOutcome.NOT_ATTEMPTED;
}
SamlSessionStore sessionStore = getTokenStore(exchange, facade, deployment, securityContext);
UndertowSamlAuthenticator authenticator = new UndertowSamlAuthenticator(securityContext, facade,
deploymentContext.resolveDeployment(facade), sessionStore);
AuthOutcome outcome = authenticator.authenticate();
if (outcome == AuthOutcome.AUTHENTICATED) {
registerNotifications(securityContext);
return AuthenticationMechanismOutcome.AUTHENTICATED;
}
if (outcome == AuthOutcome.LOGGED_OUT) {
securityContext.logout();
if (logoutPage != null) {
sendRedirect(exchange, logoutPage);
exchange.setResponseCode(302);
exchange.endExchange();
}
return AuthenticationMechanismOutcome.NOT_ATTEMPTED;
}
AuthChallenge challenge = authenticator.getChallenge();
if (challenge != null) {
exchange.putAttachment(KEYCLOAK_CHALLENGE_ATTACHMENT_KEY, challenge);
}
if (outcome == AuthOutcome.FAILED) {
return AuthenticationMechanismOutcome.NOT_AUTHENTICATED;
}
return AuthenticationMechanismOutcome.NOT_ATTEMPTED;
}
protected abstract SamlSessionStore getTokenStore(HttpServerExchange exchange, HttpFacade facade, SamlDeployment deployment, SecurityContext securityContext);
}

View file

@ -0,0 +1,25 @@
package org.keycloak.adapters.saml.undertow;
import io.undertow.security.api.SecurityContext;
import io.undertow.server.HttpServerExchange;
import org.keycloak.adapters.HttpFacade;
import org.keycloak.adapters.saml.SamlDeployment;
import org.keycloak.adapters.saml.SamlDeploymentContext;
import org.keycloak.adapters.saml.SamlSessionStore;
import org.keycloak.adapters.undertow.UndertowUserSessionManagement;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class ServletSamlAuthMech extends AbstractSamlAuthMech {
public ServletSamlAuthMech(SamlDeploymentContext deploymentContext, UndertowUserSessionManagement sessionManagement,
String logoutPage, String errorPage) {
super(deploymentContext, sessionManagement, logoutPage, errorPage);
}
@Override
protected SamlSessionStore getTokenStore(HttpServerExchange exchange, HttpFacade facade, SamlDeployment deployment, SecurityContext securityContext) {
return new ServletSamlSessionStore(exchange, sessionManagement, securityContext);
}
}

View file

@ -0,0 +1,140 @@
package org.keycloak.adapters.saml.undertow;
import io.undertow.security.api.SecurityContext;
import io.undertow.security.idm.Account;
import io.undertow.server.HttpServerExchange;
import io.undertow.server.session.Session;
import io.undertow.servlet.handlers.ServletRequestContext;
import io.undertow.servlet.spec.HttpSessionImpl;
import io.undertow.servlet.util.SavedRequest;
import io.undertow.util.Sessions;
import org.jboss.logging.Logger;
import org.keycloak.adapters.saml.SamlSession;
import org.keycloak.adapters.saml.SamlSessionStore;
import org.keycloak.adapters.undertow.UndertowUserSessionManagement;
import org.keycloak.util.KeycloakUriBuilder;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import java.security.Principal;
import java.util.List;
import java.util.Set;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class ServletSamlSessionStore implements SamlSessionStore {
protected static Logger log = Logger.getLogger(SamlSessionStore.class);
public static final String SAML_REDIRECT_URI = "SAML_REDIRECT_URI";
private final HttpServerExchange exchange;
private final UndertowUserSessionManagement sessionManagement;
private final SecurityContext securityContext;
public ServletSamlSessionStore(HttpServerExchange exchange, UndertowUserSessionManagement sessionManagement,
SecurityContext securityContext) {
this.exchange = exchange;
this.sessionManagement = sessionManagement;
this.securityContext = securityContext;
}
@Override
public void logoutAccount() {
HttpSession session = getSession(false);
if (session != null) {
session.removeAttribute(SamlSession.class.getName());
session.removeAttribute(SAML_REDIRECT_URI);
}
}
@Override
public void logoutByPrincipal(String principal) {
}
@Override
public void logoutBySsoId(List<String> ssoIds) {
}
@Override
public boolean isLoggedIn() {
HttpSession session = getSession(false);
if (session == null) {
log.debug("session was null, returning null");
return false;
}
final SamlSession samlSession = (SamlSession)session.getAttribute(SamlSession.class.getName());
if (samlSession == null) {
log.debug("SamlSession was not in session, returning null");
return false;
}
Account undertowAccount = new Account() {
@Override
public Principal getPrincipal() {
return samlSession.getPrincipal();
}
@Override
public Set<String> getRoles() {
return samlSession.getRoles();
}
};
securityContext.authenticationComplete(undertowAccount, "KEYCLOAK-SAML", false);
restoreRequest();
return true;
}
@Override
public void saveAccount(SamlSession account) {
final ServletRequestContext servletRequestContext = exchange.getAttachment(ServletRequestContext.ATTACHMENT_KEY);
HttpSession session = getSession(true);
session.setAttribute(SamlSession.class.getName(), account);
sessionManagement.login(servletRequestContext.getDeployment().getSessionManager());
}
@Override
public SamlSession getAccount() {
HttpSession session = getSession(true);
return (SamlSession)session.getAttribute(SamlSession.class.getName());
}
@Override
public String getRedirectUri() {
final ServletRequestContext sc = exchange.getAttachment(ServletRequestContext.ATTACHMENT_KEY);
HttpSessionImpl session = sc.getCurrentServletContext().getSession(exchange, true);
return (String)session.getAttribute(SAML_REDIRECT_URI);
}
@Override
public void saveRequest() {
SavedRequest.trySaveRequest(exchange);
final ServletRequestContext sc = exchange.getAttachment(ServletRequestContext.ATTACHMENT_KEY);
HttpSessionImpl session = sc.getCurrentServletContext().getSession(exchange, true);
KeycloakUriBuilder uriBuilder = KeycloakUriBuilder.fromUri(exchange.getRequestURI())
.replaceQuery(exchange.getQueryString());
if (!exchange.isHostIncludedInRequestURI()) uriBuilder.scheme(exchange.getRequestScheme()).host(exchange.getHostAndPort());
String uri = uriBuilder.build().toString();
session.setAttribute(SAML_REDIRECT_URI, uri);
}
@Override
public boolean restoreRequest() {
HttpSession session = getSession(false);
if (session == null) return false;
SavedRequest.tryRestoreRequest(exchange, session);
session.removeAttribute(SAML_REDIRECT_URI);
return false;
}
protected HttpSession getSession(boolean create) {
final ServletRequestContext servletRequestContext = exchange.getAttachment(ServletRequestContext.ATTACHMENT_KEY);
HttpServletRequest req = (HttpServletRequest) servletRequestContext.getServletRequest();
return req.getSession(create);
}
}

View file

@ -0,0 +1,43 @@
package org.keycloak.adapters.saml.undertow;
import io.undertow.security.api.SecurityContext;
import io.undertow.security.idm.Account;
import io.undertow.server.HttpServerExchange;
import org.keycloak.adapters.HttpFacade;
import org.keycloak.adapters.saml.SamlAuthenticator;
import org.keycloak.adapters.saml.SamlDeployment;
import org.keycloak.adapters.saml.SamlSession;
import org.keycloak.adapters.saml.SamlSessionStore;
import java.security.Principal;
import java.util.Set;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class UndertowSamlAuthenticator extends SamlAuthenticator {
protected SecurityContext securityContext;
public UndertowSamlAuthenticator(SecurityContext securityContext, HttpFacade facade, SamlDeployment deployment, SamlSessionStore sessionStore) {
super(facade, deployment, sessionStore);
this.securityContext = securityContext;
}
@Override
protected void completeAuthentication(final SamlSession samlSession) {
Account undertowAccount = new Account() {
@Override
public Principal getPrincipal() {
return samlSession.getPrincipal();
}
@Override
public Set<String> getRoles() {
return samlSession.getRoles();
}
};
securityContext.authenticationComplete(undertowAccount, "KEYCLOAK-SAML", false);
}
}

View file

@ -164,7 +164,7 @@ public class BaseSAML2BindingBuilder<T extends BaseSAML2BindingBuilder> {
public Document getDocument() {
return document;
}
public URI responseUri(String redirectUri, boolean asRequest) throws ConfigurationException, ProcessingException, IOException {
public URI generateURI(String redirectUri, boolean asRequest) throws ConfigurationException, ProcessingException, IOException {
String samlParameterName = GeneralConstants.SAML_RESPONSE_KEY;
if (asRequest) {
@ -173,6 +173,23 @@ public class BaseSAML2BindingBuilder<T extends BaseSAML2BindingBuilder> {
return builder.generateRedirectUri(samlParameterName, redirectUri, document);
}
public URI requestURI(String actionUrl) throws ConfigurationException, ProcessingException, IOException {
return builder.generateRedirectUri(GeneralConstants.SAML_REQUEST_KEY, actionUrl, document);
}
public URI responseURI(String actionUrl) throws ConfigurationException, ProcessingException, IOException {
return builder.generateRedirectUri(GeneralConstants.SAML_RESPONSE_KEY, actionUrl, document);
}
}
public BaseRedirectBindingBuilder redirectBinding(Document document) throws ProcessingException {
return new BaseRedirectBindingBuilder(this, document);
}
public BasePostBindingBuilder postBinding(Document document) throws ProcessingException {
return new BasePostBindingBuilder(this, document);
}

View file

@ -21,15 +21,22 @@
*/
package org.keycloak.saml.processing.core.saml.v2.util;
import org.keycloak.dom.saml.v2.assertion.EncryptedAssertionType;
import org.keycloak.dom.saml.v2.protocol.ResponseType;
import org.keycloak.saml.common.ErrorCodes;
import org.keycloak.saml.common.PicketLinkLogger;
import org.keycloak.saml.common.PicketLinkLoggerFactory;
import org.keycloak.saml.common.constants.JBossSAMLConstants;
import org.keycloak.saml.common.exceptions.ConfigurationException;
import org.keycloak.saml.common.exceptions.ParsingException;
import org.keycloak.saml.common.exceptions.ProcessingException;
import org.keycloak.saml.common.exceptions.fed.IssueInstantMissingException;
import org.keycloak.saml.common.util.DocumentUtil;
import org.keycloak.saml.common.util.StaxParserUtil;
import org.keycloak.saml.common.util.StaxUtil;
import org.keycloak.saml.processing.api.saml.v2.response.SAML2Response;
import org.keycloak.saml.processing.api.saml.v2.sig.SAML2Signature;
import org.keycloak.saml.processing.core.parsers.saml.SAMLParser;
import org.keycloak.saml.processing.core.saml.v2.writers.SAMLAssertionWriter;
import org.keycloak.dom.saml.v1.assertion.SAML11AssertionType;
import org.keycloak.dom.saml.v1.assertion.SAML11AttributeStatementType;
@ -45,13 +52,17 @@ 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.assertion.SubjectType.STSubType;
import org.keycloak.saml.processing.core.util.JAXPValidationUtil;
import org.keycloak.saml.processing.core.util.XMLEncryptionUtil;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import javax.xml.datatype.XMLGregorianCalendar;
import javax.xml.namespace.QName;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.util.ArrayList;
import java.util.List;
@ -510,4 +521,48 @@ public class AssertionUtil {
}
return roles;
}
public static AssertionType getAssertion(ResponseType responseType, PrivateKey privateKey) throws ParsingException, ProcessingException, ConfigurationException {
List<ResponseType.RTChoiceType> assertions = responseType.getAssertions();
if (assertions.isEmpty()) {
throw new ProcessingException("No assertion from response.");
}
ResponseType.RTChoiceType rtChoiceType = assertions.get(0);
EncryptedAssertionType encryptedAssertion = rtChoiceType.getEncryptedAssertion();
if (encryptedAssertion != null) {
decryptAssertion(responseType, privateKey);
}
return responseType.getAssertions().get(0).getAssertion();
}
public static ResponseType decryptAssertion(ResponseType responseType, PrivateKey privateKey) throws ParsingException, ProcessingException, ConfigurationException {
SAML2Response saml2Response = new SAML2Response();
Document doc = saml2Response.convert(responseType);
Element enc = DocumentUtil.getElement(doc, new QName(JBossSAMLConstants.ENCRYPTED_ASSERTION.get()));
if (enc == null) {
throw new ProcessingException("No encrypted assertion found.");
}
String oldID = enc.getAttribute(JBossSAMLConstants.ID.get());
Document newDoc = DocumentUtil.createDocument();
Node importedNode = newDoc.importNode(enc, true);
newDoc.appendChild(importedNode);
Element decryptedDocumentElement = XMLEncryptionUtil.decryptElementInDocument(newDoc, privateKey);
SAMLParser parser = new SAMLParser();
JAXPValidationUtil.checkSchemaValidation(decryptedDocumentElement);
AssertionType assertion = (AssertionType) parser.parse(StaxParserUtil.getXMLEventReader(DocumentUtil
.getNodeAsStream(decryptedDocumentElement)));
responseType.replaceAssertion(oldID, new ResponseType.RTChoiceType(assertion));
return responseType;
}
}

View file

@ -29,8 +29,6 @@ public class JaxrsSAML2BindingBuilder extends BaseSAML2BindingBuilder<JaxrsSAML2
protected Response buildResponse(Document responseDoc, String actionUrl, boolean asRequest) throws ProcessingException, ConfigurationException, IOException {
String str = builder.buildHtmlPostResponse(responseDoc, actionUrl, asRequest);
CacheControl cacheControl = new CacheControl();
cacheControl.setNoCache(true);
return Response.ok(str, MediaType.TEXT_HTML_TYPE)
.header("Pragma", "no-cache")
.header("Cache-Control", "no-cache, no-store").build();
@ -53,7 +51,7 @@ public class JaxrsSAML2BindingBuilder extends BaseSAML2BindingBuilder<JaxrsSAML2
}
private Response response(String redirectUri, boolean asRequest) throws ProcessingException, ConfigurationException, IOException {
URI uri = responseUri(redirectUri, asRequest);
URI uri = generateURI(redirectUri, asRequest);
if (logger.isDebugEnabled()) logger.trace("redirect-binding uri: " + uri.toString());
CacheControl cacheControl = new CacheControl();
cacheControl.setNoCache(true);

View file

@ -14,7 +14,6 @@ import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.UriBuilder;
import javax.ws.rs.core.UriInfo;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.security.Signature;
import java.security.cert.Certificate;
@ -64,23 +63,23 @@ public class SamlProtocolUtils {
return cert.getPublicKey();
}
public static void verifyRedirectSignature(PublicKey publicKey, UriInfo uriInformation) throws VerificationException {
public static void verifyRedirectSignature(PublicKey publicKey, UriInfo uriInformation, String paramKey) throws VerificationException {
MultivaluedMap<String, String> encodedParams = uriInformation.getQueryParameters(false);
String request = encodedParams.getFirst(GeneralConstants.SAML_REQUEST_KEY);
String request = encodedParams.getFirst(paramKey);
String algorithm = encodedParams.getFirst(GeneralConstants.SAML_SIG_ALG_REQUEST_KEY);
String signature = encodedParams.getFirst(GeneralConstants.SAML_SIGNATURE_REQUEST_KEY);
String decodedAlgorithm = uriInformation.getQueryParameters(true).getFirst(GeneralConstants.SAML_SIG_ALG_REQUEST_KEY);
if (request == null) throw new VerificationException("SAMLRequest as null");
if (algorithm == null) throw new VerificationException("SigAlg as null");
if (signature == null) throw new VerificationException("Signature as null");
if (request == null) throw new VerificationException("SAM 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?
UriBuilder builder = UriBuilder.fromPath("/")
.queryParam(GeneralConstants.SAML_REQUEST_KEY, request);
.queryParam(paramKey, request);
if (encodedParams.containsKey(GeneralConstants.RELAY_STATE)) {
builder.queryParam(GeneralConstants.RELAY_STATE, encodedParams.getFirst(GeneralConstants.RELAY_STATE));
}

View file

@ -460,7 +460,7 @@ public class SamlService {
return;
}
PublicKey publicKey = SamlProtocolUtils.getSignatureValidationKey(client);
SamlProtocolUtils.verifyRedirectSignature(publicKey, uriInfo);
SamlProtocolUtils.verifyRedirectSignature(publicKey, uriInfo, GeneralConstants.SAML_REQUEST_KEY);
}