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