15
15
use Symfony \Component \HttpFoundation \ParameterBag ;
16
16
use Symfony \Component \Security \Core \Exception \InvalidArgumentException ;
17
17
use Symfony \Component \Security \Csrf \Exception \TokenNotFoundException ;
18
+ use Symfony \Component \HttpFoundation \Session \SessionInterface ;
18
19
19
20
/**
20
21
* Accesses tokens in a set of cookies. A changeset records edits made to
@@ -31,22 +32,22 @@ class CookieTokenStorage implements TokenStorageInterface
31
32
const COOKIE_DELIMITER = '_ ' ;
32
33
33
34
/**
34
- * @var array
35
+ * @var array A map of tokens to be written in the response
35
36
*/
36
37
private $ transientTokens = array ();
37
38
38
39
/**
39
- * @var array
40
+ * @var array A map of tokens extracted from cookies and verified
40
41
*/
41
- private $ resolvedTokens = array ();
42
+ private $ extractedTokens = array ();
42
43
43
44
/**
44
45
* @var array
45
46
*/
46
- private $ refreshTokens = array ();
47
+ private $ nonces = array ();
47
48
48
49
/**
49
- * @var ParameterBag
50
+ * @var array
50
51
*/
51
52
private $ cookies ;
52
53
@@ -66,14 +67,14 @@ class CookieTokenStorage implements TokenStorageInterface
66
67
private $ ttl ;
67
68
68
69
/**
69
- * @param ParameterBag $cookies
70
- * @param bool $secure
71
- * @param string $secret
72
- * @param int $ttl
70
+ * @param string $cookies The raw HTTP Cookie header
71
+ * @param bool $secure
72
+ * @param string $secret
73
+ * @param int $ttl
73
74
*/
74
- public function __construct (ParameterBag $ cookies , $ secure , $ secret , $ ttl = null )
75
+ public function __construct ($ cookies , $ secure , $ secret , $ ttl = null )
75
76
{
76
- $ this ->cookies = $ cookies ;
77
+ $ this ->cookies = self :: parseCookieHeader ( $ cookies) ;
77
78
$ this ->secure = (bool ) $ secure ;
78
79
$ this ->secret = (string ) $ secret ;
79
80
$ this ->ttl = $ ttl === null ? 60 * 60 : (int ) $ ttl ;
@@ -120,7 +121,10 @@ public function setToken($tokenId, $token)
120
121
throw new InvalidArgumentException ('Empty tokens are not allowed ' );
121
122
}
122
123
123
- $ this ->updateToken ($ tokenId , $ token );
124
+ // we need to resolve the token first to record the nonces
125
+ $ this ->resolveToken ($ tokenId );
126
+
127
+ $ this ->transientTokens [$ tokenId ] = $ token ;
124
128
}
125
129
126
130
/**
@@ -130,106 +134,118 @@ public function removeToken($tokenId)
130
134
{
131
135
$ token = $ this ->resolveToken ($ tokenId );
132
136
133
- $ this ->updateToken ( $ tokenId, '' ) ;
137
+ $ this ->transientTokens [ $ tokenId] = '' ;
134
138
135
139
return '' === $ token ? null : $ token ;
136
140
}
137
141
138
142
/**
139
- * @return array
143
+ * @return Cookie[]
140
144
*/
141
145
public function createCookies ()
142
146
{
143
147
$ cookies = array ();
144
148
145
149
foreach ($ this ->transientTokens as $ tokenId => $ token ) {
146
- // FIXME empty tokens are handled by the http foundations cookie class
147
- // and are recognized as a "delete" cookie
148
- // the problem is the that the value of deleted cookies get set to
149
- // the string "deleted" and not the empty string
150
- $ cookies [] = $ this ->createTokenCookie ($ tokenId , $ token );
151
- $ cookies [] = $ this ->createVerificationCookie ($ tokenId , $ token );
152
- }
153
-
154
- foreach ($ this ->refreshTokens as $ tokenId => $ token ) {
155
- if (isset ($ this ->transientTokens [$ tokenId ])) {
156
- continue ;
150
+ if (isset ($ this ->nonces [$ tokenId ])) {
151
+ foreach (array_keys ($ this ->nonces [$ tokenId ]) as $ nonce ) {
152
+ $ cookies [] = $ this ->createDeleteCookie ($ tokenId , $ nonce );
153
+ }
157
154
}
158
155
159
- $ cookies [] = $ this ->createVerificationCookie ($ tokenId , $ token );
156
+ if ($ token !== '' ) {
157
+ $ cookies [] = $ this ->createCookie ($ tokenId , $ token );
158
+ }
160
159
}
161
160
162
161
return $ cookies ;
163
162
}
164
163
165
164
/**
166
165
* @param string $tokenId
167
- * @param bool $excludeTransient
168
166
*
169
167
* @return string
170
168
*/
171
- protected function resolveToken ($ tokenId, $ excludeTransient = false )
169
+ protected function resolveToken ($ tokenId )
172
170
{
173
- if (! $ excludeTransient && isset ($ this ->transientTokens [$ tokenId ])) {
171
+ if (isset ($ this ->transientTokens [$ tokenId ])) {
174
172
return $ this ->transientTokens [$ tokenId ];
175
173
}
176
174
177
- if (isset ($ this ->resolvedTokens [$ tokenId ])) {
178
- return $ this ->resolvedTokens [$ tokenId ];
175
+ if (isset ($ this ->extractedTokens [$ tokenId ])) {
176
+ return $ this ->extractedTokens [$ tokenId ];
179
177
}
180
178
181
- $ this ->resolvedTokens [$ tokenId ] = '' ;
179
+ $ this ->extractedTokens [$ tokenId ] = '' ;
182
180
183
- $ token = $ this ->getTokenCookieValue ($ tokenId );
184
- if ('' === $ token ) {
181
+ $ prefix = $ this ->generateCookieName ($ tokenId , '' );
182
+ $ prefixLength = strlen ($ prefix );
183
+ $ cookies = $ this ->findCookiesByPrefix ($ prefix );
184
+
185
+ // record the nonces used, so we can delete all obsolete cookies of this
186
+ // token id, if necessary
187
+ foreach ($ cookies as $ cookie ) {
188
+ $ this ->nonces [$ tokenId ][substr ($ cookie [0 ], $ prefixLength )] = true ;
189
+ }
190
+
191
+ // if there is more than one cookie for the prefix, we get cookie tossed maybe
192
+ if (count ($ cookies ) != 1 ) {
185
193
return '' ;
186
194
}
187
195
188
- $ parts = explode (self ::COOKIE_DELIMITER , $ this -> getVerificationCookieValue ( $ tokenId ), 2 );
189
- if (count ($ parts ) != 2 ) {
196
+ $ parts = explode (self ::COOKIE_DELIMITER , $ cookies [ 0 ][ 1 ], 3 );
197
+ if (count ($ parts ) != 3 ) {
190
198
return '' ;
191
199
}
200
+ list ($ expires , $ signature , $ token ) = $ parts ;
192
201
193
- list ( $ expires , $ hash ) = $ parts ;
202
+ // expired token
194
203
$ time = time ();
195
204
if (!ctype_digit ($ expires ) || $ expires < $ time ) {
196
205
return '' ;
197
206
}
198
- if (!hash_equals ($ this ->generateVerificationHash ($ tokenId , $ token , $ expires ), $ hash )) {
207
+
208
+ // invalid signature
209
+ $ nonce = substr ($ cookies [0 ][0 ], $ prefixLength );
210
+ if (!hash_equals ($ this ->generateSignature ($ tokenId , $ token , $ expires , $ nonce ), $ signature )) {
199
211
return '' ;
200
212
}
201
213
202
214
$ time += $ this ->ttl / 2 ;
203
215
if ($ expires < $ time ) {
204
- $ this ->refreshTokens [$ tokenId ] = $ token ;
216
+ $ this ->transientTokens [$ tokenId ] = $ token ;
205
217
}
206
218
207
- return $ this ->resolvedTokens [$ tokenId ] = $ token ;
219
+ return $ this ->extractedTokens [$ tokenId ] = $ token ;
208
220
}
209
221
210
222
/**
211
- * @param string $tokenId
212
- * @param string $token
223
+ * @param string $prefix
224
+ *
225
+ * @return array
213
226
*/
214
- protected function updateToken ( $ tokenId , $ token )
215
- {
216
- if ($ token === $ this ->resolveToken ( $ tokenId , true ) ) {
217
- unset( $ this -> transientTokens [ $ tokenId ]);
218
- } else {
219
- $ this -> transientTokens [ $ tokenId ] = $ token ;
227
+ protected function findCookiesByPrefix ( $ prefix ) {
228
+ $ cookies = array ();
229
+ foreach ($ this ->cookies as $ cookie ) {
230
+ if ( 0 === strpos ( $ cookie [ 0 ], $ prefix )) {
231
+ $ cookies [] = $ cookie ;
232
+ }
220
233
}
234
+
235
+ return $ cookies ;
221
236
}
222
237
223
238
/**
224
239
* @param string $tokenId
240
+ * @param string $nonce
225
241
*
226
- * @return string
242
+ * @return Cookie
227
243
*/
228
- protected function getTokenCookieValue ($ tokenId )
244
+ protected function createDeleteCookie ($ tokenId, $ nonce )
229
245
{
230
- $ name = $ this ->generateTokenCookieName ($ tokenId );
246
+ $ name = $ this ->generateCookieName ($ tokenId, $ nonce );
231
247
232
- return $ this -> cookies -> get ($ name , '' );
248
+ return new Cookie ($ name , '' , 0 , null , null , $ this -> secure , true );
233
249
}
234
250
235
251
/**
@@ -238,88 +254,81 @@ protected function getTokenCookieValue($tokenId)
238
254
*
239
255
* @return Cookie
240
256
*/
241
- protected function createTokenCookie ($ tokenId , $ token )
257
+ protected function createCookie ($ tokenId , $ token )
242
258
{
243
- $ name = $ this ->generateTokenCookieName ($ tokenId );
259
+ $ expires = time () + $ this ->ttl ;
260
+ $ nonce = self ::encodeBase64Url (random_bytes (6 ));
261
+ $ signature = $ this ->generateSignature ($ tokenId , $ token , $ expires , $ nonce );
244
262
245
- return new Cookie ($ name , $ token , 0 , null , null , $ this ->secure , false );
246
- }
263
+ $ this ->nonces [$ tokenId ][$ nonce ] = true ;
247
264
248
- /**
249
- * @param string $tokenId
250
- *
251
- * @return string
252
- */
253
- protected function generateTokenCookieName ($ tokenId )
254
- {
255
- $ encodedTokenId = rtrim (strtr (base64_encode ($ tokenId ), '+/ ' , '-_ ' ), '= ' );
265
+ $ name = $ this ->generateCookieName ($ tokenId , $ nonce );
266
+ $ value = $ expires .self ::COOKIE_DELIMITER .$ signature .self ::COOKIE_DELIMITER .$ token ;
256
267
257
- return sprintf ( ' _csrf/%s/%s ' , $ this -> secure ? ' secure ' : ' insecure ' , $ encodedTokenId );
268
+ return new Cookie ( $ name , $ value , 0 , null , null , $ this -> secure , true );
258
269
}
259
270
260
271
/**
261
272
* @param string $tokenId
273
+ * @param string $nonce
262
274
*
263
275
* @return string
264
276
*/
265
- protected function getVerificationCookieValue ($ tokenId )
277
+ protected function generateCookieName ($ tokenId, $ nonce )
266
278
{
267
- $ name = $ this ->generateVerificationCookieName ($ tokenId );
268
-
269
- return $ this ->cookies ->get ($ name , '' );
279
+ return sprintf (
280
+ '_csrf_%s_%s_%s ' ,
281
+ (int ) $ this ->secure ,
282
+ self ::encodeBase64Url ($ tokenId ),
283
+ $ nonce
284
+ );
270
285
}
271
286
272
287
/**
273
288
* @param string $tokenId
274
289
* @param string $token
275
- *
276
- * @return Cookie
277
- */
278
- protected function createVerificationCookie ($ tokenId , $ token )
279
- {
280
- $ name = $ this ->generateVerificationCookieName ($ tokenId );
281
- $ value = $ this ->generateVerificationCookieValue ($ tokenId , $ token );
282
-
283
- return new Cookie ($ name , $ value , 0 , null , null , $ this ->secure , true );
284
- }
285
-
286
- /**
287
- * @param string $tokenId
290
+ * @param int $expires
291
+ * @param string $nonce
288
292
*
289
293
* @return string
290
294
*/
291
- protected function generateVerificationCookieName ($ tokenId )
295
+ protected function generateSignature ($ tokenId, $ token , $ expires , $ nonce )
292
296
{
293
- return $ this ->generateTokenCookieName ( $ tokenId ). ' /verify ' ;
297
+ return hash_hmac ( ' sha256 ' , $ tokenId . $ token . $ expires . $ nonce . $ this ->secure , $ this -> secret ) ;
294
298
}
295
299
296
300
/**
297
- * @param string $tokenId
298
- * @param string $token
301
+ * @param string $header
299
302
*
300
- * @return string
303
+ * @return array
301
304
*/
302
- protected function generateVerificationCookieValue ( $ tokenId , $ token )
303
- {
304
- if ('' === $ token ) {
305
- return '' ;
305
+ public static function parseCookieHeader ( $ header ) {
306
+ $ header = trim (( string ) $ header );
307
+ if ('' === $ header ) {
308
+ return array () ;
306
309
}
307
310
308
- $ expires = time () + $ this ->ttl ;
309
- $ hash = $ this ->generateVerificationHash ($ tokenId , $ token , $ expires );
311
+ $ cookies = array ();
312
+ foreach (explode ('; ' , $ header ) as $ cookie ) {
313
+ if (false === strpos ($ cookie , '= ' )) {
314
+ continue ;
315
+ }
316
+
317
+ $ cookies [] = array_map (function ($ item ) {
318
+ return urldecode (trim ($ item , ' " ' ));
319
+ }, explode ('= ' , $ cookie , 2 ));
320
+ }
310
321
311
- return $ expires . self :: COOKIE_DELIMITER . $ hash ;
322
+ return $ cookies ;
312
323
}
313
324
314
325
/**
315
- * @param string $tokenId
316
- * @param string $token
317
- * @param int $expires
326
+ * @param string $data
318
327
*
319
328
* @return string
320
329
*/
321
- protected function generateVerificationHash ( $ tokenId , $ token , $ expires )
330
+ public static function encodeBase64Url ( $ data )
322
331
{
323
- return hash_hmac ( ' sha256 ' , $ tokenId . $ token . $ expires , $ this -> secret );
332
+ return rtrim ( strtr ( base64_encode ( $ data ), ' +/ ' , ' -_ ' ), ' = ' );
324
333
}
325
334
}
0 commit comments