1   package com.atlassian.seraph.auth;
2   
3   import com.atlassian.seraph.config.SecurityConfig;
4   import com.atlassian.seraph.config.SecurityConfigFactory;
5   import com.atlassian.seraph.elevatedsecurity.ElevatedSecurityGuard;
6   import com.atlassian.seraph.interceptor.LogoutInterceptor;
7   import com.atlassian.seraph.service.rememberme.RememberMeService;
8   import com.atlassian.seraph.util.RedirectUtils;
9   import com.atlassian.seraph.util.SecurityUtils;
10  import org.apache.commons.lang.StringUtils;
11  import org.apache.log4j.Logger;
12  
13  import javax.servlet.http.HttpServletRequest;
14  import javax.servlet.http.HttpServletResponse;
15  import javax.servlet.http.HttpSession;
16  import java.io.IOException;
17  import java.security.Principal;
18  import java.util.List;
19  import java.util.Map;
20  
21  import static com.atlassian.seraph.auth.LoginReason.*;
22  
23  /**
24   * This authenticator stores the currently logged in user in the session as a Principal.
25   * <p/>
26   * It also provides for cookie logins and creates cookies if needed.
27   * <p/>
28   * Includes code from Jive 1.2.4 (released under the Apache license)
29   */
30  public abstract class DefaultAuthenticator extends AbstractAuthenticator
31  {
32      /**
33       * The key used to store the user object in the session
34       */
35      public static final String LOGGED_IN_KEY = "seraph_defaultauthenticator_user";
36  
37      /**
38       * The key used to indicate that the user has logged out and session regarding of it containing a cookie is not
39       * logged in.
40       */
41      public static final String LOGGED_OUT_KEY = "seraph_defaultauthenticator_logged_out_user";
42  
43      private static final Logger log = Logger.getLogger(DefaultAuthenticator.class);
44  
45      // --------------------------------------------------------------------------------------------------------- members
46  
47      private String basicAuthParameterName;
48  
49      @Override
50      public void init(final Map<String, String> params, final SecurityConfig config)
51      {
52          super.init(params, config);
53          basicAuthParameterName = config.getAuthType();
54      }
55  
56      /**
57       * @deprecated Use {@link RoleMapper} directly
58       */
59      @Deprecated
60      @Override
61      public boolean isUserInRole(final HttpServletRequest request, final String role)
62      {
63          return getRoleMapper().hasRole(getUser(request), request, role);
64      }
65  
66      /**
67       * Tries to authenticate a user.
68       *
69       * @param httpServletRequest  the request in play
70       * @param httpServletResponse the response in play
71       * @param userName            the user name to check against the password
72       * @param password            the password to authenticate the user with
73       * @param setRememberMeCookie whether to set a remember me cookie on sucessful login
74       * @return Whether the user was authenticated. This base implementation returns false if any errors occur, rather
75       *         than throw an exception.
76       * @throws AuthenticatorException actualy this class does not throw any exceptions however the interface says we
77       *                                must and other classes may override us
78       */
79      @Override
80      public boolean login(final HttpServletRequest httpServletRequest, final HttpServletResponse httpServletResponse, final String userName, final String password, final boolean setRememberMeCookie)
81              throws AuthenticatorException
82      {
83          final String METHOD = "login : ";
84          final boolean dbg = log.isDebugEnabled();
85  
86          final Principal user = getUser(userName);
87  
88          if (user == null)
89          {
90              log.info(METHOD + "'" + userName + "' does not exist and cannot be authenticated.");
91          }
92          else
93          {
94              final boolean authenticated = authenticate(user, password);
95              if (dbg)
96              {
97                  log.debug(METHOD + "'" + userName + "' has " + (authenticated ? "been" : "not been") + " authenticated");
98              }
99              if (authenticated)
100             {
101                 if (authoriseUserAndEstablishSession(httpServletRequest, httpServletResponse, user))
102                 {
103                     if (setRememberMeCookie && httpServletResponse != null)
104                     {
105                         getRememberMeService().addRememberMeCookie(httpServletRequest, httpServletResponse, userName);
106                     }
107                     return true;
108                 }
109                 AUTHORISATION_FAILED.stampRequestResponse(httpServletRequest, httpServletResponse);
110             }
111             else
112             {
113                 log.info(METHOD + "'" + userName + "' could not be authenticated with the given password");
114             }
115         }
116 
117         if ((httpServletResponse != null))
118         {
119             log.warn(METHOD + "'" + userName + "' tried to login but they do not have USE permission or weren't found. Deleting remember me cookie.");
120 
121             getRememberMeService().removeRememberMeCookie(httpServletRequest, httpServletResponse);
122         }
123 
124         return false;
125     }
126 
127     /**
128      * Called to remove the current principal from the HttpSession and will also to remove any remember me cookies that
129      * may be in effect.
130      *
131      * @param httpServletRequest  the request in play
132      * @param httpServletResponse the response in play
133      * @return true always for this implementation!
134      * @throws AuthenticatorException this implementation never does
135      */
136     @Override
137     public boolean logout(final HttpServletRequest httpServletRequest, final HttpServletResponse httpServletResponse)
138             throws AuthenticatorException
139     {
140         final String METHOD = "logout : ";
141         final boolean dbg = log.isDebugEnabled();
142         if (dbg)
143         {
144             log.debug(METHOD + "Calling interceptors and clearing remember me cookie");
145         }
146         final List<LogoutInterceptor> interceptors = getLogoutInterceptors();
147 
148         for (final LogoutInterceptor interceptor : interceptors)
149         {
150             interceptor.beforeLogout(httpServletRequest, httpServletResponse);
151         }
152 
153         removePrincipalFromSessionContext(httpServletRequest);
154 
155         OUT.stampRequestResponse(httpServletRequest, httpServletResponse);
156 
157         // Log out is sometimes called as part of a getUser request, if the user is not found
158         // log out may be called, but some getUser calls only pass in the request, and null response.
159         if (httpServletResponse != null)
160         {
161             getRememberMeService().removeRememberMeCookie(httpServletRequest, httpServletResponse);
162         }
163 
164         for (final Object element : interceptors)
165         {
166             final LogoutInterceptor interceptor = (LogoutInterceptor) element;
167             interceptor.afterLogout(httpServletRequest, httpServletResponse);
168         }
169 
170         return true;
171     }
172 
173     /**
174      * This is called to authorise the user with the application.  The {@link RoleMapper} is invoked to see if the user
175      * is authorised to user this request  via a call to {@link #isAuthorised(javax.servlet.http.HttpServletRequest,
176      * java.security.Principal)}
177      * <p/>
178      * If successful, then the HttpSession will contain the attribute marking that the user is logged in
179      *
180      * @param httpServletRequest  the request in play
181      * @param httpServletResponse the response in play
182      * @param principal           the principal to authorise
183      * @return true if the user was authorised
184      */
185     protected boolean authoriseUserAndEstablishSession(final HttpServletRequest httpServletRequest, final HttpServletResponse httpServletResponse, final Principal principal)
186     {
187         //
188         // Are they in fact already in the session?  if someone is using Basic Auth then this will be called for every request
189         // and we don't want to tear down the session for every request
190         //
191 
192         final boolean principalAlreadyInSessionContext = isPrincipalAlreadyInSessionContext(httpServletRequest, principal);
193 
194         // start clean at this point
195         putPrincipalInSessionContext(httpServletRequest, null);
196 
197         final boolean canLogin = isAuthorised(httpServletRequest, principal);
198 
199         if (log.isDebugEnabled())
200         {
201             final String prefix = "authoriseUser : " + "'" + principal.getName() + "' ";
202             log.debug(prefix + (canLogin ? "can" : "CANNOT") + " login according to the RoleMapper");
203         }
204         if (canLogin)
205         {
206             if (!principalAlreadyInSessionContext)
207             {
208                 final SecurityConfig theConfig = getConfig();
209                 if (theConfig != null && theConfig.isInvalidateSessionOnLogin())
210                 {
211                     invalidateSession(httpServletRequest);
212                 }
213             }
214             putPrincipalInSessionContext(httpServletRequest, principal);
215             return true;
216         }
217         return false;
218     }
219 
220     /**
221      * This method is called to estblish if the principal  is authorised to use the appliction url in play
222      *
223      * @param httpServletRequest the request in play
224      * @param principal          the principal to check
225      * @return true if they are authorised to use the application at thgis point of time
226      */
227     protected boolean isAuthorised(final HttpServletRequest httpServletRequest, final Principal principal)
228     {
229         return getRoleMapper().canLogin(principal, httpServletRequest);
230     }
231 
232     /**
233      * This can be called to put the principal into the HttpSession in a Seraph ready manner
234      *
235      * @param httpServletRequest the request in play
236      * @param principal          the principal to put in the session
237      */
238     protected void putPrincipalInSessionContext(final HttpServletRequest httpServletRequest, final Principal principal)
239     {
240         final HttpSession httpSession = httpServletRequest.getSession();
241         httpSession.setAttribute(LOGGED_IN_KEY, principal);
242         httpSession.setAttribute(LOGGED_OUT_KEY, null);
243     }
244 
245     /**
246      * This can be called to remove the principal into the HttpSession in a Seraph ready manner
247      *
248      * @param httpServletRequest the request in play
249      */
250     protected void removePrincipalFromSessionContext(final HttpServletRequest httpServletRequest)
251     {
252         final HttpSession httpSession = httpServletRequest.getSession();
253         httpSession.setAttribute(LOGGED_IN_KEY, null);
254         httpSession.setAttribute(LOGGED_OUT_KEY, Boolean.TRUE);
255     }
256 
257     /**
258      * This is called to determine if the Principal is already in the HttpSession in a Seraph ready manner.
259      *
260      * @param httpServletRequest the request in play
261      * @param principal          the principal to put in the session
262      * @return true if the principal is already in the session
263      */
264     protected boolean isPrincipalAlreadyInSessionContext(final HttpServletRequest httpServletRequest, final Principal principal)
265     {
266         Principal currentPrincipal = (Principal) httpServletRequest.getSession().getAttribute(LOGGED_IN_KEY);
267         return currentPrincipal != null && currentPrincipal.getName() != null && principal != null && currentPrincipal.getName().equals(principal.getName());
268     }
269 
270     /**
271      * override this method if you need to retrieve the role mapper from elsewhere than the singleton-factory (injected
272      * dependency for instance)
273      *
274      * @return the {@link com.atlassian.seraph.auth.RoleMapper} to use
275      */
276     protected RoleMapper getRoleMapper()
277     {
278         return SecurityConfigFactory.getInstance().getRoleMapper();
279     }
280 
281     /**
282      * Retrieve a Principal for the given username. Returns null if no such user exists.
283      *
284      * @param username the name of the user to find
285      * @return a Principal for the given username.
286      */
287     protected abstract Principal getUser(final String username);
288 
289     /**
290      * Authenticate the given user and password.
291      *
292      * @param user     the user to authenticate.
293      * @param password the password of the user
294      * @return true if the user was successfully authenticated and false otherwise.
295      * @throws AuthenticatorException if an error occurs that stops the user from being authenticated (eg remote communication failure).
296      */
297     protected abstract boolean authenticate(final Principal user, final String password) throws AuthenticatorException;
298 
299     /**
300      * Returns the currently logged in user, trying in order: <p/> <ol> <li>Session, only if one exists</li> <li>Cookie,
301      * only if no session exists</li> <li>Basic authentication, if the above fail, and authType=basic</li> </ol> <p/>
302      * Warning: only in the case of cookie and basic auth will the user be authenticated.
303      *
304      * @param httpServletRequest  the request in play
305      * @param httpServletResponse a response object that may be modified if basic auth is enabled
306      * @return a Principal object for the user if found, otherwise null
307      */
308     @Override
309     public Principal getUser(final HttpServletRequest httpServletRequest, final HttpServletResponse httpServletResponse)
310     {
311         final String METHOD = "getUser : ";
312         final boolean dbg = log.isDebugEnabled();
313 
314         if (httpServletRequest.getSession(false) != null)
315         {
316             final Principal sessionUser = getUserFromSession(httpServletRequest);
317             if (sessionUser != null)
318             {
319                 OK.stampRequestResponse(httpServletRequest, httpServletResponse);
320                 return sessionUser;
321             }
322         }
323         else
324         {
325             final Principal cookieUser = getUserFromCookie(httpServletRequest, httpServletResponse);
326             if (cookieUser != null)
327             {
328                 return cookieUser;
329             }
330         }
331 
332         if (RedirectUtils.isBasicAuthentication(httpServletRequest, basicAuthParameterName))
333         {
334             final Principal basicAuthUser = getUserFromBasicAuthentication(httpServletRequest, httpServletResponse);
335             if (basicAuthUser != null)
336             {
337                 return basicAuthUser;
338             }
339         }
340 
341         if (dbg)
342         {
343             log.debug(METHOD + "User not found in either Session, Cookie or Basic Auth.");
344         }
345 
346         return null;
347     }
348 
349     /**
350      * <p> Tries to get a logged in user from the session. </p>
351      *
352      * @param httpServletRequest the current {@link HttpServletRequest}
353      * @return the logged in user in the session. <code>null</code> if there is no logged in user in the session, or the
354      *         {@link #LOGGED_OUT_KEY} is set because the user has logged out.
355      */
356     protected Principal getUserFromSession(final HttpServletRequest httpServletRequest)
357     {
358         final String METHOD = "getUserFromSession : ";
359         final boolean dbg = log.isDebugEnabled();
360         try
361         {
362             if (httpServletRequest.getSession().getAttribute(LOGGED_OUT_KEY) != null)
363             {
364                 if (dbg)
365                 {
366                     log.debug(METHOD + "Session found; user has already logged out. eg has LOGGED_OUT_KEY in session");
367                 }
368                 return null;
369             }
370             final Principal principal = (Principal) httpServletRequest.getSession().getAttribute(LOGGED_IN_KEY);
371             if (dbg)
372             {
373                 if (principal == null)
374                 {
375                     log.debug(METHOD + "Session found; BUT it has no Principal in it");
376                 }
377                 else
378                 {
379                     log.debug(METHOD + "Session found; '" + principal.getName() + "' is present");
380                 }
381             }
382             return principal;
383         }
384         catch (final Exception e)
385         {
386             log.warn(METHOD + "Exception when retrieving user from session: " + e, e);
387             return null;
388         }
389     }
390 
391     /**
392      * Extracts the username and password from the cookie and calls login to authenticate, and if successful store the
393      * token in the session.
394      *
395      * @param httpServletRequest  the HTTP request in play
396      * @param httpServletResponse the HTTP respone in play
397      * @return a Principal object for the user if successful, otherwise null
398      */
399     protected Principal getUserFromCookie(final HttpServletRequest httpServletRequest, final HttpServletResponse httpServletResponse)
400     {
401         final String METHOD = "getUserFromCookie : ";
402         final boolean dbg = log.isDebugEnabled();
403 
404 
405         final String userName = getRememberMeService().getRememberMeCookieAuthenticatedUsername(httpServletRequest, httpServletResponse);
406         if (dbg)
407         {
408             log.debug(METHOD + "Got username : '" + userName + "' from cookie, attempting to authenticate user is known");
409         }
410         if (StringUtils.isNotBlank(userName))
411         {
412             //
413             // we have a valid user name so we need to turn them into a principal
414             //
415             final Principal principal = getUser(userName);
416             if (principal != null)
417             {
418                 // we have a valid user but is their account in a valid state from a security point of view
419                 final ElevatedSecurityGuard securityGuard = getElevatedSecurityGuard();
420                 if (!securityGuard.performElevatedSecurityCheck(httpServletRequest, userName))
421                 {
422                     if (dbg)
423                     {
424                         log.debug(METHOD + "'" + userName + "' failed elevated security check");
425                     }
426                     AUTHENTICATION_DENIED.stampRequestResponse(httpServletRequest, httpServletResponse);
427                     securityGuard.onFailedLoginAttempt(httpServletRequest, userName);
428                     return null;
429                 }
430                 //
431                 // ok we have a valid Principal but is he allowed to login to the application at this stage
432                 // once we come out of this method, the user will be in the session and hence the "login session"
433                 // will have been established.
434                 //
435                 // In the past this was done all in the login() method but since don't login
436                 // we need to recall this authorisation step here
437                 //
438                 if (authoriseUserAndEstablishSession(httpServletRequest, httpServletResponse, principal))
439                 {
440                     if (dbg)
441                     {
442                         log.debug(METHOD + "Authenticated '" + userName + "' via Remember Me Cookie");
443                     }
444                     OK.stampRequestResponse(httpServletRequest, httpServletResponse);
445                     securityGuard.onSuccessfulLoginAttempt(httpServletRequest, userName);
446                     return principal;
447                 }
448                 if (dbg)
449                 {
450                     log.debug(METHOD + "'" + userName + "' failed authorisation security check");
451                 }
452                 AUTHORISATION_FAILED.stampRequestResponse(httpServletRequest, httpServletResponse);
453                 securityGuard.onFailedLoginAttempt(httpServletRequest, userName);
454             }
455         }
456         return null;
457     }
458 
459     /**
460      * Checks the Authorization header to see whether basic auth token is provided. If it is, decode it, login and
461      * return the valid user. If it isn't, basic auth is still required, so return a 401 Authorization Required header
462      * in the response.
463      *
464      * @param httpServletRequest  the HTTP request in play
465      * @param httpServletResponse a response object that <i>will</i> be modified if no token found
466      * @return a {@link java.security.Principal} or null if one cant be found
467      */
468     protected Principal getUserFromBasicAuthentication(final HttpServletRequest httpServletRequest, final HttpServletResponse httpServletResponse)
469     {
470         final String METHOD = "getUserFromSession : ";
471         final boolean dbg = log.isDebugEnabled();
472 
473         final String header = httpServletRequest.getHeader("Authorization");
474         LoginReason reason = OK;
475 
476         if (SecurityUtils.isBasicAuthorizationHeader(header))
477         {
478             if (dbg)
479             {
480                 log.debug(METHOD + "Looking in Basic Auth headers");
481             }
482 
483             final SecurityUtils.UserPassCredentials creds = SecurityUtils.decodeBasicAuthorizationCredentials(header);
484             final ElevatedSecurityGuard securityGuard = getElevatedSecurityGuard();
485             if (!securityGuard.performElevatedSecurityCheck(httpServletRequest, creds.getUsername()))
486             {
487                 if (dbg)
488                 {
489                     log.debug(METHOD + "'" + creds.getUsername() + "' failed elevated security check");
490                 }
491                 reason = AUTHENTICATION_DENIED.stampRequestResponse(httpServletRequest, httpServletResponse);
492                 securityGuard.onFailedLoginAttempt(httpServletRequest, creds.getUsername());
493             }
494             else
495             {
496                 if (dbg)
497                 {
498                     log.debug(METHOD + "'" + creds.getUsername() + "' does not require elevated security check.  Attempting authentication...");
499                 }
500 
501                 try
502                 {
503                     final boolean loggedin = login(httpServletRequest, httpServletResponse, creds.getUsername(),
504                             creds.getPassword(), false);
505                     if (loggedin)
506                     {
507                         reason = OK.stampRequestResponse(httpServletRequest, httpServletResponse);
508                         securityGuard.onSuccessfulLoginAttempt(httpServletRequest, creds.getUsername());
509                         if (dbg)
510                         {
511                             log.debug(METHOD + "Authenticated '" + creds.getUsername() + "' via Basic Auth");
512                         }
513                         return getUser(creds.getUsername());
514                     }
515                     else
516                     {
517                         reason = AUTHENTICATED_FAILED.stampRequestResponse(httpServletRequest, httpServletResponse);
518                         securityGuard.onFailedLoginAttempt(httpServletRequest, creds.getUsername());
519                     }
520                 }
521                 catch (final AuthenticatorException e)
522                 {
523                     log.warn(METHOD + "Exception trying to login '" + creds.getUsername() + "' via Basic Auth:" + e, e);
524                 }
525             }
526             try
527             {
528                 httpServletResponse.sendError(401, "Basic Authentication Failure - Reason : " + reason.toString());
529             }
530             catch (final IOException e)
531             {
532                 log.warn(METHOD + "Exception trying to send Basic Auth failed error: " + e, e);
533             }
534             return null;
535         }
536 
537         httpServletResponse.setStatus(401);
538         httpServletResponse.setHeader("WWW-Authenticate", "Basic realm=\"protected-area\"");
539         return null;
540     }
541 
542     public String getAuthType()
543     {
544         return basicAuthParameterName;
545     }
546 
547     protected List<LogoutInterceptor> getLogoutInterceptors()
548     {
549         return getConfig().getInterceptors(LogoutInterceptor.class);
550     }
551 
552     protected ElevatedSecurityGuard getElevatedSecurityGuard()
553     {
554         return getConfig().getElevatedSecurityGuard();
555     }
556 
557     protected RememberMeService getRememberMeService()
558     {
559         return getConfig().getRememberMeService();
560     }
561 
562     private void invalidateSession(HttpServletRequest httpServletRequest)
563     {
564         SessionInvalidator si = new SessionInvalidator(getConfig().getInvalidateSessionExcludeList());
565         si.invalidateSession(httpServletRequest);
566     }
567 
568 }