25
25
*/
26
26
class CookieTokenStorage implements TokenStorageInterface
27
27
{
28
+ /**
29
+ * @var string
30
+ */
31
+ const COOKIE_DELIMITER = '_ ' ;
32
+
28
33
/**
29
34
* @var array
30
35
*/
31
36
private $ transientTokens = array ();
32
37
38
+ /**
39
+ * @var array
40
+ */
41
+ private $ resolvedTokens = array ();
42
+
43
+ /**
44
+ * @var array
45
+ */
46
+ private $ refreshTokens = array ();
47
+
33
48
/**
34
49
* @var ParameterBag
35
50
*/
@@ -40,14 +55,36 @@ class CookieTokenStorage implements TokenStorageInterface
40
55
*/
41
56
private $ secure ;
42
57
58
+ /**
59
+ * @var string
60
+ */
61
+ private $ secret ;
62
+
63
+ /**
64
+ * @var int
65
+ */
66
+ private $ ttl ;
67
+
43
68
/**
44
69
* @param ParameterBag $cookies
45
70
* @param bool $secure
71
+ * @param string $secret
72
+ * @param int $ttl
46
73
*/
47
- public function __construct (ParameterBag $ cookies , $ secure )
74
+ public function __construct (ParameterBag $ cookies , $ secure, $ secret , $ ttl = null )
48
75
{
49
76
$ this ->cookies = $ cookies ;
50
77
$ this ->secure = (bool ) $ secure ;
78
+ $ this ->secret = (string ) $ secret ;
79
+ $ this ->ttl = $ ttl === null ? 60 * 60 : (int ) $ ttl ;
80
+
81
+ if ('' === $ this ->secret ) {
82
+ throw new InvalidArgumentException ('Secret must be a non-empty string ' );
83
+ }
84
+
85
+ if ($ this ->ttl < 60 ) {
86
+ throw new InvalidArgumentException ('TTL must be an integer greater than or equal to 60 ' );
87
+ }
51
88
}
52
89
53
90
/**
@@ -110,28 +147,64 @@ public function createCookies()
110
147
// and are recognized as a "delete" cookie
111
148
// the problem is the that the value of deleted cookies get set to
112
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 ;
157
+ }
113
158
114
- $ name = $ this ->generateCookieName ($ tokenId );
115
- $ cookies [] = new Cookie ($ name , $ token , 0 , null , null , $ this ->secure , true );
159
+ $ cookies [] = $ this ->createVerificationCookie ($ tokenId , $ token );
116
160
}
117
161
118
162
return $ cookies ;
119
163
}
120
164
121
165
/**
122
166
* @param string $tokenId
167
+ * @param bool $excludeTransient
123
168
*
124
169
* @return string
125
170
*/
126
- protected function res
F438
olveToken ($ tokenId )
171
+ protected function resolveToken ($ tokenId, $ excludeTransient = false )
127
172
{
128
- if (isset ($ this ->transientTokens [$ tokenId ])) {
173
+ if (! $ excludeTransient && isset ($ this ->transientTokens [$ tokenId ])) {
129
174
return $ this ->transientTokens [$ tokenId ];
130
175
}
131
176
132
- $ name = $ this ->generateCookieName ($ tokenId );
177
+ if (isset ($ this ->resolvedTokens [$ tokenId ])) {
178
+ return $ this ->resolvedTokens [$ tokenId ];
179
+ }
180
+
181
+ $ this ->resolvedTokens [$ tokenId ] = '' ;
133
182
134
- return $ this ->cookies ->get ($ name , '' );
183
+ $ token = $ this ->getTokenCookieValue ($ tokenId );
184
+ if ('' === $ token ) {
185
+ return '' ;
186
+ }
187
+
188
+ $ parts = explode (self ::COOKIE_DELIMITER , $ this ->getVerificationCookieValue ($ tokenId ), 2 );
189
+ if (count ($ parts ) != 2 ) {
190
+ return '' ;
191
+ }
192
+
193
+ list ($ expires , $ hash ) = $ parts ;
194
+ $ time = time ();
195
+ if (!ctype_digit ($ expires ) || $ expires < $ time ) {
196
+ return '' ;
197
+ }
198
+ if (!hash_equals ($ this ->generateVerificationHash ($ tokenId , $ token , $ expires ), $ hash )) {
199
+ return '' ;
200
+ }
201
+
202
+ $ time += $ this ->ttl / 2 ;
203
+ if ($ expires < $ time ) {
204
+ $ this ->refreshTokens [$ tokenId ] = $ token ;
205
+ }
206
+
207
+ return $ this ->resolvedTokens [$ tokenId ] = $ token ;
135
208
}
136
209
137
210
/**
@@ -140,9 +213,7 @@ protected function resolveToken($tokenId)
140
213
*/
141
214
protected function updateToken ($ tokenId , $ token )
142
215
{
143
- $ name = $ this ->generateCookieName ($ tokenId );
144
-
145
- if ($ token === $ this ->cookies ->get ($ name , '' )) {
216
+ if ($ token === $ this ->resolveToken ($ tokenId , true )) {
146
217
unset($ this ->transientTokens [$ tokenId ]);
147
218
} else {
148
219
$ this ->transientTokens [$ tokenId ] = $ token ;
@@ -154,8 +225,101 @@ protected function updateToken($tokenId, $token)
154
225
*
155
226
* @return string
156
227
*/
157
- protected function generateCookieName ($ tokenId )
228
+ protected function getTokenCookieValue ($ tokenId )
229
+ {
230
+ $ name = $ this ->generateTokenCookieName ($ tokenId );
231
+
232
+ return $ this ->cookies ->get ($ name , '' );
233
+ }
234
+
235
+ /**
236
+ * @param string $tokenId
237
+ * @param string $token
238
+ *
239
+ * @return Cookie
240
+ */
241
+ protected function createTokenCookie ($ tokenId , $ token )
242
+ {
243
+ $ name = $ this ->generateTokenCookieName ($ tokenId );
244
+
245
+ return new Cookie ($ name , $ token , 0 , null , null , $ this ->secure , false );
246
+ }
247
+
248
+ /**
249
+ * @param string $tokenId
250
+ *
251
+ * @return string
252
+ */
253
+ protected function generateTokenCookieName ($ tokenId )
254
+ {
255
+ $ encodedTokenId = rtrim (strtr (base64_encode ($ tokenId ), '+/ ' , '-_ ' ), '= ' );
256
+
257
+ return sprintf ('_csrf/%s/%s ' , $ this ->secure ? 'secure ' : 'insecure ' , $ encodedTokenId );
258
+ }
259
+
260
+ /**
261
+ * @param string $tokenId
262
+ *
263
+ * @return string
264
+ */
265
+ protected function getVerificationCookieValue ($ tokenId )
266
+ {
267
+ $ name = $ this ->generateVerificationCookieName ($ tokenId );
268
+
269
+ return $ this ->cookies ->get ($ name , '' );
270
+ }
271
+
272
+ /**
273
+ * @param string $tokenId
274
+ * @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
288
+ *
289
+ * @return string
290
+ */
291
+ protected function generateVerificationCookieName ($ tokenId )
292
+ {
293
+ return $ this ->generateTokenCookieName ($ tokenId ).'/verify ' ;
294
+ }
295
+
296
+ /**
297
+ * @param string $tokenId
298
+ * @param string $token
299
+ *
300
+ * @return string
301
+ */
302
+ protected function generateVerificationCookieValue ($ tokenId , $ token )
303
+ {
304
+ if ('' === $ token ) {
305
+ return '' ;
306
+ }
307
+
308
+ $ expires = time () + $ this ->ttl ;
309
+ $ hash = $ this ->generateVerificationHash ($ tokenId , $ token , $ expires );
310
+
311
+ return $ expires .self ::COOKIE_DELIMITER .$ hash ;
312
+ }
313
+
B41A
314
+ /**
315
+ * @param string $tokenId
316
+ * @param string $token
317
+ * @param int $expires
318
+ *
319
+ * @return string
320
+ */
321
+ protected function generateVerificationHash ($ tokenId , $ token , $ expires )
158
322
{
159
- return sprintf ( ' _csrf/%s/%s ' , $ this -> secure ? ' insecure ' : ' secure ' , $ tokenId );
323
+ return hash_hmac ( ' sha256 ' , $ tokenId . $ token . $ expires , $ this -> secret );
160
324
}
161
325
}
0 commit comments