saml sp
This commit is contained in:
parent
be0c359160
commit
cb8ca619ae
54 changed files with 1495 additions and 140 deletions
|
@ -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);
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
}
|
|
@ -7,5 +7,6 @@ package org.keycloak.adapters;
|
|||
public enum AuthOutcome {
|
||||
NOT_ATTEMPTED,
|
||||
FAILED,
|
||||
AUTHENTICATED
|
||||
AUTHENTICATED,
|
||||
LOGGED_OUT
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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);
|
|
@ -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
|
||||
|
|
|
@ -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);
|
|
@ -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
|
||||
|
|
|
@ -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);
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
5
integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/facade/WrappedHttpServletRequest.java
Normal file → Executable file
5
integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/facade/WrappedHttpServletRequest.java
Normal file → Executable 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();
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
|
5
pom.xml
5
pom.xml
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -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();
|
||||
}
|
|
@ -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);
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -15,5 +15,6 @@
|
|||
|
||||
<modules>
|
||||
<module>core</module>
|
||||
<module>undertow</module>
|
||||
</modules>
|
||||
</project>
|
||||
|
|
87
saml/client-adapter/undertow/pom.xml
Executable file
87
saml/client-adapter/undertow/pom.xml
Executable 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>
|
|
@ -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);
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
||||
|
|
Loading…
Reference in a new issue