/* * Copyright (c) 2016 OBiBa. All rights reserved. * * This program and the accompanying materials * are made available under the terms of the GNU Public License v3.0. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.obiba.shiro.web.filter; import java.io.IOException; import java.security.cert.X509Certificate; import javax.annotation.Nullable; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.validation.constraints.NotNull; import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.authc.CredentialsException; import org.apache.shiro.authc.IncorrectCredentialsException; import org.apache.shiro.mgt.SessionsSecurityManager; import org.apache.shiro.session.Session; import org.apache.shiro.subject.Subject; import org.apache.shiro.util.ThreadContext; import org.obiba.shiro.authc.HttpAuthorizationToken; import org.obiba.shiro.authc.HttpCookieAuthenticationToken; import org.obiba.shiro.authc.HttpHeaderAuthenticationToken; import org.obiba.shiro.authc.TicketAuthenticationToken; import org.obiba.shiro.authc.X509CertificateAuthenticationToken; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import org.springframework.web.filter.OncePerRequestFilter; import org.springframework.web.util.WebUtils; import com.google.common.base.Strings; @Component public class AuthenticationFilter extends OncePerRequestFilter { private static final Logger log = LoggerFactory.getLogger(AuthenticationFilter.class); public static final String AUTHORIZATION_HEADER = "Authorization"; public static final String AUTHORIZATION_BEARER_SCHEME = "Bearer"; public static final String OBIBA_COOKIE_ID = "obibaid"; @Autowired private SessionsSecurityManager securityManager; private String sessionIdCookieName; private String requestIdCookieName; private String headerCredentials; private String requestPrefix; @Autowired(required = false) private AuthenticationExecutor authenticationExecutor; @Value("${org.obiba.shiro.authenticationFilter.cookie.sessionId}") public void setSessionIdCookieName(String sessionIdCookieName) { this.sessionIdCookieName = sessionIdCookieName; } @Value("${org.obiba.shiro.authenticationFilter.cookie.requestId}") public void setRequestIdCookieName(String requestIdCookieName) { this.requestIdCookieName = requestIdCookieName; } @Value("${org.obiba.shiro.authenticationFilter.requestPrefix}") public void setRequestPrefix(String requestPrefix) { this.requestPrefix = requestPrefix; } /** * Use <b>Basic</b> by default * * @param headerCredentials */ @Value("${org.obiba.shiro.authenticationFilter.headerCredentials:Basic}") public void setHeaderCredentials(String headerCredentials) { this.headerCredentials = headerCredentials; } public void setAuthenticationExecutor(AuthenticationExecutor authenticationExecutor) { this.authenticationExecutor = authenticationExecutor; } @NotNull private AuthenticationExecutor getAuthenticationExecutor() { if(authenticationExecutor == null) { authenticationExecutor = new AbstractAuthenticationExecutor() { @Override protected void ensureProfile(Subject subject) { // do nothing } }; } return authenticationExecutor; } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { if(!Strings.isNullOrEmpty(requestPrefix) && !request.getRequestURI().startsWith(requestPrefix)) { filterChain.doFilter(request, response); return; } if(ThreadContext.getSubject() != null) { log.warn("Previous executing subject was not properly unbound from executing thread. Unbinding now."); ThreadContext.unbindSubject(); } try { authenticateAndBind(request); filterChain.doFilter(request, response); } catch(CredentialsException e) { response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); } catch(AuthenticationException e) { if (log.isDebugEnabled()) log.warn("Unexpected authentication error", e); else log.warn("Unexpected authentication error: {}", e.getMessage()); response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); } catch(Exception e) { log.error("Exception ", e); response.setStatus(HttpServletResponse.SC_BAD_REQUEST); } finally { unbind(); } } /** * This method will try to authenticate the user using the provided sessionId or the "Authorization" header. When no * credentials are provided, this method does nothing. This will invoke the filter chain with an anonymous subject, * which allows fetching public web resources. * * @param request */ private void authenticateAndBind(HttpServletRequest request) { Subject subject = authenticateSslCert(request); if(subject == null) { subject = authenticateAuthHeader(request); } if(subject == null) { subject = authenticateBasicHeader(request); } if(subject == null) { subject = authenticateCookie(request); } if(subject == null) { subject = authenticateTicket(request); } if(subject == null) { subject = authenticateBearerHeader(request); } if(subject != null) { Session session = subject.getSession(); log.trace("Binding subject {} session {} to executing thread {}", subject.getPrincipal(), session.getId(), Thread.currentThread().getId()); ThreadContext.bind(subject); session.touch(); log.debug("Successfully authenticated subject {}", SecurityUtils.getSubject().getPrincipal()); } } @Nullable private Subject authenticateSslCert(HttpServletRequest request) { X509Certificate[] chain = (X509Certificate[]) request.getAttribute("javax.servlet.request.X509Certificate"); if(chain == null || chain.length == 0) return null; AuthenticationToken token = new X509CertificateAuthenticationToken(chain[0]); String sessionId = extractSessionId(request); Subject subject = new Subject.Builder(securityManager).sessionId(sessionId).buildSubject(); subject.login(token); return subject; } @Nullable private Subject authenticateAuthHeader(HttpServletRequest request) { String authToken = request.getHeader(headerCredentials); if(authToken == null || authToken.isEmpty()) return null; AuthenticationToken token = new HttpHeaderAuthenticationToken(authToken); Subject subject = new Subject.Builder(securityManager).sessionId(authToken).buildSubject(); try { subject.login(token); } catch(AuthenticationException e) { return null; } return subject; } @Nullable private Subject authenticateBasicHeader(HttpServletRequest request) { String authorization = request.getHeader(AUTHORIZATION_HEADER); if(authorization == null || authorization.isEmpty() || !authorization.startsWith(headerCredentials + " ")) return null; String sessionId = extractSessionId(request); AuthenticationToken token = new HttpAuthorizationToken(headerCredentials, authorization); try { return getAuthenticationExecutor().login(token, sessionId); } catch(AuthenticationException e) { return null; } } @Nullable private Subject authenticateCookie(HttpServletRequest request) { Cookie sessionCookie = WebUtils.getCookie(request, sessionIdCookieName); Cookie requestCookie = WebUtils.getCookie(request, requestIdCookieName); if(isValid(sessionCookie)) { String sessionId = extractSessionId(request, sessionCookie); String requestId = requestCookie == null ? "" : requestCookie.getValue(); AuthenticationToken token = new HttpCookieAuthenticationToken(sessionId, request.getRequestURI(), requestId); Subject subject = new Subject.Builder(securityManager).sessionId(sessionId).buildSubject(); try { subject.login(token); } catch(AuthenticationException e) { return null; } return subject; } return null; } /** * The ticket token ID is the obiba cookie. * * @param request * @return */ @Nullable private Subject authenticateTicket(HttpServletRequest request) { Cookie ticketCookie = WebUtils.getCookie(request, OBIBA_COOKIE_ID); if(isValid(ticketCookie)) { String ticketId = ticketCookie.getValue(); AuthenticationToken token = new TicketAuthenticationToken(ticketId, request.getRequestURI(), OBIBA_COOKIE_ID); try { return getAuthenticationExecutor().login(token); } catch(AuthenticationException e) { return null; } } return null; } /** * The ticket token ID is in the Authorization header with the "Bearer" scheme. * * @param request * @return */ @Nullable private Subject authenticateBearerHeader(HttpServletRequest request) { String authorization = request.getHeader(AUTHORIZATION_HEADER); if(authorization == null || authorization.isEmpty()) return null; String schemeAndToken[] = authorization.split(" ", 2); if(schemeAndToken.length < 2) return null; if(!AUTHORIZATION_BEARER_SCHEME.equals(schemeAndToken[0])) return null; if(Strings.isNullOrEmpty(schemeAndToken[1])) return null; String ticketId = schemeAndToken[1]; AuthenticationToken token = new TicketAuthenticationToken(ticketId, request.getRequestURI(), OBIBA_COOKIE_ID); try { return getAuthenticationExecutor().login(token); } catch(AuthenticationException e) { return null; } } private boolean isValid(Cookie cookie) { return cookie != null && cookie.getValue() != null; } private String extractSessionId(HttpServletRequest request) { return extractSessionId(request, null); } private String extractSessionId(HttpServletRequest request, @Nullable Cookie sessionCookie) { String sessionId = request.getHeader(headerCredentials); if(sessionId == null) { Cookie safeSessionCookie = sessionCookie == null ? WebUtils.getCookie(request, sessionIdCookieName) : sessionCookie; if(safeSessionCookie != null) { sessionId = safeSessionCookie.getValue(); } } return sessionId; } private void unbind() { try { if(log.isTraceEnabled()) { Subject s = ThreadContext.getSubject(); if(s != null) { Session session = s.getSession(false); log.trace("Unbinding subject {} session {} from executing thread {}", s.getPrincipal(), session == null ? null : session.getId(), Thread.currentThread().getId()); } } } finally { ThreadContext.unbindSubject(); } } }