Я пишу Android-клиент, основанный на Retrofit и OkHttp, для веб-службы, основанной на Laravel и использующей защиту CSRF. Хотя я отправляю правильный токен CSRF (по крайней мере, я так считаю), веб-служба отвечает кодом состояния HTTP «419 - несоответствие CSRF». (Примечание: 419 не является официальным кодом статуса HTTP, но специфичен для Laraval).
Где ошибка в моем втором HTTP-запросе? (См. ниже).
Точнее пытаюсь написать клиент для Личи.
Веб-API Lychees задокументирован здесь. На первом этапе я отправляю HTTP-запрос к php/index.php
, чтобы получить токен CSRF.
Этот первый шаг увенчался успехом.
На втором этапе я отправляю HTTP-запрос к /api/Session::login
.
Хотя заголовок запроса включает токен CSRF, этот запрос не выполняется.
Почему?
1-й запрос:
LycheeClient: --> POST https://devel.my-domain.tld/php/index.php
LycheeClient: Content-Length: 0
LycheeClient: Accept: application/json, text/javascript, */*; q=0.01
LycheeClient: DNT: 1
LycheeClient: Origin: https://devel.my-domain.tld
LycheeClient: --> END POST (0-byte body)
1-й ответ:
LycheeClient: <-- 200 https://devel.my-domain.tld/php/index.php (959ms)
LycheeClient: date: Mon, 05 Apr 2021 19:03:22 GMT
LycheeClient: server: Apache
LycheeClient: cache-control: no-cache, private
LycheeClient: phpdebugbar-id: X1217d2d3c26dc642b6683c76249c3703
LycheeClient: feature-policy: autoplay 'self'; encrypted-media 'self'; fullscreen 'self'; picture-in-picture *; sync-xhr *
LycheeClient: x-content-type-options: nosniff
LycheeClient: x-download-options: noopen
LycheeClient: x-frame-options: deny
LycheeClient: x-permitted-cross-domain-policies: none
LycheeClient: x-xss-protection: 1; mode=block
LycheeClient: referrer-policy: no-referrer
LycheeClient: x-clockwork-id: 1617649402-3275-1189279108
LycheeClient: x-clockwork-version: 4.1.8
LycheeClient: set-cookie: XSRF-TOKEN=eyJpdiI6InRTanVBTmdBenpSMUlFV2hDMkgycnc9PSIsInZhbHVlIjoiOU0yY2pKMG5PVTY0SndoWnIzeUVxd0UzbHkrTHUxYzRjazNia3hEdHVqYUhyN1JMRG5nbkJmUFFXZjFWdFpnTzM2Z014bnpPSEYvTUZKdGkzRXM1SlRxSXdVSngwZWNBRUZJNysrOVlid2xocEg0L3BOZlVlNnltWFhNREpjS1EiLCJtYWMiOiJmZmYwZjUxZDlhNWYxZGIxYWM3NGE3MjUyZjY5Y2I0ODFjYWRlZDliZmUxNTZkNGI4NjAzODI3MmVlOTZkMzE3In0%3D; expires=Mon, 05-Apr-2021 21:03:22 GMT; Max-Age=7200; path=/; samesite=lax
LycheeClient: set-cookie: lychee_session=eyJpdiI6ImdwYXdTN2x3L0VMRmgwaXNmMG1DbVE9PSIsInZhbHVlIjoiZmNXbnNPc1FSQk42aVk3OTJUd1dNSUk0NUJqT1kxT2paWktqZWdKQ21DUFNRcWpuQmx1VUFiYUVYWW5RU2lSSEhMMVZzejFDY3E0S3pLQS9IZDJ0ViswRElQUVJOWVpQRVJUNmM3UDhsY0xQUi9yV3FoSjhseVdHS2kyck9oS0EiLCJtYWMiOiIwNzcxOWEyOGZhMGFlZDE5MzhlM2Q3ZDcwODgwOGMwZWJmYzNkMDQxNmMwOGYzYWU4ZDRhNTljNzViYzYzNTk4In0%3D; expires=Mon, 05-Apr-2021 21:03:22 GMT; Max-Age=7200; path=/; httponly; samesite=lax
LycheeClient: x-robots-tag: none
LycheeClient: strict-transport-security: max-age=31104000; includeSubDomains
LycheeClient: content-type: application/json
LycheeClient: <-- END HTTP (20941-byte body)
2-й запрос: (токен включен)
LycheeClient: --> POST https://devel.my-domain.tld/api/Session::login
LycheeClient: Content-Type: application/json; charset=UTF-8
LycheeClient: Content-Length: 37
LycheeClient: Accept: application/json, text/javascript, */*; q=0.01
LycheeClient: DNT: 1
LycheeClient: Origin: https://devel.my-domain.tld
LycheeClient: X-XSRF-TOKEN: eyJpdiI6InRTanVBTmdBenpSMUlFV2hDMkgycnc9PSIsInZhbHVlIjoiOU0yY2pKMG5PVTY0SndoWnIzeUVxd0UzbHkrTHUxYzRjazNia3hEdHVqYUhyN1JMRG5nbkJmUFFXZjFWdFpnTzM2Z014bnpPSEYvTUZKdGkzRXM1SlRxSXdVSngwZWNBRUZJNysrOVlid2xocEg0L3BOZlVlNnltWFhNREpjS1EiLCJtYWMiOiJmZmYwZjUxZDlhNWYxZGIxYWM3NGE3MjUyZjY5Y2I0ODFjYWRlZDliZmUxNTZkNGI4NjAzODI3MmVlOTZkMzE3In0
LycheeClient: Cookie: XSRF-TOKEN=eyJpdiI6InRTanVBTmdBenpSMUlFV2hDMkgycnc9PSIsInZhbHVlIjoiOU0yY2pKMG5PVTY0SndoWnIzeUVxd0UzbHkrTHUxYzRjazNia3hEdHVqYUhyN1JMRG5nbkJmUFFXZjFWdFpnTzM2Z014bnpPSEYvTUZKdGkzRXM1SlRxSXdVSngwZWNBRUZJNysrOVlid2xocEg0L3BOZlVlNnltWFhNREpjS1EiLCJtYWMiOiJmZmYwZjUxZDlhNWYxZGIxYWM3NGE3MjUyZjY5Y2I0ODFjYWRlZDliZmUxNTZkNGI4NjAzODI3MmVlOTZkMzE3In0%3D
LycheeClient: --> END POST (37-byte body)
Второй ответ:
LycheeClient: <-- 419 https://devel.my-domain.tld/api/Session::login (181ms)
LycheeClient: date: Mon, 05 Apr 2021 19:03:23 GMT
LycheeClient: server: Apache
LycheeClient: cache-control: no-cache, private
LycheeClient: phpdebugbar-id: Xfec172b4e199e85ba49731cef5942e7b
LycheeClient: feature-policy: autoplay 'self'; encrypted-media 'self'; fullscreen 'self'; picture-in-picture *; sync-xhr *
LycheeClient: x-content-type-options: nosniff
LycheeClient: x-download-options: noopen
LycheeClient: x-frame-options: deny
LycheeClient: x-permitted-cross-domain-policies: none
LycheeClient: x-xss-protection: 1; mode=block
LycheeClient: referrer-policy: no-referrer
LycheeClient: x-clockwork-id: 1617649403-0046-418871694
LycheeClient: x-clockwork-version: 4.1.8
LycheeClient: server-timing: app; dur=49.594879150391; desc = "Application", db; dur=26.89; desc = "Database", timeline-event-total; dur=49.700975418091; desc = "Total execution time.", timeline-event-initialisation; dur=5.7110786437988; desc = "Application initialisation.", timeline-event-boot; dur=4.6510696411133; desc = "Framework booting.", timeline-event-run; dur=43.990135192871; desc = "Framework running."
LycheeClient: set-cookie: lychee_session=eyJpdiI6ImZuN1JGZmVTQjBzMzc1RWFtdWFDMFE9PSIsInZhbHVlIjoiNE5GdWY3UWFuV3NUZ2VHcEdIUnNnbTNDbXVNa0JZKzJjWGwyd2s4RnArclg3ZUZoU1F3cUwrQ1FLNHlBTzk4SUVpdjZGaVU3Mkt6eEJJOExPeEpRcGJJU3ZpNC9LVy8xQXRQRjd3MVBzQk1CZDM5eXJnY20ySVhXMlRSc24ya1QiLCJtYWMiOiJmZmUyMmMwNWIzYTY0NWMxNTkyNjJlNGM1YzZmOWY2NWM5OTBmOWQxNDAwZTI4YTFlNGIzZTk2MzViNTg3NmE1In0%3D; expires=Mon, 05-Apr-2021 21:03:23 GMT; Max-Age=7200; path=/; httponly; samesite=lax
LycheeClient: x-robots-tag: none
LycheeClient: strict-transport-security: max-age=31104000; includeSubDomains
LycheeClient: content-type: application/json
Второй ответ:
{
"message": "CSRF token mismatch.",
"exception": "Symfony\\Component\\HttpKernel\\Exception\\HttpException",
"file": "/var/www/mhnnet/lychee-dev/vendor/laravel/framework/src/Illuminate/Foundation/Exceptions/Handler.php",
"line": 373,
"trace": [ ... ]
}
Вы абсолютно правы. Я включил cookie сеанса в предыдущий тест, который тоже не увенчался успехом, и поэтому я удалил cookie сеанса, чтобы посмотреть, не изменится ли что-нибудь. Конечно, сеансовые куки необходимы. Извините, что опубликовал неправильный вывод. Настоящая проблема в другом. Маркер CSRF, который отправляется клиенту в заголовке Set-Cookie
, заканчивается на %3D
. Если значение токена CSRF копируется в заголовок X-XSRF-TOKEN
следующего запроса, этот завершающий %3D
должен быть удален из значения. Почему?
Я не совсем уверен в этом, %3D
- это =
с urlen-кодированием. Я подозреваю, что это потому, что для файла cookie вам нужно urlencode =
, но для заголовка вы этого не делаете, поэтому вы можете заменить его на =
, однако здесь он, похоже, работает с ним или без него, потому что =
обычно указывает заполнение в строке base64 так что может быть не обязательно
Нет, это не только не нужно, %3D
нельзя помещать в заголовок, и его нельзя заменять на =
, он должен быть удален полностью. Если в заголовке присутствует %3D
или =
, Laraval отвечает кодом 419. Хорошо, похоже, мне нужно удалить заполнение, когда токен CSRF отправляется обратно на сервер. Это задокументированное поведение по умолчанию?
Нет, на самом деле это не кажется правильным поведением, если честно. XSRF-TOKEN - это зашифрованный токен, который включает в себя проверочную подпись, поэтому его изменение и отправка обратно обычно приводят к сбою проверки подписи. Не уверен, что здесь происходит
Вам также необходимо отправить файл cookie сеанса с токеном. Я не вижу этого во втором запросе