1   package com.atlassian.seraph.service.rememberme;
2   
3   import com.atlassian.seraph.cookie.HttpOnlyCookieKit;
4   import com.atlassian.seraph.spi.rememberme.RememberMeConfiguration;
5   import com.atlassian.seraph.spi.rememberme.RememberMeTokenDao;
6   import org.apache.commons.lang.StringUtils;
7   import org.apache.log4j.Logger;
8   
9   import java.io.UnsupportedEncodingException;
10  import java.net.URLDecoder;
11  import java.net.URLEncoder;
12  import javax.servlet.http.Cookie;
13  import javax.servlet.http.HttpServletRequest;
14  import javax.servlet.http.HttpServletResponse;
15  
16  /**
17   * This default RememberMeService needs to have a certain SPI implementations into it so that it can function.  This is
18   * what the application needs to provide.  Most of the other default implementations can be used as is.
19   */
20  public class DefaultRememberMeService implements RememberMeService
21  {
22      private static final Logger log = Logger.getLogger(DefaultRememberMeService.class);
23  
24      private final RememberMeConfiguration rememberMeConfiguration;
25      private final RememberMeTokenDao rememberMeTokenDao;
26      private final RememberMeTokenGenerator rememberMeTokenGenerator;
27  
28      public DefaultRememberMeService(final RememberMeConfiguration rememberMeConfiguration, final RememberMeTokenDao rememberMeTokenDao, final RememberMeTokenGenerator rememberMeTokenGenerator)
29      {
30          this.rememberMeConfiguration = rememberMeConfiguration;
31          this.rememberMeTokenDao = rememberMeTokenDao;
32          this.rememberMeTokenGenerator = rememberMeTokenGenerator;
33      }
34  
35      public String getRememberMeCookieAuthenticatedUsername(final HttpServletRequest httpServletRequest, final HttpServletResponse httpServletResponse)
36      {
37          // do they have the remember me cookie set
38          RememberMeToken cookieToken = getCookieValue(httpServletRequest);
39          if (cookieToken != null)
40          {
41              // yes they do so lets check it against the app
42              RememberMeToken storedToken = rememberMeTokenDao.findById(cookieToken.getId());
43              if (storedToken != null)
44              {
45                  if (cookieToken.getRandomString().equals(storedToken.getRandomString()))
46                  {
47                      return storedToken.getUserName();
48                  }
49              }
50              // ok they token is not valid so we need to remove it from the request
51              removeRememberMeCookie(httpServletRequest, httpServletResponse);
52          }
53          return null;
54      }
55  
56      public void addRememberMeCookie(final HttpServletRequest httpServletRequest, final HttpServletResponse httpServletResponse, final String authenticatedUsername)
57      {
58          final RememberMeToken token = rememberMeTokenGenerator.generateToken(authenticatedUsername);
59          final RememberMeToken persistedToken = rememberMeTokenDao.save(token);
60  
61          final String desiredCookieName = rememberMeConfiguration.getCookieName();
62          Cookie cookie = findRememberCookie(httpServletRequest, desiredCookieName);
63          if (cookie == null)
64          {
65              cookie = new Cookie(desiredCookieName, persistedToken.getRandomString());
66          }
67          setValuesIntoCookie(httpServletRequest, cookie, toCookieValue(persistedToken),
68                  rememberMeConfiguration.getCookieMaxAgeInSeconds(),
69                  rememberMeConfiguration.getCookieDomain(httpServletRequest),
70                  rememberMeConfiguration.getCookiePath(httpServletRequest),
71                  rememberMeConfiguration.isInsecureCookieAlwaysUsed()
72          );
73          setRememberMeCookie(httpServletResponse, cookie);
74      }
75  
76      public void removeRememberMeCookie(final HttpServletRequest httpServletRequest, final HttpServletResponse httpServletResponse)
77      {
78          final Cookie cookie = findRememberCookie(httpServletRequest, rememberMeConfiguration.getCookieName());
79          if (cookie != null)
80          {
81              setValuesIntoCookie(httpServletRequest, cookie, "", 0,
82                      rememberMeConfiguration.getCookieDomain(httpServletRequest),
83                      rememberMeConfiguration.getCookiePath(httpServletRequest),
84                      rememberMeConfiguration.isInsecureCookieAlwaysUsed()
85              );
86              setRememberMeCookie(httpServletResponse, cookie);
87          }
88      }
89  
90      private void setValuesIntoCookie(final HttpServletRequest httpServletRequest, final Cookie cookie, final String value, final int maxAgeInSeconds, final String cookieDomain, final String cookiePath, final boolean isInsecureCookieUsed)
91      {
92          if (StringUtils.isNotBlank(cookieDomain))
93          {
94              cookie.setDomain(cookieDomain);
95          }
96          if (StringUtils.isNotBlank(cookiePath))
97          {
98              cookie.setPath(cookiePath);
99          }
100         if (!isInsecureCookieUsed)
101         {
102             cookie.setSecure(httpServletRequest.isSecure());
103         }
104         cookie.setMaxAge(maxAgeInSeconds);
105         cookie.setValue(escapeInvalidCookieCharacters(value));
106     }
107 
108     private void setRememberMeCookie(final HttpServletResponse httpServletResponse, final Cookie cookie)
109     {
110         if (rememberMeConfiguration.isCookieHttpOnly())
111         {
112             HttpOnlyCookieKit.setCookie(httpServletResponse, cookie);
113         }
114         else
115         {
116             httpServletResponse.addCookie(cookie);
117         }
118     }
119 
120     private String toCookieValue(final RememberMeToken persistedToken)
121     {
122         return persistedToken.getId() + ":" + persistedToken.getRandomString();
123     }
124 
125     /**
126      * Returns the value of the remember me cookie if its present ot NULL if its not there
127      *
128      * @param httpServletRequest the request in play
129      *
130      * @return the RememberMeToken  or null if the cookie is not there or is not a valid value
131      */
132     private RememberMeToken getCookieValue(final HttpServletRequest httpServletRequest)
133     {
134         final Cookie cookie = findRememberCookie(httpServletRequest, rememberMeConfiguration.getCookieName());
135         if (cookie != null)
136         {
137             return parseIntoToken(unescapeInvalidCookieCharacters(cookie.getValue()));
138         }
139         return null;
140     }
141 
142     private RememberMeToken parseIntoToken(final String value)
143     {
144         if (StringUtils.isBlank(value))
145         {
146             return null;
147         }
148         int indexColon = value.indexOf(':');
149         if (indexColon <= 0 || indexColon == value.length() - 1)
150         {
151             return null;
152         }
153         Long id;
154         try
155         {
156             id = Long.parseLong(value.substring(0, indexColon));
157         }
158         catch (NumberFormatException e)
159         {
160             return null;
161         }
162         String randomString = value.substring(indexColon + 1);
163         return DefaultRememberMeToken.builder(id, randomString).build();
164     }
165 
166     private Cookie findRememberCookie(HttpServletRequest httpServletRequest, final String cookieName)
167     {
168         final Cookie[] cookies = httpServletRequest.getCookies();
169         for (Cookie cookie : cookies)
170         {
171             if (cookieName.equalsIgnoreCase(cookie.getName()))
172             {
173                 return cookie;
174             }
175         }
176         return null;
177 
178     }
179 
180     private static final String URL_ENCODING = "UTF-8";
181 
182     /**
183      * Escape invalid cookie characters, see SER-117
184      *
185      * @param s the String to escape characters for.
186      *
187      * @return the encoded string.
188      *
189      * @see #unescapeInvalidCookieCharacters(String)
190      */
191     private static String escapeInvalidCookieCharacters(final String s)
192     {
193         try
194         {
195             return URLEncoder.encode(s, URL_ENCODING);
196         }
197         catch (final UnsupportedEncodingException e)
198         {
199             throw new AssertionError(e);
200         }
201     }
202 
203     /**
204      * Un-escape invalid cookie characters, see SER-117
205      *
206      * @param s the String to escape characters for.
207      *
208      * @return the encoded string.
209      *
210      * @see #escapeInvalidCookieCharacters(String)
211      */
212     private static String unescapeInvalidCookieCharacters(final String s)
213     {
214         try
215         {
216             return URLDecoder.decode(s, URL_ENCODING);
217         }
218         catch (final UnsupportedEncodingException e)
219         {
220             log.fatal("UTF-8 encoding unsupported !!?!! How is that possible in java?", e);
221             throw new AssertionError(e);
222         }
223     }
224 }