REST Assured Authentication: OAuth2, JWT, and Basic Auth Testing
Testing authenticated APIs is unavoidable in any real project. Most production endpoints require authentication — Basic Auth, Bearer tokens, OAuth2, or API keys. REST Assured provides built-in support for common authentication schemes and the flexibility to handle anything custom. This guide covers the patterns you'll actually use.
Basic Authentication
HTTP Basic Auth sends base64-encoded username:password with every request. REST Assured handles this with a single line:
given()
.auth().basic("username", "password")
.when()
.get("/api/protected-resource")
.then()
.statusCode(200);This adds the Authorization: Basic dXNlcm5hbWU6cGFzc3dvcmQ= header automatically.
Preemptive Basic Auth
By default, REST Assured sends basic auth only if the server challenges with WWW-Authenticate. For servers that don't send challenges, use preemptive authentication:
given()
.auth().preemptive().basic("username", "password")
.when()
.get("/api/resource")
.then()
.statusCode(200);Preemptive authentication sends the header on the first request without waiting for a 401 challenge — both faster and more compatible.
Shared Basic Auth Config
Avoid repeating credentials in every test using RequestSpecification:
RequestSpecification authSpec = new RequestSpecBuilder()
.setAuth(preemptive().basic("admin", "secret"))
.setBaseUri("https://api.example.com")
.build();
// In tests:
given()
.spec(authSpec)
.when()
.get("/admin/users")
.then()
.statusCode(200);Bearer Token Authentication
Most modern APIs use Bearer tokens — JWTs or opaque tokens passed in the Authorization header.
Simple Bearer Token
String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...";
given()
.header("Authorization", "Bearer " + token)
.when()
.get("/api/profile")
.then()
.statusCode(200);Obtaining a Token via Login
Test the full authentication flow by first logging in:
public class AuthHelper {
private static final String AUTH_URL = "https://api.example.com/auth/login";
public static String getToken(String email, String password) {
return given()
.contentType(ContentType.JSON)
.body(String.format("""
{"email": "%s", "password": "%s"}
""", email, password))
.when()
.post(AUTH_URL)
.then()
.statusCode(200)
.extract()
.path("token");
}
public static String getAdminToken() {
return getToken(
System.getenv("ADMIN_EMAIL"),
System.getenv("ADMIN_PASSWORD")
);
}
public static String getUserToken() {
return getToken(
System.getenv("TEST_USER_EMAIL"),
System.getenv("TEST_USER_PASSWORD")
);
}
}Use in test setup:
@SpringBootTest(webEnvironment = RANDOM_PORT)
class ProtectedApiTest {
private static String userToken;
private static String adminToken;
@BeforeAll
static void authenticate() {
userToken = AuthHelper.getUserToken();
adminToken = AuthHelper.getAdminToken();
}
@Test
void shouldAllowUserToGetOwnProfile() {
given()
.header("Authorization", "Bearer " + userToken)
.when()
.get("/api/profile")
.then()
.statusCode(200)
.body("email", equalTo(System.getenv("TEST_USER_EMAIL")));
}
@Test
void shouldAllowAdminToListAllUsers() {
given()
.header("Authorization", "Bearer " + adminToken)
.when()
.get("/api/admin/users")
.then()
.statusCode(200);
}
@Test
void shouldDenyRegularUserFromAdminEndpoint() {
given()
.header("Authorization", "Bearer " + userToken)
.when()
.get("/api/admin/users")
.then()
.statusCode(403);
}
}JWT Token Testing
JWTs carry claims as base64-encoded JSON. REST Assured tests can validate JWT structure and claims:
Validating JWT Claims in Response
import io.restassured.path.json.JsonPath;
import java.util.Base64;
@Test
void shouldReturnValidJwtOnLogin() {
String responseToken = given()
.contentType(ContentType.JSON)
.body("{\"email\":\"user@example.com\",\"password\":\"pass123\"}")
.when()
.post("/api/auth/login")
.then()
.statusCode(200)
.body("token", notNullValue())
.body("expiresIn", greaterThan(0))
.extract()
.path("token");
// Decode and validate JWT payload
String[] parts = responseToken.split("\\.");
String payload = new String(Base64.getUrlDecoder().decode(parts[1]));
JsonPath claims = new JsonPath(payload);
assertThat(claims.getString("sub"), not(emptyString()));
assertThat(claims.getLong("exp"), greaterThan(System.currentTimeMillis() / 1000));
assertThat(claims.getString("email"), equalTo("user@example.com"));
}Token Refresh Flow
@Test
void shouldRefreshExpiredToken() {
// Get initial tokens
Map<String, String> authResponse = given()
.contentType(ContentType.JSON)
.body("{\"email\":\"user@example.com\",\"password\":\"pass123\"}")
.when()
.post("/api/auth/login")
.then()
.statusCode(200)
.extract()
.as(new TypeRef<Map<String, String>>() {});
String accessToken = authResponse.get("accessToken");
String refreshToken = authResponse.get("refreshToken");
// Use refresh token to get new access token
String newAccessToken = given()
.contentType(ContentType.JSON)
.body(String.format("{\"refreshToken\":\"%s\"}", refreshToken))
.when()
.post("/api/auth/refresh")
.then()
.statusCode(200)
.body("accessToken", notNullValue())
.extract()
.path("accessToken");
// Verify new token works
given()
.header("Authorization", "Bearer " + newAccessToken)
.when()
.get("/api/profile")
.then()
.statusCode(200);
}OAuth2 Authentication
REST Assured has built-in OAuth2 support via the oauth2() method:
Client Credentials Flow
String accessToken = given()
.formParam("grant_type", "client_credentials")
.formParam("client_id", System.getenv("OAUTH_CLIENT_ID"))
.formParam("client_secret", System.getenv("OAUTH_CLIENT_SECRET"))
.formParam("scope", "read:users write:users")
.when()
.post("https://auth.example.com/oauth/token")
.then()
.statusCode(200)
.extract()
.path("access_token");
// Use the token
given()
.auth().oauth2(accessToken)
.when()
.get("/api/users")
.then()
.statusCode(200);The auth().oauth2(token) method adds Authorization: Bearer {token} — it's equivalent to setting the header manually but more expressive.
Authorization Code Flow (Integration Test)
For the Authorization Code flow, you typically have a test user with pre-provisioned tokens or use a test-specific login endpoint:
public class OAuth2Helper {
// For test environments with a simplified token endpoint
public static String getTokenForUser(String userId) {
return given()
.contentType(ContentType.JSON)
.body(String.format("{\"userId\":\"%s\",\"testSecret\":\"%s\"}",
userId, System.getenv("TEST_TOKEN_SECRET")))
.when()
.post("/api/test/tokens") // Test-only endpoint
.then()
.statusCode(200)
.extract()
.path("access_token");
}
}Testing OAuth2 Protected Endpoints
@Test
void shouldAccessResourceWithValidToken() {
String token = OAuth2Helper.getTokenForUser("user-123");
given()
.auth().oauth2(token)
.when()
.get("/api/resources/123")
.then()
.statusCode(200);
}
@Test
void shouldReturn401WithoutToken() {
given()
.when()
.get("/api/resources/123")
.then()
.statusCode(401)
.header("WWW-Authenticate", containsString("Bearer"));
}
@Test
void shouldReturn401WithInvalidToken() {
given()
.auth().oauth2("invalid-token-12345")
.when()
.get("/api/resources/123")
.then()
.statusCode(401);
}
@Test
void shouldReturn403WithInsufficientScope() {
// Token with read scope only
String readOnlyToken = OAuth2Helper.getTokenForScope("read:resources");
given()
.auth().oauth2(readOnlyToken)
.when()
.delete("/api/resources/123") // Requires write scope
.then()
.statusCode(403);
}API Key Authentication
Many APIs use API keys in headers or query parameters:
API Key in Header
given()
.header("X-API-Key", System.getenv("API_KEY"))
.when()
.get("/api/data")
.then()
.statusCode(200);API Key in Query Parameter
given()
.queryParam("api_key", System.getenv("API_KEY"))
.when()
.get("/api/data")
.then()
.statusCode(200);API Key with Version Header
RequestSpecification apiKeySpec = new RequestSpecBuilder()
.setBaseUri("https://api.example.com")
.addHeader("X-API-Key", System.getenv("API_KEY"))
.addHeader("X-API-Version", "2024-01-01")
.setContentType(ContentType.JSON)
.build();Testing Authentication Failures
Comprehensive auth testing covers failure cases too:
class AuthenticationFailureTest {
@Test
void shouldReturn401WithNoCredentials() {
given()
.when()
.get("/api/protected")
.then()
.statusCode(401)
.header("WWW-Authenticate", notNullValue());
}
@Test
void shouldReturn401WithExpiredToken() {
// Use a pre-generated expired token for testing
String expiredToken = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1c2VyQGV4YW1wbGUuY29tIiwiZXhwIjoxNjAwMDAwMDAwfQ.invalid";
given()
.header("Authorization", "Bearer " + expiredToken)
.when()
.get("/api/protected")
.then()
.statusCode(401)
.body("error", equalTo("token_expired"));
}
@Test
void shouldReturn401WithMalformedToken() {
given()
.header("Authorization", "Bearer not.a.valid.jwt")
.when()
.get("/api/protected")
.then()
.statusCode(401);
}
@Test
void shouldReturn401WithWrongBasicCredentials() {
given()
.auth().preemptive().basic("wrong-user", "wrong-password")
.when()
.get("/api/protected")
.then()
.statusCode(401);
}
@Test
void shouldReturn403ForUnauthorizedAction() {
// Authenticated but not authorized
String regularUserToken = AuthHelper.getUserToken();
given()
.header("Authorization", "Bearer " + regularUserToken)
.when()
.delete("/api/admin/users/123") // Admin-only action
.then()
.statusCode(403)
.body("error", equalTo("forbidden"));
}
}Secure Credential Management
Never hardcode credentials in test files:
// BAD — credentials in code
given()
.auth().basic("admin", "SuperSecret123!") // Don't do this
// GOOD — credentials from environment variables
given()
.auth().basic(
System.getenv("TEST_ADMIN_USER"),
System.getenv("TEST_ADMIN_PASSWORD")
)Centralize credential loading:
public class TestCredentials {
public static final String ADMIN_USER = requireEnv("TEST_ADMIN_USER");
public static final String ADMIN_PASSWORD = requireEnv("TEST_ADMIN_PASSWORD");
public static final String API_KEY = requireEnv("TEST_API_KEY");
private static String requireEnv(String key) {
String value = System.getenv(key);
if (value == null || value.isBlank()) {
throw new IllegalStateException(
"Required environment variable '" + key + "' is not set"
);
}
return value;
}
}In CI, set these as repository secrets — never commit them to version control.
Spring Security Test Integration
When testing Spring Boot apps with Spring Security, use @WithMockUser for MockMvc-based tests:
@WebMvcTest(UserController.class)
class SecuredControllerTest {
@Autowired MockMvc mockMvc;
@BeforeEach
void setup() {
RestAssuredMockMvc.mockMvc(mockMvc);
}
@Test
@WithMockUser(roles = "ADMIN")
void shouldAllowAdminAccess() {
given()
.when()
.get("/api/admin/dashboard")
.then()
.statusCode(200);
}
@Test
@WithMockUser(roles = "USER")
void shouldDenyUserFromAdminDashboard() {
given()
.when()
.get("/api/admin/dashboard")
.then()
.statusCode(403);
}
@Test
void shouldReturn401ForUnauthenticated() {
given()
.when()
.get("/api/admin/dashboard")
.then()
.statusCode(401);
}
}Summary
REST Assured's authentication support covers the full spectrum of real-world API security patterns:
| Auth Type | REST Assured Method |
|---|---|
| Basic Auth | auth().basic(user, pass) |
| Basic Auth (preemptive) | auth().preemptive().basic(user, pass) |
| Bearer token | header("Authorization", "Bearer " + token) |
| OAuth2 Bearer | auth().oauth2(token) |
| API key (header) | header("X-API-Key", key) |
| API key (param) | queryParam("api_key", key) |
| Custom auth | header("Authorization", scheme + " " + credentials) |
Comprehensive authentication testing goes beyond happy paths. Always test:
- Missing credentials (401)
- Invalid credentials (401)
- Expired tokens (401)
- Valid credentials but insufficient permissions (403)
This approach, combined with REST Assured's Spring Boot integration and TestNG parameterized tests, gives you complete coverage of authentication behavior across your API surface.