OAuth 2.0 Concepts for Testers
OAuth 2.0 (RFC 6749) lets an application get limited access to an API without seeing the user's password. The user signs in at a server they trust and approves what the application may do; the application then receives an access token that carries only that permission. The RFC's own example: a printing service reads your photos at a photo-sharing site without ever knowing your password there.
Why it matters
OAuth defects are security defects. A redirect URI that is not matched exactly can deliver codes to an attacker, a missing state check accepts a forged sign-in, an unenforced scope turns read access into write access, and a refresh token that never expires keeps a lost phone signed in. RFC 9700, the current OAuth security best practice, describes these attacks and their countermeasures.
For a tester, one sign-in crosses the browser, the client, the authorization server and the API. Knowing the roles and messages turns "login does not work" into the exact step that failed and its error.
The four roles and what they exchange
RFC 6749 defines four roles: the resource owner (usually the user), the client (the application that wants access), the authorization server (signs the user in, asks for consent, issues tokens) and the resource server (the API). A confidential client, such as a web app with a back end, can keep a secret; a public client, such as a single-page or mobile app, cannot.
- Authorization code: short-lived and single-use, returned through the browser; RFC 6749 recommends at most 10 minutes.
- Access token: sent to the API, usually as a Bearer token as in the API authentication lesson. OAuth does not fix its format: a JWT or an opaque string.
- Refresh token: optional and longer-lived; sent only to the authorization server for new access tokens, never to the API.
- Scope: permissions such as
appointments.read. The server may grant fewer than requested and must then return the granted scope.
Try it: OAuth Token Decoder Try it: Mock Authentication Response
Client credentials, refresh and expiry
With no user involved, such as a nightly reporting job, a confidential client uses the client credentials grant: it authenticates with its own credentials and receives an access token for its own permissions, normally without a refresh token.
Access tokens are short-lived, so a leaked one is useful only briefly. On expiry the client sends grant_type=refresh_token and cannot gain scope the user never approved. For public clients RFC 9700 requires refresh tokens to be sender-constrained (cryptographically bound to that client instance, so a copied token is useless on its own) or rotated: with rotation each refresh returns a new refresh token and invalidates the old one.
# Basic credentials = Base64 of client_id:client_secret (example values from RFC 6749)
POST /token HTTP/1.1
Host: auth.example.com
Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW
Content-Type: application/x-www-form-urlencoded
grant_type=client_credentials&scope=reports.read
HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: no-store
{"access_token":"2YotnFZFEjr1zCsicMWpAA","token_type":"Bearer",
"expires_in":900,"scope":"reports.read"}OpenID Connect in brief
OAuth 2.0 tells the client what it may access, not who the user is. OpenID Connect (OIDC) adds identity: with the scope openid, the token response also carries an ID token, a JWT for the client that states who signed in, with claims such as iss, sub (the user's id at that issuer), aud (the client's own client_id), exp and, if the client sent one, nonce.
The client validates the ID token as the JWT guide describes and keeps it; only the access token goes to the API.
Worked example: testing a booking app's sign-in
A clinic booking app, a public client, uses the authorization code flow with PKCE and OIDC; its API requires appointments.read to list and appointments.write to book. You test the integration in the test environment with test users and the test client registration, not the identity provider's own product. The token endpoint returns its error value in a JSON body; the API returns it in the WWW-Authenticate challenge (RFC 6750). Most conditions are negative:
- Sign in, approve and list appointments →
200; the grantedscopeequals what the user approved. - Alter
statein the callback → the app rejects it and creates no session. - Point
redirect_uriat another host → an error page and no redirect. - Redeem a code twice, or send a wrong
code_verifier→400withinvalid_grant. - Deny consent → the app handles
error=access_deniedwith a clear message. - Book with a read-only token →
403withinsufficient_scope. - Call the API with an expired access token or with the ID token →
401withinvalid_token. - With rotation on, reuse an old refresh token →
400withinvalid_grant, and RFC 9700 expects the newest refresh token to be revoked as well.
Try it: API Authentication Tester Try it: JWT Expiration Checker
Common mistakes
- Treating an access token as proof of who the user is. Identity comes from a validated OIDC ID token.
- Testing only the happy path in the browser, never the token endpoint with a reused code, a wrong verifier or a changed
redirect_uri. - Checking that the API accepts valid tokens but not that it rejects expired ones, ones issued for another API, or ones without the required scope.
- Pasting real tokens or client secrets into tickets, chats or screenshots instead of redacting them.
- Security-testing a third-party identity provider without written permission instead of your own integration.
The QA perspective
Start at design review. For each client ask which grant it uses, whether it is public or confidential, whether PKCE is enforced, which redirect URIs are registered, which endpoint needs which scope, how long tokens live and whether refresh tokens rotate. The answers are your test basis; a missing answer is already a finding.
Many expected results are configuration, so work with the developers and the authorization server's administrator. Record the client_id, grant, requested and granted scopes, endpoint, status, OAuth error value and request id, and redact every token and secret.
Practice exercise
Check a PKCE pair and a token response
In the test environment the booking app requested scope=openid appointments.read with the PKCE pair below and received the token response under it. Verify the pair, review the response and write four negative test conditions for the token endpoint.
code_verifier: dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
code_challenge: E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM (method S256)
HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: no-store
{
"access_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6IjEifQ...",
"token_type": "Bearer",
"expires_in": 900,
"refresh_token": "tGzv3JOkF0XG5Qx2TlKWIA",
"scope": "openid appointments.read appointments.write profile"
}- Hash the
code_verifierin the SHA-256 Generator with Base64url output and compare it with thecode_challenge. - Compare the granted
scopewith the requested one. - Check what OpenID Connect requires when
openidis requested. - Write four token-endpoint conditions with the expected status and
errorvalue.
Show the model answer
The hash equals the challenge, so the pair is valid (it is the example in RFC 7636, Appendix B).
appointments.write and profile were granted but neither requested nor approved: a least-privilege defect unless a documented server policy explains it.
No id_token came back although openid was requested, so the client cannot tell who signed in; OpenID Connect requires it here. expires_in (15 minutes) and Cache-Control: no-store are as expected.
Each condition expects 400 with invalid_grant and no tokens: a code redeemed twice (then check that tokens from the first exchange are revoked, where supported); a wrong code_verifier; a redirect_uri that differs from the authorization request; an expired code.
Frequently asked questions
Is OAuth 2.0 an authentication protocol?
No. OAuth 2.0 is a framework for delegated authorization: it lets a client obtain an access token for an API. It does not define how the client learns who the user is. OpenID Connect adds that with the ID token, which is why sign-in features should be built on OIDC rather than on OAuth 2.0 alone.
Why use PKCE when the client already has a client secret?
The secret authenticates the client but does not bind a particular authorization code to the request that produced it. PKCE does, which also blocks an attacker from injecting a stolen code. RFC 9700 therefore requires PKCE for public clients and recommends it for confidential clients as well.
Are the implicit and password grants still acceptable?
RFC 9700 says the implicit grant should not be used and the resource owner password credentials grant must not be used. If your product still relies on either, record it as a risk and ask the team for a migration plan to the authorization code grant with PKCE.
Can I practise these tests against a public identity provider?
Only within what that provider allows developers to do, and never with attack-style or high-volume tests. Run negative and security tests in your own test environment, with test users and a test client registration, or on systems you have written permission to test.
References
- RFC 6749: The OAuth 2.0 Authorization FrameworkThe OAuth 2.0 roles, grant types and token endpoints that issue access and refresh tokens.
- RFC 9700: Best Current Practice for OAuth 2.0 SecurityCurrent security guidance for OAuth 2.0: PKCE for public clients, exact redirect matching, sender-constrained tokens.
- RFC 7636: Proof Key for Code Exchange by OAuth Public ClientsPKCE: the code_verifier and code_challenge (S256) that bind an OAuth authorization code to the client that requested it, defeating code interception.
- OpenID Connect Core 1.0 incorporating errata set 2The identity layer on top of OAuth 2.0: the ID Token and how to validate it, the authentication flows, standard claims and the UserInfo endpoint.
Practise with Mutqan tools
Decode OAuth 2.0 / OpenID Connect access and ID tokens and understand scopes, audiences and lifetimes.
Generate mock login/token responses: access token, refresh token, expiry, user profile and OAuth2/OIDC shapes.
Generate SHA-256 hashes of text — the standard for integrity checks and signatures.
Test Bearer, Basic, API-key and custom-header authentication against an endpoint and compare authorised vs unauthorised responses.
Check whether a JWT is expired, not yet valid, or how long remains until expiry.
Go deeper
Your progress on this topic
Not started
Mark the topic complete when you have studied it and done the exercise.
Sign in to track your progress
Mark topics complete, continue where you left off and unlock specialisation exams.