Authentication Testing
Authentication testing verifies every layer of identity and access: credentials, tokens, expiry, revocation, roles and scopes. Test that the right callers get in, the wrong ones are rejected, and access is limited to what each role is allowed to do.
What is this?
Authentication testing verifies identity and access control across the whole token lifecycle: issuing credentials, presenting tokens, expiry, revocation, roles and scopes. You prove that the right callers get in, the wrong ones are rejected, and each identity can only reach what it is allowed to. Because auth bugs are security-critical, the suite must cover happy paths, negative cases and boundary conditions such as near-expiry tokens.
Why do you need it?
- Broken access control is a top OWASP category; automated tests are the cheapest proof that identity checks actually work.
- Token lifecycle — issue, refresh, expiry, revoke — is exactly where integration bugs hide between services.
- Role and scope tests catch authorization leaks where one user reads another user's data.
- Replay and tamper checks verify that a copied or edited token is rejected by the server.
- Automated auth suites gate releases so a misconfigured identity provider never ships silently.
Mental model
A boarding pass
An access token
Your ID and ticket get you through check-in (authentication), but the boarding pass decides which gates and which cabin you may enter (authorization). Just like a pass, a token carries who you are, when it expires, and which zones you can access — and testing verifies each of those claims.
How it works
- Obtain a credential set for each identity you need to cover: user, admin, read-only client and an expired token.
- Request an access token via the documented flow — password grant, client credentials, authorization code or API key.
- Call protected endpoints with, without and with an invalid token, asserting on the 401/403 outcomes.
- Verify the token's claims (issuer, audience, expiry, scopes) against the expected values for that identity.
- Exercise refresh behavior: a refresh token rotates, expires, and cannot be replayed once consumed.
From credentials to an allow/deny decision at the API.
Key commands
curl -s -X POST https://auth.example.com/oauth/token -H 'Content-Type: application/x-www-form-urlencoded' -d 'grant_type=client_credentials&client_id=web-app&client_secret=s3cret'Request an access token with the client_credentials grant.
curl -s https://api.example.com/v1/me -H "Authorization: Bearer $TOKEN"Call a protected endpoint with a token stored in the TOKEN variable.
jq -R 'split(".")[1] | @base64d | fromjson' <<< "$TOKEN"Decode and pretty-print the JWT payload to inspect iss, exp and scopes.
{"iss":"https://auth.example.com","exp":1750000000,"scope":["orders:read"]}curl -s -o /dev/null -w '%{http_code}\n' https://api.example.com/v1/adminCheck that an unauthenticated request to an admin route is rejected.
401
openssl dgst -sha256 -mac HMAC -macopt hexkey:$(cat secret.key) data.txtGenerate an HMAC signature the way many API-key flows do, for signature tests.
Examples
OAuth2 client_credentials with a role-bound call
import io.restassured.RestAssured;import io.restassured.http.ContentType;import org.junit.jupiter.api.Test; class OAuthApiTest { @Test void clientCanReadItsOwnScope() { String token = RestAssured .given() .contentType(ContentType.URLENC) .formParam("grant_type", "client_credentials") .formParam("client_id", "web-app") .formParam("client_secret", "s3cret") .when() .post("/oauth/token") .then() .statusCode(200) .extract().path("access_token"); // The client is scoped to read, so a write call must be rejected. RestAssured .given() .auth().oauth2(token) .accept(ContentType.JSON) .body("{\"order\":{\"items\":[1,2]}}") .when() .post("/v1/orders") .then() .statusCode(403); // Reading its own scope is fine. RestAssured .given() .auth().oauth2(token) .accept(ContentType.JSON) .when() .get("/v1/orders?status=open") .then() .statusCode(200); }}The first call exchanges client credentials for an access token using the client_credentials grant. The same token is then used twice: once to assert a 403 when the token's read-only scope attempts a write, and once to assert 200 on a read it is allowed. Positive and negative authorization cases live in the same test so the boundary is explicit.
In production
- CI runs an auth matrix covering user, admin and anonymous callers against every environment.
- Rotating client secrets or tightening token expiry is validated by the auth suite before the change ships.
- Security reviews cite the auth suite as evidence that OWASP A01-Broken Access Control cases are covered.
- Refresh-token rotation tests prove a stolen refresh token cannot be replayed across sessions.
Best practices
- Never put real credentials in tests — inject them via environment variables or a secrets manager.
- Create dedicated test identities per environment, scoped so they have no access to production data.
- Assert on both sides of the boundary: 200 for allowed, 401 for missing credentials, 403 for insufficient scope.
- Test expiry boundaries with short-lived tokens or clock control so 401 behavior is verified without waiting.
- Verify the token's issuer, audience and scopes instead of trusting the status code alone.
Common mistakes
- Testing only with an admin token, so privilege-escalation leaks go completely unnoticed.
- Hard-coding tokens that expire, making the suite red for reasons unrelated to the code.
- Reusing one identity for happy-path and negative tests, masking authorization issues.
- Checking only the HTTP code while ignoring the error body that reveals token reason codes.
Common issues
401 invalid_token on every call right after loginThe access token was never captured or was not attached to the request headers.
Store the token from the token response into the variable used by the Authorization header and re-run.
403 Forbidden for a valid tokenThe token's scopes or the caller's role do not include the requested resource.
Decode the token, compare its scopes with the endpoint requirement, and use an identity with sufficient privileges.
Refresh token fails on its second useThe provider rotates refresh tokens and revokes the old one once it is used.
Re-read the new refresh token from every refresh response instead of caching the original.
Debugging tips
- Decode the JWT payload with jq to inspect iss, aud, exp and scopes of the failing token.
- Read the error body — a 401 with invalid_token differs from one with expired_token.
- Verify the token is sent in the right format: Authorization: Bearer <token>.
- Check the identity provider logs to confirm the credential was even accepted at the token endpoint.
Interview checkpoint
Hands-on lab
- Request a token via the client_credentials grant
- Call a protected endpoint with a bearer token
- Verify unauthenticated and invalid-token requests are rejected
- Verify role and scope enforcement with an under-privileged token
Test the OAuth2 Token Lifecycle
45 min · intermediate
Quick quiz
Which HTTP status indicates the request is missing or has invalid credentials?
A valid access token grants access to every endpoint on the API.
A token with the read scope receives a 403 when calling POST /v1/orders. What does this indicate?
Cheat sheet
grant_type=client_credentialsMachine-to-machine token grantgrant_type=passwordDirect user credentials (less common)Authorization: Bearer <token>Present an access tokenjq -R 'split(".")[1] | @base64d | fromjson'Decode a JWT payload401No credentials or invalid credentials403Authenticated but not authorized200/201Authenticated and authorized