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              // ok they token is not valid so we need to remove it from the request
56              removeRememberMeCookie(httpServletRequest, httpServletResponse);
57          }
58          return null;
59      }
60  
61      private boolean isExpired(RememberMeToken storedToken)
62      {
63          return storedToken.getCreatedTime() + TimeUnit.SECONDS.toMillis(rememberMeConfiguration.getCookieMaxAgeInSeconds()) < System.currentTimeMillis();
64      }
65  
66      public void addRememberMeCookie(final HttpServletRequest httpServletRequest, final HttpServletResponse httpServletResponse, final String authenticatedUsername)
67      {
68          final RememberMeToken token = rememberMeTokenGenerator.generateToken(authenticatedUsername);
69          final RememberMeToken persistedToken = rememberMeTokenDao.save(token);
70  
71          final String desiredCookieName = rememberMeConfiguration.getCookieName();
72          Cookie cookie = findRememberCookie(httpServletRequest, desiredCookieName);
73          if (cookie == null)
74          {
75              cookie = new Cookie(desiredCookieName, persistedToken.getRandomString());
76          }
77          setValuesIntoCookie(httpServletRequest, cookie, toCookieValue(persistedToken),
78                  rememberMeConfiguration.getCookieMaxAgeInSeconds(),
79                  rememberMeConfiguration.getCookieDomain(httpServletRequest),
80                  rememberMeConfiguration.getCookiePath(httpServletRequest),
81                  rememberMeConfiguration.isInsecureCookieAlwaysUsed()
82          );
83          setRememberMeCookie(httpServletRequest, httpServletResponse, cookie);
84      }
85  
86      public void removeRememberMeCookie(final HttpServletRequest httpServletRequest, final HttpServletResponse httpServletResponse)
87      {
88          final Cookie cookie = findRememberCookie(httpServletRequest, rememberMeConfiguration.getCookieName());
89          if (cookie != null)
90          {
91              RememberMeToken cookieToken = parseIntoToken(cookie);
92              
93              setValuesIntoCookie(httpServletRequest, cookie, "", 0,
94                      rememberMeConfiguration.getCookieDomain(httpServletRequest),
95                      rememberMeConfiguration.getCookiePath(httpServletRequest),
96                      rememberMeConfiguration.isInsecureCookieAlwaysUsed()
97              );
98              setRememberMeCookie(httpServletRequest, httpServletResponse, cookie);
99  
100             // and now remove it from the application store as well
101             if (cookieToken != null)
102             {
103                 rememberMeTokenDao.remove(cookieToken.getId());
104             }
105         }
106     }
107 
108     private void setValuesIntoCookie(final HttpServletRequest httpServletRequest, final Cookie cookie,
109         final String value, final int maxAgeInSeconds, final String cookieDomain,
110         final String cookiePath, final boolean isInsecureCookieUsed)
111     {
112         if (StringUtils.isNotBlank(cookieDomain))
113         {
114             cookie.setDomain(cookieDomain);
115         }
116         if (StringUtils.isNotBlank(cookiePath))
117         {
118             cookie.setPath(cookiePath);
119         }
120         if (!isInsecureCookieUsed)
121         {
122             cookie.setSecure(httpServletRequest.isSecure());
123         }
124         cookie.setMaxAge(maxAgeInSeconds);
125         cookie.setValue(escapeInvalidCookieCharacters(value));
126     }
127 
128     private void setRememberMeCookie(final HttpServletRequest httpServletRequest,
129         final HttpServletResponse httpServletResponse, final Cookie cookie)
130     {
131         if (rememberMeConfiguration.isCookieHttpOnly(httpServletRequest))
132         {
133             HttpOnlyCookies.addHttpOnlyCookie(httpServletResponse, cookie);
134         }
135         else
136         {
137             httpServletResponse.addCookie(cookie);
138         }
139     }
140 
141     private String toCookieValue(final RememberMeToken persistedToken)
142     {
143         return persistedToken.getId() + ":" + persistedToken.getRandomString();
144     }
145 
146     /**
147      * Returns the value of the remember me cookie if its present ot NULL if its not there
148      *
149      * @param httpServletRequest the request in play
150      *
151      * @return the RememberMeToken  or null if the cookie is not there or is not a valid value
152      */
153     private RememberMeToken getCookieValue(final HttpServletRequest httpServletRequest)
154     {
155         final Cookie cookie = findRememberCookie(httpServletRequest, rememberMeConfiguration.getCookieName());
156         if (cookie != null)
157         {
158             return parseIntoToken(cookie);
159         }
160         return null;
161     }
162 
163     private RememberMeToken parseIntoToken(final Cookie cookie)
164     {
165         final String value = unescapeInvalidCookieCharacters(cookie.getValue());
166         if (StringUtils.isBlank(value))
167         {
168             return null;
169         }
170         int indexColon = value.indexOf(':');
171         if (indexColon <= 0 || indexColon == value.length() - 1)
172         {
173             return null;
174         }
175         Long id;
176         try
177         {
178             id = Long.parseLong(value.substring(0, indexColon));
179         }
180         catch (NumberFormatException e)
181         {
182             return null;
183         }
184         String randomString = value.substring(indexColon + 1);
185         return DefaultRememberMeToken.builder(id, randomString).build();
186     }
187 
188     private Cookie findRememberCookie(HttpServletRequest httpServletRequest, final String cookieName)
189     {
190         final Cookie[] cookies = httpServletRequest.getCookies();
191         if (cookies != null)
192         {
193             for (Cookie cookie : cookies)
194             {
195                 if (cookieName.equalsIgnoreCase(cookie.getName()))
196                 {
197                     return cookie;
198                 }
199             }
200         }
201         return null;
202 
203     }
204 
205     private static final String URL_ENCODING = "UTF-8";
206 
207     /**
208      * Escape invalid cookie characters, see SER-117
209      *
210      * @param s the String to escape characters for.
211      *
212      * @return the encoded string.
213      *
214      * @see #unescapeInvalidCookieCharacters(String)
215      */
216     private static String escapeInvalidCookieCharacters(final String s)
217     {
218         try
219         {
220             return URLEncoder.encode(s, URL_ENCODING);
221         }
222         catch (final UnsupportedEncodingException e)
223         {
224             throw new AssertionError(e);
225         }
226     }
227 
228     /**
229      * Un-escape invalid cookie characters, see SER-117
230      *
231      * @param s the String to escape characters for.
232      *
233      * @return the encoded string.
234      *
235      * @see #escapeInvalidCookieCharacters(String)
236      */
237     private static String unescapeInvalidCookieCharacters(final String s)
238     {
239         try
240         {
241             return URLDecoder.decode(s, URL_ENCODING);
242         }
243         catch (final UnsupportedEncodingException e)
244         {
245             log.fatal("UTF-8 encoding unsupported !!?!! How is that possible in java?", e);
246             throw new AssertionError(e);
247         }
248     }
249 }