blob: 411dc67f8dd210bda5070daabee9fc580b138261 [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.knox.gateway.provider.federation.jwt.filter;
import org.apache.commons.lang3.StringUtils;
import org.apache.knox.gateway.config.GatewayConfig;
import org.apache.knox.gateway.i18n.messages.MessagesFactory;
import org.apache.knox.gateway.provider.federation.jwt.JWTMessages;
import org.apache.knox.gateway.security.PrimaryPrincipal;
import org.apache.knox.gateway.services.security.token.impl.JWT;
import org.apache.knox.gateway.services.security.token.impl.JWTToken;
import org.apache.knox.gateway.util.CertificateUtils;
import org.apache.knox.gateway.util.CookieUtils;
import org.eclipse.jetty.http.MimeTypes;
import javax.security.auth.Subject;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.text.ParseException;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.StringTokenizer;
public class SSOCookieFederationFilter extends AbstractJWTFilter {
private static final JWTMessages LOGGER = MessagesFactory.get( JWTMessages.class );
public static final String XHR_HEADER = "X-Requested-With";
public static final String XHR_VALUE = "XMLHttpRequest";
private static final String GATEWAY_PATH = "gateway.path";
public static final String SSO_COOKIE_NAME = "sso.cookie.name";
public static final String SSO_EXPECTED_AUDIENCES = "sso.expected.audiences";
public static final String SSO_AUTHENTICATION_PROVIDER_URL = "sso.authentication.provider.url";
public static final String SSO_VERIFICATION_PEM = "sso.token.verification.pem";
public static final String X_FORWARDED_HOST = "X-Forwarded-Host";
public static final String X_FORWARDED_PORT = "X-Forwarded-Port";
public static final String X_FORWARDED_PROTO = "X-Forwarded-Proto";
private static final String ORIGINAL_URL_QUERY_PARAM = "originalUrl=";
private static final String DEFAULT_SSO_COOKIE_NAME = "hadoop-jwt";
/* A semicolon separated list of paths that need to bypass authentication */
private static final String SSO_UNAUTHENTICATED_PATHS_PARAM = "gateway.knox.sso.unauthenticated.path.list";
private static final String DEFAULT_SSO_UNAUTHENTICATED_PATHS_PARAM = "favicon.ico";
private String cookieName;
private String authenticationProviderUrl;
private String gatewayPath;
private Set<String> unAuthenticatedPaths = new HashSet(20);
@Override
public void init( FilterConfig filterConfig ) throws ServletException {
super.init(filterConfig);
// configured cookieName
cookieName = filterConfig.getInitParameter(SSO_COOKIE_NAME);
if (cookieName == null) {
cookieName = DEFAULT_SSO_COOKIE_NAME;
}
// expected audiences or null
String expectedAudiences = filterConfig.getInitParameter(SSO_EXPECTED_AUDIENCES);
if (expectedAudiences != null) {
audiences = parseExpectedAudiences(expectedAudiences);
}
// url to SSO authentication provider
authenticationProviderUrl = filterConfig.getInitParameter(SSO_AUTHENTICATION_PROVIDER_URL);
if (authenticationProviderUrl == null) {
LOGGER.missingAuthenticationProviderUrlConfiguration();
}
// token verification pem
String verificationPEM = filterConfig.getInitParameter(SSO_VERIFICATION_PEM);
// setup the public key of the token issuer for verification
if (verificationPEM != null) {
publicKey = CertificateUtils.parseRSAPublicKey(verificationPEM);
}
/* get unauthenticated paths list */
String unAuthPathString = filterConfig.getInitParameter(SSO_UNAUTHENTICATED_PATHS_PARAM);
/* if no list specified use default value */
if (StringUtils.isBlank(unAuthPathString)) {
unAuthPathString = DEFAULT_SSO_UNAUTHENTICATED_PATHS_PARAM;
}
final StringTokenizer st = new StringTokenizer(unAuthPathString, ";,");
while (st.hasMoreTokens()) {
unAuthenticatedPaths.add(st.nextToken());
}
// gateway path for deriving an idp url when missing
setGatewayPath(filterConfig);
configureExpectedParameters(filterConfig);
}
private void setGatewayPath(FilterConfig filterConfig) {
gatewayPath = filterConfig.getInitParameter(GATEWAY_PATH);
if (gatewayPath == null || gatewayPath.isEmpty()) {
final GatewayConfig gatewayConfig = filterConfig.getServletContext() == null ? null
: (GatewayConfig) filterConfig.getServletContext().getAttribute(GatewayConfig.GATEWAY_CONFIG_ATTRIBUTE);
if (gatewayConfig != null) {
gatewayPath = gatewayConfig.getGatewayPath();
}
}
}
@Override
public void destroy() {
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse res = (HttpServletResponse) response;
List<Cookie> ssoCookies = CookieUtils.getCookiesForName(req, cookieName);
if (ssoCookies.isEmpty()) {
/* check for unauthenticated paths to bypass */
for (final String path : unAuthenticatedPaths) {
if (req.getRequestURI().contains(path)) {
/* This path is configured as an unauthenticated path let the request through */
final Subject sub = new Subject();
sub.getPrincipals().add(new PrimaryPrincipal("anonymous"));
LOGGER.unauthenticatedPathBypass(path, req.getRequestURI());
continueWithEstablishedSecurityContext(sub, req, res, chain);
}
}
if ("OPTIONS".equals(req.getMethod())) {
// CORS preflight requests to determine allowed origins and related config
// must be able to continue without being redirected
Subject sub = new Subject();
sub.getPrincipals().add(new PrimaryPrincipal("anonymous"));
continueWithEstablishedSecurityContext(sub, req, res, chain);
} else {
sendRedirectToLoginURL(req, res);
}
} else {
for(Cookie ssoCookie : ssoCookies) {
String wireToken = ssoCookie.getValue();
try {
JWT token = new JWTToken(wireToken);
if (validateToken(req, res, chain, token)) {
Subject subject = createSubjectFromToken(token);
continueWithEstablishedSecurityContext(subject, req, res, chain);
// we found a valid cookie we don't need to keep checking anymore
return;
}
} catch (ParseException ignore) {
// Ignore the error since cookie was invalid
// Fall through to keep checking if there are more cookies
}
}
// There were no valid cookies found so redirect to login url
if(res != null && !res.isCommitted()) {
sendRedirectToLoginURL(req, res);
}
}
}
private void sendRedirectToLoginURL(HttpServletRequest request, HttpServletResponse response)
throws IOException {
String loginURL = constructLoginURL(request);
LOGGER.sendRedirectToLoginURL(loginURL);
response.sendRedirect(loginURL);
}
@Override
protected void handleValidationError(HttpServletRequest request, HttpServletResponse response,
int status, String error) throws IOException {
/* We don't need redirect if this is a XHR request */
if (request.getHeader(XHR_HEADER) != null &&
request.getHeader(XHR_HEADER).equalsIgnoreCase(XHR_VALUE)) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType(MimeTypes.Type.TEXT_PLAIN.toString());
if(error != null && !error.isEmpty()) {
final byte[] data = error.getBytes(StandardCharsets.UTF_8);
response.setContentLength(data.length);
response.getOutputStream().write(data);
}
} else {
String loginURL = constructLoginURL(request);
response.sendRedirect(loginURL);
}
}
/**
* Create the URL to be used for authentication of the user in the absence of
* a JWT token within the incoming request.
*
* @param request for getting the original request URL
* @return url to use as login url for redirect
*/
protected String constructLoginURL(HttpServletRequest request) {
String delimiter = "?";
if (authenticationProviderUrl == null) {
authenticationProviderUrl = deriveDefaultAuthenticationProviderUrl(request);
}
if (authenticationProviderUrl.contains("?")) {
delimiter = "&";
}
return authenticationProviderUrl + delimiter
+ ORIGINAL_URL_QUERY_PARAM
+ request.getRequestURL().append(getOriginalQueryString(request));
}
/**
* Derive a provider URL from the request assuming that the
* KnoxSSO endpoint is local to the endpoint serving this request.
* @param request origin request
* @return url that is based on KnoxSSO endpoint
*/
public String deriveDefaultAuthenticationProviderUrl(HttpServletRequest request) {
String scheme;
String host;
int port;
if (!beingProxied(request)) {
scheme = request.getScheme();
host = request.getServerName();
port = request.getServerPort();
}
else {
scheme = request.getHeader(X_FORWARDED_PROTO);
host = request.getHeader(X_FORWARDED_HOST);
port = Integer.parseInt(request.getHeader(X_FORWARDED_PORT));
}
StringBuilder sb = new StringBuilder(scheme);
sb.append("://").append(host);
if (!host.contains(":")) {
sb.append(':').append(port);
}
sb.append('/').append(gatewayPath).append("/knoxsso/api/v1/websso");
return sb.toString();
}
private boolean beingProxied(HttpServletRequest request) {
return (request.getHeader(X_FORWARDED_HOST) != null);
}
private String getOriginalQueryString(HttpServletRequest request) {
String originalQueryString = request.getQueryString();
return (originalQueryString == null) ? "" : "?" + originalQueryString;
}
}