View Javadoc
1   package com.atlassian.refapp.auth.internal;
2   
3   import com.atlassian.refapp.auth.external.WebSudoSessionManager;
4   import com.atlassian.seraph.auth.AbstractAuthenticator;
5   import com.atlassian.seraph.auth.AuthenticatorException;
6   import com.atlassian.seraph.auth.RoleMapper;
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.interceptor.LogoutInterceptor;
12  import com.atlassian.seraph.util.RedirectUtils;
13  import com.atlassian.user.EntityException;
14  import com.atlassian.user.UserManager;
15  import com.atlassian.user.security.authentication.Authenticator;
16  import com.atlassian.user.util.Base64Encoder;
17  import org.apache.commons.logging.Log;
18  import org.apache.commons.logging.LogFactory;
19  
20  import javax.servlet.http.Cookie;
21  import javax.servlet.http.HttpServletRequest;
22  import javax.servlet.http.HttpServletResponse;
23  import java.io.IOException;
24  import java.security.Principal;
25  import java.util.Iterator;
26  import java.util.List;
27  import java.util.Map;
28  
29  public class AtlassianUserAuthenticator extends AbstractAuthenticator {
30      /**
31       * The key used to store the user object in the session
32       */
33      public static final String LOGGED_IN_KEY = "seraph_defaultauthenticator_user";
34  
35      /**
36       * The key used to indicate that the user has logged out and session regarding of it containing a cookie is not
37       * logged in.
38       */
39      public static final String LOGGED_OUT_KEY = "seraph_defaultauthenticator_logged_out_user";
40  
41      private final Log log = LogFactory.getLog(getClass());
42  
43      // --------------------------------------------------------------------------------------------------------- members
44  
45      private final UserManager userManager;
46      private final Authenticator authenticator;
47      private final WebSudoSessionManager websudoManager;
48  
49      private String loginCookieKey;
50      private String authType;
51      private int autoLoginCookieAge;
52      private String loginCookiePath;
53  
54      public AtlassianUserAuthenticator(UserManager userManager, Authenticator authenticator, WebSudoSessionManager websudoManager) {
55          this.userManager = userManager;
56          this.authenticator = authenticator;
57          this.websudoManager = websudoManager;
58      }
59  
60      public void init(Map params, SecurityConfig config) {
61          if (log.isDebugEnabled()) {
62              log.debug(this.getClass().getName() + " $Revision: 16581 $ initializing");
63          }
64          super.init(params, config);
65          this.loginCookieKey = config.getLoginCookieKey();
66          this.authType = config.getAuthType();
67          this.autoLoginCookieAge = config.getAutoLoginCookieAge();
68          this.loginCookiePath = config.getLoginCookiePath();
69      }
70  
71      /**
72       * @deprecated Use {@link RoleMapper} directly
73       */
74      public boolean isUserInRole(HttpServletRequest request, String role) {
75          return getRoleMapper().hasRole(getUser(request), request, role);
76      }
77  
78      /**
79       * Tries to authenticate a user (via OSUser). If successful, sets a session attribute and cookie indicating their
80       * logged-in status.
81       *
82       * @return Whether the user was authenticated. This base implementation returns false if any errors occur, rather
83       * than throw an exception.
84       */
85      public boolean login(HttpServletRequest request, HttpServletResponse response, String username, String password, boolean cookie)
86              throws AuthenticatorException {
87          final boolean dbg = log.isDebugEnabled();
88          CookieHandler cookieHandler = CookieFactory.getCookieHandler();
89  
90          // check that they can login (they have the USE permission or ADMINISTER permission)
91          boolean authenticated = authenticate(username, password);
92          if (dbg) {
93              log.debug("User : " + username + " has " + (authenticated ? "been" : "no been") + " authenticated");
94          }
95          if (authenticated) {
96              Principal user = getUser(username);
97              request.getSession().setAttribute(LOGGED_IN_KEY, user);
98              request.getSession().setAttribute(LOGGED_OUT_KEY, null);
99              if (request.getParameter("os_websudo") != null) {
100                 websudoManager.createWebSudoSession(request);
101             }
102 
103             final boolean canLogin = getRoleMapper().canLogin(user, request);
104             if (dbg) {
105                 log.debug("User : " + username + " " + (canLogin ? "can" : "CANT") + " login according to the RoleMapper");
106             }
107             if (canLogin) {
108                 if (cookie && response != null) {
109                     cookieHandler.setCookie(request, response, getLoginCookieKey(), encodeCookie(username, password), autoLoginCookieAge, getCookiePath(request));
110                 }
111                 return true;
112             } else {
113                 request.getSession().removeAttribute(LOGGED_IN_KEY);
114             }
115         }
116 
117         if (response != null && cookieHandler.getCookie(request, getLoginCookieKey()) != null) {
118             log.warn("User: " + username + " tried to login but they do not have USE permission or weren't found. Deleting cookie.");
119 
120             try {
121                 cookieHandler.invalidateCookie(request, response, getLoginCookieKey(), getCookiePath(request));
122             } catch (Exception e) {
123                 log.error("Could not invalidate cookie: " + e, e);
124             }
125         }
126 
127         return false;
128     }
129 
130     // override this method if you need to retrieve the role mapper from elsewhere than the singleton-factory (injected
131     // depency for instance)
132     protected RoleMapper getRoleMapper() {
133         return SecurityConfigFactory.getInstance().getRoleMapper();
134     }
135 
136 
137     public boolean logout(HttpServletRequest request, HttpServletResponse response) throws AuthenticatorException {
138         boolean dbg = log.isDebugEnabled();
139         if (dbg) {
140             log.debug("logout requested.  Calling interceptors and clearing cookies");
141         }
142         List interceptors = getLogoutInterceptors();
143         CookieHandler cookieHandler = CookieFactory.getCookieHandler();
144 
145         for (Iterator iterator = interceptors.iterator(); iterator.hasNext(); ) {
146             LogoutInterceptor interceptor = (LogoutInterceptor) iterator.next();
147             interceptor.beforeLogout(request, response);
148         }
149 
150         request.getSession().setAttribute(LOGGED_IN_KEY, null);
151         request.getSession().setAttribute(LOGGED_OUT_KEY, Boolean.TRUE);
152 
153         // Logout is sometimes called as part of a getUser request, if the user is not found
154         // logout may be called, but some getUser calls only pass in the request, and null response.
155         if (response != null && cookieHandler.getCookie(request, getLoginCookieKey()) != null) {
156             try {
157                 cookieHandler.invalidateCookie(request, response, getLoginCookieKey(), getCookiePath(request));
158             } catch (Exception e) {
159                 log.error("Could not invalidate cookie: " + e, e);
160             }
161         }
162 
163         for (Iterator iterator = interceptors.iterator(); iterator.hasNext(); ) {
164             LogoutInterceptor interceptor = (LogoutInterceptor) iterator.next();
165             interceptor.afterLogout(request, response);
166         }
167 
168         return true;
169     }
170 
171     /**
172      * Returns the currently logged in user, trying in order:
173      *
174      * <ol>
175      * <li>Session, only if one exists</li>
176      * <li>Cookie, only if no session exists</li>
177      * <li>Basic authentication, if the above fail, and authType=basic</li>
178      * </ol>
179      * <p><b>Warning</b>: only in the case of cookie and basic auth will the user be authenticated. </p>
180      *
181      * @param response a response object that may be modified if basic auth is enabled
182      * @return a Principal object for the user if found, otherwise null
183      */
184     public Principal getUser(HttpServletRequest request, HttpServletResponse response) {
185         final boolean dbg = log.isDebugEnabled();
186         if (request.getSession(false) != null) {
187             Principal sessionUser = getUserFromSession(request);
188             if (sessionUser != null) {
189                 if (dbg) {
190                     log.debug("Session found; BUT user doesn't exist");
191                 }
192                 return sessionUser;
193             }
194         } else {
195             Principal cookieUser = getUserFromCookie(request, response);
196             if (cookieUser != null) return cookieUser;
197             log.debug("Cannot log user in via a cookie");
198         }
199 
200         if (RedirectUtils.isBasicAuthentication(request, authType)) {
201             Principal basicAuthUser = getUserFromBasicAuthentication(request, response);
202             if (basicAuthUser != null) return basicAuthUser;
203         }
204 
205         if (dbg) {
206             log.debug("User not logged in.");
207         }
208 
209         return null;
210     }
211 
212     /**
213      * Extracts the username and password from the cookie and calls login to authenticate, and if successful store the
214      * token in the session.
215      *
216      * @param request  to get cookie for
217      * @param response to set cookie in
218      * @return a Principal object for the user if successful, otherwise null
219      */
220     protected Principal getUserFromCookie(HttpServletRequest request, HttpServletResponse response) {
221         final boolean dbg = log.isDebugEnabled();
222         final String cookieName = getLoginCookieKey();
223         final Cookie cookie = CookieFactory.getCookieHandler().getCookie(request, cookieName);
224         if (cookie == null) {
225             return null;
226         }
227 
228         final String cookieValue = cookie.getValue();
229         if (dbg) {
230             log.debug("Found cookie : '" + cookieName + "' with value : '" + cookieValue + "'");
231         }
232         final String[] values = decodeCookie(cookieValue);
233         if (values == null) {
234             if (dbg) {
235                 log.debug("Unable to decode " + cookieName + " cookie with value : '" + cookieValue + "'");
236             }
237             return null;
238         }
239 
240         final String username = values[0];
241         final String password = values[1];
242         if (dbg) {
243             log.debug("Got username : '" + username + "' and password from cookie, attempting to authenticate user");
244         }
245 
246         try {
247             if (!login(request, response, username, password, false)) {
248                 return null;
249             }
250         } catch (Exception e) {
251             log.warn("Cookie login for user : '" + username + "' failed with exception: " + e, e);
252             return null;
253         }
254 
255         if (dbg) {
256             log.debug("Logged user : '" + username + "' in via a cookie");
257         }
258         return getUserFromSession(request);
259     }
260 
261     /**
262      * <p>
263      * Tries to get a logged in user from the session.
264      * </p>
265      *
266      * @param request the current {@link HttpServletRequest}
267      * @return the logged in user in the session. <code>null</code> if there is no logged in user in the session, or
268      * the {@link #LOGGED_OUT_KEY} is set because the user has logged out.
269      */
270     protected Principal getUserFromSession(HttpServletRequest request) {
271         final boolean dbg = log.isDebugEnabled();
272         try {
273             if (request.getSession().getAttribute(LOGGED_OUT_KEY) != null) {
274                 if (dbg) {
275                     log.debug("Session found; user has already logged out");
276                 }
277                 return null;
278             }
279             if (request.getSession().getAttribute(LOGGED_IN_KEY) == null) {
280                 return null;
281             }
282             final Principal principal = (Principal) request.getSession().getAttribute(LOGGED_IN_KEY);
283             if (dbg) {
284                 if (principal == null) {
285                     log.debug("Session found; BUT it has no Principal in it");
286                 } else {
287                     log.debug("Session found; user : '" + principal.getName() + "' already logged in");
288                 }
289             }
290             return principal;
291         } catch (Exception e) {
292             log.warn("Exception when retrieving user from session: " + e, e);
293             return null;
294         }
295     }
296 
297     /**
298      * Checks the Authorization header to see whether basic auth token is provided. If it is, decode it, login and
299      * return the valid user. If it isn't, basic auth is still required, so return a 401 Authorization Required header
300      * in the response.
301      *
302      * @param request  object to get headers from
303      * @param response a response object that <i>will</i> be modified if no token found
304      * @return Logged user or <code>null</code> if no user logger in
305      */
306     protected Principal getUserFromBasicAuthentication(HttpServletRequest request, HttpServletResponse response) {
307         final boolean dbg = log.isDebugEnabled();
308         String header = request.getHeader("Authorization");
309 
310         if (header != null && header.startsWith("Basic ")) {
311             if (dbg) {
312                 log.debug("Looking in Basic Auth headers");
313             }
314             String base64Token = header.substring(6);
315             String token = new String(Base64Encoder.decode(base64Token.getBytes()));
316 
317             String username = "";
318             String password = "";
319 
320             int delim = token.indexOf(":");
321 
322             if (delim != -1) {
323                 username = token.substring(0, delim);
324                 password = token.substring(delim + 1);
325             }
326 
327             try {
328                 if (login(request, response, username, password, false)) {
329                     if (dbg) {
330                         log.debug("Logged in user : '" + username + "' via basic auth");
331                     }
332                     return getUser(username);
333                 }
334             } catch (AuthenticatorException e) {
335                 log.warn("Exception trying to login user : '" + username + "' via basic auth:" + e, e);
336             }
337             try {
338                 response.sendError(401);
339             } catch (IOException e) {
340                 log.warn("Exception trying to send basic auth failed error: " + e, e);
341             }
342             return null;
343         }
344 
345         if (response == null) {
346             return null;
347         }
348 
349         response.setStatus(401);
350         response.setHeader("WWW-Authenticate", "BASIC realm=\"protected-area\"");
351         return null;
352     }
353 
354     /**
355      * Root the login cookie at the same location as the webapp.
356      * <p> Anyone wanting a different cookie path policy can
357      * override the authenticator and provide one. </p>
358      *
359      * @param request to c
360      * @return login path
361      */
362     protected String getCookiePath(HttpServletRequest request) {
363         if (getLoginCookiePath() != null)
364             return getLoginCookiePath();
365 
366         String path = request.getContextPath();
367         if (path == null || path.equals("")) {
368             return "/";
369         }
370 
371         // The spec says this should never happen, but just to be sure...
372         if (!path.startsWith("/")) {
373             return "/" + path;
374         }
375 
376         return path;
377     }
378 
379     protected String getLoginCookieKey() {
380         return loginCookieKey;
381     }
382 
383     public String getAuthType() {
384         return authType;
385     }
386 
387     protected List getLogoutInterceptors() {
388         return getConfig().getInterceptors(LogoutInterceptor.class);
389     }
390 
391     protected String encodeCookie(final String username, final String password) {
392         return CookieFactory.getCookieEncoder().encodePasswordCookie(username, password, getConfig().getCookieEncoding());
393     }
394 
395     protected String[] decodeCookie(final String value) {
396         return CookieFactory.getCookieEncoder().decodePasswordCookie(value, getConfig().getCookieEncoding());
397     }
398 
399     protected String getLoginCookiePath() {
400         return loginCookiePath;
401     }
402 
403     protected Principal getUser(String username) {
404         try {
405             return userManager.getUser(username);
406         } catch (EntityException e) {
407             return null;
408         }
409     }
410 
411     protected boolean authenticate(String username, String password) {
412         try {
413             boolean authenticated = authenticator.authenticate(username, password);
414             if (authenticated)
415                 log.info("User '" + username + "' successfully logged in");
416             else
417                 log.info("Cannot login user '" + username + "' as they used an incorrect password");
418             return authenticated;
419         } catch (EntityException e) {
420             log.info("Cannot login user '" + username + "' as they do not exist.");
421             return false;
422         }
423     }
424 }