/** * 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.hadoop.gateway.provider.federation.jwt.filter; import java.io.IOException; import java.security.Principal; import java.security.PrivilegedActionException; import java.security.PrivilegedExceptionAction; import java.util.Date; import java.util.HashSet; import java.util.Set; import javax.security.auth.Subject; import javax.servlet.Filter; import javax.servlet.FilterChain; import javax.servlet.FilterConfig; import javax.servlet.ServletContext; 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 org.apache.hadoop.gateway.i18n.messages.MessagesFactory; import org.apache.hadoop.gateway.provider.federation.jwt.JWTMessages; import org.apache.hadoop.gateway.security.PrimaryPrincipal; import org.apache.hadoop.gateway.services.GatewayServices; import org.apache.hadoop.gateway.services.security.token.JWTokenAuthority; import org.apache.hadoop.gateway.services.security.token.TokenServiceException; import org.apache.hadoop.gateway.services.security.token.impl.JWTToken; public class SSOCookieFederationFilter extends AbstractJWTFilter implements Filter { static JWTMessages log = MessagesFactory.get( JWTMessages.class ); private static final String ORIGINAL_URL_QUERY_PARAM = "originalUrl="; 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"; private static final String DEFAULT_SSO_COOKIE_NAME = "hadoop-jwt"; protected JWTokenAuthority authority = null; private String cookieName = null; private String authenticationProviderUrl = null; @Override public void init( FilterConfig filterConfig ) throws ServletException { ServletContext context = filterConfig.getServletContext(); if (context != null) { GatewayServices services = (GatewayServices) context.getAttribute(GatewayServices.GATEWAY_SERVICES_ATTRIBUTE); if (services != null) { authority = (JWTokenAuthority) services.getService(GatewayServices.TOKEN_SERVICE); } } // 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) { log.missingAuthenticationProviderUrlConfiguration(); throw new ServletException("Required authentication provider URL is missing."); } } public void destroy() { } @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { String wireToken = null; HttpServletRequest req = (HttpServletRequest) request; String loginURL = constructLoginURL(req); wireToken = getJWTFromCookie(req); if (wireToken == null) { if (req.getMethod().equals("OPTIONS")) { // 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, (HttpServletResponse) response, chain); } log.sendRedirectToLoginURL(loginURL); ((HttpServletResponse) response).sendRedirect(loginURL); } else { JWTToken token = new JWTToken(wireToken); boolean verified = false; try { verified = authority.verifyToken(token); if (verified) { if (tokenIsStillValid(token)) { boolean audValid = validateAudiences(token); if (audValid) { Subject subject = createSubjectFromToken(token); continueWithEstablishedSecurityContext(subject, (HttpServletRequest)request, (HttpServletResponse)response, chain); } else { log.failedToValidateAudience(); ((HttpServletResponse) response).sendRedirect(loginURL); } } else { log.tokenHasExpired(); ((HttpServletResponse) response).sendRedirect(loginURL); } } else { log.failedToVerifyTokenSignature(); ((HttpServletResponse) response).sendRedirect(loginURL); } } catch (TokenServiceException e) { log.unableToVerifyToken(e); ((HttpServletResponse) response).sendRedirect(loginURL); } } } /** * Encapsulate the acquisition of the JWT token from HTTP cookies within the * request. * * @param req servlet request to get the JWT token from * @return serialized JWT token */ protected String getJWTFromCookie(HttpServletRequest req) { String serializedJWT = null; Cookie[] cookies = req.getCookies(); if (cookies != null) { for (Cookie cookie : cookies) { if (cookieName.equals(cookie.getName())) { log.cookieHasBeenFound(cookieName); serializedJWT = cookie.getValue(); break; } } } return serializedJWT; } /** * 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.contains("?")) { delimiter = "&"; } String loginURL = authenticationProviderUrl + delimiter + ORIGINAL_URL_QUERY_PARAM + request.getRequestURL().append(getOriginalQueryString(request)); return loginURL; } private String getOriginalQueryString(HttpServletRequest request) { String originalQueryString = request.getQueryString(); return (originalQueryString == null) ? "" : "?" + originalQueryString; } private void sendUnauthorized(ServletResponse response) throws IOException { ((HttpServletResponse) response).sendError(HttpServletResponse.SC_UNAUTHORIZED); return; } private void continueWithEstablishedSecurityContext(Subject subject, final HttpServletRequest request, final HttpServletResponse response, final FilterChain chain) throws IOException, ServletException { try { Subject.doAs( subject, new PrivilegedExceptionAction<Object>() { @Override public Object run() throws Exception { chain.doFilter(request, response); return null; } } ); } catch (PrivilegedActionException e) { Throwable t = e.getCause(); if (t instanceof IOException) { throw (IOException) t; } else if (t instanceof ServletException) { throw (ServletException) t; } else { throw new ServletException(t); } } } private Subject createSubjectFromToken(JWTToken token) { final String principal = token.getSubject(); @SuppressWarnings("rawtypes") HashSet emptySet = new HashSet(); Set<Principal> principals = new HashSet<Principal>(); Principal p = new PrimaryPrincipal(principal); principals.add(p); // The newly constructed Sets check whether this Subject has been set read-only // before permitting subsequent modifications. The newly created Sets also prevent // illegal modifications by ensuring that callers have sufficient permissions. // // To modify the Principals Set, the caller must have AuthPermission("modifyPrincipals"). // To modify the public credential Set, the caller must have AuthPermission("modifyPublicCredentials"). // To modify the private credential Set, the caller must have AuthPermission("modifyPrivateCredentials"). javax.security.auth.Subject subject = new javax.security.auth.Subject(true, principals, emptySet, emptySet); return subject; } }