REST Assured Authentication: OAuth2, JWT, and Basic Auth Testing

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.

Read more