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