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