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 principal = new Principal()
87          {
88              public String getName()
89              {
90                  return userName;
91              }
92          };
93  
94          final boolean authenticated = authenticate(principal, password);
95          if (dbg)
96          {
97              log.debug(METHOD + "'" + userName + "' has " + (authenticated ? "been" : "not been") + " authenticated");
98          }
99          if (authenticated)
100         {
101             final Principal user = getUser(userName);
102             if (authoriseUserAndEstablishSession(httpServletRequest, httpServletResponse, user))
103             {
104                 if (setRememberMeCookie && httpServletResponse != null)
105                 {
106                     getRememberMeService().addRememberMeCookie(httpServletRequest, httpServletResponse, userName);
107                 }
108                 return true;
109             }
110             AUTHORISATION_FAILED.stampRequestResponse(httpServletRequest, httpServletResponse);
111         }
112         else
113         {
114             log.info(METHOD + "'" + userName + "' could not be authenticated with the given password");
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      * Authenticates the given user and password. Returns <tt>true</tt> if the authentication succeeds,
291      * and <tt>false</tt> if the authentication details are invalid or if the user is not found.
292      * Implementations of this method must not attempt to downcast the user to an implementation class.
293      *
294      * @param user     the user to authenticate. This object only stores the username of the user.
295      * @param password the password of the user
296      * @return true if the user was successfully authenticated and false otherwise.
297      * @throws AuthenticatorException if an error occurs that stops the user from being authenticated (eg remote communication failure).
298      */
299     protected abstract boolean authenticate(final Principal user, final String password) throws AuthenticatorException;
300 
301     /**
302      * Returns the currently logged in user, trying in order: <p/> <ol> <li>Session, only if one exists</li> <li>Cookie,
303      * only if no session exists</li> <li>Basic authentication, if the above fail, and authType=basic</li> </ol> <p/>
304      * Warning: only in the case of cookie and basic auth will the user be authenticated.
305      *
306      * @param httpServletRequest  the request in play
307      * @param httpServletResponse a response object that may be modified if basic auth is enabled
308      * @return a Principal object for the user if found, otherwise null
309      */
310     @Override
311     public Principal getUser(final HttpServletRequest httpServletRequest, final HttpServletResponse httpServletResponse)
312     {
313         final String METHOD = "getUser : ";
314         final boolean dbg = log.isDebugEnabled();
315 
316         // Rules
317         // - Do not create a session if there is not one already
318         // - If there is a user in the session, return it
319         // - Do not use a remember me cookie if this request was a logout request (e.g. make no assumption about the existence of a session or the existence of a user in that session - see SER-110, SER-114 and SER-95)
320         // - If this is NOT a logout request, feel free to check the remember me cookie
321 
322         final HttpSession session = httpServletRequest.getSession(false);
323         if (session != null)
324         {
325             final Principal sessionUser = getUserFromSession(httpServletRequest);
326             if (sessionUser != null)
327             {
328                 OK.stampRequestResponse(httpServletRequest, httpServletResponse);
329                 return sessionUser;
330             }
331         }
332 
333         // Look for remember me cookie, unless the user has logged out on this request (see Rules comment above)
334         if (!OUT.isStamped(httpServletRequest))
335         {
336             final Principal cookieUser = getUserFromCookie(httpServletRequest, httpServletResponse);
337             if (cookieUser != null)
338             {
339                 return cookieUser;
340             }
341         }
342 
343         if (RedirectUtils.isBasicAuthentication(httpServletRequest, basicAuthParameterName))
344         {
345             final Principal basicAuthUser = getUserFromBasicAuthentication(httpServletRequest, httpServletResponse);
346             if (basicAuthUser != null)
347             {
348                 return basicAuthUser;
349             }
350         }
351 
352         if (dbg)
353         {
354             log.debug(METHOD + "User not found in either Session, Cookie or Basic Auth.");
355         }
356 
357         return null;
358     }
359 
360     /**
361      * This is called to refresh the Principal object that has been retreived from the HTTP session.
362      * <p/>
363      * By default this will called {@link #getUser(String)} again to get a fresh user.
364      *
365      * @param httpServletRequest the HTTP request in play
366      * @param principal          the Principal in play
367      * @return a fresh up to date principal
368      */
369     protected Principal refreshPrincipalObtainedFromSession(HttpServletRequest httpServletRequest, Principal principal)
370     {
371         Principal freshPrincipal = principal;
372         if (principal != null && principal.getName() != null)
373         {
374             freshPrincipal = getUser(principal.getName());
375             putPrincipalInSessionContext(httpServletRequest, freshPrincipal);
376         }
377         return freshPrincipal;
378     }
379 
380     /**
381      * <p> Tries to get a logged in user from the session. </p>
382      *
383      * @param httpServletRequest the current {@link HttpServletRequest}
384      * @return the logged in user in the session. <code>null</code> if there is no logged in user in the session, or the
385      *         {@link #LOGGED_OUT_KEY} is set because the user has logged out.
386      */
387     protected Principal getUserFromSession(final HttpServletRequest httpServletRequest)
388     {
389         final String METHOD = "getUserFromSession : ";
390         final boolean dbg = log.isDebugEnabled();
391         try
392         {
393             if (httpServletRequest.getSession().getAttribute(LOGGED_OUT_KEY) != null)
394             {
395                 if (dbg)
396                 {
397                     log.debug(METHOD + "Session found; user has already logged out. eg has LOGGED_OUT_KEY in session");
398                 }
399                 return null;
400             }
401             final Principal principal = (Principal) httpServletRequest.getSession().getAttribute(LOGGED_IN_KEY);
402             if (dbg)
403             {
404                 if (principal == null)
405                 {
406                     log.debug(METHOD + "Session found; BUT it has no Principal in it");
407                 }
408                 else
409                 {
410                     log.debug(METHOD + "Session found; '" + principal.getName() + "' is present");
411                 }
412             }
413             return refreshPrincipalObtainedFromSession(httpServletRequest, principal);
414         }
415         catch (final Exception e)
416         {
417             log.warn(METHOD + "Exception when retrieving user from session: " + e, e);
418             return null;
419         }
420     }
421 
422     /**
423      * Extracts the username and password from the cookie and calls login to authenticate, and if successful store the
424      * token in the session.
425      *
426      * @param httpServletRequest  the HTTP request in play
427      * @param httpServletResponse the HTTP respone in play
428      * @return a Principal object for the user if successful, otherwise null
429      */
430     protected Principal getUserFromCookie(final HttpServletRequest httpServletRequest, final HttpServletResponse httpServletResponse)
431     {
432         final String METHOD = "getUserFromCookie : ";
433         final boolean dbg = log.isDebugEnabled();
434 
435 
436         final String userName = getRememberMeService().getRememberMeCookieAuthenticatedUsername(httpServletRequest, httpServletResponse);
437         if (dbg)
438         {
439             log.debug(METHOD + "Got username : '" + userName + "' from cookie, attempting to authenticate user is known");
440         }
441         if (StringUtils.isNotBlank(userName))
442         {
443             //
444             // we have a valid user name so we need to turn them into a principal
445             //
446             final Principal principal = getUser(userName);
447             if (principal != null)
448             {
449                 // we have a valid user but is their account in a valid state from a security point of view
450                 final ElevatedSecurityGuard securityGuard = getElevatedSecurityGuard();
451                 if (!securityGuard.performElevatedSecurityCheck(httpServletRequest, userName))
452                 {
453                     if (dbg)
454                     {
455                         log.debug(METHOD + "'" + userName + "' failed elevated security check");
456                     }
457                     AUTHENTICATION_DENIED.stampRequestResponse(httpServletRequest, httpServletResponse);
458                     securityGuard.onFailedLoginAttempt(httpServletRequest, userName);
459                     return null;
460                 }
461                 //
462                 // ok we have a valid Principal but is he allowed to login to the application at this stage
463                 // once we come out of this method, the user will be in the session and hence the "login session"
464                 // will have been established.
465                 //
466                 // In the past this was done all in the login() method but since don't login
467                 // we need to recall this authorisation step here
468                 //
469                 if (authoriseUserAndEstablishSession(httpServletRequest, httpServletResponse, principal))
470                 {
471                     if (dbg)
472                     {
473                         log.debug(METHOD + "Authenticated '" + userName + "' via Remember Me Cookie");
474                     }
475                     OK.stampRequestResponse(httpServletRequest, httpServletResponse);
476                     securityGuard.onSuccessfulLoginAttempt(httpServletRequest, userName);
477                     return principal;
478                 }
479                 if (dbg)
480                 {
481                     log.debug(METHOD + "'" + userName + "' failed authorisation security check");
482                 }
483                 AUTHORISATION_FAILED.stampRequestResponse(httpServletRequest, httpServletResponse);
484                 securityGuard.onFailedLoginAttempt(httpServletRequest, userName);
485             }
486         }
487         return null;
488     }
489 
490     /**
491      * Checks the Authorization header to see whether basic auth token is provided. If it is, decode it, login and
492      * return the valid user. If it isn't, basic auth is still required, so return a 401 Authorization Required header
493      * in the response.
494      *
495      * @param httpServletRequest  the HTTP request in play
496      * @param httpServletResponse a response object that <i>will</i> be modified if no token found
497      * @return a {@link java.security.Principal} or null if one cant be found
498      */
499     protected Principal getUserFromBasicAuthentication(final HttpServletRequest httpServletRequest, final HttpServletResponse httpServletResponse)
500     {
501         final String METHOD = "getUserFromSession : ";
502         final boolean dbg = log.isDebugEnabled();
503 
504         final String header = httpServletRequest.getHeader("Authorization");
505         LoginReason reason = OK;
506 
507         if (SecurityUtils.isBasicAuthorizationHeader(header))
508         {
509             if (dbg)
510             {
511                 log.debug(METHOD + "Looking in Basic Auth headers");
512             }
513 
514             final SecurityUtils.UserPassCredentials creds = SecurityUtils.decodeBasicAuthorizationCredentials(header);
515             final ElevatedSecurityGuard securityGuard = getElevatedSecurityGuard();
516             if (!securityGuard.performElevatedSecurityCheck(httpServletRequest, creds.getUsername()))
517             {
518                 if (dbg)
519                 {
520                     log.debug(METHOD + "'" + creds.getUsername() + "' failed elevated security check");
521                 }
522                 reason = AUTHENTICATION_DENIED.stampRequestResponse(httpServletRequest, httpServletResponse);
523                 securityGuard.onFailedLoginAttempt(httpServletRequest, creds.getUsername());
524             }
525             else
526             {
527                 if (dbg)
528                 {
529                     log.debug(METHOD + "'" + creds.getUsername() + "' does not require elevated security check.  Attempting authentication...");
530                 }
531 
532                 try
533                 {
534                     final boolean loggedin = login(httpServletRequest, httpServletResponse, creds.getUsername(),
535                             creds.getPassword(), false);
536                     if (loggedin)
537                     {
538                         reason = OK.stampRequestResponse(httpServletRequest, httpServletResponse);
539                         securityGuard.onSuccessfulLoginAttempt(httpServletRequest, creds.getUsername());
540                         if (dbg)
541                         {
542                             log.debug(METHOD + "Authenticated '" + creds.getUsername() + "' via Basic Auth");
543                         }
544                         return getUser(creds.getUsername());
545                     }
546                     else
547                     {
548                         reason = AUTHENTICATED_FAILED.stampRequestResponse(httpServletRequest, httpServletResponse);
549                         securityGuard.onFailedLoginAttempt(httpServletRequest, creds.getUsername());
550                     }
551                 }
552                 catch (final AuthenticatorException e)
553                 {
554                     log.warn(METHOD + "Exception trying to login '" + creds.getUsername() + "' via Basic Auth:" + e, e);
555                 }
556             }
557             try
558             {
559                 httpServletResponse.sendError(401, "Basic Authentication Failure - Reason : " + reason.toString());
560             }
561             catch (final IOException e)
562             {
563                 log.warn(METHOD + "Exception trying to send Basic Auth failed error: " + e, e);
564             }
565             return null;
566         }
567 
568         httpServletResponse.setStatus(401);
569         httpServletResponse.setHeader("WWW-Authenticate", "Basic realm=\"protected-area\"");
570         return null;
571     }
572 
573     public String getAuthType()
574     {
575         return basicAuthParameterName;
576     }
577 
578     protected List<LogoutInterceptor> getLogoutInterceptors()
579     {
580         return getConfig().getInterceptors(LogoutInterceptor.class);
581     }
582 
583     protected ElevatedSecurityGuard getElevatedSecurityGuard()
584     {
585         return getConfig().getElevatedSecurityGuard();
586     }
587 
588     protected RememberMeService getRememberMeService()
589     {
590         return getConfig().getRememberMeService();
591     }
592 
593     private void invalidateSession(HttpServletRequest httpServletRequest)
594     {
595         SessionInvalidator si = new SessionInvalidator(getConfig().getInvalidateSessionExcludeList());
596         si.invalidateSession(httpServletRequest);
597     }
598 
599 }