Автентифікація та авторизація
Зміст
Нічний прогін падає з 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; звідси клас багів «пароль із діакритикою працює в одному клієнті й падає в іншому» (єдине дозволене значення параметра charset — UTF-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 він не потрапляє взагалі |
кука з HttpOnly | JavaScript її не бачить, зате браузер шле її автоматично — це вектор 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 до застосунку не потрапляє. Друга: межі виданого токена задає параметр 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, міжсайтова підробка запиту) — , у якій сторонній ресурс змушує браузер уже автентифікованого користувача виконати небажану дію на довіреному сайті.
Корінь проблеми — та сама зручність, що робить сесії простими: браузер сам чіпляє куку до кожного запиту на відповідний домен. Кука доводить, що запит іде від браузера користувача, але не доводить, що його ініціювала справжня сторінка застосунку. Форма для цього навіть не потрібна: коли користувач завантажує сторінку, браузер намагається дістати ресурс-картинку, який насправді є запитом .
Розрив закриває патерн синхронізованого токена (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знімається.
Підсумок
- Автентифікація відповідає на «хто ти?», авторизація — на «що тобі можна?»; це різні стадії, але жорсткого порядку між ними HTTP не встановлює, і
403без облікових даних цілком законний. - Протокол стану не тримає, тому доказ особи їде в кожному запиті заново — кукою, яку браузер підставляє сам, або заголовком
Authorization, який ставить клієнтський код. - Серверна сесія тримає стан у застосунку й легко відкликається; токен тримає стан у собі й масштабується, але достроково не гаситься.
- Підпис JWT гарантує цілісність, а не конфіденційність: payload читає будь-хто, у кого є токен.
- Вибір сховища токена — це вибір вектора атаки (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 Glossary —
authenticationяк процедура іauthorizationяк наданий дозвіл.
Доказ особи доводиться нести в кожному запиті заново
- RFC 9110 — HTTP Semantics — §3.3: HTTP як протокол без стану, семантика запиту зрозуміла ізольовано.
- MDN — An overview of HTTP — безстановий протокол проти застосунку, який стан має.
- Roy T. Fielding — Architectural Styles and the Design of Network-based Software Architectures, ch.5 (REST) — обмеження stateless: запит несе все потрібне сам.
- RFC 6265 — HTTP State Management Mechanism — браузер за метаданими сам вирішує, чи повернути кукі в заголовку
Cookie. - RFC 6750 — The OAuth 2.0 Authorization Framework: Bearer Token Usage — форма
credentials = "Bearer" 1*SP b64token.
Схеми Basic і Bearer: що саме їде в заголовку
- MDN — Authorization header — синтаксис «схема + пробіл + дані», нечутливість схеми до регістру, зняття заголовка на редиректах між походженнями.
- RFC 7617 — The 'Basic' HTTP Authentication Scheme — приклад
Aladdin:open sesame, передавання пароля відкритим текстом,SHOULD NOTбез HTTPS, двокрапка в логіні,charsetіrealm. - RFC 6750 — The OAuth 2.0 Authorization Framework: Bearer Token Usage — сенс схеми «на пред'явника», жорсткий формат значення й обов'язковий TLS.
- MDN — HTTP authentication —
403на валідні, але недостатні креденшели; комплект заголовків проксі й код407.
Серверна сесія проти токена: де саме живе стан
- RFC 6265 — HTTP State Management Mechanism — обмін «логін → запис сесії → ідентифікатор кукою → кука на кожному запиті».
- MDN — Using HTTP cookies — сервер тримає стан у себе, а браузеру віддає лише ключ; вимоги до сесійної кукі.
- RFC 7519 — JSON Web Token (JWT) — перевірка підпису замість пошуку в базі й складність дострокового відкликання.
- RFC 6749 — The OAuth 2.0 Authorization Framework — токен доступу як обліковка, що підтверджує видану авторизацію.
- MDN — Authorization header — токен їде в заголовку вручну, на відміну від кукі.
JWT: три частини, які читає будь-хто
- RFC 7519 — JSON Web Token (JWT) — призначення формату, base64url-частини й змінність їх числа, JWT Claims Set, зареєстровані claims,
jti,NumericDateдляexp, вимога перевірити захист перед довірою. - RFC 7515 — JSON Web Signature (JWS) — над чим саме обчислюється підпис.
- RFC 7518 — JSON Web Algorithms (JWA) — імена й зміст
HS256,RS256,ES256; різниця симетричного та асиметричного підпису. - OWASP Cheat Sheet — JSON Web Token — підписаний токен проти зашифрованого, спільний секрет як підробки з боку перевіряльника.
- RFC 6750 — The OAuth 2.0 Authorization Framework: Bearer Token Usage — передавання токена схемою
Bearerі обов'язковий захищений транспорт.
Де тримати токен і чим кожен вибір платить
- MDN — Web Storage API — область видимості
localStorageіsessionStorage, доступ будь-якого скрипту походження. - MDN — Cross-site scripting (XSS) — внесений інʼєкцією скрипт як механізм крадіжки токена зі сховища.
- OWASP — Cross Site Scripting (XSS) — доступ внесеного інʼєкцією скрипту до кукі й сесійних токенів, захоплення сесії як найтяжчий наслідок.
- RFC 6265 — HTTP State Management Mechanism —
HttpOnlyяк обмеження кукі лише HTTP-запитами. - MDN — Using HTTP cookies —
HttpOnlyзакриває куку відdocument.cookie, але не від автоматичного надсилання. - Playwright — Authentication — що саме покриває збережений стан і чого в ньому немає (
sessionStorage).
OAuth 2.0: як дати доступ, не віддаючи пароля
- RFC 6749 — The OAuth 2.0 Authorization Framework — делегований доступ і згода, чотири ролі, authorization code через посередника,
scope, access- і . - RFC 6750 — The OAuth 2.0 Authorization Framework: Bearer Token Usage — пред'явлення токена в
Authorizationі обов'язковий TLS. - OpenID Connect Core 1.0 — шар ідентичності над OAuth, на якому насправді стоїть «вхід через провайдера».
- RFC 7636 — Proof Key for Code Exchange (PKCE) — захист обміну коду для публічних клієнтів.
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 як спосіб перевірити свіжість токена до запиту.
Чим автентифікація відрізняється від авторизації?
Це відповіді на два різні питання: «хто ти?» і «що тобі дозволено?». Перша стадія встановлює особу — паролем, токеном, кодом чи біометрією; безпековий канон так і групує фактори: те, що людина знає, те, що має, і те, чим вона є. Друга стадія вирішує, чи має вже впізнаний користувач право саме на цю дію: читач не редагує чужий проєкт, менеджер не бачить чужих рахунків. розводить поняття ще жорсткіше — за типом сутності: перше є процедурою встановлення особи, друге — виданим дозволом на доступ до ресурсів, тобто це не дві половини одного механізму. Для тестувальника це дві окремі групи вимог і два окремі набори тестів: одні перевіряють вхід, інші — рольову модель. Найшвидше межу видно у кодах відповіді: 401 вказує на проблему з доказом особи, 403 — на виконати зрозумілий запит.
У чому різниця між 401 і 403?
401 означає, що запит не несе валідних облікових даних для цього ресурсу, а 403 — що сервер запит зрозумів, але виконувати його відмовляється. У 401 є формальний обов'язок, про який часто забувають: відповідь мусить нести заголовок WWW-Authenticate щонайменше з одним (challenge), який каже клієнту, яку схему застосувати. Звідси й типовий цикл: запит без даних → 401 із челенджем → повторний запит уже з Authorization. Окрема поведінка, що збиває з пантелику в браузері: якщо креденшели валідні, але їх недостатньо, сервер має відповісти 403, і повторної спроби браузер уже не запропонує — діалог логіну не з'явиться. Назва 401 Unauthorized при цьому історично невдала: за змістом це радше «неавтентифікований», хоча самого слова специфікація не вживає — це наша оцінка, а не цитата. Практичне правило діагностики: 401 веде розслідування до входу й транспорту креденшелів, 403 — до моделі прав.
Що відбувається першим — автентифікація чи авторизація?
Питання-пастка: перевіряють, чи повторите ви завчену схему «спершу 401, потім 403». Жорсткого порядку стадій HTTP не наказує. У RFC 9110 §15.5.4 умова про облікові дані сформульована саме умовно — «якщо дані в запиті були надані», — а поруч стоїть пряме застереження, що запит може бути заборонений із причин, узагалі не пов'язаних із креденшелами. Тобто 403 цілком законний і для запиту, у якому доказу особи не було зовсім. Дзеркальне право сервера з того ж параграфа: щоб не видавати сам факт існування закритого ресурсу, замість 403 дозволено віддати 404. Суперечності з безпековим каноном, який фіксує перевірку прав після успішного встановлення особи, тут немає: канон описує процес, а RFC — коди відповіді, і виводити з процесу гарантію про код не можна. Сильна відповідь на цьому й будується, а практичний наслідок такий: у негативному сценарії з правами очікуйте 403 або 404, інакше тест зламається від законного вибору сервера сховати ресурс.
Чому доказ особи доводиться нести в кожному запиті заново?
Бо протокол за побудовою не тримає стану: семантику окремого запиту має бути зрозуміло ізольовано, і сервер не має права припускати зв'язок між двома запитами. Те саме обмеження REST формулює як вимогу — запит несе все, що потрібно для його обробки, і не спирається на контекст, збережений на сервері. Мотив суто прагматичний: сервер, який нікого не пам'ятає, легко множиться, і будь-яка машина за балансувальником обробить будь-який запит. Форм у цього доказу дві. Кука: сервер видає її в Set-Cookie, а рішення повертати чи ні ухвалює далі сам браузер, звіряючи адресу запиту з метаданими, збереженими біля неї. Токен: клієнт зберігає його самостійно й свідомо додає в Authorization. Уся тема — сесії, JWT, OAuth — це надбудови над цим одним фактом, а для автотесту з нього випливає головне питання до застосунку: у якій формі в ньому живе доказ особи і як він потрапляє в кожен запит.
Як влаштований Basic Auth і чому base64 — це не захист?
Пару «логін-пароль» склеюють через двокрапку, проганяють через і відправляють заголовком зі схемою Basic; канонічний приклад самої специфікації — пара Aladdin / open sesame, яка перетворюється на QWxhZGRpbjpvcGVuIHNlc2FtZQ==. Ключове тут — різниця між кодуванням і шифруванням: base64 розгортається назад тривіально, тож пароль фактично летить мережею відкритим текстом, і RFC говорить це прямо, а схему без зовнішнього захищеного каналу на кшталт TLS безпечною не вважає (модальність — SHOULD NOT, а не заборона). Звідси два готові негативні кейси. Перший: логін із двокрапкою всередині невалідний, бо перша двокрапка й розділяє пару, а все після неї стає частиною пароля. Другий: початкове означення схеми кодування символів не задавало взагалі, тож реалізації розійшлися між ISO-8859-1 і UTF-8 — так народжується клас багів «пароль із діакритикою приймається одним клієнтом і відхиляється іншим»; параметр charset при цьому має єдине дозволене значення UTF-8 і, на відміну від обов'язкового realm, є необов'язковим. Для AQA це щоденна практика: закриті стенди часто стоять саме за Basic Auth, і креденшели передають на рівні контексту (httpCredentials у Playwright), а не клікають у нативному діалозі браузера.
Що таке схема Bearer і що вона означає для безпеки?
«Bearer» — це «пред'явник»: будь-хто, хто заволодів токеном, використає його рівно так само, як законний власник, і жодного додаткового доказу володіння схема не вимагає. Тому токен бережуть як ключ від квартири, а захищений транспорт тут обов'язковий, а не бажаний. Формат значення жорсткий: слово Bearer, щонайменше один пробіл, далі сам токен; сама назва схеми при цьому нечутлива до регістру, а от структуру значення диктує саме схема, а не заголовок. Практичний наслідок для тестів: найчастіша причина 401 на «стовідсотково валідному» токені — не токен, а зіпсований рядок заголовка. У Playwright заголовок задають один раз на рівні конфігурації через extraHTTPHeaders, щоб мати єдине місце оновлення й менше шансів десь його загубити.
API віддає 401, хоча токен точно валідний. Де шукати?
Перевіряти треба не токен, а те, що реально поїхало в запиті. Перший підозрюваний — сам рядок заголовка: забуте слово Bearer, зайвий пробіл, обрізаний під час копіювання токен. Другий — редиректи: заголовок Authorization знімається при переході на інше , тож клієнт його «точно слав», а до кінцевого сервісу він не доїхав; у мережевій панелі це видно як запит без заголовка після переходу. Третій — корпоративна мережа: у власний комплект (407 Proxy Authentication Required, Proxy-Authenticate, Proxy-Authorization), і плутанина з ресурсними заголовками дає рівно той самий симптом «чомусь не автентифікується». І окремо тримайте в голові межу: якщо креденшели валідні, але недостатні, правильна відповідь — 403, тож 401 тут узагалі не про права. Порядок дій простий: подивитися сирий запит, який дійшов до сервера, і лише потім чіпати логіку логіну.
Серверна сесія проти токена: у чому головна різниця?
Розводить ці підходи одна річ, і це не синтаксис креденшела, а адреса, за якою лежить стан. У серверній сесії весь контекст — хто ви, ролі, кошик — лежить записом на сервері, а браузер носить лише ключ до нього: непрозорий ідентифікатор у куці, який сам собою нічого не означає; браузер підставляє його автоматично на кожен запит. У токен-підході контекст запаковано в сам токен: сервер перевіряє підпис і нічого не шукає у себе, тому будь-який вузол кластера самодостатній. Ціна дзеркальна. Сесію відкликають миттєво — досить видалити запис; токен достроково погасити важко, поки він не протермінувався, тож або чекають exp, або тримають окремі списки відкликання. Сесія натомість змушує тримати одне сховище на всі вузли: без нього кожен переїзд запиту на сусідню машину за балансувальником читається як розлогінення. Для тесту ця різниця визначає спосіб підстановки стану: куку кладуть на рівні , токен — у заголовок.
Що таке JWT і з яких частин він складається?
JWT — компактний формат передавання тверджень (claims) між двома сторонами, безпечний для вживання в URL; його й проєктували під середовища, де довжина обмежена, — заголовок Authorization чи параметри адреси. Технічно це кілька закодованих у base64url шматків, зчеплених крапками; звичний вигляд header.payload.signature дає подання JWS Compact. Деталь, на якій легко помилитися: число частин специфікація навмисно не фіксує — воно залежить від подання, і в JWE Compact їх п'ять. Тому перевірка «у токені рівно дві крапки» ловить подання, а не відповідність формату. Самі твердження лежать звичайним JSON-обʼєктом, і частину полів зареєстрував сам стандарт: iss (хто видав), sub (про кого), aud (для кого), exp (з якого моменту не приймати), nbf (раніше за який ще недійсний), iat (час випуску), jti (унікальний ідентифікатор токена). Підпис обчислюють над закодованими header і payload, з'єднаними крапкою, — саме тому зміна будь-якого символа в payload ламає перевірку.
Payload JWT зашифрований? Чи можна класти туди пароль?
Ні й ні — і це найпоширеніша плутанина в темі. Header і payload лише закодовані в base64url, тож будь-хто, у кого є токен, прочитає їх звичайним декодером без жодного ключа; шифрування з'являється лише в окремому варіанті — JWE. Підпис гарантує цілісність, а не конфіденційність: він доводить, що вміст не підмінили, і нічого не приховує. Тому паролі, секрети й чутливі персональні дані в payload не кладуть. Із цього ж випливає базова негативна перевірка: підмінили роль у payload — підпис має зламатися, і сервер зобов'язаний відхилити токен; дзеркальне правило для бекенду — довіряти написаному в токені, доки не перевірено його криптографічний захист, сервер не має права. Тесту ж ця прозорість payload лише на руку: із нього дістають exp, sub чи роль і будують на цьому перевірки.
Як перевірити свіжість токена ще до запиту?
Розкодувати другу частину токена звичайним base64url-декодером і прочитати exp — бібліотека для цього не потрібна, бо payload не зашифрований. Формат значення — NumericDate, тобто кількість секунд від 1970-01-01T00:00:00Z UTC, причому нецілі значення стандарт теж . Звідси найтиповіша помилка в коді тестів: Date.now() віддає мілісекунди, тож без множення на 1000 різниця перетворюється на велетенське від'ємне число, токен виглядає завжди простроченим і рефреш смикається на кожній перевірці. Практична цінність підходу в тому, що свіжість перевіряють до запиту, а не постфактум по 401: якщо до закінчення лишилося менше запасу, скажімо хвилина, токен оновлюють наперед і гонки на межі не виникає взагалі.
Чим HS256 відрізняється від RS256 і чому це важливо?
Це імена алгоритмів підпису із заголовка токена: HS256 — HMAC на SHA-256, RS256 — RSASSA-PKCS1-v1_5 на SHA-256, ES256 — ECDSA на кривій P-256 із SHA-256. Різниця між сімействами не академічна, а модельна. HMAC симетричний: він доводить лише володіння спільним секретом, а отже той, хто вміє токен перевіряти, вміє його й підробити — усі учасники з ключем рівні у правах. ламають цю рівність: право підпису лишається у власника приватного ключа, а сторона перевірки обходиться публічним і підробити ним нічого не може. Практичний наслідок: якщо ваш сервіс лише споживає чужі токени, симетричний секрет розширює коло тих, хто може випустити валідний токен від вашого імені, і це вже привід для питання до архітектури. Суміжні пастки на кшталт alg: none і підміни алгоритму розбирають окремо в темі безпеки — тут важливо не сплутати сімейства.
Де тримати токен на клієнті й чим платить кожен вибір?
Ідеального місця немає: вибір сховища — це вибір . localStorage переживає перезавантаження, але доступний будь-якому скрипту сторінки, тож будь-який XSS означає крадіжку токена; sessionStorage дає той самий вектор у межах вкладки, а до збереженого стану Playwright він узагалі не потрапляє. Кука з HttpOnly закрита від JavaScript — внесений скрипт її не прочитає, — зате браузер шле її автоматично, а це вже CSRF. Токен у пам'яті сторінки звужує вікно для XSS, але не переживає F5 і вимагає механізму оновлення. Механіка XSS пояснює, чому це серйозно: чужий скрипт, внесений інʼєкцією в довірену сторінку, браузер вважає «своїм», тож той дістає доступ до кукі й сесійних токенів, а найтяжчий наслідок — захоплення акаунта. Звідси й типовий компроміс, якщо схему обираєте ви: недовгий токен доступу тримати в памʼяті, а довший — у куці, закритій атрибутом HttpOnly. І окрема деталь, яку читають навпаки: дані з Web Storage самі на сервер не їдуть — застосунок мусить явно покласти їх у тіло запиту чи в заголовок.
Що таке CSRF і чому воно взагалі можливе?
влаштована так: чужий ресурс руками браузера, у якому користувач уже залогінений, виконує на довіреному сайті дію, якої той не замовляв. Причина криється в зручності, заради якої сесії й люблять, — куку до запиту на її домен браузер додає без питань. Тобто кука доводить, що запит іде від браузера користувача, але нічого не каже про те, чи ініціювала його справжня сторінка застосунку. Форма для атаки навіть не потрібна: достатньо, щоб при завантаженні чужої сторінки браузер спробував дістати «картинку», яка насправді є запитом на переказ. Саме тому HttpOnly тут не рятує — він закриває куку від скрипту, але автоматичного надсилання не скасовує. Звідси й дзеркальне спостереження: там, де кукі немає взагалі, а доступ тримається на заголовку, класичний CSRF зазвичай не спрацьовує — заголовок ніхто автоматично не додасть, це має зробити код.
Як закривають CSRF і чому CORS тут не захист?
Класичний рубіж — патерн синхронізованого токена, який OWASP називає одним із найпоширеніших і рекомендованих заходів. Сервер вкладає непередбачуване значення у видану сторінку — прихованим полем форми або тегом meta, — клієнт відсилає його назад разом із небезпечною дією — полем форми або заголовком X-CSRF-Token, — і сервер порівнює отримане з тим, що запамʼятав для цієї сесії. Опора всієї конструкції — : сторонній сайт вмісту вашої сторінки не читає, тож значення токена йому невідоме. Звідси окрема вимога: сам токен кукою не передають — інакше браузер відправив би його разом із підробленим запитом так само автоматично. Атрибут SameSite при цьому дає , а не заміну токена — вони доповнюють один одного, а не конкурують. І межа, яку плутають постійно: предмет CORS — чи дозволено прочитати відповідь, предмет CSRF — сама дія, що виконується незалежно від того, побачить її ініціатор чи ні; тому «у нас налаштований CORS» не є відповіддю на питання про CSRF. Для AQA із цього виходить типове джерело «непояснимих» 403: запит на мутацію, надісланий повз форму, спершу забирає значення токена зі сторінки й везе його заголовком.
Як працює «Увійти через Google»?
Це OAuth 2.0 — фреймворк делегованого доступу, який відповідає на питання, як відкрити застосунку A вузьку частину ваших даних у сервісі B, залишивши пароль від B при собі. У протоколі чотири ролі: власник ресурсу (користувач), клієнт (ваш застосунок), сервер авторизації (автентифікує власника, збирає згоду й видає токени) і сервер ресурсів (тримає дані й приймає запити з токеном). У потоці коду авторизації браузер їде на сторінку провайдера, там відбувається логін і згода, назад повертається , і вже бекенд обмінює його на токен напряму, сервер-до-сервера. Ключових ідей дві: код видається через сервер авторизації як , тому облікових даних користувача клієнт не бачить взагалі; а межі виданого доступу задає scope — застосунок отримує, скажімо, лише ім'я й пошту. Поруч працює пара токенів: access підтверджує видану клієнту авторизацію й відкриває доступ до ресурсів, а refresh має єдине призначення — отримати новий access замість недійсного чи протермінованого, не тягнучи користувача через вхід ще раз; тому access роблять коротким, а refresh бережуть надійніше й смикають рідко. Дві межі, які люблять питати: OAuth 2.0 сам собою про авторизацію, а власне вхід дає надбудова ; для публічних клієнтів без безпечного місця для секрету — SPA, мобільних — потік доповнюють , що захищає обмін коду від перехоплення.
Як автоматизувати вхід через зовнішнього провайдера в e2e-тестах?
Коротка відповідь: реальний UI провайдера в тестах не ганяють. Причин кілька: провайдери навмисно розпізнають роботів — , попередження про підозрілий вхід, відмова headless-браузеру; сам потік перекидає користувача між доменами; та й тестовий акаунт зазвичай прикритий другим фактором. Разом це означає, що зелений колір вашої починає залежати від чужого інтерфейсу. Принцип, який варто вимовити на співбесіді: чужого провайдера ви не тестуєте — за OAuth Google відповідає Google. Ваша зона — те, що ваш застосунок правильно обробляє повернутий код або токен. Робочі шляхи: провайдера на рівні мережі, піднімати тестовий IdP із передбачуваними токенами або взагалі обходити UI й отримувати стан через бекенд. Один окремий smoke-тест із живим провайдером лишити можна — тягнути його крізь усю сюїту не можна.
Чим API key відрізняється від токена користувача?
Їх плутають, бо обидва — просто рядки в запиті, хоча ідентифікують вони різне. Ключ відповідає на питання «який сервіс стукає»: він належить застосунку чи інтеграції, живе довго — у конфізі або секретах CI — і зазвичай має широкі фіксовані права. Токен користувача відповідає на «хто саме працює»: він короткий, прив'язаний до сесії й успадковує роль конкретної людини. Транспорт теж різний: ключ часто їде кастомним заголовком або параметром, токен — схемою Bearer чи кукою. Для AQA із цього виходить пряме правило вибору креденшела під тест: рольові перевірки й видимість даних ганяють токеном конкретного користувача, а не ключем проєкту. Інакше тест «позеленіє» там, де жива людина отримала б 403, — і рівно ця підміна ховає найдорожчі баги моделі прав.
Як ви логінитесь в автотестах?
Починати відповідь варто не з інструмента, а з тези: вхід і перевірювана фіча — це дві окремі вимоги, а одна перевірка обслуговує одну. Проведіть тест кабінету через форму входу — і зламаний вхід дасть червоний саме на кабінеті, який працює бездоганно; сама форма має власний тест. Далі — механіка: вхід проходить рівно раз, у setup-проєкті, звідки стан лягає у файл, а решта тестів просто піднімається з нього вже автентифікованою; умову застосовності дока Playwright називає прямо — так роблять для тестів без серверного стану. Стан сховища взаємозамінний між API- і браузерним контекстом, тому «залогінься запитом, перевіряй в UI» є офіційно підтримуваним сценарієм, а не хаком. Далі метод підстановки випливає з того, у якому вигляді стан існує: значення зі сховища записує скрипт, що спрацьовує раніше за сторінку, а HttpOnly-куку доводиться додавати на рівні контексту браузера — коду сторінки вона не видна. Сильна відповідь закінчується не на «зробили storageState», а на переліку того, що з ним ламається: файл стану протухає сам собою, тож його місце в теці, яку прибирають на старті прогону; в UI-режимі setup-проєкт за замовчуванням не запускається, тож там тест іде зі станом із минулого разу — звідси «у CI зелено, а локально 401», і звідси ж порада дока час від часу запускати auth.setup.ts руками; sessionStorage у стан не потрапляє взагалі; кілька ролей означають кілька файлів, а дві ролі в одному тесті — два контексти; у файлі лежать робочі креденшели, тому в репозиторій він не їде; а кукі після ланцюжка редиректів зʼявляються не миттєво, тож орієнтуватися треба на кінцеву адресу, а не на фіксовану паузу.
Нічний прогін падає з 401 посеред сюїти, а локально все зелене. Що це?
Класичний «плаваючий» від експірації: токен, отриманий на старті прогону, доживає своє посеред довгої сюїти, і локально це не відтворюється просто тому, що локальний прогін коротший за час життя токена. Стратегій дві, і вони не виключають одна одну. Перша — читати exp із запасом і оновлювати токен наперед, поки до закінчення ще лишається час; так гонки на межі не буде взагалі. Друга — обгорнути транспортний шар: спіймали 401 — тихо оновили токен і повторили запит рівно один раз, без нескінченних . Обидві тримаються на одній умові: токен зберігається централізовано, а не копіюється по тестах, — інакше оновлення в одному місці решта просто не побачить. І перед тим як лікувати, варто відсіяти схожий за виглядом симптом іншої природи: 401 після переходу між походженнями зовні не відрізнити, а лікується він зовсім інакше.
Три кейси, у яких модель автентифікації прямо вирішує, зелений тест чи : як розвести «не впізнав» і «не дозволив», не збудувавши на цьому крихкий ; як прочитати exp із токена й перестати ловити нічні 401; і як підставити стан логіну так, щоб він доїхав до запиту. Скрізь — що дивитися і чому.
Кейс 1. «Не пустило»: розвести 401 і 403, не переоцінивши жоден
Найдорожча помилка в перевірках прав — читати як одне ціле. Це два різні рубежі: 401 каже, що валідних облікових даних у запиті немає, 403 — що запит зрозумілий, але виконувати його сервер відмовляється. Асерт expect(status).toBeGreaterThan(400) замітає цю різницю під килим: протермінований токен тихо «підтвердить» рольову модель, якої ніхто не перевірив.
import { test, expect, request } from '@playwright/test';
test('без креденшелів — 401 із викликом', async () => {
const anon = await request.newContext();
const res = await anon.get('https://api.example.com/admin/users');
expect(res.status(), 'без облікових даних очікуємо саме 401').toBe(401);
// відповідь 401 зобовʼязана нести виклик — його відсутність є дефектом сервера
expect(res.headers()['www-authenticate']).toBeTruthy();
});
test('читач на адмінському ресурсі: впізнали, але не дозволили', async () => {
const reader = await request.newContext({
extraHTTPHeaders: { Authorization: `Bearer ${READER_TOKEN}` },
});
const res = await reader.get('https://api.example.com/admin/users');
// сервер має право приховати сам факт існування ресурсу — 404 тут теж законний
expect([403, 404], 'валідний користувач без прав').toContain(res.status());
});
А ось матриця для розслідування 401, який «не мав статися»:
| Симптом | Підозрюваний | Дешева перевірка |
|---|---|---|
401 на щойно виданому токені | зіпсований рядок заголовка: немає слова Bearer, зайвий пробіл, обрізане значення | подивитися сирий заголовок запиту, що дійшов до сервера |
| запит «точно був із токеном», а сервер його не бачить | перехід між походженнями, після якого Authorization у запиті вже немає | звірити ланцюжок редиректів і наявність заголовка в останньому запиті |
401 тільки в корпоративній мережі | плутанина ресурсних заголовків із проксі-комплектом (407, Proxy-Authorization) | той самий запит повз проксі |
401 посеред довгої сюїти, на старті було зелено | токен протермінувався під час прогону | прочитати exp із payload і порівняти з часом падіння |
тест «без прав» отримав 401 замість очікуваного 403 | креденшели взагалі не підставилися — перевірка прав не виконувалася | окремим запитом переконатися, що під цим токеном дозволена дія проходить |
Що дивитися і чому:
- Жорсткий
expect(403)— популярний спосіб зробити . Приховування закритого ресурсу за404є дозволеною поведінкою сервера, а не багом; , що очікує рівно403, зламається від легальної зміни на бекенді. Асертити варто множину прийнятних кодів, а конкретний вибір — фіксувати в вимозі. 401безWWW-Authenticate— самостійний дефект. Заголовок несе , за яким клієнт розуміє, яку схему застосувати; без нього цикл «запит → челендж → повтор ізAuthorization» не замикається.- Зелений негативний тест ще нічого не доводить. Якщо токен не підставився, відмову дасть будь-який — і перевірка ролі перетвориться на перевірку відсутності креденшелів. Позитивна контрольна дія під тим самим токеном коштує один запит і рятує від хибної впевненості.
Кейс 2. exp із payload: перестати ловити нічні 401
Сюїта стабільно зелена локально й падає в нічному CI — типова ознака того, що прогін просто довший за час життя токена. Payload не зашифрований, лише закодований, тож свіжість перевіряється до запиту, без бібліотек і без чекання відмови.
// друга частина токена — звичайний base64url, ключ для читання не потрібен
function readClaims(token: string): Record<string, unknown> {
const payload = token.split('.')[1];
return JSON.parse(Buffer.from(payload, 'base64url').toString('utf8'));
}
// exp — NumericDate, тобто СЕКУНДИ від епохи; Date.now() — мілісекунди
function expiresWithin(token: string, seconds = 60): boolean {
const exp = Number(readClaims(token).exp);
return exp * 1000 - Date.now() < seconds * 1000;
}
Далі транспортний шар сам тримає токен свіжим: оновлює наперед і залишає рівно один на гонку біля межі.
async function authorized(path: string, ctx: AuthCtx) {
if (expiresWithin(ctx.accessToken)) {
ctx.accessToken = await refresh(ctx.refreshToken); // оновлення наперед
}
let res = await fetch(path, { headers: bearer(ctx.accessToken) });
if (res.status === 401) {
ctx.accessToken = await refresh(ctx.refreshToken);
res = await fetch(path, { headers: bearer(ctx.accessToken) }); // один ретрай, не цикл
}
return res;
}
Той самий доступ до payload дає й негативну перевірку підпису — базовий тест на те, що бекенд не вірить вмісту токена на слово:
test('підмінена роль у payload не приймається', async ({ request }) => {
const claims = readClaims(USER_TOKEN);
const forged = `${USER_TOKEN.split('.')[0]}.${encodeClaims({ ...claims, role: 'admin' })}.${USER_TOKEN.split('.')[2]}`;
const res = await request.get('/api/admin/users', {
headers: { Authorization: `Bearer ${forged}` },
});
expect(res.status(), 'зламаний підпис має відхилятися до будь-якої перевірки прав').toBe(401);
});
Що дивитися і чому:
- Множення на 1000 не косметичне.
expвимірюється в секундах, аDate.now()— у мілісекундах; забудете множник — різниця стане велетенським відʼємним числом, токен виглядатиме вічно простроченим, і рефреш смикатиметься на кожному запиті. Дробове значенняexpпри цьому законне, тож зводити його до цілого «для краси» не треба. - Ретрай має бути рівно один.
401, який не лікується оновленням токена, — це інший діагноз: неправильний рядок заголовка, недостатні права або знятий на редиректі заголовок. Цикл ретраїв перетворює виразний дефект на довгий . - Токен живе в одному місці, а не в кожному тесті. Копія токена в тесті не бачить оновлення, зробленого сусідом, — і
401починає «плавати» непередбачувано. Для API-шару достатньо задати заголовок один раз у конфізі черезextraHTTPHeaders. - Перевірка підпису — окремий тест, а не побічний ефект. Вона доводить, що сервер не ухвалює рішень за самим лише вмістом токена. Якщо підмінений payload проходить, решта тестів на права нічого не вартує.
Кейс 3. Логін без форми: куди доїжджає стан, а куди ні
Форма входу в кожному тесті домішує чужу вимогу до перевірки фічі. Стан здобувають один раз, а далі вся складність у тому, як його підставити: спосіб диктує форма стану, і саме тут ховаються два тихі провали.
// auth.setup.ts — логін запитом, стан у файл, який чиститься перед прогоном
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' });
});
Куку з HttpOnly зі сторінки не поставити — JavaScript її не бачить, тож вона додається на рівні контексту, і domain мусить збігатися з хостом, який має її нести:
await context.addCookies([{
name: 'session_id', value: SESSION,
domain: 'app.example.com', path: '/', // саме цей хост, не www і не .example.com
httpOnly: true, secure: true,
}]);
Токен зі сховища, навпаки, кладуть скриптом, який виконується до завантаження сторінки:
await context.addInitScript((token) => {
window.localStorage.setItem('access_token', token);
}, ACCESS_TOKEN);
А якщо застосунок захищений синхронізованим токеном, пряма мутація в обхід форми впаде на 403, поки значення не витягнуто зі сторінки:
// підставлена сесія цього не скасовує: кука доводить «від браузера», а не «зі сторінки»
const csrf = await page.getAttribute('meta[name="csrf-token"]', 'content');
await page.request.post('/api/projects', {
headers: { 'X-CSRF-Token': csrf },
data: { title: 'Some project name' },
});
Що дивитися і чому:
- Неправильний
domainвиглядає точно як розлогінений користувач. Кука лягає у стор, але браузер не відправляє її на потрібний хост — жодної помилки, просто тест бачить сторінку логіну. Звіряти требаdomainіpathкукі з адресою запиту, який має її нести, а не сам факт наявності у сторі. sessionStorageу стан не потрапляє. Якщо автентифікація спирається саме на нього, файл стану порожній там, де це важливо, і відновлювати сесію доводиться вручну — це не баг збереження, а відсутність API для цього сховища.- «У CI зелено, а локально в UI-режимі
401» не є парадоксом. Setup-проєкт в UI-режимі за замовчуванням не виконується, тож тест іде зі станом, який лежить у файлі з минулого разу: у CI setup відпрацював і стан свіжий, локально — ні. Дока радить час від часу запускатиauth.setup.tsруками. Другий бік тієї самої монети — протухання: файл стану ніхто не оновить сам, тому його місце в теці, яку чистять перед прогоном. - Кукі зʼявляються не в ту ж мить. Вхід зазвичай проходить ланцюжок редиректів, і стан, знятий посеред нього, буде неповним. Орієнтир — адреса, на якій ланцюжок завершився, або наявність усіх потрібних кукі; фіксована пауза тут не рішення, а відкладений .
- Файл стану — це живі секрети. У ньому лежать кукі й заголовки, якими можна вдати тестовий акаунт, тому в репозиторій він не комітиться; кілька ролей означають кілька таких файлів, а дві ролі в одному тесті — два контексти.
Дві стадії й коди відповіді
- Знаю різницю «хто ти?» проти «що тобі можна?» і жорсткіше розведення ISTQB: перше — процедура встановлення особи, друге — виданий дозвіл на доступ; можу назвати три фактори автентифікації (знаю / маю / є).
- Можу пояснити
401(у запиті немає валідних облікових даних, а відповідь зобов'язана нестиWWW-Authenticateіз ) проти403(запит зрозумілий, але сервер відмовляє), і памʼятаю окрему поведінку: на валідні, але недостатні креденшели йде403, і повторної спроби браузер уже не запропонує. - Знаю, що жорсткого порядку стадій HTTP не наказує:
403законний і для запиту без облікових даних, а сервер має право віддати404замість403, щоб приховати ресурс, — тож негативний тест на права очікує403або404, а «прийшов403, отже роль не та» є гіпотезою (канон описує процес, RFC — коди).
Транспорт доказу особи
- Можу пояснити, чому доказ особи їде в кожному запиті заново (протокол без стану, обмеження REST) і назвати дві його форми: куку браузер повертає сам за збереженими метаданими, токен клієнтський код кладе в
Authorizationвручну. - Знаю, який саме атрибут кукі несе тут автентифікацію:
HttpOnly— кука закрита відdocument.cookie, але браузер надсилає її й далі, тож у тесті вона підставляється лише на рівні ; решту атрибутів, час життя кукі й механіку сховищ повністю розбирає глава про кукі та сховище браузера.
Схеми Basic і Bearer
- Розумію Basic:
base64(логін:пароль)— кодування, а не шифрування, пароль іде відкритим текстом, тож без TLS схема не рекомендована; памʼятаю два готові негативні кейси — двокрапка в логіні невалідна, а неузгоджене кодування символів дає баги з діакритикою (charsetмає єдине значенняUTF-8і є необовʼязковим, на відміну відrealm); у Playwright креденшели задаю черезhttpCredentials. - Знаю схему Bearer («на предʼявника», жорсткий формат «слово, пробіл, токен», обовʼязковий TLS, один
extraHTTPHeadersу конфізі) і порядок розслідування401на валідному токені: сирий рядок заголовка → чи не знявсяAuthorizationна редиректі між → чи не плутанина з -комплектом (407,Proxy-Authenticate,Proxy-Authorization).
Сесія, токен і JWT
- Знаю різницю «серверна сесія проти токена»: де живе стан (запис на сервері проти самого токена), як відкликається (миттєво проти чекання
exp), чим їде (кука автоматично проти заголовка вручну) і що клієнт бачить (непрозорий ідентифікатор проти всього вмісту); памʼятаю й ціну сесії — спільне сховище на весь кластер. - Знаю будову JWT (
header.payload.signatureу base64url) і що число частин специфікація не фіксує — воно залежить від подання, у JWE Compact їх пʼять; орієнтуюся в зареєстрованих claims (iss,sub,aud,exp,nbf,iat,jti— теж зареєстрований) і памʼятаю, щоexp— цеNumericDate, тобто секунди від епохи, де нецілі значення теж законні. - Можу пояснити, що підпис захищає від підробки, але не від читання (шифрує лише варіант JWE), тому секрети в payload не кладуть, а сервер не має довіряти вмісту токена до перевірки криптозахисту; знаю різницю симетричного
HS256і асиметричнихRS256/ES256— спільний секрет означає, що перевіряльник здатен і підробити. - Розумію різницю API key проти токена користувача: перший каже «який сервіс стукає» й зазвичай має ширші фіксовані права, другий — «хто саме з користувачів працює», і права бере з його ролі; ганяючи перевірку під ключем проєкту, легко дістати зелений там, де реальна людина впіймала б
403.
Сховище токена й вектори атак
- Можу назвати ціну кожного сховища:
localStorageіsessionStorageчитає будь-який скрипт сторінки (вектор XSS, а внесений скрипт дістає кукі й сесійні токени аж до захоплення акаунта), кука зHttpOnlyзакрита від JavaScript, але шлеться автоматично (вектор CSRF), памʼять не переживає перезавантаження; памʼятаю, щоsessionStorageу збережений стан Playwright не потрапляє взагалі, що дані Web Storage самі на сервер не їдуть, і що найзахищеніший компроміс — короткий токен доступу в памʼяті плюс довший у куці зHttpOnly.
OAuth 2.0 і CSRF
- Розумію OAuth 2.0: чотири ролі (власник ресурсу, клієнт, сервер авторизації, сервер ресурсів), потік коду авторизації через , через який пароль до клієнта не потрапляє, межі доступу через
scope; знаю призначення пари токенів (access підтверджує видану авторизацію й відкриває ресурси, refresh потрібен лише щоб дістати новий access замість протермінованого, без повторного входу користувача); знаю, що вхід через провайдера технічно стоїть на , а публічні клієнти доповнюють потік . - Можу пояснити, чому реальний UI провайдера в не тягнуть (антибот-захист, редиректи, другий фактор на тестовому акаунті, залежність від чужого інтерфейсу) і чим його замінюють — на рівні мережі або тестовим IdP; тримаю в голові принцип, з якого починається відповідь: чужого провайдера ви не тестуєте, ваша зона — те, як ваш застосунок обробляє повернутий код або токен.
- Можу назвати корінь CSRF: браузер сам чіпляє куку, тож вона доводить походження від браузера, а не від справжньої сторінки застосунку; форма для не потрібна (завантаження «картинки» може бути ),
HttpOnlyвід цього не рятує, а чисто token-based API зAuthorizationзазвичай не вразливе. - Знаю патерн синхронізованого токена: непередбачуване значення у виданій сторінці, повернення полем або заголовком, звірка з тим, що збережено для сесії; тримається на політиці одного походження, кукою сам токен не передають,
SameSiteдає , а не заміну; і не плутаю CSRF із CORS — CORS вирішує, чи можна прочитати відповідь, тоді як шкідлива дія проходить і без жодного доступу до неї.
Логін в автотестах
- Можу пояснити головний мотив від UI-логіну: логін і цільова фіча — різні вимоги, тож падіння логіну не має фарбувати чужий тест; знаю рекомендований шлях (автентифікація в setup-проєкті, стан у файл, кожен тест стартує з нього), його умову застосовності (тести без серверного стану) і те, що стан сховища взаємозамінний між API- і браузерним контекстом.
- Знаю, що метод підстановки випливає з форми стану: значення зі сховища записую скриптом, який спрацьовує раніше за сторінку, а
HttpOnly-куку додаю тільки контекстом браузера, звіряючиdomainіpathіз реальним хостом запиту. - Тримаю в голові шість місць, де збережений стан ламається: протухання файла, невиконаний setup-проєкт в UI-режимі, відсутність
sessionStorageу стані, кілька ролей як кілька файлів (дві ролі в одному тесті — це два контексти), живі секрети у файлі стану й неміттєве виставлення кукі після ланцюжка редиректів, через яке чекати треба кінцевого URL, а не фіксованої паузи. - Знаю дві стратегії проти «плаваючого»
401у довгих прогонах — читатиexpіз запасом і оновлювати токен заздалегідь або на отриманий401робити рівно одну повторну спробу — і памʼятаю умову, без якої обидві не працюють: токен зберігається централізовано, а не копіюється по тестах.
Квіз
Перед стартом
- Питань: 17
- Поріг «зараховано»: ≥70% правильних відповідей.
- Результат впливає на прогрес; завалені питання підуть у чергу повторення.
- Квіз впливає на компліт теми: тема стає «пройдено», лише коли прочитано теорію І квіз складено на ≥70%.
Питання
На які два питання відповідають автентифікація й авторизація?
