Experience Real OAuth
Learn how OAuth actually works by doing it — a guided, hands-on journey built for QA and automation engineers, not a documentation portal.
Sign in once, and watch the whole flow happen
Experience a complete OAuth 2.0 Authorization Code + PKCE flow: sign in with your Playwright Identity, approve consent, and securely land inside CineVerse using real OAuth — not a simulation.
◆ Continue with Playwright IdentityJust exploring? Use the demo identity identity.demo@playwrightautomation.com / Password123.
Want to see just the first step on its own? Sign in directly to the Authorization Server to learn how it proves who you are (authentication) and opens a server-side session — separate from what an application is allowed to access (authorization).
Sign in to the Authorization Server →You completed a real Authorization Code + PKCE flow. Behind the scenes your browser performed:
- Authorization Request
- User Authentication
- Consent
- Authorization Code
- Token Exchange
- UserInfo
- Protected Resource Access
Would you like to learn how it worked?
Replay the OAuth conversation
Your browser already completed this inside CineVerse. Now reproduce every step manually — the same requests, in the same order, against the real backend. Watch what each request does, what gets stored, and what changes.
-
The story. Your browser showed a sign-in screen for your Playwright Identity, then sent your credentials to the Authorization Server to start a session.
🌐 Browser↓POST /api/oauth/as/login↓Auth Server checks credentialsPurpose. Create the Authorization Server session that identifies you for the rest of the flow.
-
The story. Your browser redirected to /oauth/authorize carrying a PKCE challenge, asking the Authorization Server to begin issuing an authorization code.
🌐 Browser↓GET /oauth/authorize (+ PKCE)↓Waiting for consent…Purpose. Start the Authorization Code flow — with PKCE, so no client secret is needed.
-
The story. The Authorization Server asked whether CineVerse may access your identity, and you clicked Allow.
👤 You review scopes↓POST /api/oauth/authorize/decision↓Auth Server issues a codePurpose. Record your approval, then issue a short-lived authorization code.
-
The story. Your browser sent the temporary authorization code — plus the PKCE verifier — to the token endpoint, and received tokens back.
🌐 Browser↓POST /oauth/token (code + verifier)↓Token endpoint mints tokensPurpose. Turn the one-time code into an access token (and an id_token).
-
The story. Your browser attached the access token as an Authorization: Bearer header and called protected endpoints.
🌐 Browser (Bearer token)↓GET /oauth/userinfo · /resource/profile↓Resource Server verifies the tokenPurpose. Prove the token works — read protected identity data with it.
Why. The server set an HttpOnly pw_as_session cookie — it now trusts your browser for the consent step.
POST/api/oauth/as/loginContent-Type: application/json{ "email": "…", "password": "…" }—
In automation this is your login request. Capture the Set-Cookie: pw_as_session once, then reuse it via Playwright storageState (or a saved cookie) so later tests skip the UI login entirely.
{{baseUrl}}/api/oauth/as/loginContent-Type: application/json
{
"email": "identity.demo@playwrightautomation.com",
"password": "Password123"
}Set automatically — the response returns Set-Cookie: pw_as_session
| Variable | Value | Source |
|---|---|---|
baseUrl | — | Environment |
Execute this step to see the real request (with variables resolved).
200 OK
{ "subject": "pwsub_…", "expiresAt": … }
Set-Cookie: pw_as_session=…; HttpOnlyWhy / next / mistakes
Why this request exists. It authenticates the resource owner and starts the AS session that /authorize needs.
What happens next. The browser can now request authorization (Step 2).
Common mistakes. Forgetting cookies are per-host — run the whole flow on one origin (localhost).
Why. The browser was sent to the consent screen. No authorization code exists yet — issued only after you approve.
—
PKCE means your test code needs no client secret — generate a fresh code_verifier / code_challenge each run. Assert the response is a 302 whose Location points at /oauth/consent — there is no code yet.
{{baseUrl}}/oauth/authorize?client_id=pwc_demo_public_pkce&redirect_uri={{redirect_uri}}&response_type=code&scope=openid profile profile.read&state={{state}}&nonce={{nonce}}&code_challenge={{pkce_challenge}}&code_challenge_method=S256Uses the pw_as_session cookie from Step 1 (Postman keeps it automatically)
| Variable | Value | Source |
|---|---|---|
baseUrl | — | Environment |
client_id | — | Environment |
redirect_uri | — | Environment |
scope | — | Environment |
response_type | — | Literal · code |
state | — | Runtime generated |
nonce | — | Runtime generated |
code_verifier | — | Runtime generated · before Authorize |
pkce_challenge | — | Derived from code_verifier · SHA-256 |
Execute this step to see the real request (with variables resolved).
302 Found Location: /oauth/consent?txn=…&csrf=…
Why / next / mistakes
Why this request exists. It kicks off the Authorization Code flow and binds a PKCE challenge to this browser.
What happens next. You approve consent, which produces the authorization code (Step 3).
Common mistakes. Sending no code_challenge, or a redirect_uri not registered for the client.
Why. The code (pwac_…) is temporary, single-use, and useless without the PKCE verifier that never left your browser.
GET /api/oauth/authorize/context?txn=…POST /api/oauth/authorize/decision—
Consent is prompted once, then reused on later runs. A robust test handles both paths: the explicit approval (POST /authorize/decision) and the auto-issue redirect when a grant already exists. The code is single-use — never assert on a reused one.
{{baseUrl}}/api/oauth/authorize/decisionContent-Type: application/json
{
"txn": "{{txn}}",
"decision": "allow",
"csrf": "{{csrf}}"
}Uses the pw_as_session cookie from Step 1 (Postman keeps it automatically)
| Variable | Value | Source |
|---|---|---|
baseUrl | — | Environment |
txn | — | From Step 2 · authorize transaction |
csrf | — | From Step 2 · authorize transaction |
Execute this step to see the real request (with variables resolved).
200 OK
{ "redirect": "…?code=pwac_…" }Why / next / mistakes
Why this request exists. It records your consent and mints the authorization code bound to this transaction.
What happens next. The browser exchanges the code for tokens (Step 4).
Common mistakes. Reusing an old txn/csrf, or exchanging the code twice (it is single-use).
Why. The authorization code is now spent; the access_token is what your app presents to protected APIs.
POST/oauth/tokenContent-Type: application/x-www-form-urlencoded—
This is the request most API tests replay directly: POST /oauth/token → Bearer token, reused across every later call. Assert 200 + access_token; a spent or wrong code returns invalid_grant — a useful negative test.
{{baseUrl}}/oauth/tokenContent-Type: application/x-www-form-urlencoded
grant_type=authorization_code&code={{code}}&redirect_uri={{redirect_uri}}&client_id=pwc_demo_public_pkce&code_verifier={{code_verifier}}None — public client with PKCE (no client secret)
| Variable | Value | Source |
|---|---|---|
baseUrl | — | Environment |
client_id | — | Environment |
redirect_uri | — | Environment |
code | — | From Step 3 response |
code_verifier | — | Runtime generated · Step 2 |
Execute this step to see the real request (with variables resolved).
200 OK
{ "access_token": "…", "id_token": "…", "token_type": "Bearer", "expires_in": 300, "scope": "…" }Why / next / mistakes
Why this request exists. It proves possession of the PKCE verifier and swaps the code for tokens.
What happens next. The browser calls protected resources with the access token (Step 5).
Common mistakes. Wrong/missing code_verifier, a mismatched redirect_uri, or re-using a spent code.
Why. The resource server verified the token and returned 200 — no cookie involved. Remove the header and it becomes 401.
GET /oauth/userinfo · GET /api/oauth/resource/profileAuthorization: Bearer <access_token>—
—
Send Authorization: Bearer <token> on each call. Remove it → 401; use a token whose scope does not cover the resource → 403 — both are valuable negative tests. No cookie is involved in API auth.
{{baseUrl}}/oauth/userinfoAccept: application/json
Authorization: Bearer {{access_token}}| Variable | Value | Source |
|---|---|---|
baseUrl | — | Environment |
access_token | — | From Step 4 response |
Execute this step to see the real request (with variables resolved).
200 OK
{ "sub": "pwsub_…", "name": "…" }
(also: GET {{baseUrl}}/api/oauth/resource/profile)Why / next / mistakes
Why this request exists. It shows the access token is accepted by the resource server as proof of authorization.
What happens next. You are done — or automate the whole thing (Automation tab).
Common mistakes. Sending no/expired token (→ 401), or a token whose scope does not cover the resource (→ 403).
Browser (UI)
The browser attaches the session cookie to every request. In UI automation you reuse it as storageState.
API
You attach the token per request. In API automation you obtain it once and inject it into every call.
Prefer an API client? Download a ready-to-run Postman collection and a matching localhost environment — the same requests you just replayed, pre-wired with PKCE generation and automatic token capture.
- 1Import Collection
- 2Import Environment — then select it
- 3Run Login (request 1)
- 4Run Authorize → Consent → Token (access token saved automatically)
- 5Run UserInfo (request 5)
- ✓Done — you called a protected API with a real token.
Automate OAuth like an enterprise QA team
You experienced it (Tab 1) and replayed every request (Tab 2). Now the real question: how do QA automation engineers automate this without logging in before every test?
Learn how Playwright, Selenium and Cypress authenticate once, save the session or token, and reuse it across hundreds of automated tests — instead of logging in through the UI before every single test.
Run Tab 2 · Step 4 (Token) to enable this.
Be precise: a Bearer token authenticates API requests — it does not automatically become a browser cookie session. Real frameworks reuse the session (storageState) for the UI and the token for APIs.
Where automation plugs in.
Where. End-to-end UI suites — the browser stays logged in via the saved session cookie.
Where. Fast API/contract tests — attach the token as Authorization: Bearer.
Where. Big suites — skip the slow UI login, seed the session, then drive the UI.
Clean, commented, framework-idiomatic — the authenticate-once pattern in each stack.
In Playwright you inject the token into an APIRequestContext (or set an extraHTTPHeaders Authorization header) — every API call is now authenticated.
6 Playwright best practices
- storageState() — save the authenticated cookies/origin storage to a file and reuse it.
- request.newContext() / APIRequestContext — log in and call APIs without a browser.
- globalSetup — authenticate once before the whole suite runs.
- Project dependencies — a "setup" project that other projects depend on for auth.
- Authentication fixtures — per-role fixtures that hand each test the right state.
- Reuse login across specs instead of logging in per test.
- Parallel execution — every worker reuses the same saved state, no login storms.
- Multiple users / Admin & User — one storageState file per role; pick per test.
- Role testing — swap storageState to assert role-specific behaviour.
7 Enterprise folder structure
framework/ ├─ tests/ # specs / scenarios ├─ pages/ # Page Objects (UI locators & actions) ├─ fixtures/ # custom test fixtures (inject auth, data) ├─ api/ # API clients & token helpers ├─ auth/ # login flows + globalSetup (authenticate once) ├─ storage/ # saved storageState JSON (gitignored) ├─ config/ # env & runtime config ├─ helpers/ # shared utilities └─ data/ # test data / fixtures
auth/ + storage/ are the heart of reuse: log in once in auth/, drop the result in storage/, and every test reads it.
8 Common mistakes
- Logging in through the UI in every test — authenticate once, reuse storageState.
- Generating a fresh token on every request — cache it until it expires.
- Sharing an expired storageState — refresh it in globalSetup or on expiry.
- Mixing cookies and Bearer tokens — cookies auth the UI, Bearer auths APIs; don’t cross them.
- Ignoring refresh tokens — renew the access token instead of re-logging in.
- Using UI login when an API login works — API login is faster and more stable.
9 When to use which strategy
| You need… | Use |
|---|---|
| UI tests | Storage State (session cookie) |
| API tests | Bearer access token |
| Both (hybrid) | API login → inject session for UI + token for APIs |
| Multiple users | One storageState file per user |
| Admin + user | Separate authentication contexts / projects |
| Long suites | Authenticate once in globalSetup |
Next, implement these patterns yourself — log in once, save the state, and reuse it across a real suite.