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