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         public SecurityHttpRequestWrapper(final HttpServletRequest delegateHttpServletRequest)
201         {
202             super(delegateHttpServletRequest);
203         }
204 
205         public String getRemoteUser()
206         {
207             Principal user = getUserPrincipal();
208             return (user == null) ? null : user.getName();
209         }
210 
211         public Principal getUserPrincipal()
212         {
213             return getSecurityConfig().getAuthenticationContext().getUser();
214         }
215     }
216 
217     /**
218      * Redirect the response to the original destination if present
219      *
220      * @param httpServletRequest  the HTTP request in play
221      * @param httpServletResponse the HTTP response in play
222      *
223      * @return true if a redirect was needed and issued
224      *
225      * @throws IOException If the redirect thorws IOException. See {@link HttpServletResponse#sendRedirect}
226      */
227     protected boolean redirectToOriginalDestination(final HttpServletRequest httpServletRequest, final HttpServletResponse httpServletResponse)
228             throws IOException
229     {
230         final String METHOD = "redirectToOriginalDestination : ";
231         final boolean dbg = log.isDebugEnabled();
232 
233         // First try to get a redirect URL from the session
234         String redirectURL = (String) httpServletRequest.getSession().getAttribute(getSecurityConfig().getOriginalURLKey());
235         if (redirectURL != null)
236         {
237             // remove the session parameter
238             httpServletRequest.getSession().setAttribute(getSecurityConfig().getOriginalURLKey(), null);
239         }
240         else
241         {
242             // No session attribute - try to get a redirect from the request.
243             redirectURL = httpServletRequest.getParameter(RequestParameterConstants.OS_DESTINATION);
244         }
245 
246         if (redirectURL == null)
247         {
248             return false;
249         }
250 
251         // Check redirect for header injection and potential phishing attack - see SER-127 and SER-128
252         if (!getSecurityConfig().getRedirectPolicy().allowedRedirectDestination(redirectURL, httpServletRequest))
253         {
254             // Redirect Destination is not allowed.
255             log.warn(METHOD + "Redirect request to '" + redirectURL + "' is not allowed. Will send user to the context root instead.");
256             // note that the context path will get added below.
257             redirectURL = "/";
258         }
259 
260         if (!isAbsoluteUrl(redirectURL))
261         {
262             // Reading the javadoc for HttpServletResponse.sendRedirect() leads me to believe that this is overkill or wrong.
263             // We should just leave the leading slash of relative paths, and the Servlet container will interpret the context for us.
264             redirectURL = RedirectUtils.appendPathToContext(httpServletRequest.getContextPath(), redirectURL);
265         }
266 
267         if (dbg)
268         {
269             log.debug(METHOD + "Login redirect to: " + redirectURL);
270         }
271 
272         httpServletResponse.sendRedirect(redirectURL);
273         return true;
274     }
275 
276     protected boolean isAbsoluteUrl(final String url)
277     {
278         try
279         {
280             URI uri = new URI(url);
281             return uri.isAbsolute();
282         }
283         catch (URISyntaxException e)
284         {
285             return false;
286         }
287     }
288 
289     protected Authenticator getAuthenticator()
290     {
291         return getSecurityConfig().getAuthenticator();
292     }
293 
294     protected ElevatedSecurityGuard getElevatedSecurityGuard()
295     {
296         return getSecurityConfig().getElevatedSecurityGuard();
297     }
298 
299     protected SecurityConfig getSecurityConfig()
300     {
301         if (securityConfig == null)
302         {
303             securityConfig = (SecurityConfig) filterConfig.getServletContext().getAttribute(SecurityConfig.STORAGE_KEY);
304         }
305         return securityConfig;
306     }
307 }