1   package com.atlassian.seraph.filter;
2   
3   import com.atlassian.seraph.RequestParameterConstants;
4   import com.atlassian.seraph.auth.AuthenticationContext;
5   import com.atlassian.seraph.auth.AuthenticationContextAwareAuthenticator;
6   import com.atlassian.seraph.auth.Authenticator;
7   import com.atlassian.seraph.config.SecurityConfig;
8   import com.atlassian.seraph.config.SecurityConfigFactory;
9   import com.atlassian.seraph.elevatedsecurity.ElevatedSecurityGuard;
10  import com.atlassian.seraph.util.RedirectUtils;
11  import org.apache.log4j.Logger;
12  
13  import java.io.IOException;
14  import java.net.URI;
15  import java.net.URISyntaxException;
16  import java.security.Principal;
17  import javax.servlet.Filter;
18  import javax.servlet.FilterChain;
19  import javax.servlet.FilterConfig;
20  import javax.servlet.ServletException;
21  import javax.servlet.ServletRequest;
22  import javax.servlet.ServletResponse;
23  import javax.servlet.http.HttpServletRequest;
24  import javax.servlet.http.HttpServletRequestWrapper;
25  import javax.servlet.http.HttpServletResponse;
26  import javax.servlet.http.HttpSession;
27  
28  /**
29   * This is a base authentication filter. It delegates the actual login process to a child class but takes care of the
30   * redirection process.
31   * <p/>
32   * If the authentication is successful, the user will be redirected by the filter to the URL given by the session
33   * attribute at SecurityFilter.ORIGINAL_URL_KEY.
34   * <p/>
35   * If this URL doesn't exist, it will look for a parameter 'os_destination' to use as the redirected URL instead.
36   * <p/>
37   * If neither is found, it is assumed that the page will check the authorisation status and handle redirection itself.
38   * <p/>
39   * From the any other filter in the request, or the servlet/JSP/action which processes the request, you can look up the
40   * status of the authorisation attempt. The status is a String request attribute, with the key 'os_authstatus'.
41   * <p/>
42   * The possible statuses are:
43   * <ul>
44   * <li> LoginFilter.LOGIN_SUCCESS - the login was processed, and user was logged in </li>
45   * <li>LoginFilter.LOGIN_FAILURE - the login was processed, the user gave a bad username or password </li>
46   * <li>LoginFilter.LOGIN_ERROR - the login was processed, an exception occurred trying to log the user in </li>
47   * <li>LoginFilter.LOGIN_NOATTEMPT - the login was no processed, no form parameters existed </li>
48   * </ul>
49   */
50  public abstract class BaseLoginFilter implements Filter
51  {
52      private static final Logger log = Logger.getLogger(BaseLoginFilter.class);
53  
54      private FilterConfig filterConfig = null;
55      protected static final String ALREADY_FILTERED = "loginfilter.already.filtered";
56      public static final String LOGIN_SUCCESS = "success";
57      public static final String LOGIN_FAILED = "failed";
58      public static final String LOGIN_ERROR = "error";
59      public static final String LOGIN_NOATTEMPT = null;
60      public static final String OS_AUTHSTATUS_KEY = "os_authstatus";
61      public static final String AUTHENTICATION_ERROR_TYPE = "auth_error_type";
62      private SecurityConfig securityConfig = null;
63  
64      public BaseLoginFilter()
65      {
66          super();
67      }
68  
69      public void init(final FilterConfig config)
70      {
71          this.filterConfig = config;
72      }
73  
74      public void destroy()
75      {
76          filterConfig = null;
77      }
78  
79      /**
80       * @return a FilterConfig
81       *
82       * @deprecated Not needed in latest version of Servlet 2.3 API
83       */
84      public FilterConfig getFilterConfig()
85      {
86          return filterConfig;
87      }
88  
89      /**
90       * @param filterConfig a FilterConfig
91       *
92       * @deprecated Not needed in latest version of Servlet 2.3 API - replaced by init().
93       */
94      public void setFilterConfig(final FilterConfig filterConfig)
95      {
96          if (filterConfig != null) //it seems that Orion 1.5.2 calls this with a null config.
97          {
98              init(filterConfig);
99          }
100     }
101 
102     public void doFilter(final ServletRequest servletRequest, final ServletResponse servletResponse, final FilterChain filterChain)
103             throws IOException, ServletException
104     {
105         final String METHOD = "doFilter : ";
106         final boolean dbg = log.isDebugEnabled();
107 
108         // wrap the request with one that returns the User as the Principal
109         HttpServletRequest httpServletRequest = new SecurityHttpRequestWrapper((HttpServletRequest) servletRequest);
110         HttpServletResponse httpServletResponse = (HttpServletResponse) servletResponse;
111 
112         if (servletRequest.getAttribute(ALREADY_FILTERED) == null && getSecurityConfig().getController().isSecurityEnabled())
113         {
114             httpServletRequest.setAttribute(ALREADY_FILTERED, Boolean.TRUE);
115             httpServletRequest.setAttribute(OS_AUTHSTATUS_KEY, LOGIN_NOATTEMPT);
116 
117             if (dbg)
118             {
119                 log.debug(METHOD + "____ Attempting login for : '" + getRequestUrl(httpServletRequest) + "'");
120             }
121             //
122             // this will call the derived classes to perform the actual login process
123             //
124             String status = login(httpServletRequest, httpServletResponse);
125             httpServletRequest.setAttribute(OS_AUTHSTATUS_KEY, status);
126             if (dbg)
127             {
128                 final String userName = httpServletRequest.getRemoteUser();
129                 log.debug(METHOD + "Login completed for '" + userName + "' - " + OS_AUTHSTATUS_KEY + " = '" + status + "'");
130             }
131 
132             // if we successfully logged in - look for an original URL to forward to
133             if (LOGIN_SUCCESS.equals(status) && redirectToOriginalDestination(httpServletRequest, httpServletResponse))
134             {
135                 return;
136             }
137             // NOTE : LOGIN_NOATTEMPT is a symbolic constant for null which is a language level symbolic constant for...well...null
138             //noinspection StringEquality
139             if (status == LOGIN_NOATTEMPT)
140             {
141                 issuePossibleRedirectIfUserIsAlreadyLoggedIn(httpServletRequest, httpServletResponse);
142             }
143         }
144         filterChain.doFilter(httpServletRequest, httpServletResponse);
145     }
146 
147     private String getRequestUrl(final HttpServletRequest httpServletRequest)
148     {
149         return httpServletRequest.getServletPath() +
150                 (httpServletRequest.getPathInfo() == null ? "" : httpServletRequest.getPathInfo()) +
151                 (httpServletRequest.getQueryString() == null ? "" : "?" + httpServletRequest.getQueryString());
152     }
153 
154     private void issuePossibleRedirectIfUserIsAlreadyLoggedIn(final HttpServletRequest httpServletRequest, final HttpServletResponse httpServletResponse)
155             throws IOException
156     {
157         /*
158           Allow a redirect for an already logged in user under the following conditions
159             1: They are already logged in
160             2: They have os_destination in their request parameters
161             3: They do NOT have seraph_originalurl in their session. This avoids circular redirects when a User
162             tries to access a URL they do not have (role-based) privileges to view.
163         */
164         if (httpServletRequest.getParameterMap().get(RequestParameterConstants.OS_DESTINATION) != null)
165         {
166             Principal principal = getAuthenticator().getUser(httpServletRequest, httpServletResponse);
167             if (principal != null)
168             {
169                 HttpSession session = httpServletRequest.getSession();
170                 if (session != null && session.getAttribute(SecurityConfigFactory.getInstance().getOriginalURLKey()) == null)
171                 {
172                     redirectToOriginalDestination(httpServletRequest, httpServletResponse);
173                 }
174             }
175         }
176     }
177 
178     /**
179      * Performs the actual authentication (if required) and returns the status code. Status code is chosen to be one of
180      * these:
181      * <p/>
182      * The possible statuses are:
183      * <ul>
184      * <li> BaseLoginFilter.LOGIN_SUCCESS - the login was processed, and user was logged in
185      * <li> BaseLoginFilter.LOGIN_FAILURE - the login was processed, the user gave a bad username or password
186      * <li> BaseLoginFilter.LOGIN_ERROR - the login was processed, an exception occurred trying to log the user in
187      * <li> BaseLoginFilter.LOGIN_NOATTEMPT - the login was no processed, no form parameters existed
188      * </ul>
189      * <p/>
190      * When there is an error on login, implementations should set a request attribute with name
191      * {@link #AUTHENTICATION_ERROR_TYPE} and a type of {@link com.atlassian.seraph.auth.AuthenticationErrorType} in
192      * order to indicate the type of error.
193      *
194      * @param httpServletRequest  the HTTP request in play
195      * @param httpServletResponse the HTTP response in play
196      *
197      * @return authentication status
198      */
199     public abstract String login(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse);
200 
201     /**
202      * This request wrapper class overrrides the {@link javax.servlet.http.HttpServletRequest#getRemoteUser()} and
203      * {@link javax.servlet.http.HttpServletRequest#getUserPrincipal()} methods and calls the current {@link
204      * com.atlassian.seraph.auth.Authenticator} to provide these values.
205      */
206     class SecurityHttpRequestWrapper extends HttpServletRequestWrapper
207     {
208         private HttpServletRequest delegateHttpServletRequest;
209 
210         public SecurityHttpRequestWrapper(final HttpServletRequest delegateHttpServletRequest)
211         {
212             super(delegateHttpServletRequest);
213             this.delegateHttpServletRequest = delegateHttpServletRequest;
214         }
215 
216         public String getRemoteUser()
217         {
218             Principal user = getUserPrincipal();
219             return (user == null) ? null : user.getName();
220         }
221 
222         public Principal getUserPrincipal()
223         {
224             if(getAuthenticator().getClass().isAnnotationPresent(AuthenticationContextAwareAuthenticator.class))
225             {
226                 return getAuthenticationContext().getUser();
227             }
228             else
229             {
230                 return getAuthenticator().getUser(delegateHttpServletRequest);
231             }
232         }
233     }
234 
235     /**
236      * Redirect the response to the original destination if present
237      *
238      * @param httpServletRequest  the HTTP request in play
239      * @param httpServletResponse the HTTP response in play
240      *
241      * @return true if a redirect was needed and issued
242      *
243      * @throws IOException If the redirect thorws IOException. See {@link HttpServletResponse#sendRedirect}
244      */
245     protected boolean redirectToOriginalDestination(final HttpServletRequest httpServletRequest, final HttpServletResponse httpServletResponse)
246             throws IOException
247     {
248         final String METHOD = "redirectToOriginalDestination : ";
249         final boolean dbg = log.isDebugEnabled();
250 
251         // First try to get a redirect URL from the session
252         String redirectURL = (String) httpServletRequest.getSession().getAttribute(getSecurityConfig().getOriginalURLKey());
253         if (redirectURL != null)
254         {
255             // remove the session parameter
256             httpServletRequest.getSession().setAttribute(getSecurityConfig().getOriginalURLKey(), null);
257         }
258         else
259         {
260             // No session attribute - try to get a redirect from the request.
261             redirectURL = httpServletRequest.getParameter(RequestParameterConstants.OS_DESTINATION);
262         }
263 
264         if (redirectURL == null)
265         {
266             return false;
267         }
268 
269         // Check redirect for header injection and potential phishing attack - see SER-127 and SER-128
270         if (!getSecurityConfig().getRedirectPolicy().allowedRedirectDestination(redirectURL, httpServletRequest))
271         {
272             // Redirect Destination is not allowed.
273             log.warn(METHOD + "Redirect request to '" + redirectURL + "' is not allowed. Will send user to the context root instead.");
274             // note that the context path will get added below.
275             redirectURL = "/";
276         }
277 
278         if (!isAbsoluteUrl(redirectURL))
279         {
280             // Reading the javadoc for HttpServletResponse.sendRedirect() leads me to believe that this is overkill or wrong.
281             // We should just leave the leading slash of relative paths, and the Servlet container will interpret the context for us.
282             redirectURL = RedirectUtils.appendPathToContext(httpServletRequest.getContextPath(), redirectURL);
283         }
284 
285         if (dbg)
286         {
287             log.debug(METHOD + "Login redirect to: " + redirectURL);
288         }
289 
290         httpServletResponse.sendRedirect(redirectURL);
291         return true;
292     }
293 
294     protected boolean isAbsoluteUrl(final String url)
295     {
296         try
297         {
298             URI uri = new URI(url);
299             return uri.isAbsolute();
300         }
301         catch (URISyntaxException e)
302         {
303             return false;
304         }
305     }
306 
307     protected Authenticator getAuthenticator()
308     {
309         return getSecurityConfig().getAuthenticator();
310     }
311 
312     protected ElevatedSecurityGuard getElevatedSecurityGuard()
313     {
314         return getSecurityConfig().getElevatedSecurityGuard();
315     }
316 
317     protected SecurityConfig getSecurityConfig()
318     {
319         if (securityConfig == null)
320         {
321             securityConfig = (SecurityConfig) filterConfig.getServletContext().getAttribute(SecurityConfig.STORAGE_KEY);
322         }
323         return securityConfig;
324     }
325 
326     protected AuthenticationContext getAuthenticationContext()
327     {
328         return getSecurityConfig().getAuthenticationContext();
329     }
330 }