Skip to content
SDETRole
SDETRole

Lesson 4 of 9 · API Testing

API Authentication

API keys, Basic auth, bearer tokens, JWTs, OAuth 2.0 and sessions: how APIs identify callers, and the negative tests that find real authentication bugs.

intermediate 8 min read Updated

You will learn to

  • Distinguish authentication from authorisation and map failures to 401 and 403
  • Explain API keys, Basic auth, bearer tokens, JWTs, OAuth 2.0 and session cookies
  • Obtain and use a token in a two-step login flow against a real API
  • Design negative tests for missing, malformed, invalid and over-privileged credentials

Prerequisites

  • HTTP Fundamentals
  • REST APIs Explained

Most API defects that make the news are not crashes. They are requests that should have been refused and were not: one customer reading another customer’s orders, an expired token that still works, an admin endpoint that only checks whether you are logged in. Testing authentication well means understanding how an API decides who you are, and then deliberately sending it everything it should reject.

This lesson builds on HTTP Fundamentals. The examples use the SDETRole Practice API at https://sdetrole.com/api/mock, which has a real login endpoint and a protected endpoint you can call from curl, Postman or code.

Authentication vs authorisation

Two questions, two status codes:

  • Authentication (authn): who are you? The API checks credentials. Missing or invalid credentials should produce 401 Unauthorized, ideally with a WWW-Authenticate header naming the scheme the API expects.
  • Authorisation (authz): are you allowed to do this? The API knows who you are and checks permissions. A refusal should produce 403 Forbidden, or sometimes 404 Not Found when the API deliberately hides that a resource exists.

Keeping these separate in your head makes bug reports precise. “Viewer role gets 401 on DELETE” and “viewer role gets 200 on DELETE” are very different defects.

The common schemes

API keys

A long random string issued to a client application, sent in a header such as X-API-Key: abc123 (or, worse, a query parameter). API keys identify an application, not a user, and usually never expire on their own. Test that revoked keys are rejected, that keys are not accepted in URLs (they end up in logs and browser history), and that one tenant’s key cannot read another tenant’s data.

HTTP Basic

The client sends Authorization: Basic <base64(username:password)>. Base64 is an encoding, not encryption: anyone who sees the header can decode it with a Base64 decoder. Basic auth is only acceptable over HTTPS, and is mostly found on internal tools and legacy services.

Bearer tokens

The client first exchanges credentials for a token, then sends Authorization: Bearer <token> on every request. “Bearer” means whoever holds the token can use it, so tokens are usually short-lived. A bearer token can be:

  • Opaque: a random string the server looks up in a store. You cannot read anything from it.
  • A JWT (JSON Web Token): three Base64URL-encoded parts separated by dots, header.payload.signature. The payload carries claims such as sub (subject), exp (expiry, in seconds since the Unix epoch), iat (issued at), aud (audience) and often roles or scopes.
{
  "sub": "user-2",
  "role": "tester",
  "scope": "users:read orders:read",
  "iat": 1794218400,
  "exp": 1794222000
}

Anyone can decode a JWT payload; the signature is what stops tampering. Paste tokens from your test environment into the JWT Decoder to see what the server is trusting you with, and never paste production tokens into any website.

OAuth 2.0 and OpenID Connect

OAuth 2.0 is a framework for delegated access: a client gets an access token from an authorisation server and presents it to a resource server (the API). The flows you will meet:

Flow Used by What a tester needs to know
Authorisation code with PKCE Web and mobile apps acting for a user Involves a browser redirect and login page; automate it via a test identity provider or by seeding tokens, not by scraping the login UI in every test
Client credentials Service-to-service calls No user involved; a client ID and secret are exchanged for a token in one POST, which is easy to automate
Refresh token Long-lived sessions Check that refresh tokens rotate, expire and are revoked on logout

OpenID Connect adds an identity layer on top (an ID token describing the user). For test automation the practical outcome is the same: get a token once, reuse it, and test what happens when it is wrong.

Session cookies

Browser-facing APIs often use a session ID in a cookie set by Set-Cookie after login. Test the cookie attributes (HttpOnly, Secure, SameSite), that the session is invalidated on logout, and that state-changing requests are protected against cross-site request forgery.

A two-step login flow, for real

The Practice API uses a simple bearer-token flow. First, log in with the practice credentials:

curl -i -X POST https://sdetrole.com/api/mock/auth/login \
  -H "Content-Type: application/json" \
  -d '{"username":"tester","password":"practice123"}'
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Cache-Control: no-store

{
  "accessToken": "sdr_practice_7f3c9a1e",
  "tokenType": "Bearer",
  "expiresIn": 3600
}

Then call the protected endpoint with the token:

curl -i https://sdetrole.com/api/mock/auth/me \
  -H "Authorization: Bearer sdr_practice_7f3c9a1e"
{
  "id": 2,
  "username": "tester",
  "name": "Lucas Martin",
  "role": "tester",
  "scopes": ["users:read", "orders:read"]
}

The practice token is opaque (it is not a JWT), and the API is stateless, so the same token always works. Real systems issue a new token per login and expire it after expiresIn seconds.

Negative tests that find real bugs

Happy-path login is the least interesting test. The value is in the requests that must fail, and in checking that they fail correctly. Against /auth/me, the Practice API distinguishes each case:

Request Expected detail in the problem+json body
No Authorization header 401 Missing Authorization header.
Authorization: Basic dGVzdGVyOnByYWN0aWNlMTIz 401 Authorization must use the Bearer scheme.
Authorization: Bearer wrong-token 401 The access token is invalid.
Correct bearer token 200 (user profile)

Every 401 also carries a WWW-Authenticate header, for example:

HTTP/1.1 401 Unauthorized
Content-Type: application/problem+json; charset=utf-8
WWW-Authenticate: Bearer realm="sdetrole-practice", error="invalid_token"

{
  "type": "https://sdetrole.com/practice/api-playground#problem-401",
  "title": "Unauthorized",
  "status": 401,
  "detail": "The access token is invalid."
}

The login endpoint has its own failure modes: wrong password gives 401 with Invalid username or password., a body without username or password gives 400, and a body sent without a JSON Content-Type gives 415.

On a production-like system, extend the list with:

  • Expired token: generate one with a short lifetime (or ask developers for a test hook) and confirm it is rejected after exp.
  • Tampered JWT: change one character of the payload, or change the header alg to none, and confirm a 401. Accepting either is a critical defect.
  • Token for the wrong audience or environment: a staging token must not work in production.
  • Revoked token: log out, then reuse the old token.
  • Horizontal access: user A requests /orders/{id} for an order that belongs to user B. Expect 403 or 404, never 200. This class of bug is called Broken Object Level Authorisation and tops the OWASP API Security Top 10.
  • Vertical access: a viewer calls an admin-only endpoint. Expect 403.
  • Error message leakage: “unknown username” vs “wrong password” lets attackers enumerate accounts. A single generic message, like the practice API’s, is safer.

Automating auth efficiently

Logging in through the API before every single test is slow and adds a dependency to every test. The usual pattern is:

  1. Obtain a token once per worker or per suite (a fixture, a @BeforeAll method, or a collection-level pre-request script).
  2. Attach it to a shared request configuration so tests do not repeat the header.
  3. Keep a separate, small set of tests that exercise the login endpoint itself, including all the negative cases above.

Here is the shape in plain JavaScript (Node 22):

const BASE = 'https://sdetrole.com/api/mock';

async function getToken() {
  const res = await fetch(`${BASE}/auth/login`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ username: process.env.API_USER, password: process.env.API_PASSWORD }),
  });
  if (!res.ok) throw new Error(`Login failed with ${res.status}`);
  const { accessToken } = await res.json();
  return accessToken;
}

const token = await getToken();
const me = await fetch(`${BASE}/auth/me`, { headers: { Authorization: `Bearer ${token}` } });
console.log(me.status, (await me.json()).scopes); // 200 [ 'users:read', 'orders:read' ]

The following lessons show the same pattern in Postman, Playwright and REST Assured.

Practice exercise

Using the API Playground or curl, write and run a test matrix for the Practice API’s authentication endpoints. Cover at least six cases across POST /auth/login and GET /auth/me. For each case, record the request, the expected status, the actual status, the detail text and the WWW-Authenticate header if present. Then answer: which of these cases would you automate as part of every CI run, and which would you leave to a periodic security review?

Show a model answer
# Request Expected Notes
1 POST /auth/login with tester / practice123 200 Body has accessToken, tokenType: "Bearer", expiresIn: 3600
2 POST /auth/login with a wrong password 401 Invalid username or password. and a WWW-Authenticate: Bearer header
3 POST /auth/login with {"username":"tester"} only 400 Missing password is a malformed request, not a failed login
4 POST /auth/login with a JSON body but Content-Type: text/plain 415 The server refuses to guess the body format
5 GET /auth/me with no header 401 Missing Authorization header.
6 GET /auth/me with Basic ... 401 error="invalid_request" in WWW-Authenticate
7 GET /auth/me with Bearer wrong-token 401 error="invalid_token"
8 GET /auth/me with the real token 200 role is tester, scopes contains users:read

All eight are fast and deterministic, so they belong in every CI run. Expiry, revocation, tampered-JWT and cross-user access tests also belong in CI on a real system when you can create the conditions reliably; deeper work such as brute-force protection and token-leak hunting suits a periodic security review.

Key takeaways

  • 401 means “who are you?”; 403 means “I know who you are, and no”. Check both the code and the WWW-Authenticate header.
  • API keys identify applications, Basic auth sends encoded (not encrypted) credentials, and bearer tokens, opaque or JWT, are the most common scheme for modern APIs.
  • A JWT payload is readable by anyone; only the signature protects it. Tampered and alg: none tokens must be rejected.
  • Get tokens once per worker or suite and share them; keep dedicated tests for the login flow.
  • The most valuable auth tests are negative: missing, malformed, expired, revoked and over-privileged credentials, and access to other users’ objects.

Key terms

Finished this lesson?

Progress is saved in this browser. No account needed.

Type to search across SDETRole.

Full-text search by Pagefind · runs in your browser Open search page →