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