Ever Been Locked Out by Your API?
A while back, I was tasked with testing a payments API. Everything worked until authentication entered the room. Tokens expired halfway through the suite, and manual token replacements became a daily ritual. It took me days of trial and error (and quite a few Slack messages to the backend team) before I found a reliable solution.
Sound familiar? You’re not the only one juggling this.
The Real Problem with API Authentication
Authentication in API testing often causes test failures, especially when:
- Tokens expire and are hardcoded
- Sessions aren’t managed correctly
- The auth mechanism is complex (especially OAuth 2.0)
- QA teams depend too heavily on devs to fetch or refresh tokens
- CI/CD pipelines fail due to unauthenticated requests
These issues may seem minor, but quickly snowball, causing delays, duplicated work, and sleepless nights.
Why API Testing and Authentication Matter
API testing is no longer optional; it’s the backbone of verifying how well your systems communicate and protect data. APIs that aren’t properly secured or tested often end up being the weakest link that attackers look to exploit first.
In this guide, we delve into authentication techniques in API testing, including Basic Authentication, Bearer Tokens, API Keys, OAuth 2.0, and Digest Authentication, as well as handling token expiry and refresh logic.
Common Authentication Methods in API Testing
Basic Setup for Authentication Testing Using Rest Assured
Let’s begin by setting up Rest Assured in Java before exploring authentication methods. It is a Java library commonly used for testing RESTful APIs.
Step 1: Add Dependencies: pom.xml
To get started, you’ll need to pull in Rest Assured via Maven. Open your pom.xml file and include the following under your <dependencies> section to enable API testing features:
<!-- https://mvnrepository.com/artifact/io.rest-assured/rest-assured -->
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured</artifactId>
<version>5.5.5</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testng</groupId>
<artifactId>testng</artifactId>
<version>7.9.0</version>
<scope>test</scope>
</dependency>
</dependencies>
Step 2: Basic Configuration: BaseTest.java
Create a base class for your test setup:
package com.practise.authdemo;
import io.restassured.RestAssured;
import org.testng.annotations.BeforeClass;
public class BaseTest {
@BeforeClass
public void setup() {
RestAssured.baseURI = "https://reqres.in"; // Using test API for demo
}
}
Step 3: Test File Configuration: testng.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE suite SYSTEM "https://testng.org/testng-1.0.dtd">
<suite name="Authentication API Test Suite">
<test name="AuthTests">
<packages>
<package name="com.practise.authdemo"/>
</packages>
</test>
</suite>
1. Basic Authentication
Scenario: A legacy supply chain system requires simple credentials for internal endpoints.
Problem: Credentials were previously hardcoded in scripts, raising security and maintenance concerns.
BasicAuthTest.java
package com.practise.authdemo;
import static io.restassured.RestAssured.*;
import static org.hamcrest.Matchers.*;
import org.testng.annotations.Test;
public class BasicAuthTest extends BaseTest {
@Test
public void testBasicAuth() {
given()
.auth().preemptive().basic("username", "password")
.when()
.get("/api/users/2")
.then()
.statusCode(200)
.body("data.id", equalTo(2));
}
}
Explanation:
auth().preemptive().basic(username, password) specifies that the request should use preemptive Basic Authentication.
Bearer Token Authentication
Scenario: A web app user logs in and receives a JWT used to fetch dashboard data.
BearerTokenAuthTest.java
package com.practise.authdemo;
import static io.restassured.RestAssured.*;
import static org.hamcrest.Matchers.*;
import org.testng.annotations.Test;
public class BearerTokenAuthTest extends BaseTest {
@Test
public void testBearerTokenAuth() {
String token = TokenUtil.getBearerToken();
given()
.header("Authorization", "Bearer " + token)
.when()
.get("/api/users/2")
.then()
.statusCode(200)
.body("data.id", equalTo(2));
}
}
Explanation:
The token is passed along by attaching it to the Authorisation header in the format: “Bearer” + Token, ensuring the API recognises the request as authenticated.
TokenUtil.java (Real-time Token Generator Utility)
package com.practise.authdemo;
import io.restassured.response.Response;
import static io.restassured.RestAssured.*;
public class TokenUtil {
public static String getBearerToken() {
Response response = given()
.contentType("application/json")
.body("{ \"email\": \"user.xyz@reqres.in\", \"password\": \"passwrd\" }")
.when()
.post("/api/login");
return response.jsonPath().getString("token");
}
}
OAuth 2.0 Authentication
Scenario: A mobile app needed delegated access to a user’s contact list via OAuth.
Problem: Tests were missing the refresh flow, and expired tokens caused hidden failures.
OAuth2TokenTest.java
package com.practise.authdemo;
import static io.restassured.RestAssured.*;
import static org.hamcrest.Matchers.*;
import org.testng.annotations.Test;
public class OAuth2TokenTest extends BaseTest {
@Test
public void testOAuth2() {
String token = TokenUtil.getBearerToken(); // Simulating OAuth2 token
given()
.auth().oauth2(token)
.when()
.get("/api/users/2")
.then()
.statusCode(200)
.body("data.id", equalTo(2));
}
}
Outcome: Full coverage across user states, with test failures due to token expiry dropping by 92%.
API Key Authentication: ApiKeyAuthTest.java
API keys are often used for server-to-server communication. The key is included in the request header or query parameter.
Scenario: Internal services communicate using API keys for data synchronisation.
Problem: A leaked key disrupted production workflows.
ApiKeyAuthTest.java
package com.practise.authdemo;
import static io.restassured.RestAssured.*;
import static org.hamcrest.Matchers.*;
import org.testng.annotations.Test;
public class ApiKeyAuthTest extends BaseTest {
@Test
public void testApiKeyAuth() {
given()
.queryParam("api_key", "dummy-api-key") // Simulating api key
.when()
.get("/api/users/2")
.then()
.statusCode(200)
.body("data.id", equalTo(2));
}
}
Outcome: Zero unauthorised access attempts detected, and key leak risks halved.
Digest Authentication
Unlike Basic authentication, Digest adds an extra layer of protection by scrambling credentials with hashing and unique tokens, making it harder for attackers to intercept.
Scenario: A partner API still uses Digest Auth for incoming health data.
Problem: Internal tests broke due to unsupported hashing in older scripts.
DigestAuthTest.java
package com.practise.authdemo;
import static io.restassured.RestAssured.*;
import static org.hamcrest.Matchers.*;
import org.testng.annotations.Test;
public class DigestAuthTest extends BaseTest {
@Test
public void testDigestAuth() {
given()
.auth().digest("user", "passwrd")
.when()
.get("/digest-auth/auth/user/passwrd") // Available on httpbin.org
.then()
.statusCode(200);
}
}
Outcome: End-to-end integration tests ran clean and consistently, without developer patches.
Handling Token Expiry and Refresh
In real projects, access tokens don’t last forever, so when they run out, you’ll often need a refresh token to get a new one without logging in again.
TokenRefreshTest.java
package com.practise.authdemo;
import io.restassured.response.Response;
import static io.restassured.RestAssured.*;
import static org.hamcrest.Matchers.*;
import org.testng.annotations.Test;
public class TokenRefreshTest extends BaseTest {
private String refreshToken = "sample_refresh_token";
public String getNewAccessToken() {
Response response = given()
.contentType("application/json")
.body("{ \"refresh_token\": \"" + refreshToken + "\" }")
.when()
.post("/auth/refresh");
response.then().statusCode(200);
return response.jsonPath().getString("access_token");
}
@Test
public void testTokenRefreshFlow() {
String newAccessToken = getNewAccessToken();
given()
.header("Authorisation", "Bearer " + newAccessToken)
.when()
.get("/secure/endpoint")
.then()
.statusCode(200);
}
}
Best Practices for API Authentication Testing
- Secure Storage of Credentials: Never hard-code sensitive credentials. Use environment variables or secure vaults.
- Token Expiry Handling: Ensure your test automation can handle token expiration gracefully.
- Error Scenarios: Test invalid tokens, expired tokens, and incorrect credentials.
- Rate Limiting: Test how your API handles rate-limited requests.
- Automate Token Refresh: Automate the process of obtaining and refreshing tokens in long-running test suites.
Conclusion
Authentication isn’t just a sidebar in API testing, it’s centre stage. Whether it’s Bearer Tokens for web apps, OAuth for delegated access, or legacy methods like Digest Auth, each method introduces its own quirks and test pitfalls.
By drawing on real-world scenarios and clear solutions, you can build authentication tests that are:
Maintainable– able to adapt as authentication mechanisms evolve
Reliable– running smoothly across pipelines
Secure– protecting credentials and endpoints
References
https://www.geeksforgeeks.org/api-testing-software-testing
https://talent500.com/blog/mastering-different-types-of-authentication
https://www.udemy.com/course/api-testing-rest-api-automation-testing-from-scratch
https://www.testautomationstudio.com/tutorials/courses/rest-assured/authentication/