Rest Assured Authentication Practice: Auth, Tokens & Validation (Solutions)
This practice set covers the two areas every API automation interview probes: authentication (getting, reusing and rejecting tokens) and response validation (proving the response is right, not just that it arrived). Each group of exercises is followed by solution code in REST Assured and TestNG. Practice on Restful Booker, Fake Store API and GoRest, or your own project's API.
Part 1 — Authentication and Tokens
Exercises
- Generate a token from the login endpoint with valid credentials.
- Extract the token from the response without hard-coding it.
- Store it once per test class and reuse it for every secured request.
- Send it the way the API expects (Bearer header, cookie, API key).
- Call a secured endpoint without a token.
- Call it with an invalid or tampered token.
- Call it with an expired token.
- Check role-based access: allowed and forbidden operations per role.
- Try to read another user's resource by changing the ID.
- Log out, then reuse the old token.
- Refresh an access token (where the API supports it).
- Handle different auth types: Basic, Bearer, API key, OAuth 2.0 client credentials.
- Keep credentials out of code and CI logs.
Solutions: Generate, Extract, Reuse (1–4)
public abstract class AuthBase {
protected static RequestSpecification userSpec;
@BeforeClass(alwaysRun = true)
public void login() {
userSpec = specFor(System.getenv("API_USER"), System.getenv("API_PASSWORD"));
}
protected static RequestSpecification specFor(String username, String password) {
String token = given()
.baseUri(ConfigReader.get("apiBaseUrl"))
.contentType(ContentType.JSON)
.body(Map.of("username", username, "password", password))
.when()
.post("/auth/login")
.then()
.statusCode(200)
.body("token", not(emptyOrNullString()))
.extract().path("token");
return new RequestSpecBuilder()
.setBaseUri(ConfigReader.get("apiBaseUrl"))
.setContentType(ContentType.JSON)
.addHeader("Authorization", "Bearer " + token)
// Restful Booker instead: .addCookie("token", token)
.build();
}
}
One login per class, a reusable RequestSpecification, and a builder method that makes a spec for any user — which exercises 8 and 9 need. Credentials come from environment variables (exercise 13).
More: REST Assured Authentication.
Solutions: Negative Token Tests (5–7)
public class TokenNegativeTest extends AuthBase {
@Test
public void missingTokenIsRejected() {
given().baseUri(ConfigReader.get("apiBaseUrl"))
.when().get("/orders")
.then().statusCode(401);
}
@Test
public void invalidTokenIsRejected() {
given().baseUri(ConfigReader.get("apiBaseUrl"))
.header("Authorization", "Bearer not-a-real-token")
.when().get("/orders")
.then()
.statusCode(401)
.body(not(containsString("not-a-real-token")));
}
@Test
public void expiredTokenIsRejected() {
String expired = ConfigReader.get("expiredToken");
given().baseUri(ConfigReader.get("apiBaseUrl"))
.header("Authorization", "Bearer " + expired)
.when().get("/orders")
.then().statusCode(401);
}
}
Missing, invalid and expired tokens should all return 401 Unauthorized; 403 Forbidden means authenticated but not allowed. Some APIs differ (Restful Booker returns 403 for updates without a valid token) — assert what the API documents, and question it if it contradicts the standard.
Solutions: Roles, Ownership and Logout (8–10)
@DataProvider(name = "accessMatrix")
public Object[][] accessMatrix() {
return new Object[][] {
{ "admin", "DELETE", "/users/42", 204 },
{ "editor", "DELETE", "/users/42", 403 },
{ "viewer", "PATCH", "/users/42", 403 },
{ "viewer", "GET", "/users/42", 200 },
};
}
@Test(dataProvider = "accessMatrix")
public void roleBasedAccess(
String role,
String method,
String path,
int expected) {
given().spec(
specFor(
role + "_user",
System.getenv(role.toUpperCase() + "_PASSWORD")
)
)
.when().request(method, path)
.then().statusCode(expected);
}
@Test
public void userCannotReadAnotherUsersOrders() {
given().spec(userSpec)
.when().get("/users/{id}/orders", "someone-elses-id")
.then().statusCode(anyOf(is(403), is(404)));
}
@Test
public void tokenIsInvalidAfterLogout() {
RequestSpecification spec =
specFor(
System.getenv("API_USER"),
System.getenv("API_PASSWORD")
);
given().spec(spec)
.get("/orders")
.then().statusCode(200);
given().spec(spec)
.post("/auth/logout")
.then().statusCode(anyOf(is(200), is(204)));
given().spec(spec)
.get("/orders")
.then().statusCode(401);
}
A data-driven access matrix covers every role and operation with one test method. The "another user's data" check targets a very common real vulnerability.
Stateless JWTs sometimes stay valid after logout until they expire — if the logout test behaves that way, confirm whether it is by design.
More: API Security Testing.
Part 2 — Response Validation
Exercises
- Status code for every positive and negative case.
- Headers: Content-Type, Location after create, caching and security headers.
- Body values, including nested objects.
- Data types of every important field.
- Arrays: size, every element, and search by value.
- Null vs empty vs missing fields.
- Error responses: structure and a helpful message, no stack traces.
- Timestamps: format and plausibility.
- Decimal precision for prices and totals.
- The whole contract with a JSON schema.
- Response time within the agreed limit.
Solutions
@Test
public void orderResponseIsValid() {
Response res = given().spec(userSpec)
.when().get("/orders/{id}", orderId)
.then()
.statusCode(200) // 1
.contentType(ContentType.JSON) // 2
.header("Cache-Control", containsString("no-store"))
.body("id", equalTo(orderId)) // 3
.body("customer.address.city", not(emptyOrNullString()))
.body("total", instanceOf(Float.class)) // 4
.body("items", hasSize(greaterThan(0))) // 5
.body("items.quantity", everyItem(greaterThan(0)))
.body(
"items.find { it.sku == 'BACKPACK-01' }.price",
notNullValue()
)
.body("$", hasKey("couponCode")) // 6
.body("couponCode", nullValue())
.body(
matchesJsonSchemaInClasspath("schemas/order.json")
) // 10
.time(lessThan(1500L)) // 11
.extract().response();
Instant created = Instant.parse(res.path("createdAt")); // 8
Assert.assertTrue(
created.isBefore(Instant.now().plusSeconds(60))
);
BigDecimal total =
new BigDecimal(res.path("total").toString()); // 9
Assert.assertTrue(
total.scale() <= 2,
"total has more than 2 decimals: " + total
);
}
@Test
public void errorResponseIsHelpfulAndSafe() { // 7
given().spec(userSpec)
.body("{}")
.when()
.post("/orders")
.then()
.statusCode(400)
.body("message", containsStringIgnoringCase("items"))
.body(not(containsString("Exception")))
.body(not(containsString("at com.")));
}
Null vs empty vs missing: hasKey proves the field exists; nullValue() checks a null value; emptyString() or empty() checks an empty string or array. They mean different things to clients, so check what the contract says.
For money, compare with BigDecimal — floating-point totals can be off in the last digit.
Deeper coverage:
Soft vs Hard Assertions
REST Assured stops at the first failing then() check (hard). To see every field failure in one run, extract the response and use TestNG's SoftAssert, calling assertAll() at the end — useful for large payloads, but keep critical checks hard.
See each token and assertion flow through given(), when() and then() step by step in the REST Assured Visualizer.
From Real Projects
In my API automation work, I treated authentication and response validation as reusable parts of the automation framework rather than writing the same logic separately for every test. With Java, REST Assured and TestNG, I worked with API scenarios where requests depended on authentication and where the response needed to be validated beyond simply checking for a successful status code.
For authentication, I preferred keeping credentials outside the test logic and using reusable request specifications for secured API calls. This made individual tests easier to read because the test could concentrate on the actual business behavior instead of repeating authentication setup.
Response validation was equally important. I normally considered the status code, response body, important fields, data types and negative behavior together. A 200 response alone did not prove that an API was behaving correctly. For example, an incorrect value, missing field or unexpected response structure could still pass a basic status-code assertion.
I also used TestNG capabilities such as reusable test setup and data-driven execution when scenarios needed to be covered across different inputs. When debugging failures, I checked the request, authentication, response status, response body and test data systematically rather than immediately changing the assertion.
The biggest lesson for me was that API automation becomes much more maintainable when authentication, request configuration and validation are treated as framework-level concerns. Individual tests can then remain focused on business scenarios while the common infrastructure handles repeated API testing requirements.
FAQs
How do you handle a token in REST Assured?
Log in once in @BeforeClass, extract the token with extract().path("token"), and put it in a reusable RequestSpecification used by every test.
Which token scenarios should you test?
Valid, missing, invalid, expired, wrong role, another user's resource, reuse, logout and refresh.
What's the difference between 401 and 403?
401 means the request is not authenticated, such as missing, invalid or expired credentials. 403 means the request is authenticated but the user is not permitted to perform the operation.
Which response checks matter most?
Status code, key body values and types, required fields, error format, and the overall contract via JSON schema.
How do you validate null vs missing fields?
Use hasKey("field") for presence, nullValue() for null values, and emptyString()/empty() for empty strings and arrays.
How do you compare money values safely?
Convert the value to BigDecimal and compare with a defined scale, rather than using floating-point equality.
Related