blob: 013e007f0b8affe1b520a685cbeb132ce38c6448 [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.jetty9;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.security.cert.X509Certificate;
import java.time.Instant;
import java.util.Map;
import java.util.Map.Entry;
import javax.servlet.ServletOutputStream;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import javax.xml.bind.JAXBException;
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.FedizConfigurator;
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.metadata.MetadataDocumentHandler;
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.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.http.MimeTypes;
import org.eclipse.jetty.security.ServerAuthException;
import org.eclipse.jetty.security.UserAuthentication;
import org.eclipse.jetty.security.authentication.DeferredAuthentication;
import org.eclipse.jetty.security.authentication.LoginAuthenticator;
import org.eclipse.jetty.security.authentication.SessionAuthentication;
import org.eclipse.jetty.server.Authentication;
import org.eclipse.jetty.server.Authentication.User;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.UserIdentity;
import org.eclipse.jetty.util.MultiMap;
import org.eclipse.jetty.util.URIUtil;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
/**
* Federation Authenticator.
* <p>
* This authenticator implements form authentication will redirect to the Identity Provider
* by sending a WS-Federation SignIn request.
* </p>
* <p>
* The federation authenticator redirects unauthenticated requests to an Identity Provider which use any kind of
* mechanism to authenticate the user.
* FederationAuthentication uses {@link SessionAuthentication} to wrap Authentication results so that they are
* associated with the session.
* </p>
*/
public class FederationAuthenticator extends LoginAuthenticator {
public static final String J_URI = "org.eclipse.jetty.security.form_URI";
public static final String J_POST = "org.eclipse.jetty.security.form_POST";
public static final String J_CONTEXT = "org.eclipse.jetty.security.form_CONTEXT";
private static final Logger LOG = Log.getLogger(FederationAuthenticator.class);
private static final String SECURITY_TOKEN_ATTR = "org.apache.fediz.SECURITY_TOKEN";
private String configFile;
private FedizConfigurator configurator;
private String encoding = "UTF-8";
public FederationAuthenticator() {
}
/**
*
*/
@Override
public void setConfiguration(AuthConfiguration configuration) {
super.setConfiguration(configuration);
// is called after the bean setting -> do initialization here
LOG.debug(configuration.getInitParameterNames().toString());
try {
File f = new File(getConfigFile());
if (!f.exists()) {
String jettyHome = System.getProperty("jetty.home");
if (jettyHome != null && jettyHome.length() > 0) {
f = new File(jettyHome.concat(File.separator + getConfigFile()));
}
}
configurator = new FedizConfigurator();
configurator.loadConfig(f);
LOG.debug("Fediz configuration read from " + f.getAbsolutePath());
} catch (JAXBException | IOException e) {
//[TODO] use other exception
throw new RuntimeException("Failed to load Fediz configuration",
e);
//throw new ServerAuthException("Failed to load Fediz configuration",
// e);
}
}
/* ------------------------------------------------------------ */
public String getAuthMethod() {
return "WSFED";
}
public String getConfigFile() {
return configFile;
}
public void setConfigFile(String configFile) {
this.configFile = configFile;
}
public String getEncoding() {
return encoding;
}
public void setEncoding(String encoding) {
this.encoding = encoding;
}
/* ------------------------------------------------------------ */
public Authentication validateRequest(ServletRequest req, ServletResponse res, boolean mandatory)
throws ServerAuthException {
HttpServletRequest request = (HttpServletRequest)req;
HttpServletResponse response = (HttpServletResponse)res;
HttpSession session = request.getSession(true);
String contextName = request.getSession().getServletContext().getContextPath();
if (contextName == null || contextName.isEmpty()) {
contextName = "/";
}
FedizContext fedConfig = getContextConfiguration(contextName);
// Check to see if it is a metadata request
MetadataDocumentHandler mdHandler = new MetadataDocumentHandler(fedConfig);
if (mdHandler.canHandleRequest(request)) {
Authentication authentication = Authentication.SEND_FAILURE;
if (mdHandler.handleRequest(request, response)) {
authentication = Authentication.SEND_CONTINUE;
}
return authentication;
}
if (!mandatory) {
return new DeferredAuthentication(this);
}
try {
req.setCharacterEncoding(this.encoding);
} catch (UnsupportedEncodingException ex) {
LOG.warn("Unsupported encoding '" + this.encoding + "'", ex);
}
try {
String action = request.getParameter(FederationConstants.PARAM_ACTION);
Authentication authentication = null;
// Handle a request for authentication.
if (isSignInRequest(request, fedConfig)) {
authentication = handleSignInRequest(request, response, session, fedConfig);
} else if (FederationConstants.ACTION_SIGNOUT_CLEANUP.equals(action)) {
authentication = handleSignOutCleanup(response, session);
} else if (!FederationConstants.ACTION_SIGNOUT.equals(action) && action != null) {
LOG.warn("Not supported action found in parameter wa: " + action);
response.sendError(HttpServletResponse.SC_BAD_REQUEST);
authentication = Authentication.UNAUTHENTICATED;
}
if (authentication != null) {
return authentication;
}
// Look for cached authentication
authentication = handleCachedAuthentication(request, response, session, fedConfig);
if (authentication != null) {
return authentication;
}
// if we can't send challenge
if (DeferredAuthentication.isDeferred(response)) {
LOG.debug("auth deferred {}", session.getId());
return Authentication.UNAUTHENTICATED;
}
// remember the current URI
synchronized (session) {
// But only if it is not set already, or we save every uri that leads to a login form redirect
if (session.getAttribute(J_URI) == null) { // || alwaysSaveUri)
StringBuffer buf = request.getRequestURL();
if (request.getQueryString() != null) {
buf.append('?').append(request.getQueryString());
}
session.setAttribute(J_URI, buf.toString());
if (MimeTypes.Type.FORM_ENCODED.asString().equals(req.getContentType())
&& HttpMethod.POST.asString().equals(request.getMethod())) {
Request baseRequest = (Request)req;
//(req instanceof Request)?(Request)req:HttpConnection.getCurrentConnection().getRequest();
// Load the parameters (previously extractParameters)
baseRequest.getParameterMap();
session.setAttribute(J_POST, new MultiMap<String>(baseRequest.getQueryParameters()));
}
}
}
FedizProcessor wfProc =
FedizProcessorFactory.newFedizProcessor(fedConfig.getProtocol());
signInRedirectToIssuer(request, response, wfProc, session);
return Authentication.SEND_CONTINUE;
} catch (IOException e) {
throw new ServerAuthException(e);
}
/*
* catch (ServletException e) { throw new ServerAuthException(e); }
*/
}
private Authentication handleSignInRequest(HttpServletRequest request, HttpServletResponse response,
HttpSession session, FedizContext fedConfig) throws IOException {
FedizResponse wfRes = null;
if (LOG.isDebugEnabled()) {
LOG.debug("SignIn request found");
}
String action = request.getParameter(FederationConstants.PARAM_ACTION);
String responseToken = getResponseToken(request, fedConfig);
if (responseToken == null) {
if (LOG.isDebugEnabled()) {
LOG.debug("SignIn request must contain a response token from the IdP");
}
response.sendError(HttpServletResponse.SC_BAD_REQUEST);
return Authentication.SEND_FAILURE;
} else {
FedizRequest wfReq = new FedizRequest();
wfReq.setAction(action);
wfReq.setResponseToken(responseToken);
wfReq.setState(getState(request));
wfReq.setRequest(request);
wfReq.setRequestState((RequestState) session.getAttribute(J_CONTEXT));
X509Certificate[] certs =
(X509Certificate[])request.getAttribute("javax.servlet.request.X509Certificate");
wfReq.setCerts(certs);
FederationLoginService fedLoginService = (FederationLoginService)this._loginService;
UserIdentity user = fedLoginService.login(null, wfReq, fedConfig);
if (user != null) {
session = renewSession(request, response);
// Redirect to original request
String nuri;
synchronized (session) {
// Check the context
RequestState savedRequestState = (RequestState) session.getAttribute(J_CONTEXT);
String receivedContext = getState(request);
if (savedRequestState == null || !savedRequestState.getState().equals(receivedContext)) {
LOG.warn("The received wctx/RelayState parameter does not match the saved value");
response.sendError(HttpServletResponse.SC_FORBIDDEN);
return Authentication.UNAUTHENTICATED;
}
nuri = (String) session.getAttribute(J_URI);
if (nuri == null || nuri.length() == 0) {
nuri = request.getContextPath();
if (nuri.length() == 0) {
nuri = URIUtil.SLASH;
}
}
Authentication cached = new SessionAuthentication(getAuthMethod(), user, wfRes);
session.setAttribute(SessionAuthentication.__J_AUTHENTICATED, cached);
}
FederationUserIdentity fui = (FederationUserIdentity)user;
session.setAttribute(SECURITY_TOKEN_ATTR, fui.getToken());
response.setContentLength(0);
response.sendRedirect(response.encodeRedirectURL(nuri));
return new FederationAuthentication(getAuthMethod(), user);
}
// not authenticated
if (LOG.isDebugEnabled()) {
LOG.debug("WSFED authentication FAILED");
}
response.sendError(HttpServletResponse.SC_FORBIDDEN);
return Authentication.UNAUTHENTICATED;
}
}
private Authentication handleSignOutCleanup(HttpServletResponse response, HttpSession session) throws IOException {
if (LOG.isDebugEnabled()) {
LOG.debug("SignOutCleanup request found");
LOG.debug("SignOutCleanup action...");
}
session.invalidate();
final ServletOutputStream responseOutputStream = response.getOutputStream();
InputStream inputStream = this.getClass().getClassLoader().getResourceAsStream("logout.jpg");
if (inputStream == null) {
LOG.warn("Could not write logout.jpg");
return Authentication.SEND_FAILURE;
}
int read = 0;
byte[] buf = new byte[1024];
while ((read = inputStream.read(buf)) != -1) {
responseOutputStream.write(buf, 0, read);
}
inputStream.close();
responseOutputStream.flush();
return Authentication.SEND_SUCCESS;
}
private Authentication handleCachedAuthentication(HttpServletRequest request, HttpServletResponse response,
HttpSession session, FedizContext fedConfig) throws IOException {
Authentication authentication =
(Authentication) session.getAttribute(SessionAuthentication.__J_AUTHENTICATED);
if (authentication != null) {
// Has authentication been revoked?
if (authentication instanceof Authentication.User
&& isTokenExpired(fedConfig, ((Authentication.User)authentication).getUserIdentity())) {
session.removeAttribute(SessionAuthentication.__J_AUTHENTICATED);
} else {
//logout
String action = request.getParameter(FederationConstants.PARAM_ACTION);
boolean logout = FederationConstants.ACTION_SIGNOUT.equals(action);
String logoutUrl = fedConfig.getLogoutURL();
String uri = request.getRequestURI();
if (uri == null) {
uri = URIUtil.SLASH;
}
String contextName = request.getSession().getServletContext().getContextPath();
if (contextName == null || contextName.isEmpty()) {
contextName = "/";
}
if (logout || logoutUrl != null && !logoutUrl.isEmpty() && uri.equals(contextName + logoutUrl)) {
session.invalidate();
FedizProcessor wfProc =
FedizProcessorFactory.newFedizProcessor(fedConfig.getProtocol());
signOutRedirectToIssuer(request, response, wfProc);
return Authentication.SEND_CONTINUE;
}
String jUri = (String)session.getAttribute(J_URI);
@SuppressWarnings("unchecked")
MultiMap<String> jPost = (MultiMap<String>)session.getAttribute(J_POST);
if (jUri != null && jPost != null) {
StringBuffer buf = request.getRequestURL();
if (request.getQueryString() != null) {
buf.append('?').append(request.getQueryString());
}
if (jUri.equals(buf.toString())) {
// This is a retry of an original POST request
// so restore method and parameters
session.removeAttribute(J_POST);
Request baseRequest = (Request)request;
// (req instanceof Request)?(Request)
// req:HttpConnection.getCurrentConnection().getRequest();
baseRequest.setMethod(HttpMethod.POST.asString());
baseRequest.setQueryParameters(jPost);
}
} else if (jUri != null) {
session.removeAttribute(J_URI);
}
return authentication;
}
}
return null;
}
private boolean isTokenExpired(FedizContext fedConfig, UserIdentity userIdentity) {
if (fedConfig.isDetectExpiredTokens()) {
try {
FederationUserIdentity fui = (FederationUserIdentity)userIdentity;
Instant tokenExpires = fui.getExpiryDate();
if (tokenExpires == null) {
LOG.debug("Token doesn't expire");
return false;
}
Instant currentTime = Instant.now();
if (!currentTime.isAfter(tokenExpires)) {
return false;
} else {
LOG.warn("Token already expired. Clean up and redirect");
return true;
}
} catch (ClassCastException ex) {
LOG.warn("UserIdentity must be instance of FederationUserIdentity");
throw new IllegalStateException("UserIdentity must be instance of FederationUserIdentity");
}
}
return false;
}
private boolean isSignInRequest(ServletRequest request, FedizContext fedConfig) {
if (fedConfig.getProtocol() instanceof FederationProtocol
&& FederationConstants.ACTION_SIGNIN.equals(
request.getParameter(FederationConstants.PARAM_ACTION))) {
return true;
} else if (fedConfig.getProtocol() instanceof SAMLProtocol
&& request.getParameter(SAMLSSOConstants.SAML_RESPONSE) != null) {
return true;
}
return false;
}
private String getResponseToken(ServletRequest request, FedizContext fedConfig) {
if (fedConfig.getProtocol() instanceof FederationProtocol) {
return request.getParameter(FederationConstants.PARAM_RESULT);
} else if (fedConfig.getProtocol() instanceof SAMLProtocol) {
return request.getParameter(SAMLSSOConstants.SAML_RESPONSE);
}
return null;
}
private String getState(ServletRequest request) {
if (request.getParameter(FederationConstants.PARAM_CONTEXT) != null) {
return request.getParameter(FederationConstants.PARAM_CONTEXT);
} else if (request.getParameter(SAMLSSOConstants.RELAY_STATE) != null) {
return request.getParameter(SAMLSSOConstants.RELAY_STATE);
}
return null;
}
/* ------------------------------------------------------------ */
public boolean secureResponse(ServletRequest req, ServletResponse res, boolean mandatory,
User validatedUser) throws ServerAuthException {
return true;
}
/**
* Called to redirect sign-in to the IDP/Issuer
*
* @param request
* Request we are processing
* @param response
* Response we are populating
* @param processor
* FederationProcessor
* @param session The HTTPSession
* @throws IOException
* If the forward to the login page fails and the call to
* {@link HttpServletResponse#sendError(int, String)} throws an
* {@link IOException}
*/
protected void signInRedirectToIssuer(HttpServletRequest request, HttpServletResponse response,
FedizProcessor processor, HttpSession session)
throws IOException {
//Not supported in jetty 7.6
//String contextName = request.getServletContext().getContextPath();
String contextName = request.getSession().getServletContext().getContextPath();
if (contextName == null || contextName.isEmpty()) {
contextName = "/";
}
FedizContext fedCtx = this.configurator.getFedizContext(contextName);
try {
RedirectionResponse redirectionResponse = processor.createSignInRequest(request, fedCtx);
String redirectURL = redirectionResponse.getRedirectionURL();
if (redirectURL != null) {
Map<String, String> headers = redirectionResponse.getHeaders();
if (!headers.isEmpty()) {
for (Entry<String, String> entry : headers.entrySet()) {
response.addHeader(entry.getKey(), entry.getValue());
}
}
synchronized (session) {
session.setAttribute(J_CONTEXT, redirectionResponse.getRequestState());
}
response.sendRedirect(redirectURL);
} else {
LOG.warn("Failed to create SignInRequest.");
response.sendError(
HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Failed to create SignInRequest.");
}
} catch (ProcessingException ex) {
LOG.warn("Failed to create SignInRequest: " + ex.getMessage());
response.sendError(
HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Failed to create SignInRequest.");
}
}
protected void signOutRedirectToIssuer(HttpServletRequest request, HttpServletResponse response,
FedizProcessor processor)
throws IOException {
//Not supported in jetty 7.6
//String contextName = request.getServletContext().getContextPath();
String contextName = request.getSession().getServletContext().getContextPath();
if (contextName == null || contextName.isEmpty()) {
contextName = "/";
}
FedizContext fedCtx = this.configurator.getFedizContext(contextName);
try {
RedirectionResponse redirectionResponse =
processor.createSignOutRequest(request, null, fedCtx); //TODO
String redirectURL = redirectionResponse.getRedirectionURL();
if (redirectURL != null) {
Map<String, String> headers = redirectionResponse.getHeaders();
if (!headers.isEmpty()) {
for (Entry<String, String> entry : headers.entrySet()) {
response.addHeader(entry.getKey(), entry.getValue());
}
}
response.sendRedirect(redirectURL);
} else {
LOG.warn("Failed to create SignOutRequest.");
response.sendError(
HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Failed to create SignOutRequest.");
}
} catch (ProcessingException ex) {
LOG.warn("Failed to create SignOutRequest: " + ex.getMessage());
response.sendError(
HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Failed to create SignOutRequest.");
}
}
private 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 jettyHome = System.getProperty("jetty.home");
if (jettyHome != null && jettyHome.length() > 0) {
config.setRelativePath(jettyHome);
}
return config;
}
/* ------------------------------------------------------------ */
/**
* This Authentication represents a just completed Federation authentication. Subsequent requests from the same
* user are authenticated by the presents of a {@link SessionAuthentication} instance in their session.
*/
public static class FederationAuthentication extends UserAuthentication implements
Authentication.ResponseSent {
public FederationAuthentication(String method, UserIdentity userIdentity) {
super(method, userIdentity);
}
@Override
public String toString() {
return "WSFED" + super.toString();
}
}
}