1. Project Clover database Tue Dec 20 2016 21:24:09 CET
  2. Package org.xwiki.csrf.internal

File DefaultCSRFToken.java

 

Coverage histogram

../../../../img/srcFileCovDistChart9.png
38% of files have more coverage

Code metrics

14
50
11
1
261
142
22
0.44
4.55
11
2

Classes

Class Line # Actions
DefaultCSRFToken 64 50 0% 22 8
0.893333389.3%
 

Contributing tests

This file is covered by 9 tests. .

Source view

1    /*
2    * See the NOTICE file distributed with this work for additional
3    * information regarding copyright ownership.
4    *
5    * This is free software; you can redistribute it and/or modify it
6    * under the terms of the GNU Lesser General Public License as
7    * published by the Free Software Foundation; either version 2.1 of
8    * the License, or (at your option) any later version.
9    *
10    * This software is distributed in the hope that it will be useful,
11    * but WITHOUT ANY WARRANTY; without even the implied warranty of
12    * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13    * Lesser General Public License for more details.
14    *
15    * You should have received a copy of the GNU Lesser General Public
16    * License along with this software; if not, write to the Free
17    * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
18    * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
19    */
20    package org.xwiki.csrf.internal;
21   
22    import java.io.UnsupportedEncodingException;
23    import java.net.URLEncoder;
24    import java.security.NoSuchAlgorithmException;
25    import java.security.SecureRandom;
26    import java.util.concurrent.ConcurrentHashMap;
27    import java.util.concurrent.ConcurrentMap;
28   
29    import javax.inject.Inject;
30    import javax.inject.Singleton;
31    import javax.servlet.http.HttpServletRequest;
32   
33    import org.apache.commons.codec.binary.Base64;
34    import org.slf4j.Logger;
35    import org.xwiki.bridge.DocumentAccessBridge;
36    import org.xwiki.component.annotation.Component;
37    import org.xwiki.component.phase.Initializable;
38    import org.xwiki.component.phase.InitializationException;
39    import org.xwiki.container.Container;
40    import org.xwiki.container.Request;
41    import org.xwiki.container.servlet.ServletRequest;
42    import org.xwiki.container.servlet.filters.SavedRequestManager;
43    import org.xwiki.csrf.CSRFToken;
44    import org.xwiki.csrf.CSRFTokenConfiguration;
45    import org.xwiki.model.reference.DocumentReference;
46   
47    /**
48    * Concrete implementation of the {@link CSRFToken} component.
49    * <p>
50    * This implementation uses a <code>user =&gt; token</code> map to store the tokens. The tokens are random BASE64
51    * encoded bit-strings.
52    * </p>
53    * <p>
54    * TODO Expire tokens every couple of hours (configurable). Expiration can be implemented using two maps, oldTokens and
55    * currentTokens, old tokens are replaced by current tokens every 1/2 period, check is performed on both and new tokens
56    * are added to the current tokens.
57    * </p>
58    *
59    * @version $Id: c6c6938e23a130b321353829654912e7bb4fc29c $
60    * @since 2.5M2
61    */
62    @Component
63    @Singleton
 
64    public class DefaultCSRFToken implements CSRFToken, Initializable
65    {
66    /** Length of the random string in bytes. */
67    private static final int TOKEN_LENGTH = 16;
68   
69    /** Resubmission template name. */
70    private static final String RESUBMIT_TEMPLATE = "resubmit";
71   
72    /** Token storage (one token per user). */
73    private final ConcurrentMap<DocumentReference, String> tokens = new ConcurrentHashMap<DocumentReference, String>();
74   
75    /** Token for guest user. */
76    private String guestToken;
77   
78    /** Random number generator. */
79    private SecureRandom random;
80   
81    /** Used to find out the current user name and the current document. */
82    @Inject
83    private DocumentAccessBridge docBridge;
84   
85    /** Needed to access the current request. */
86    @Inject
87    private Container container;
88   
89    /** CSRFToken component configuration. */
90    @Inject
91    private CSRFTokenConfiguration configuration;
92   
93    /**
94    * The logger to log.
95    */
96    @Inject
97    private Logger logger;
98   
99    /**
100    * {@inheritDoc}
101    * <p>
102    * Initializes the storage and random number generator.
103    * </p>
104    */
 
105  29 toggle @Override
106    public void initialize() throws InitializationException
107    {
108  29 try {
109  29 this.random = SecureRandom.getInstance("SHA1PRNG");
110    } catch (NoSuchAlgorithmException e) {
111    // use the default implementation then
112  0 this.random = new SecureRandom();
113  0 this.logger.warn("CSRFToken: Using default implementation of SecureRandom");
114    }
115  29 byte[] seed = this.random.generateSeed(TOKEN_LENGTH);
116  29 this.random.setSeed(seed);
117  29 this.logger.debug("CSRFToken: Anti-CSRF secret token component has been initialized");
118    }
119   
120    /**
121    * Set the source of random numbers manually.
122    *
123    * @param random a source of random numbers to use.
124    */
 
125  9 toggle protected void setRandom(final SecureRandom random)
126    {
127  9 this.random = random;
128    }
129   
 
130  2488 toggle @Override
131    public String getToken()
132    {
133  2488 DocumentReference key = getTokenKey();
134    // Handle the case where the current user is Guest
135  2488 if (key == null) {
136  193 if (guestToken == null) {
137  30 guestToken = newToken();
138    }
139  193 return guestToken;
140    }
141   
142    // Get the token if it has already been created
143  2295 String token = this.tokens.get(key);
144  2295 if (token != null) {
145  2212 return token;
146    }
147   
148    // create fresh token if needed
149  83 synchronized (this.tokens) {
150  83 if (!this.tokens.containsKey(key)) {
151  83 this.tokens.put(key, newToken());
152    }
153  83 return this.tokens.get(key);
154    }
155    }
156   
 
157  113 toggle private String newToken()
158    {
159  113 byte[] bytes = new byte[TOKEN_LENGTH];
160  113 this.random.nextBytes(bytes);
161    // Base64 encoded token can contain __ or -- which breaks the layout (see XWIKI-5996). Replacing them
162    // with x reduces randomness a bit, but it seems that other special characters are either used in XWiki
163    // syntax or not URL-safe
164  113 return Base64.encodeBase64URLSafeString(bytes).replaceAll("[_=+-]", "x");
165    }
166   
 
167  30 toggle @Override
168    public void clearToken()
169    {
170  30 this.logger.debug("Forgetting CSRF token for [{}]", getTokenKey());
171  30 this.tokens.remove(getTokenKey());
172    }
173   
 
174  330 toggle @Override
175    public boolean isTokenValid(String token)
176    {
177  330 if (!this.configuration.isEnabled()) {
178  0 return true;
179    }
180  330 String storedToken = getToken();
181  330 if (token == null || token.equals("") || !storedToken.equals(token)) {
182  7 this.logger.warn("CSRFToken: Secret token verification failed, token: \"" + token
183    + "\", stored token: \"" + storedToken + "\"");
184  7 return false;
185    }
186  322 return true;
187    }
188   
 
189  2 toggle @Override
190    public String getResubmissionURL()
191    {
192    // request URL is the one that performs the modification
193  2 String srid = SavedRequestManager.saveRequest(getRequest());
194  2 String resubmitUrl = getRequest().getRequestURI();
195  2 resubmitUrl += '?' + SavedRequestManager.getSavedRequestIdentifier() + "=" + srid;
196  2 String query = "resubmit=" + urlEncode(resubmitUrl);
197   
198    // back URL is the URL of the document that was about to be modified, so in most
199    // cases we can redirect back to the correct document (if the user clicks "no")
200  2 String backUrl = getDocumentURL(this.docBridge.getCurrentDocumentReference(), null);
201  2 query += "&xback=" + urlEncode(backUrl);
202   
203    // redirect to the resubmission template
204  2 query += "&xpage=" + RESUBMIT_TEMPLATE;
205  2 return backUrl + "?" + query;
206    }
207   
208    /**
209    * Satisfy checkstyle ("view" used twice).
210    *
211    * @param reference reference of the current document
212    * @param query query part of the URL
213    * @return URL of the given document
214    */
 
215  2 toggle private String getDocumentURL(DocumentReference reference, String query)
216    {
217  2 return this.docBridge.getDocumentURL(reference, "view", query, null);
218    }
219   
220    /**
221    * URL-encode given string.
222    *
223    * @param str the string to encode
224    * @return URL-encoded string
225    */
 
226  4 toggle private String urlEncode(String str)
227    {
228    // TODO find out which encoding is used for response
229  4 String encoding = "UTF-8";
230  4 try {
231  4 return URLEncoder.encode(str, encoding);
232    } catch (UnsupportedEncodingException exception) {
233    // Shouldn't happen, UTF-8 is always available
234  0 return "";
235    }
236    }
237   
238    /**
239    * Get the underlying HTTP request. Throws a runtime error if it is not a servlet request.
240    *
241    * @return HTTP servlet request
242    */
 
243  4 toggle private HttpServletRequest getRequest()
244    {
245  4 Request request = this.container.getRequest();
246  4 if (request instanceof ServletRequest) {
247  4 return ((ServletRequest) request).getHttpServletRequest();
248    }
249  0 throw new RuntimeException("Not supported request type");
250    }
251   
252    /**
253    * Get the token map key for the current user.
254    *
255    * @return key for the token map
256    */
 
257  2548 toggle private DocumentReference getTokenKey()
258    {
259  2548 return this.docBridge.getCurrentUserReference();
260    }
261    }