blob: 7bb8ab7255d716b48c095ba2391a8e7cdb0c519b [file] [log] [blame]
/**
* 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.IOException;
import java.io.InputStream;
import java.net.URI;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.ws.rs.HttpMethod;
import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.container.ContainerResponseContext;
import javax.ws.rs.container.ContainerResponseFilter;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.Cookie;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.ResponseBuilder;
import org.w3c.dom.Document;
import org.apache.cxf.fediz.core.FederationConstants;
import org.apache.cxf.fediz.core.RequestState;
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.util.CookieUtils;
import org.apache.cxf.fediz.cxf.plugin.state.ResponseState;
import org.apache.cxf.helpers.IOUtils;
import org.apache.cxf.jaxrs.ext.MessageContext;
import org.apache.cxf.jaxrs.impl.HttpHeadersImpl;
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 FedizRedirectBindingFilter extends AbstractServiceProviderFilter
implements ContainerResponseFilter {
private static final Logger LOG = LoggerFactory.getLogger(FedizRedirectBindingFilter.class);
@Context
private MessageContext messageContext;
private boolean redirectOnInitialSignIn;
public void filter(ContainerRequestContext context) {
Message m = JAXRSUtils.getCurrentMessage();
FedizContext fedConfig = getFedizContext(m);
// See if it is a Metadata request
if (isMetadataRequest(context, fedConfig)) {
return;
}
// See if it is a Logout request
if (isLogoutRequest(context, m, fedConfig)) {
return;
}
String httpMethod = context.getMethod();
MultivaluedMap<String, String> params = null;
try {
if (HttpMethod.GET.equals(httpMethod)) {
params = context.getUriInfo().getQueryParameters();
} else if (HttpMethod.POST.equals(httpMethod)) {
String strForm = IOUtils.toString(context.getEntityStream());
params = JAXRSUtils.getStructuredParams(strForm, "&", true, false);
}
} catch (Exception ex) {
LOG.debug(ex.getMessage(), ex);
throw ExceptionUtils.toInternalServerErrorException(ex, null);
}
if (isSignoutCleanupRequest(fedConfig, m, params)) {
return;
} else if (checkSecurityContext(fedConfig, m)) {
return;
} else {
if (isSignInRequired(fedConfig, params)) {
processSignInRequired(context, fedConfig);
} else if (isSignInRequest(fedConfig, params)) {
processSignInRequest(context, fedConfig, params, m);
} else {
LOG.error("SignIn parameter is incorrect or not supported");
throw ExceptionUtils.toBadRequestException(null, null);
}
}
}
private void processSignInRequest(ContainerRequestContext context, FedizContext fedConfig,
MultivaluedMap<String, String> params, Message m) {
String responseToken = getResponseToken(fedConfig, params);
String state = getState(fedConfig, params);
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);
}
FedizResponse wfRes =
validateSignInRequest(fedConfig, params, responseToken, state);
// Validate AudienceRestriction
List<String> audienceURIs = fedConfig.getAudienceUris();
HttpServletRequest request = messageContext.getHttpServletRequest();
validateAudienceRestrictions(wfRes, audienceURIs, request);
// Set the security context
String securityContextKey = UUID.randomUUID().toString();
long currentTime = System.currentTimeMillis();
Date notOnOrAfter = wfRes.getTokenExpires();
long expiresAt = 0;
if (notOnOrAfter != null) {
expiresAt = notOnOrAfter.getTime();
} else {
expiresAt = currentTime + getStateTimeToLive();
}
String webAppDomain = getWebAppDomain();
String token = DOM2Writer.nodeToString(wfRes.getToken());
// Add "Authenticated" role
List<String> roles = wfRes.getRoles();
if (roles == null || roles.size() == 0) {
roles = Collections.singletonList("Authenticated");
} else if (fedConfig.isAddAuthenticatedRole()) {
roles = new ArrayList<>(roles);
roles.add("Authenticated");
}
String webAppContext = getWebAppContext(m);
ResponseState responseState =
new ResponseState(token,
state,
webAppContext,
webAppDomain,
currentTime,
expiresAt);
responseState.setClaims(wfRes.getClaims());
responseState.setRoles(roles);
responseState.setIssuer(wfRes.getIssuer());
responseState.setSubject(wfRes.getUsername());
getStateManager().setResponseState(securityContextKey, responseState);
long stateTimeToLive = getStateTimeToLive();
String contextCookie = CookieUtils.createCookie(SECURITY_CONTEXT_TOKEN,
securityContextKey,
webAppContext,
webAppDomain,
stateTimeToLive);
// Redirect with cookie set
if (isRedirectOnInitialSignIn()) {
ResponseBuilder response =
Response.seeOther(new UriInfoImpl(m).getAbsolutePath());
response.header(HttpHeaders.SET_COOKIE, contextCookie);
context.abortWith(response.build());
} else {
try {
setSecurityContext(responseState, m, wfRes.getToken());
context.setProperty(SECURITY_CONTEXT_TOKEN, contextCookie);
} catch (Exception ex) {
reportError("INVALID_RESPONSE_STATE");
}
}
}
}
private void processSignInRequired(ContainerRequestContext context, FedizContext fedConfig) {
// Unauthenticated -> redirect
FedizProcessor processor =
FedizProcessorFactory.newFedizProcessor(fedConfig.getProtocol());
HttpServletRequest request = messageContext.getHttpServletRequest();
try {
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));
}
}
// Save the RequestState
RequestState requestState = redirectionResponse.getRequestState();
if (requestState != null && requestState.getState() != null) {
getStateManager().setRequestState(requestState.getState(), requestState);
String contextCookie =
CookieUtils.createCookie(SECURITY_CONTEXT_STATE,
requestState.getState(),
request.getRequestURI(),
getWebAppDomain(),
getStateTimeToLive());
response.header(HttpHeaders.SET_COOKIE, contextCookie);
}
context.abortWith(response.build());
} else {
LOG.warn("Failed to create SignInRequest.");
throw ExceptionUtils.toInternalServerErrorException(null, null);
}
} catch (Exception ex) {
LOG.debug(ex.getMessage(), ex);
throw ExceptionUtils.toInternalServerErrorException(ex, null);
}
}
private boolean isMetadataRequest(ContainerRequestContext context, FedizContext fedConfig) {
String requestPath = context.getUriInfo().getPath();
// See if it is a Metadata request
if (requestPath.indexOf(FederationConstants.METADATA_PATH_URI) != -1
|| requestPath.indexOf(getMetadataURI(fedConfig)) != -1) {
if (LOG.isInfoEnabled()) {
LOG.info("Metadata document requested");
}
FedizProcessor wfProc =
FedizProcessorFactory.newFedizProcessor(fedConfig.getProtocol());
try {
HttpServletRequest request = messageContext.getHttpServletRequest();
Document metadata = wfProc.getMetaData(request, fedConfig);
String metadataStr = DOM2Writer.nodeToString(metadata);
ResponseBuilder response = Response.ok(metadataStr, "text/xml");
context.abortWith(response.build());
return true;
} catch (Exception ex) {
LOG.error("Failed to get metadata document: " + ex.getMessage());
throw ExceptionUtils.toInternalServerErrorException(ex, null);
}
}
return false;
}
private boolean isLogoutRequest(ContainerRequestContext context, Message message,
FedizContext fedConfig) {
//logout
String logoutUrl = fedConfig.getLogoutURL();
if (logoutUrl != null && !logoutUrl.isEmpty()) {
String requestPath = "/" + context.getUriInfo().getPath();
if (requestPath.equals(logoutUrl) || requestPath.equals(logoutUrl + "/")) {
cleanupContext(message);
try {
FedizProcessor processor =
FedizProcessorFactory.newFedizProcessor(fedConfig.getProtocol());
HttpServletRequest request = messageContext.getHttpServletRequest();
RedirectionResponse redirectionResponse =
processor.createSignOutRequest(request, null, fedConfig); //TODO
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());
return true;
}
} catch (Exception ex) {
LOG.debug(ex.getMessage(), ex);
throw ExceptionUtils.toInternalServerErrorException(ex, null);
}
}
}
return false;
}
private void cleanupContext(Message message) {
HttpHeaders headers = new HttpHeadersImpl(message);
Map<String, Cookie> cookies = headers.getCookies();
if (cookies.containsKey(SECURITY_CONTEXT_TOKEN)) {
String contextKey = cookies.get(SECURITY_CONTEXT_TOKEN).getValue();
getStateManager().removeResponseState(contextKey);
}
if (cookies.containsKey(SECURITY_CONTEXT_STATE)) {
String contextKey = cookies.get(SECURITY_CONTEXT_STATE).getValue();
getStateManager().removeRequestState(contextKey);
}
}
private String getMetadataURI(FedizContext fedConfig) {
if (fedConfig.getProtocol().getMetadataURI() != null) {
return fedConfig.getProtocol().getMetadataURI();
} else if (fedConfig.getProtocol() instanceof FederationProtocol) {
return FederationConstants.METADATA_PATH_URI;
} else if (fedConfig.getProtocol() instanceof SAMLProtocol) {
return SAMLSSOConstants.FEDIZ_SAML_METADATA_PATH_URI;
}
return FederationConstants.METADATA_PATH_URI;
}
private boolean isSignInRequired(FedizContext fedConfig, MultivaluedMap<String, String> params) {
if (params != null && fedConfig.getProtocol() instanceof FederationProtocol
&& params.getFirst(FederationConstants.PARAM_ACTION) == null) {
return true;
} else if (params != null && fedConfig.getProtocol() instanceof SAMLProtocol
&& params.getFirst(SAMLSSOConstants.RELAY_STATE) == null) {
return true;
}
return false;
}
private boolean isSignInRequest(FedizContext fedConfig, MultivaluedMap<String, String> params) {
if (params != null && fedConfig.getProtocol() instanceof FederationProtocol
&& FederationConstants.ACTION_SIGNIN.equals(
params.getFirst(FederationConstants.PARAM_ACTION))) {
return true;
} else if (params != null && fedConfig.getProtocol() instanceof SAMLProtocol
&& params.getFirst(SAMLSSOConstants.RELAY_STATE) != null) {
return true;
}
return false;
}
private boolean isSignoutCleanupRequest(FedizContext fedConfig, Message m, MultivaluedMap<String, String> params) {
boolean signoutCleanup = false;
if (params != null && fedConfig.getProtocol() instanceof FederationProtocol
&& FederationConstants.ACTION_SIGNOUT_CLEANUP.equals(
params.getFirst(FederationConstants.PARAM_ACTION))) {
signoutCleanup = true;
} /* TODO else if (params != null && fedConfig.getProtocol() instanceof SAMLProtocol
&& params.getFirst(SAMLSSOConstants.RELAY_STATE) != null) {
signoutCleanup = true;
}*/
if (signoutCleanup) {
if (LOG.isDebugEnabled()) {
LOG.debug("SignOutCleanup request found");
LOG.debug("SignOutCleanup action...");
}
cleanupContext(m);
HttpServletResponse response = messageContext.getHttpServletResponse();
try {
final ServletOutputStream responseOutputStream = response.getOutputStream();
InputStream inputStream = this.getClass().getClassLoader().getResourceAsStream("logout.jpg");
if (inputStream == null) {
LOG.warn("Could not write logout.jpg");
return true;
}
int read = 0;
byte[] buf = new byte[1024];
while ((read = inputStream.read(buf)) != -1) {
responseOutputStream.write(buf, 0, read);
}
inputStream.close();
responseOutputStream.flush();
} catch (Exception ex) {
LOG.debug(ex.getMessage(), ex);
throw ExceptionUtils.toInternalServerErrorException(ex, null);
}
return true;
}
return false;
}
private String getResponseToken(FedizContext fedConfig, MultivaluedMap<String, String> params) {
if (params != null && fedConfig.getProtocol() instanceof FederationProtocol) {
return params.getFirst(FederationConstants.PARAM_RESULT);
} else if (params != null && fedConfig.getProtocol() instanceof SAMLProtocol) {
return params.getFirst(SAMLSSOConstants.SAML_RESPONSE);
}
return null;
}
private String getState(FedizContext fedConfig, MultivaluedMap<String, String> params) {
if (params != null && fedConfig.getProtocol() instanceof FederationProtocol) {
return params.getFirst(FederationConstants.PARAM_CONTEXT);
} else if (params != null && fedConfig.getProtocol() instanceof SAMLProtocol) {
return params.getFirst(SAMLSSOConstants.RELAY_STATE);
}
return null;
}
private FedizResponse validateSignInRequest(
FedizContext fedConfig,
MultivaluedMap<String, String> params,
String responseToken,
String state
) {
FedizRequest wfReq = new FedizRequest();
wfReq.setAction(params.getFirst(FederationConstants.PARAM_ACTION));
wfReq.setResponseToken(responseToken);
if (state == null || state.getBytes().length <= 0) {
LOG.error("Invalid RelayState/WCTX");
throw ExceptionUtils.toBadRequestException(null, null);
}
wfReq.setState(state);
wfReq.setRequestState(getStateManager().removeRequestState(state));
if (wfReq.getRequestState() == null) {
LOG.error("Missing Request State");
throw ExceptionUtils.toBadRequestException(null, null);
}
if (CookieUtils.isStateExpired(wfReq.getRequestState().getCreatedAt(), false, 0,
getStateTimeToLive())) {
LOG.error("EXPIRED_REQUEST_STATE");
throw ExceptionUtils.toBadRequestException(null, null);
}
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());
try {
return wfProc.processRequest(wfReq, fedConfig);
} catch (ProcessingException ex) {
LOG.error("Federation processing failed: " + ex.getMessage());
throw ExceptionUtils.toNotAuthorizedException(ex, 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() + "]");
}
}
}
public boolean isRedirectOnInitialSignIn() {
return redirectOnInitialSignIn;
}
public void setRedirectOnInitialSignIn(boolean redirectOnInitialSignIn) {
this.redirectOnInitialSignIn = redirectOnInitialSignIn;
}
@Override
public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext)
throws IOException {
String tokenContext = (String)requestContext.getProperty(SECURITY_CONTEXT_TOKEN);
if (tokenContext != null) {
responseContext.getHeaders().add(HttpHeaders.SET_COOKIE, tokenContext);
}
}
}