vyvchy
    Теми розділу

    03 · Веб і мережі для AQA

    Автентифікація та авторизація

    Зміст

    Нічний прогін падає з 401 посеред , хоча вдень усе зелене. Тест на права ловить 403 — і команда півдня шукає роль, якої насправді не бракувало. За обома історіями стоїть одне: у застосунку є модель автентифікації, і поки її ніхто не проговорив уголос, кожен її наслідок виглядає як загадка.

    Ця глава — повний виклад теми на сайті, інші глави посилаються сюди. Мета: вміти сказати про будь-який застосунок, у якій формі в ньому живе доказ особи, як він потрапляє в кожен запит і що з цього ламає автотест.

    Дві стадії — і жоден порядок між ними протоколом не наказаний

    Автентифікація (authentication) відповідає на питання «хто ти?», авторизація (authorization) — на питання «що тобі можна?». Безпековий канон формулює обидва питання дослівно — «Who are you?» і «Are you allowed to do this?» — і називає фактори автентифікації поіменно: щось, що ти знаєш (пароль), щось, що маєш (токен), щось, чим ти є (біометрія).

    розводить ці поняття ще різкіше — за типом об'єкта: authentication — «a procedure determining whether a person or a process is, in fact, who or what it is declared to be», а authorization — «permission given to a user or process to access resources». Тобто перше — процедура, друге — наданий дозвіл, і це різні сутності, а не дві половини одного механізму.

    Найшвидше різницю видно у кодах відповіді:

    КодЩо каже специфікаціяПрактично
    401запит «lacks valid authentication credentials for the target resource»; відповідь MUST нести WWW-Authenticate щонайменше з одним челенджем (challenge)доказу особи немає або він невалідний
    403«the server understood the request but refuses to fulfill it»запит зрозумілий, але виконувати його сервер відмовляється

    Типовий цикл такий: запит без облікових даних → 401 із WWW-Authenticate → повторний запит уже з Authorization.

    А тепер найважливіше в главі — спрощення, яке треба зняти, а не завчити. Формулювання «спершу автентифікація, потім авторизація; спершу 401, потім 403» тримається в головах роками, але жорсткого порядку стадій HTTP не встановлює. У RFC 9110 §15.5.4 умова про облікові дані записана саме умовно — «If authentication credentials were provided in the request…», — а далі стоїть прямо: «a request might be forbidden for reasons unrelated to the credentials». Тобто 403 цілком законний і для запиту, у якому даних для входу не було взагалі. Дзеркальне застереження того ж параграфа: сервер, який хоче приховати саме існування забороненого ресурсу, «MAY instead respond with a status code of 404 (Not Found)».

    Безпековий канон послідовність таки фіксує: авторизація «occurs after successful authentication». Суперечності немає — OWASP описує процес перевірки, RFC — коди відповіді. Помилка починається тоді, коли з процесу виводять гарантію про код: «прийшов 403 — отже, роль не та» є гіпотезою, а не висновком.

    Наша практика (не канон). Далі — два наші висновки, канонічного джерела під ними немає. Перший: назва 401 Unauthorized історично невдала, за змістом це «unauthenticated» — такого слова специфікація не вживає, це наша оцінка. Другий про дизайн перевірки: у негативному сценарії з правами очікуйте 403 або 404, а не жорстко 403, — інакше тест ламається від законного вибору сервера приховати ресурс.

    Доказ особи доводиться нести в кожному запиті заново

    HTTP визначено як протокол без стану: семантику кожного запиту можна зрозуміти ізольовано, тому сервер не має припускати зв'язок між двома запитами. Те саме обмеження в REST сформульоване як вимога — кожен запит містить усю інформацію, потрібну для його розуміння, і не спирається на збережений на сервері контекст. Мотив прагматичний: якщо сервер не мусить пам'ятати клієнтів, будь-який із десятка однакових серверів обробить будь-який запит.

    Звідси росте вся тема. Доказ «це знову я» їде з кожним запитом, і форм у нього дві. Кукі (cookie): сервер ставить її заголовком Set-Cookie, а далі браузер сам за збереженими метаданими вирішує, чи повернути пари ім'я=значення в заголовку Cookie. Токен (token): клієнт зберігає його самостійно й додає в заголовок Authorization, форму якого специфікація задає дослівно — credentials = "Bearer" 1*SP b64token. Асиметрія тут принципова: куку повертає сам браузер за збереженими метаданими, а токен у заголовок кладе код застосунку. Сесії, JWT і OAuth — надбудови над цим одним фактом.

    Схеми Basic і Bearer: що саме їде в заголовку

    Authorization — заголовок запиту, тобто його ставить клієнт. Синтаксис однаковий для всіх схем: назва схеми, пробіл, облікові дані; сама назва нечутлива до регістру. А от формат значення диктує схема, а не заголовок.

    Basic (RFC 7617) — найпростіша схема: логін і пароль з'єднують двокрапкою, кодують у і кладуть у заголовок.

    GET /secret HTTP/1.1
    Host: example.com
    Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==

    Це канонічний приклад самої специфікації — логін Aladdin, пароль open sesame. Розгортається він назад тривіально, бо base64 — це кодування, а не шифрування. RFC формулює наслідок різкіше за будь-який переказ: схема «is not considered to be a secure method of user authentication unless used in conjunction with some external secure system such as TLS», бо «the user-id and password are passed over the network as cleartext»; модальність заборони при цьому саме SHOULD NOT без HTTPS.

    Два готові негативні кейси беруться прямо звідти. Перший: логін із двокрапкою невалідний — перша двокрапка розділяє пару, а весь текст після неї вважається частиною пароля. Другий: початкове означення схеми взагалі не задавало кодування символів, тож реалізації обрали хто ISO-8859-1, хто UTF-8; звідси клас багів «пароль із діакритикою працює в одному клієнті й падає в іншому» (єдине дозволене значення параметра charsetUTF-8, і на відміну від обов'язкового realm він OPTIONAL).

    Для AQA у цієї схеми є щоденний бік: закриті staging-стенди часто стоять саме за Basic Auth, і креденшели там передають на рівні контексту (у Playwright — опція httpCredentials), а не клікають рідний діалог браузера.

    Bearer (RFC 6750) — схема «на пред'явника»: будь-хто, хто володіє токеном, використає його так само, як легітимний власник, і жодного додаткового доказу володіння не потрібно. Формат жорсткий — слово Bearer, щонайменше один пробіл, значення токена; TLS при цьому обов'язковий до застосування.

    GET /api/projects HTTP/1.1
    Host: api.example.com
    Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.SflKxw

    Саме через жорсткість формату часта причина 401 на «явно валідному» токені — сам рядок заголовка: забуте слово Bearer або зайвий пробіл. Перевіряти треба точний вміст заголовка в реальному запиті, а не сам токен.

    Ще дві поведінки пояснюють запит, який «раптово став неавтентифікованим». Перша: заголовок Authorization «is stripped from cross-origin redirects» — після редиректу на інше він не переживає переходу, і сервіс віддає 401, хоча клієнт його «точно слав». Друга: якщо сервер отримав валідні, але недостатні креденшели, він «should respond with the 403 Forbidden status code», і — на відміну від 401 — браузер нової спроби вже не запропонує. Окремий комплект заголовків живе у 407 Proxy Authentication Required, Proxy-Authenticate, Proxy-Authorization: ресурсна й проксі-автентифікація можуть співіснувати, тому для них і знадобилися різні коди та заголовки. За нашим досвідом саме їх і плутають, коли «чомусь не автентифікується» лише в корпоративній мережі.

    Серверна сесія проти токена: де саме живе стан

    Різниця між двома підходами одна, і вона не про формат рядка, а про місце зберігання стану.

    Серверна сесія. Логін → сервер створює у себе запис із усім станом (хто ви, ролі, кошик) → віддає браузеру лише ключ, ідентифікатор сесії, кукою → на кожен наступний запит браузер сам надсилає цю куку, і сервер знаходить свій запис. У браузері лежить непрозорий рядок, який сам по собі не означає нічого.

    Токен. Сервер запаковує контекст користувача в сам токен, підписує його й віддає клієнту. Далі сервер перевіряє підпис і нічого не шукає у себе в базі — але й відкликати такий токен достроково важко: поки він не протермінувався, він валідний.

    КритерійСерверна сесіяТокен
    Де живе станна серверіу самому токені, в клієнта
    Відкликаннямиттєве — видалити записскладне — чекати exp або тримати списки
    Транспорткука, браузер підставляє самAuthorization, ставить клієнтський код
    Що бачить клієнтнепрозорий ідентифікаторувесь вміст токена

    Атрибути кукі, час її життя й механіку сховищ браузера повністю розбирає глава «Кукі, сесії та сховище браузера» — тут беремо з неї рівно те, без чого не зрозуміти автентифікацію.

    Наша практика (не канон). Два спостереження без канонічного джерела. Перше: серверна сесія вимагає спільного сховища на весь кластер — інакше користувача «викидає» щоразу, як його запит потрапляє на іншу машину за балансувальником. Друге: API key і session token плутають, бо обидва — просто рядки в запиті, хоча ідентифікують різне. Ключ відповідає на «який сервіс стукає» й зазвичай має ширші фіксовані права; токен — на «який користувач працює» й успадковує його роль. Тест під ключем проєкту легко «позеленіє» там, де жива людина отримала б 403.

    JWT: три частини, які читає будь-хто

    JWT (JSON Web Token) — компактний і безпечний для URL формат передавання тверджень (claims) між двома сторонами; він розрахований саме на середовища з обмеженням довжини — заголовок Authorization і параметри URI. У token-based підходах зазвичай застосовують саме його.

    Технічно JWT — послідовність URL-безпечних частин, розділених крапками, кожна закодована в base64url. Звична трійка header.payload.signature — це подання JWS Compact Serialization, але саме число частин специфікація навмисно не фіксує: «the number of parts in the JWT is dependent upon the representation», і JWE Compact дає їх п'ять. Тест, який жорстко перевіряє «рівно дві крапки», перевіряє подання, а не формат.

    Набір тверджень — звичайний JSON-об'єкт, JWT Claims Set. Частина полів зареєстрована самою специфікацією:

    ClaimЗначення
    issхто видав токен
    subпро кого токен, зазвичай ID користувача
    audдля якого одержувача він призначений
    expмомент, з якого токен не можна приймати
    nbfмомент, раніше за який токен ще недійсний
    iatчас випуску
    jtiунікальний ідентифікатор токена

    Дві дрібниці, на яких спотикаються парсери в тестах. jtiтеж зареєстрований claim, а не домовленість застосунку: його значення MUST призначатися так, щоб імовірність збігу була мізерною, тоді як сам ужиток claim-а — OPTIONAL. А exp — це NumericDate, тобто «the number of seconds from 1970-01-01T00:00:00Z UTC», причому «non-integer values can be represented»: дробове значення тут законне.

    Підпис обчислюється над base64url(header) + "." + base64url(payload), а імена алгоритмів у заголовку токена означені окремо: HS256 — «HMAC using SHA-256», RS256 — «RSASSA-PKCS1-v1_5 using SHA-256», ES256 — «ECDSA using P-256 and SHA-256». Різниця між сімействами практична: HMAC доводить лише володіння спільним секретом, тож той, хто перевіряє токен, може його й підробити. В асиметричній схемі видавець підписує приватним ключем, а перевіряльник має лише публічний — і не підробить нічого.

    Головне для тестування: підпис захищає токен від підробки, але не від читання. Header і payload лише закодовані, а не зашифровані — шифрування з'являється тільки у варіанті JWE. Тому в payload не кладуть паролів і секретів, а зміна ролі в payload — базова негативна перевірка: підпис має зламатися. Дзеркальне правило для сервера: сам лише вміст токена не є підставою для рішення про довіру, поки не перевірено криптографічний захист.

    Для тесту «відкритість» payload — інструмент, а не проблема: exp читається без жодної бібліотеки, тож свіжість токена перевіряється до запиту, а не постфактум по 401.

    // payload JWT — це просто base64url, декодер бібліотеки не потребує
    function decodeJwtPayload(token: string): Record<string, unknown> {
      const payload = token.split('.')[1];
      return JSON.parse(Buffer.from(payload, 'base64url').toString('utf8'));
    }
    
    const claims = decodeJwtPayload(token);
    const expiresAt = new Date(Number(claims.exp) * 1000); // exp — секунди, не мілісекунди

    Пастку alg: none, , вимоги до довжини секрету й відкликання токенів розбирає окрема глава розділу про безпеку для QA — сюди береться формат і механізм.

    Де тримати токен і чим кожен вибір платить

    Ідеального місця немає: вибір сховища — це вибір , і він же визначає, як тест до цього стану дотягнеться.

    СховищеЗа що платимоДоступність із тесту
    localStorageбудь-який скрипт сторінки прочитає токен — це вектор XSSчитається й пишеться скриптом сторінки
    sessionStorageтой самий вектор, лише в межах вкладкитак само; але в збережений стан Playwright він не потрапляє взагалі
    кука з HttpOnlyJavaScript її не бачить, зате браузер шле її автоматично — це вектор CSRFлише на рівні контексту браузера
    пам'ять (JS-змінна)вікно для XSS вужче, але стан не переживає перезавантаженняне персистується взагалі

    Механіка обох векторів варта одного абзацу. XSS (cross-site scripting) — це чужого скрипту на довірену сторінку; браузер вважає його «своїм», тож скрипт отримує доступ до кукі, сесійних токенів та іншої чутливої інформації, збереженої для цього сайту, а найтяжчий наслідок — й акаунта. Саме тут працює HttpOnly: він обмежує куку лише HTTP-запитами, тож document.cookie її не бачить і виконаний чужий скрипт токен сесії не прочитає. Але ту саму куку браузер і далі надсилає сам — а це вже CSRF. Повний розбір XSS живе в главі «HTTPS, TLS і безпека».

    Одна деталь Web Storage, яку часто прочитують навпаки: дані звідти не їдуть на сервер самі — якщо вони мають туди дістатися, застосунок явно кладе їх у тіло запиту або в заголовок Authorization.

    Наша практика (не канон). Формулювання «у Web Storage немає аналога HttpOnly» — наш вивід із зіставлення двох джерел, прямим твердженням у жодному з них воно не стоїть. Практичний висновок теж наш: якщо схему обираєте ви, найзахищеніший компроміс — короткий токен доступу в пам'яті плюс довший у куці з HttpOnly.

    OAuth 2.0: як дати доступ, не віддаючи пароля

    OAuth 2.0 — фреймворк делегованого доступу: він дає сторонньому застосунку обмежений доступ до HTTP-сервісу від імені власника ресурсу через явну згоду. Питання, на яке він відповідає: як дати застосунку A доступ до частини даних у сервісі B, не віддаючи A ваш пароль від B. Саме на ньому побудований «Увійти через Google».

    У протоколі чотири ролі: власник ресурсу (кінцевий користувач, якщо це людина), клієнт (застосунок, що звертається до захищених ресурсів від його імені), сервер авторизації (видає токени після автентифікації власника й отримання згоди) і сервер ресурсів (зберігає дані й приймає запити з токеном).

    Сервер авторизаціїКлієнт (ваш застосунок)Браузер користувачаСервер авторизаціїКлієнт (ваш застосунок)Браузер користувача«Увійти через Google»редірект на endpoint авторизаціїлогін і згода (consent)редірект назад із authorization codeповертає codeобмін code на токен (сервер-до-сервера)access token у межах scopeСервер авторизаціїКлієнт (ваш застосунок)Браузер користувачаСервер авторизаціїКлієнт (ваш застосунок)Браузер користувача«Увійти через Google»редірект на endpoint авторизаціїлогін і згода (consent)редірект назад із authorization codeповертає codeобмін code на токен (сервер-до-сервера)access token у межах scope

    Ключових ідей тут дві. Перша: код авторизації видається через сервер авторизації як , тому клієнт ніколи не бачить облікових даних користувача — пароль від Google до застосунку не потрапляє. Друга: межі виданого токена задає параметр scope, який клієнт запитує на авторизації або токена, — застосунок отримує доступ, наприклад, лише до пошти й імені. Далі токен пред'являють у заголовку Authorization зі схемою Bearer, тож TLS обов'язковий.

    Поруч живе пара токенів. Access token — обліковка, що підтверджує видану клієнту авторизацію й дає доступ до ресурсів. Refresh token видається сервером авторизації й потрібен лише для того, щоб отримати новий access-токен, коли поточний став невалідним або протермінувався, — без повторного входу користувача. Ротацію, відкликання й час життя цієї пари розбирає розділ про безпеку для QA.

    Дві межі, які варто тримати чітко. Термінологічна: OAuth 2.0 — про авторизацію (доступ до ресурсів), а власне автентифікацію дає — надбудова над ним; «Вхід через Google» технічно спирається саме на OIDC. Технічна: для публічних клієнтів (SPA, мобільні), у яких немає безпечного місця для секрету, потік доповнюють (Proof Key for Code Exchange) — механізмом, що захищає обмін коду від перехоплення; його механіка, як і вимоги до redirect_uri, теж лежить у розділі про безпеку.

    Наша практика (не канон). Канонічного джерела під цим немає — далі досвід. Повний OAuth-потік через реальний UI провайдера в тесті ганяти не варто: провайдери цілеспрямовано детектують автоматизацію (, «підозрілий вхід», блокування headless), потік стрибає між доменами, а тестовий акаунт майже завжди тягне другий фактор. Принцип простий: чужого провайдера ви не тестуєте — OAuth Google є відповідальністю Google. Перевіряти треба, що ваш застосунок правильно обробляє повернутий код або токен, тож провайдера на рівні мережі (механіку розбирає глава «Перехоплення й мокання мережі») або піднімають тестовий IdP.

    CSRF: кука доводить не те, що здається

    CSRF (cross-site request forgery, міжсайтова підробка запиту) — , у якій сторонній ресурс змушує браузер уже автентифікованого користувача виконати небажану дію на довіреному сайті.

    bank.example.comБраузерЧужий сайтКористувачbank.example.comБраузерЧужий сайтКористувачбраузер чіпляє куку самвідкриває сторінкуприхована форма шле POST /transferPOST /transfer + сесійна кука200 OK, переказ виконаноbank.example.comБраузерЧужий сайтКористувачbank.example.comБраузерЧужий сайтКористувачбраузер чіпляє куку самвідкриває сторінкуприхована форма шле POST /transferPOST /transfer + сесійна кука200 OK, переказ виконано

    Корінь проблеми — та сама зручність, що робить сесії простими: браузер сам чіпляє куку до кожного запиту на відповідний домен. Кука доводить, що запит іде від браузера користувача, але не доводить, що його ініціювала справжня сторінка застосунку. Форма для цього навіть не потрібна: коли користувач завантажує сторінку, браузер намагається дістати ресурс-картинку, який насправді є запитом .

    Розрив закриває патерн синхронізованого токена (synchronizer token pattern) — за формулюванням OWASP, «один із найпоширеніших і рекомендованих» способів пом'якшити CSRF. Сервер вкладає непередбачуване значення у видану сторінку (приховане поле форми або тег meta), клієнт повертає його разом із небезпечною дією (поле форми або заголовок на кшталт X-CSRF-Token), а сервер звіряє надіслане зі збереженим для цієї сесії. Тримається все на політиці одного походження: чужий сайт не прочитає вмісту сторінки застосунку, а отже не знає токена. Сам токен при цьому не передають кукою — інакше він поїхав би автоматично разом із підробленим запитом. Сусідній рубіж — атрибут кукі SameSite; OWASP називає його корисною , яка у більшості інсталяцій повноцінного CSRF-захисту не заміняє. Вимоги до токена, дефолти й обходи SameSite та CSRF в API — предмет окремої глави розділу про безпеку.

    Одну межу плутають постійно: CSRF — це не CORS. CORS про читання відповіді, CSRF — про небажану дію, яка відбувається навіть тоді, коли відповідь прочитати неможливо. Чому чужий сайт не бачить вашої сторінки й чому CORS не є захистом від CSRF, розбирає глава «CORS і політика одного походження».

    Наша практика (не канон). Джерела цього прямо не кажуть, але на практиці саме CSRF-захист дає «непояснимі» 403: тест, який б'є по мутаційному ендпоінту напряму, в обхід форми, мусить спершу дістати токен зі сторінки й передати його заголовком. Підстановка готового стану сесії цього не скасовує. І другий наш висновок: чисто token-based API (без кукі, з Authorization: Bearer) до CSRF зазвичай не вразливе, бо заголовок автоматично не підставляється — його треба свідомо додати.

    Як логінитися в автотесті без форми входу

    Причина відмовитися від UI-логіну глибша за швидкість: логін і цільова фіча — різні вимоги, і змішувати їх в одній перевірці не можна. Якщо тест кабінету щоразу проходить форму входу, падіння логіну «фарбує в червоне» тест кабінету, хоча кабінет ні до чого. Дока Playwright називає той самий мотив ширше: ізоляція контекстів існує заради відтворюваності й захисту від каскадних падінь, а готовий стан знімає логін із кожного тесту.

    Рекомендований шлях — автентифікуватися один раз у setup-проєкті, зберегти стан і стартувати з нього кожен тест. Умову застосовності дока називає прямо: це підхід «for tests without server-side state».

    // auth.setup.ts — логін через API, стан у файл
    import { test as setup, request } from '@playwright/test';
    
    setup('authenticate', async () => {
      const api = await request.newContext();
      await api.post('https://app.example.com/api/login', {
        data: { email: process.env.USER_EMAIL, password: process.env.USER_PASSWORD },
      });
      await api.storageState({ path: 'playwright/.auth/user.json' });
    });

    Стан сховища взаємозамінний між браузерним і API-контекстом — це офіційно підтримуваний шлях «залогінься запитом, перевіряй в UI». Далі кожен тест стартує вже автентифікованим: storageState оголошено в конфігурації, а setup-проєкт — залежністю решти проєктів.

    Що з цього ламається на практиці:

    • Стан протухає, і ніхто за цим не стежить. Видалити протухлий файл — обов'язок автора тестів; один із виходів — писати стан у теку, яка чиститься перед кожним прогоном.
    • В UI-режимі setup-проєкт за замовчуванням не виконується — заради швидкості. Тому UI-режим стартує зі станом, збереженим минулого разу, і коли той протухає, 401 приходить саме локально, тоді як у CI setup відпрацював і стан свіжий; дока радить час від часу запускати auth.setup.ts руками.
    • sessionStorage у стані не зберігається — API для цього немає; автентифікацію, що спирається на нього, доводиться відновлювати вручну.
    • Кілька ролей — це кілька файлів стану, зроблених одним setup-проєктом; роль обирають на рівні файла або групи тестів, а дві ролі в одному тесті — це два контексти.
    • Файл стану містить живі секрети — кукі й заголовки, якими можна вдати вас або тестовий акаунт; дока наполегливо не радить його .
    • Кукі виставляються не миттєво. Логін часто йде через кілька редиректів, тож чекати треба кінцевого URL або стану, у якому всі кукі вже виставлені; фіксованої паузи дока не пропонує взагалі.

    Спосіб підстановки диктує форма стану. Токен зі сховища кладуть скриптом до завантаження сторінки, а куку з HttpOnly — лише на рівні , бо зі сторінки JavaScript до неї не дотягнеться:

    // HttpOnly-куку зі сторінки не поставити — тільки через контекст
    await context.addCookies([{
      name: 'session_id', value: SESSION,
      domain: 'app.example.com', path: '/', httpOnly: true, secure: true,
    }]);

    Для API-шару діє те саме правило одного місця: токен налаштовують один раз у конфігурації через extraHTTPHeaders, звідки його підхоплює вбудована request, — менше місць, де він може протекти або розійтися.

    Наша практика (не канон). Стратегії проти «плаваючого» 401 у довгих прогонах джерела не описують — вони наші. Робочих дві: читати exp із запасом і оновлювати токен наперед або ловити 401 і повторювати запит рівно один раз після оновлення.

    Типові помилки

    • Виглядає як «спершу 401, потім 403», а насправді порядку стадій протокол не наказує: 403 законний і тоді, коли облікових даних у запиті не було взагалі.
    • Виглядає як «прийшов 403 — отже, роль не та», а насправді заборона може бути з причин, не пов'язаних із креденшелами, а замість 403 сервер має право віддати 404, щоб приховати ресурс.
    • Виглядає як «токен підписаний, отже дані в безпеці», а насправді підпис захищає від підробки, а не від читання: header і payload лише закодовані.
    • Виглядає як «Basic Auth шифрує пароль», а насправді base64 — кодування; пароль летить відкритим текстом, і без TLS схема захисту не дає.
    • Виглядає як «HttpOnly рятує від усього», а насправді він закриває куку від скрипту, але браузер і далі надсилає її сам — це і є CSRF.
    • Виглядає як «у нас CORS налаштований, CSRF не страшний», а насправді CORS про читання відповіді, а небажана дія відбувається й тоді, коли відповідь прочитати неможливо.
    • Виглядає як «токен валідний, а сервер віддає 401», а насправді зіпсований рядок заголовка або редирект на інше походження, на якому Authorization знімається.

    Підсумок

    1. Автентифікація відповідає на «хто ти?», авторизація — на «що тобі можна?»; це різні стадії, але жорсткого порядку між ними HTTP не встановлює, і 403 без облікових даних цілком законний.
    2. Протокол стану не тримає, тому доказ особи їде в кожному запиті заново — кукою, яку браузер підставляє сам, або заголовком Authorization, який ставить клієнтський код.
    3. Серверна сесія тримає стан у застосунку й легко відкликається; токен тримає стан у собі й масштабується, але достроково не гаситься.
    4. Підпис JWT гарантує цілісність, а не конфіденційність: payload читає будь-хто, у кого є токен.
    5. Вибір сховища токена — це вибір вектора атаки (XSS проти CSRF), і він же визначає, як тест підставить стан: сховище — скриптом, HttpOnly-куку — лише через контекст браузера.

    Можливі питання

    • «Чим 401 відрізняється від 403 Слабка відповідь зупиняється на «не впізнав / не можна»; сильна додає межу — заборона може бути не пов'язана з креденшелами, а сервер має право віддати 404 замість 403.
    • «Що йде першим — автентифікація чи авторизація?» Перевіряють, чи повторите ви завчену схему. Очікують розрізнення рамок: як процес перевірка прав іде після встановлення особи, але як коди відповіді жорсткого порядку немає.
    • «Payload JWT зашифрований?» Питання-пастка про підпис: дивляться, чи не сплутаєте цілісність із конфіденційністю.
    • «Чому Basic Auth вважають небезпечним?» Перевіряють різницю між кодуванням і шифруванням, а не знання схеми.
    • «Що таке CSRF і чому воно взагалі можливе?» Слухають корінь: браузер підставляє куку сам, тож вона доводить походження від браузера, а не від сторінки застосунку.
    • «Як влаштований вхід через Google?» Очікують потік коду авторизації своїми словами й головну ідею — пароль до застосунку не потрапляє; бонусом іде межа OAuth проти OpenID Connect.
    • «Як ви логінитесь в автотестах?» Сильна відповідь починається не з інструмента, а з тези «логін і фіча — різні вимоги», далі — стан раз на прогін і окремо те, що з ним ламається: протухання, секрети у файлі, sessionStorage, перед першою мутацією.

    Джерела

    Дві стадії — і жоден порядок між ними протоколом не наказаний

    • RFC 9110 — HTTP Semantics — §15.5.4 і §15.5.2: умовність застереження про облікові дані, «forbidden for reasons unrelated to the credentials», право віддати 404 замість 403, обов'язковий WWW-Authenticate у 401.
    • MDN — HTTP authentication — автентифікація як доведення особи проти авторизації як рішення про право на дію.
    • MDN — Authorization header — цикл «запит без даних → 401 з → повтор із Authorization».
    • OWASP Cheat Sheet — Security Terminology — два питання дослівно, три фактори автентифікації, порядок «after successful authentication».
    • ISTQB Glossaryauthentication як процедура і authorization як наданий дозвіл.

    Доказ особи доводиться нести в кожному запиті заново

    Схеми Basic і Bearer: що саме їде в заголовку

    Серверна сесія проти токена: де саме живе стан

    JWT: три частини, які читає будь-хто

    Де тримати токен і чим кожен вибір платить

    • MDN — Web Storage API — область видимості localStorage і sessionStorage, доступ будь-якого скрипту походження.
    • MDN — Cross-site scripting (XSS) — внесений інʼєкцією скрипт як механізм крадіжки токена зі сховища.
    • OWASP — Cross Site Scripting (XSS) — доступ внесеного інʼєкцією скрипту до кукі й сесійних токенів, захоплення сесії як найтяжчий наслідок.
    • RFC 6265 — HTTP State Management MechanismHttpOnly як обмеження кукі лише HTTP-запитами.
    • MDN — Using HTTP cookiesHttpOnly закриває куку від document.cookie, але не від автоматичного надсилання.
    • Playwright — Authentication — що саме покриває збережений стан і чого в ньому немає (sessionStorage).

    OAuth 2.0: як дати доступ, не віддаючи пароля

    CSRF: кука доводить не те, що здається

    • OWASP — CSRF Prevention Cheat Sheet — означення атаки, синхронізований токен як один із найпоширеніших рекомендованих заходів, вимоги до нього, опора на , заборона везти токен кукою, SameSite як глибина захисту, що повноцінного захисту не заміняє.
    • MDN — Using HTTP cookies — автоматичне надсилання кукі на відповідний домен як корінь проблеми.
    • MDN — Cross-site request forgery (CSRF) — стано-змінний запит без форми: завантаження картинки як транзакція.
    • MDN — Same-origin policy — чому чужий сайт не прочитає сторінку застосунку.
    • MDN — Cross-Origin Resource Sharing (CORS) — CORS про читання відповіді, а не про заборону дії.

    Як логінитися в автотесті без форми входу

    • Playwright — Authentication — setup-проєкт як залежність, умова «без серверного стану», непідтримка sessionStorage, протухання стану й UI-режим, кілька ролей, секрети у файлі стану, очікування кінцевого URL замість паузи.
    • Playwright — API testing — взаємозамінність storage state між API- і браузерним контекстом, extraHTTPHeaders один раз у конфігурації.
    • Playwright — class BrowserContext — підстановка кукі на рівні контексту.
    • RFC 6265 — HTTP State Management Mechanism — чому HttpOnly-кука недосяжна для коду сторінки.
    • RFC 7519 — JSON Web Token (JWT)exp у payload як спосіб перевірити свіжість токена до запиту.

    Пояснення

    «Поясни» працює з власним API-ключем Anthropic: запит іде з вашого браузера прямо до Anthropic.

    Свій API-ключ (BYOK)

    Вставте власний ключ Anthropic — пояснення працюватиме на реальній моделі. Ключ зберігається лише у вашому браузері (localStorage), нікуди не надсилається, крім api.anthropic.com, і не логується.