Repository: cxf-fediz Updated Branches: refs/heads/master 60325dc51 -> dc6478468
Added initial CXF plugin Project: http://git-wip-us.apache.org/repos/asf/cxf-fediz/repo Commit: http://git-wip-us.apache.org/repos/asf/cxf-fediz/commit/dc647846 Tree: http://git-wip-us.apache.org/repos/asf/cxf-fediz/tree/dc647846 Diff: http://git-wip-us.apache.org/repos/asf/cxf-fediz/diff/dc647846 Branch: refs/heads/master Commit: dc647846870f96d5484909586681670b9d14fcca Parents: 60325dc Author: Colm O hEigeartaigh <[email protected]> Authored: Thu Jul 31 15:47:19 2014 +0100 Committer: Colm O hEigeartaigh <[email protected]> Committed: Thu Jul 31 15:47:19 2014 +0100 ---------------------------------------------------------------------- .../cxf/fediz/core/samlsso/ResponseState.java | 39 +++ plugins/cxf/pom.xml | 17 + .../plugin/AbstractServiceProviderFilter.java | 318 +++++++++++++++++++ .../cxf/fediz/cxf/plugin/CXFFedizPrincipal.java | 56 ++++ .../fediz/cxf/plugin/FedizSecurityContext.java | 61 ++++ .../cxf/fediz/cxf/plugin/Messages.properties | 33 ++ .../cxf/plugin/SamlRedirectBindingFilter.java | 265 ++++++++++++++++ 7 files changed, 789 insertions(+) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/cxf-fediz/blob/dc647846/plugins/core/src/main/java/org/apache/cxf/fediz/core/samlsso/ResponseState.java ---------------------------------------------------------------------- diff --git a/plugins/core/src/main/java/org/apache/cxf/fediz/core/samlsso/ResponseState.java b/plugins/core/src/main/java/org/apache/cxf/fediz/core/samlsso/ResponseState.java index dfbf9ff..a959f4c 100644 --- a/plugins/core/src/main/java/org/apache/cxf/fediz/core/samlsso/ResponseState.java +++ b/plugins/core/src/main/java/org/apache/cxf/fediz/core/samlsso/ResponseState.java @@ -19,11 +19,14 @@ package org.apache.cxf.fediz.core.samlsso; import java.io.Serializable; +import java.util.List; import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessorType; import javax.xml.bind.annotation.XmlRootElement; +import org.apache.cxf.fediz.core.Claim; + @XmlRootElement @XmlAccessorType(XmlAccessType.FIELD) public class ResponseState implements Serializable { @@ -36,6 +39,10 @@ public class ResponseState implements Serializable { private String webAppDomain; private long createdAt; private long expiresAt; + private List<String> roles; + private String issuer; + private List<Claim> claims; + private String subject; public ResponseState() { @@ -78,4 +85,36 @@ public class ResponseState implements Serializable { public String getAssertion() { return assertion; } + + public List<String> getRoles() { + return roles; + } + + public void setRoles(List<String> roles) { + this.roles = roles; + } + + public List<Claim> getClaims() { + return claims; + } + + public void setClaims(List<Claim> claims) { + this.claims = claims; + } + + public String getIssuer() { + return issuer; + } + + public void setIssuer(String issuer) { + this.issuer = issuer; + } + + public String getSubject() { + return subject; + } + + public void setSubject(String subject) { + this.subject = subject; + } } http://git-wip-us.apache.org/repos/asf/cxf-fediz/blob/dc647846/plugins/cxf/pom.xml ---------------------------------------------------------------------- diff --git a/plugins/cxf/pom.xml b/plugins/cxf/pom.xml index e7a2ca6..6df62e6 100644 --- a/plugins/cxf/pom.xml +++ b/plugins/cxf/pom.xml @@ -64,6 +64,23 @@ </dependency> </dependencies> <build> + <sourceDirectory>${basedir}/src/main/java</sourceDirectory> + <resources> + <resource> + <directory>src/main/java</directory> + <excludes> + <exclude>**/*.java</exclude> + </excludes> + </resource> + <resource> + <directory>src/main/resources</directory> + <includes> + <include>**/*</include> + </includes> + </resource> + </resources> + + <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> http://git-wip-us.apache.org/repos/asf/cxf-fediz/blob/dc647846/plugins/cxf/src/main/java/org/apache/cxf/fediz/cxf/plugin/AbstractServiceProviderFilter.java ---------------------------------------------------------------------- diff --git a/plugins/cxf/src/main/java/org/apache/cxf/fediz/cxf/plugin/AbstractServiceProviderFilter.java b/plugins/cxf/src/main/java/org/apache/cxf/fediz/cxf/plugin/AbstractServiceProviderFilter.java new file mode 100644 index 0000000..8b90f7d --- /dev/null +++ b/plugins/cxf/src/main/java/org/apache/cxf/fediz/cxf/plugin/AbstractServiceProviderFilter.java @@ -0,0 +1,318 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.apache.cxf.fediz.cxf.plugin; + +import java.io.File; +import java.io.StringReader; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Date; +import java.util.Map; +import java.util.ResourceBundle; + +import javax.annotation.PostConstruct; +import javax.annotation.Priority; +import javax.ws.rs.Priorities; +import javax.ws.rs.container.ContainerRequestFilter; +import javax.ws.rs.container.PreMatching; +import javax.ws.rs.core.Cookie; +import javax.ws.rs.core.HttpHeaders; +import javax.xml.bind.JAXBException; + +import org.apache.cxf.common.classloader.ClassLoaderUtils; +import org.apache.cxf.common.i18n.BundleUtils; +import org.apache.cxf.fediz.core.SAMLSSOConstants; +import org.apache.cxf.fediz.core.SecurityTokenThreadLocal; +import org.apache.cxf.fediz.core.config.FedizConfigurator; +import org.apache.cxf.fediz.core.config.FedizContext; +import org.apache.cxf.fediz.core.config.SAMLProtocol; +import org.apache.cxf.fediz.core.samlsso.ResponseState; +import org.apache.cxf.fediz.core.util.DOMUtils; +import org.apache.cxf.jaxrs.impl.HttpHeadersImpl; +import org.apache.cxf.jaxrs.impl.UriInfoImpl; +import org.apache.cxf.jaxrs.utils.HttpUtils; +import org.apache.cxf.message.Message; +import org.apache.cxf.security.SecurityContext; +import org.apache.cxf.staxutils.StaxUtils; +import org.apache.wss4j.common.ext.WSSecurityException; +import org.apache.wss4j.common.saml.SamlAssertionWrapper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@PreMatching +@Priority(Priorities.AUTHENTICATION + 1) +public abstract class AbstractServiceProviderFilter implements ContainerRequestFilter { + + public static final String SECURITY_CONTEXT_TOKEN = + "org.apache.fediz.SECURITY_TOKEN"; + protected static final ResourceBundle BUNDLE = + BundleUtils.getBundle(AbstractServiceProviderFilter.class); + private static final Logger LOG = LoggerFactory.getLogger(AbstractServiceProviderFilter.class); + + private String webAppDomain; + // private boolean addWebAppContext = true; + // private boolean addEndpointAddressToContext; + + private FedizConfigurator configurator; + private String configFile; + + public String getConfigFile() { + return configFile; + } + + public void setConfigFile(String configFile) { + this.configFile = configFile; + } + + @PostConstruct + public synchronized void configure() throws JAXBException, MalformedURLException { + if (configurator == null) { + try { + File f = new File(configFile); + if (!f.exists()) { + URL url = ClassLoaderUtils.getResource(configFile, + AbstractServiceProviderFilter.class); + if (url == null) { + url = new URL(configFile); + } + if (url != null) { + f = new File(url.getPath()); + } + } + configurator = new FedizConfigurator(); + configurator.loadConfig(f); + LOG.debug("Fediz configuration read from " + f.getAbsolutePath()); + } catch (JAXBException e) { + LOG.error("Error in parsing configuration", e); + throw e; + } catch (MalformedURLException e) { + LOG.error("Error in loading configuration file", e); + throw e; + } + } + } + + protected boolean checkSecurityContext(Message m) { + HttpHeaders headers = new HttpHeadersImpl(m); + Map<String, Cookie> cookies = headers.getCookies(); + + Cookie securityContextCookie = cookies.get(SECURITY_CONTEXT_TOKEN); + + ResponseState responseState = getValidResponseState(securityContextCookie, m); + if (responseState == null) { + return false; + } + + Cookie relayStateCookie = cookies.get(SAMLSSOConstants.RELAY_STATE); + if (relayStateCookie == null) { + reportError("MISSING_RELAY_COOKIE"); + return false; + } + String originalRelayState = responseState.getRelayState(); + if (!originalRelayState.equals(relayStateCookie.getValue())) { + // perhaps the response state should also be removed + reportError("INVALID_RELAY_STATE"); + return false; + } + + // Create SecurityContext + try { + SamlAssertionWrapper assertionWrapper = + new SamlAssertionWrapper( + StaxUtils.read(new StringReader(responseState.getAssertion())).getDocumentElement()); + setSecurityContext(responseState, m, assertionWrapper); + } catch (Exception ex) { + reportError("INVALID_RESPONSE_STATE"); + return false; + } + + return true; + } + + protected void setSecurityContext( + ResponseState responseState, Message m, SamlAssertionWrapper assertionWrapper + ) throws WSSecurityException { + CXFFedizPrincipal principal = + new CXFFedizPrincipal(responseState.getSubject(), responseState.getClaims(), + assertionWrapper.toDOM(DOMUtils.createDocument())); + + SecurityTokenThreadLocal.setToken(principal.getLoginToken()); + FedizSecurityContext context = + new FedizSecurityContext(principal, responseState.getRoles()); + m.put(SecurityContext.class, context); + } + + protected ResponseState getValidResponseState(Cookie securityContextCookie, + Message m) { + if (securityContextCookie == null) { + // most likely it means that the user has not been offered + // a chance to get logged on yet, though it might be that the browser + // has removed an expired cookie from its cache; warning is too noisy in the + // former case + reportTrace("MISSING_RESPONSE_STATE"); + return null; + } + String contextKey = securityContextCookie.getValue(); + + FedizContext fedizConfig = getFedizContext(m); + SAMLProtocol protocol = (SAMLProtocol)fedizConfig.getProtocol(); + ResponseState responseState = protocol.getStateManager().getResponseState(contextKey); + + if (responseState == null) { + reportError("MISSING_RESPONSE_STATE"); + return null; + } + if (isStateExpired(responseState.getCreatedAt(), responseState.getExpiresAt(), fedizConfig)) { + reportError("EXPIRED_RESPONSE_STATE"); + protocol.getStateManager().removeResponseState(contextKey); + return null; + } + // TODO String webAppContext = getWebAppContext(m); + if (webAppDomain != null + && (responseState.getWebAppDomain() == null + || !webAppDomain.equals(responseState.getWebAppDomain()))) { + // TODO || responseState.getWebAppContext() == null + // TODO || !webAppContext.equals(responseState.getWebAppContext())) { + protocol.getStateManager().removeResponseState(contextKey); + reportError("INVALID_RESPONSE_STATE"); + return null; + } + if (responseState.getAssertion() == null) { + reportError("INVALID_RESPONSE_STATE"); + return null; + } + return responseState; + } + + protected String createCookie(String name, + String value, + String path, + String domain, + long stateTimeToLive) { + + String contextCookie = name + "=" + value; + // Setting a specific path restricts the browsers + // to return a cookie only to the web applications + // listening on that specific context path + if (path != null) { + contextCookie += ";Path=" + path; + } + + // Setting a specific domain further restricts the browsers + // to return a cookie only to the web applications + // listening on the specific context path within a particular domain + if (domain != null) { + contextCookie += ";Domain=" + domain; + } + + // Keep the cookie across the browser restarts until it actually expires. + // Note that the Expires property has been deprecated but apparently is + // supported better than 'max-age' property by different browsers + // (Firefox, IE, etc) + Date expiresDate = new Date(System.currentTimeMillis() + stateTimeToLive); + String cookieExpires = HttpUtils.getHttpDateFormat().format(expiresDate); + contextCookie += ";Expires=" + cookieExpires; + //TODO: Consider adding an 'HttpOnly' attribute + + return contextCookie; + } + + protected boolean isStateExpired(long stateCreatedAt, long expiresAt, FedizContext fedizConfig) { + Date currentTime = new Date(); + long stateTimeToLive = ((SAMLProtocol)fedizConfig.getProtocol()).getStateTimeToLive(); + if (currentTime.after(new Date(stateCreatedAt + stateTimeToLive))) { + return true; + } + + if (expiresAt > 0 && currentTime.after(new Date(expiresAt))) { + return true; + } + + return false; + } + + protected FedizContext getFedizContext(Message message) { + String contextName = new UriInfoImpl(message).getRequestUri().getPath(); + if (contextName == null || contextName.isEmpty()) { + contextName = "/"; + } + String[] contextPath = contextName.split("/"); + if (contextPath.length > 0) { + contextName = "/" + contextPath[1]; + } + return getContextConfiguration(contextName); + } + + protected FedizContext getContextConfiguration(String contextName) { + if (configurator == null) { + throw new IllegalStateException("No Fediz configuration available"); + } + FedizContext config = configurator.getFedizContext(contextName); + if (config == null) { + throw new IllegalStateException("No Fediz configuration for context :" + contextName); + } + String catalinaBase = System.getProperty("catalina.base"); + if (catalinaBase != null && catalinaBase.length() > 0) { + config.setRelativePath(catalinaBase); + } + return config; + } + + protected void reportError(String code) { + org.apache.cxf.common.i18n.Message errorMsg = + new org.apache.cxf.common.i18n.Message(code, BUNDLE); + LOG.warn(errorMsg.toString()); + } + + protected void reportTrace(String code) { + if (LOG.isDebugEnabled()) { + org.apache.cxf.common.i18n.Message errorMsg = + new org.apache.cxf.common.i18n.Message(code, BUNDLE); + LOG.debug(errorMsg.toString()); + } + } +/* + * TODO + private String getWebAppContext(Message m) { + if (addWebAppContext) { + if (addEndpointAddressToContext) { + return new UriInfoImpl(m).getBaseUri().getRawPath(); + } else { + String httpBasePath = (String)m.get("http.base.path"); + return URI.create(httpBasePath).getRawPath(); + } + } else { + return "/"; + } + } + */ + public String getWebAppDomain() { + return webAppDomain; + } + + public void setWebAppDomain(String webAppDomain) { + this.webAppDomain = webAppDomain; + } +/* + public void setAddWebAppContext(boolean addWebAppContext) { + this.addWebAppContext = addWebAppContext; + } + */ + +} http://git-wip-us.apache.org/repos/asf/cxf-fediz/blob/dc647846/plugins/cxf/src/main/java/org/apache/cxf/fediz/cxf/plugin/CXFFedizPrincipal.java ---------------------------------------------------------------------- diff --git a/plugins/cxf/src/main/java/org/apache/cxf/fediz/cxf/plugin/CXFFedizPrincipal.java b/plugins/cxf/src/main/java/org/apache/cxf/fediz/cxf/plugin/CXFFedizPrincipal.java new file mode 100644 index 0000000..787883f --- /dev/null +++ b/plugins/cxf/src/main/java/org/apache/cxf/fediz/cxf/plugin/CXFFedizPrincipal.java @@ -0,0 +1,56 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.apache.cxf.fediz.cxf.plugin; + +import java.util.List; + +import org.w3c.dom.Element; +import org.apache.cxf.fediz.core.Claim; +import org.apache.cxf.fediz.core.ClaimCollection; +import org.apache.cxf.fediz.core.FedizPrincipal; + +public class CXFFedizPrincipal implements FedizPrincipal { + + private final String subject; + private final List<Claim> claims; + private final Element token; + + public CXFFedizPrincipal(String subject, List<Claim> claims, Element token) { + this.subject = subject; + this.claims = claims; + this.token = token; + } + + @Override + public String getName() { + return subject; + } + + @Override + public ClaimCollection getClaims() { + return new ClaimCollection(claims); + } + + @Override + public Element getLoginToken() { + return token; + } + + +} http://git-wip-us.apache.org/repos/asf/cxf-fediz/blob/dc647846/plugins/cxf/src/main/java/org/apache/cxf/fediz/cxf/plugin/FedizSecurityContext.java ---------------------------------------------------------------------- diff --git a/plugins/cxf/src/main/java/org/apache/cxf/fediz/cxf/plugin/FedizSecurityContext.java b/plugins/cxf/src/main/java/org/apache/cxf/fediz/cxf/plugin/FedizSecurityContext.java new file mode 100644 index 0000000..d20aa71 --- /dev/null +++ b/plugins/cxf/src/main/java/org/apache/cxf/fediz/cxf/plugin/FedizSecurityContext.java @@ -0,0 +1,61 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.apache.cxf.fediz.cxf.plugin; + +import java.security.Principal; +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; + +import org.apache.cxf.common.security.SimplePrincipal; +import org.apache.cxf.security.SecurityContext; + +public class FedizSecurityContext implements SecurityContext { + + private Principal principal; + private Set<Principal> roles; + + public FedizSecurityContext(Principal principal, + Collection<String> roles) { + this.principal = principal; + this.roles = new HashSet<Principal>(); + if (roles != null) { + for (String role : roles) { + this.roles.add(new SimplePrincipal(role)); + } + } + } + + @Override + public Principal getUserPrincipal() { + return principal; + } + + @Override + public boolean isUserInRole(String role) { + for (Principal p : roles) { + if (p.getName().equals(role)) { + return true; + } + } + + return false; + } + +} http://git-wip-us.apache.org/repos/asf/cxf-fediz/blob/dc647846/plugins/cxf/src/main/java/org/apache/cxf/fediz/cxf/plugin/Messages.properties ---------------------------------------------------------------------- diff --git a/plugins/cxf/src/main/java/org/apache/cxf/fediz/cxf/plugin/Messages.properties b/plugins/cxf/src/main/java/org/apache/cxf/fediz/cxf/plugin/Messages.properties new file mode 100644 index 0000000..13c3d92 --- /dev/null +++ b/plugins/cxf/src/main/java/org/apache/cxf/fediz/cxf/plugin/Messages.properties @@ -0,0 +1,33 @@ +# +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. +# +# +MISSING_TARGET_URI=Target URI is missing +INVALID_TARGET_URI=Target URI is invalid +MISSING_REQUEST_STATE=Request State is not available +EXPIRED_REQUEST_STATE=Request State has expired +MISSING_SAML_RESPONSE=SamlResponse parameter is missing +INVALID_SAML_RESPONSE=SamlResponse parameter is invalid +MISSING_ASSERTION_SERVICE_URL=RequestAssertionConsumerService URI is not set +MISSING_RESPONSE_STATE=Response State is not available +INVALID_RESPONSE_STATE=Response State is invalid +EXPIRED_RESPONSE_STATE=Response State has expired +MISSING_RELAY_STATE=RelayState is missing +MISSING_RELAY_COOKIE=RelayState cookie is not available +INVALID_RELAY_STATE=RelayState is invalid http://git-wip-us.apache.org/repos/asf/cxf-fediz/blob/dc647846/plugins/cxf/src/main/java/org/apache/cxf/fediz/cxf/plugin/SamlRedirectBindingFilter.java ---------------------------------------------------------------------- diff --git a/plugins/cxf/src/main/java/org/apache/cxf/fediz/cxf/plugin/SamlRedirectBindingFilter.java b/plugins/cxf/src/main/java/org/apache/cxf/fediz/cxf/plugin/SamlRedirectBindingFilter.java new file mode 100644 index 0000000..a456f21 --- /dev/null +++ b/plugins/cxf/src/main/java/org/apache/cxf/fediz/cxf/plugin/SamlRedirectBindingFilter.java @@ -0,0 +1,265 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.apache.cxf.fediz.cxf.plugin; + +import java.net.URI; +import java.security.cert.X509Certificate; +import java.util.Collections; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.container.ContainerRequestContext; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.ResponseBuilder; + +import org.apache.cxf.fediz.core.FederationConstants; +import org.apache.cxf.fediz.core.SAMLSSOConstants; +import org.apache.cxf.fediz.core.config.FederationProtocol; +import org.apache.cxf.fediz.core.config.FedizContext; +import org.apache.cxf.fediz.core.config.SAMLProtocol; +import org.apache.cxf.fediz.core.exception.ProcessingException; +import org.apache.cxf.fediz.core.processor.FedizProcessor; +import org.apache.cxf.fediz.core.processor.FedizProcessorFactory; +import org.apache.cxf.fediz.core.processor.FedizRequest; +import org.apache.cxf.fediz.core.processor.FedizResponse; +import org.apache.cxf.fediz.core.processor.RedirectionResponse; +import org.apache.cxf.fediz.core.samlsso.ResponseState; +import org.apache.cxf.jaxrs.ext.MessageContext; +import org.apache.cxf.jaxrs.impl.UriInfoImpl; +import org.apache.cxf.jaxrs.utils.ExceptionUtils; +import org.apache.cxf.jaxrs.utils.JAXRSUtils; +import org.apache.cxf.message.Message; +import org.apache.wss4j.common.util.DOM2Writer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class SamlRedirectBindingFilter extends AbstractServiceProviderFilter { + + private static final Logger LOG = LoggerFactory.getLogger(SamlRedirectBindingFilter.class); + + @Context + private MessageContext messageContext; + + public void filter(ContainerRequestContext context) { + Message m = JAXRSUtils.getCurrentMessage(); + if (checkSecurityContext(m)) { + return; + } else { + try { + FedizContext fedConfig = getFedizContext(m); + if (isSignInRequired(context, fedConfig)) { + // Unauthenticated -> redirect + FedizProcessor processor = + FedizProcessorFactory.newFedizProcessor(fedConfig.getProtocol()); + + HttpServletRequest request = messageContext.getHttpServletRequest(); + RedirectionResponse redirectionResponse = + processor.createSignInRequest(request, fedConfig); + String redirectURL = redirectionResponse.getRedirectionURL(); + if (redirectURL != null) { + ResponseBuilder response = Response.seeOther(new URI(redirectURL)); + Map<String, String> headers = redirectionResponse.getHeaders(); + if (!headers.isEmpty()) { + for (String headerName : headers.keySet()) { + response.header(headerName, headers.get(headerName)); + } + } + + context.abortWith(response.build()); + } else { + LOG.warn("Failed to create SignInRequest."); + throw ExceptionUtils.toInternalServerErrorException(null, null); + } + } else if (isSignInRequest(context, fedConfig)) { + String responseToken = getResponseToken(context, fedConfig); + + if (responseToken == null) { + if (LOG.isDebugEnabled()) { + LOG.debug("SignIn request must contain a response token from the IdP"); + } + throw ExceptionUtils.toBadRequestException(null, null); + } else { + // processSignInRequest + if (LOG.isDebugEnabled()) { + LOG.debug("Process SignIn request"); + LOG.debug("token=\n" + responseToken); + } + + FedizRequest wfReq = new FedizRequest(); + MultivaluedMap<String, String> params = context.getUriInfo().getQueryParameters(); + wfReq.setAction(params.getFirst(FederationConstants.PARAM_ACTION)); + wfReq.setResponseToken(responseToken); + wfReq.setState(params.getFirst("RelayState")); + HttpServletRequest request = messageContext.getHttpServletRequest(); + wfReq.setRequest(request); + + X509Certificate certs[] = + (X509Certificate[])request.getAttribute("javax.servlet.request.X509Certificate"); + wfReq.setCerts(certs); + + FedizProcessor wfProc = + FedizProcessorFactory.newFedizProcessor(fedConfig.getProtocol()); + FedizResponse wfRes = null; + try { + wfRes = wfProc.processRequest(wfReq, fedConfig); + } catch (ProcessingException ex) { + LOG.error("Federation processing failed: " + ex.getMessage()); + throw ExceptionUtils.toNotAuthorizedException(ex, null); + } + + // Validate AudienceRestriction + List<String> audienceURIs = fedConfig.getAudienceUris(); + validateAudienceRestrictions(wfRes, audienceURIs, request); + + // Set the security context + String securityContextKey = UUID.randomUUID().toString(); + + SAMLProtocol protocol = (SAMLProtocol)fedConfig.getProtocol(); + + long currentTime = System.currentTimeMillis(); + Date notOnOrAfter = wfRes.getTokenExpires(); + long expiresAt = 0; + if (notOnOrAfter != null) { + expiresAt = notOnOrAfter.getTime(); + } else { + expiresAt = currentTime + protocol.getStateTimeToLive(); + } + + String webAppDomain = protocol.getWebAppDomain(); + String token = DOM2Writer.nodeToString(wfRes.getToken()); + List<String> roles = wfRes.getRoles(); + if (roles == null || roles.size() == 0) { + roles = Collections.singletonList("Authenticated"); + } + + ResponseState responseState = + new ResponseState(token, // TODO + params.getFirst("RelayState"), + null, // TODO + webAppDomain, + currentTime, + expiresAt); + responseState.setClaims(wfRes.getClaims()); + responseState.setRoles(roles); + responseState.setIssuer(wfRes.getIssuer()); + responseState.setSubject(wfRes.getUsername()); + protocol.getStateManager().setResponseState(securityContextKey, responseState); + + long stateTimeToLive = protocol.getStateTimeToLive(); + String contextCookie = createCookie(SECURITY_CONTEXT_TOKEN, + securityContextKey, + null, // TODO + webAppDomain, + stateTimeToLive); + + // Redirect with cookie set + ResponseBuilder response = + Response.seeOther(new UriInfoImpl(m).getAbsolutePath()); + response.header("Set-Cookie", contextCookie); + + context.abortWith(response.build()); + } + + } else { + LOG.error("SignIn parameter is incorrect or not supported"); + throw ExceptionUtils.toBadRequestException(null, null); + } + } catch (Exception ex) { + LOG.debug(ex.getMessage(), ex); + throw ExceptionUtils.toInternalServerErrorException(ex, null); + } + } + } + + private boolean isSignInRequired(ContainerRequestContext context, FedizContext fedConfig) { + + MultivaluedMap<String, String> params = context.getUriInfo().getQueryParameters(); + if (fedConfig.getProtocol() instanceof FederationProtocol + && params.getFirst(FederationConstants.PARAM_ACTION) == null) { + return true; + } else if (fedConfig.getProtocol() instanceof SAMLProtocol + && params.getFirst(SAMLSSOConstants.RELAY_STATE) == null) { + return true; + } + + return false; + } + + private boolean isSignInRequest(ContainerRequestContext context, FedizContext fedConfig) { + + MultivaluedMap<String, String> params = context.getUriInfo().getQueryParameters(); + if (fedConfig.getProtocol() instanceof FederationProtocol + && FederationConstants.ACTION_SIGNIN.equals( + params.getFirst(FederationConstants.PARAM_ACTION))) { + return true; + } else if (fedConfig.getProtocol() instanceof SAMLProtocol + && params.getFirst(SAMLSSOConstants.RELAY_STATE) != null) { + return true; + } + + return false; + } + + private String getResponseToken(ContainerRequestContext context, FedizContext fedConfig) { + + MultivaluedMap<String, String> params = context.getUriInfo().getQueryParameters(); + if (fedConfig.getProtocol() instanceof FederationProtocol) { + return params.getFirst(FederationConstants.PARAM_RESULT); + } else if (fedConfig.getProtocol() instanceof SAMLProtocol) { + return params.getFirst(SAMLSSOConstants.SAML_RESPONSE); + } + + return null; + } + + private void validateAudienceRestrictions( + FedizResponse wfRes, + List<String> audienceURIs, + HttpServletRequest request + ) { + // Validate the AudienceRestriction in Security Token (e.g. SAML) + // against the configured list of audienceURIs + if (wfRes.getAudience() != null) { + boolean validAudience = false; + for (String a : audienceURIs) { + if (wfRes.getAudience().startsWith(a)) { + validAudience = true; + break; + } + } + + if (!validAudience) { + LOG.warn("Token AudienceRestriction [" + wfRes.getAudience() + + "] doesn't match with specified list of URIs."); + throw ExceptionUtils.toForbiddenException(null, null); + } + + if (LOG.isDebugEnabled() && request.getRequestURL().indexOf(wfRes.getAudience()) == -1) { + LOG.debug("Token AudienceRestriction doesn't match with request URL [" + + wfRes.getAudience() + "] [" + + request.getRequestURL() + "]"); + } + } + } +}
