Security bugs in APIs are usually authorisation bugs: a normal user reaching an admin endpoint, one customer reading another's order, a tampered token that still works. This guide automates those checks with REST Assured so they run on every build, from authentication and role-based access control to JWT tampering, BOLA and injection.

Basic Authentication

// Basic Auth: credentials sent as Base64(username:password) in Authorization header
// Use only over HTTPS — Base64 is encoding, not encryption

given()
    .auth().basic("admin@test.com", "Admin@1234")
.when()
    .get("/api/admin/users")
.then()
    .statusCode(200);

// Negative: wrong password → 401
given()
    .auth().basic("admin@test.com", "WrongPassword")
.when()
    .get("/api/admin/users")
.then()
    .statusCode(401)
    .body("error", equalTo("Unauthorized"))
    .body("message", containsString("Invalid credentials"));

// Negative: no auth → 401
given()
    .spec(reqSpec)  // no auth added
.when()
    .get("/api/admin/users")
.then()
    .statusCode(401);
Advertisement

API Key Authentication

// API Key: sent as header or query parameter

// As header (more secure)
given()
    .header("X-API-Key", ConfigReader.get("api.key"))
.when()
    .get("/api/data")
.then()
    .statusCode(200);

// As query parameter (less secure — key visible in logs/URLs)
given()
    .queryParam("api_key", ConfigReader.get("api.key"))
.when()
    .get("/api/data")
.then()
    .statusCode(200);

// Negative tests for API Key
// 1. Missing API key
given().when().get("/api/data").then().statusCode(401);

// 2. Invalid/expired API key
given()
    .header("X-API-Key", "invalid-key-xyz")
.when()
    .get("/api/data")
.then()
    .statusCode(401)
    .body("code", equalTo("INVALID_API_KEY"));

// 3. Revoked API key — DB state: key marked as revoked
given()
    .header("X-API-Key", revokedApiKey)
.when()
    .get("/api/data")
.then()
    .statusCode(401)
    .body("code", equalTo("REVOKED_API_KEY"));

OAuth2 + JWT — The Complete Pattern

// ─── STEP 1: Get Token via OAuth2 Password Grant ───────────────────────
public class TokenManager {

    private static final Map<String, CachedToken> cache = new ConcurrentHashMap<>();

    public static String getToken(String role) {
        return cache.compute(role, (k, existing) -> {
            if (existing == null || existing.isExpiredOrExpiringSoon()) {
                return new CachedToken(fetchToken(role));
            }
            return existing;
        }).getValue();
    }

    private static String fetchToken(String role) {
        String email    = ConfigReader.get("user." + role + ".email");
        String password = ConfigReader.get("user." + role + ".password");

        return given()
            .baseUri(ConfigReader.get("auth.base.url"))
            .contentType("application/x-www-form-urlencoded")
            .formParam("grant_type",    "password")
            .formParam("client_id",     ConfigReader.get("oauth.client.id"))
            .formParam("client_secret", ConfigReader.get("oauth.client.secret"))
            .formParam("username",      email)
            .formParam("password",      password)
            .formParam("scope",         "openid profile email")
        .when()
            .post("/oauth/token")
        .then()
            .statusCode(200)
            .extract().jsonPath().getString("access_token");
    }

    // ── Client Credentials flow (service-to-service) ──────────────────
    public static String getServiceToken() {
        return given()
            .baseUri(ConfigReader.get("auth.base.url"))
            .contentType("application/x-www-form-urlencoded")
            .formParam("grant_type",    "client_credentials")
            .formParam("client_id",     ConfigReader.get("service.client.id"))
            .formParam("client_secret", ConfigReader.get("service.client.secret"))
            .formParam("scope",         "api.read api.write")
        .when().post("/oauth/token")
        .then().statusCode(200)
               .extract().jsonPath().getString("access_token");
    }
}

// ─── STEP 2: Use token in API tests ─────────────────────────────────────
@Test
public void adminCanListAllUsers() {
    given()
        .spec(reqSpec)
        .header("Authorization", "Bearer " + TokenManager.getToken("admin"))
    .when()
        .get("/api/admin/users")
    .then()
        .statusCode(200)
        .body("data.size()", greaterThan(0));
}

JWT Validation Tests

// JWT structure: header.payload.signature (Base64 encoded, dot-separated)

// Test 1: Validate JWT claims in the token itself
@Test
public void jwtToken_hasCorrectClaims() throws Exception {
    String token = TokenManager.getToken("admin");

    // Decode payload (middle part of JWT)
    String[] parts   = token.split("\\.");
    String   payload = new String(Base64.getUrlDecoder().decode(parts[1]));
    JsonNode claims  = new ObjectMapper().readTree(payload);

    // Validate claims
    assertThat(claims.get("sub").asText())    .isNotEmpty();
    assertThat(claims.get("role").asText())   .isEqualTo("ADMIN");
    assertThat(claims.get("iss").asText())    .isEqualTo("https://auth.myapp.com");
    assertThat(claims.get("exp").asLong())    .isGreaterThan(Instant.now().getEpochSecond());
    assertThat(claims.has("iat"))             .isTrue();
}

// Test 2: Expired token → 401
@Test
public void expiredToken_returns401() {
    String expiredToken = TestTokenGenerator.generateExpiredToken();

    given()
        .header("Authorization", "Bearer " + expiredToken)
    .when()
        .get("/api/users")
    .then()
        .statusCode(401)
        .body("code",    equalTo("TOKEN_EXPIRED"))
        .body("message", containsString("token has expired"));
}

// Test 3: Tampered token → 401
@Test
public void tamperedToken_returns401() {
    String validToken   = TokenManager.getToken("customer");
    String tamperedToken = validToken.substring(0, validToken.length() - 5) + "XXXXX";

    given()
        .header("Authorization", "Bearer " + tamperedToken)
    .when()
        .get("/api/users")
    .then()
        .statusCode(401)
        .body("code", equalTo("INVALID_TOKEN"));
}

Role-Based Access Control (RBAC) Tests

// Test matrix: which role can access which endpoint
// This is one of the most important security test suites

@DataProvider(name = "rbacMatrix")
public Object[][] rbacMatrix() {
    return new Object[][] {
     // { role,      endpoint,           method, expectedCode, description }
        { "ADMIN",   "/api/admin/users", "GET",  200, "Admin can list users"        },
        { "MANAGER", "/api/admin/users", "GET",  200, "Manager can list users"      },
        { "CUSTOMER","/api/admin/users", "GET",  403, "Customer CANNOT list users"  },
        { "ADMIN",   "/api/users/42",   "DELETE",200, "Admin can delete users"      },
        { "MANAGER", "/api/users/42",   "DELETE",403, "Manager CANNOT delete users" },
        { "CUSTOMER","/api/users/42",   "DELETE",403, "Customer CANNOT delete users"},
        { "CUSTOMER","/api/orders",      "POST", 201, "Customer can create orders"  },
        { "CUSTOMER","/api/orders",      "GET",  200, "Customer can view own orders"},
    };
}

@Test(dataProvider = "rbacMatrix")
public void testRBAC(String role, String endpoint, String method,
                      int expectedCode, String desc) {
    RequestSender request = given()
        .spec(reqSpec)
        .header("Authorization", "Bearer " + TokenManager.getToken(role))
    .when();

    Response response = switch (method) {
        case "GET"    -> request.get(endpoint);
        case "POST"   -> request.post(endpoint);
        case "DELETE" -> request.delete(endpoint);
        default       -> throw new IllegalArgumentException("Unknown method: " + method);
    };

    assertThat(response.getStatusCode())
        .as("RBAC test: %s", desc)
        .isEqualTo(expectedCode);
}

API Security Testing

OWASP API Security Top 10 — Every SDET Must Know

The Open Web Application Security Project defines the 10 most critical API security risks.

As a Senior SDET you are expected to write tests for all of these.

#OWASP RiskWhat to TestExample Test
1Broken Object Level Auth (BOLA)User A accesses User B's data by changing ID in URLGET /users/43 with User 42's token → must return 403
2Broken AuthExpired/invalid tokens accepted, weak passwords allowedExpired JWT → 401; brute force → 429
3Broken Object Property AuthUser can update fields they should not (e.g. role, isAdmin)PATCH /users/42 {role:"ADMIN"} with customer token → 403
4Unrestricted Resource ConsumptionNo rate limiting, no pagination limitslimit=999999 → bounded; 1000 rapid requests → 429
5Broken Function Level AuthNon-admin accessing admin-only endpointsGET /admin/stats with customer token → 403
6Unrestricted Access to Sensitive FlowsMass account creation, OTP bypassPOST /auth/send-otp 100x rapid → 429
7Server Side Request Forgery (SSRF)API fetches a URL you control — redirects internallyPass internal IP as URL param → should be blocked
8Security MisconfigurationDebug endpoints exposed, CORS too permissiveGET /debug/env → 404; check CORS headers
9Improper Inventory ManagementOld API versions still accessible and unpatchedGET /v0/users or /api-old/users → 404 or 410
10Unsafe Consumption of APIsTrusting third-party API data without validationSend malicious data via webhook → validated correctly

BOLA — Horizontal Privilege Escalation

// BOLA Test: User A CANNOT read User B's private data
@Test
public void bola_userCannotAccessAnotherUsersOrders() {
    // Setup: two users with their tokens
    String userAToken = TokenManager.getToken("customerA");  // userId = 42
    String userBToken = TokenManager.getToken("customerB");  // userId = 43

    // User A can access their own orders
    given().header("Authorization", "Bearer " + userAToken)
    .when().get("/api/users/42/orders")
    .then().statusCode(200);

    // User A CANNOT access User B's orders — must be 403, not 200!
    given().header("Authorization", "Bearer " + userAToken)
    .when().get("/api/users/43/orders")  // User B's endpoint
    .then()
        .statusCode(403)  // If this returns 200 → BOLA vulnerability!
        .body("error.code", equalTo("ACCESS_DENIED"));
}

// Mass BOLA test: scan multiple IDs
@Test
public void bola_scanMultipleIds_allReturn403() {
    String customerToken = TokenManager.getToken("customer");
    int[] otherUserIds = {1, 2, 3, 100, 999};  // IDs that belong to other users

    for (int id : otherUserIds) {
        given().header("Authorization", "Bearer " + customerToken)
        .when().get("/api/users/" + id + "/private-data")
        .then().statusCode(anyOf(equalTo(403), equalTo(404)));
        // 404 is acceptable (hides existence); 200 is a vulnerability!
    }
}

Injection & Input Validation Security Tests

@DataProvider(name = "injectionPayloads")
public Object[][] injectionPayloads() {
    return new Object[][] {
        // SQL Injection attempts
        { "1' OR '1'='1",          "SQL injection" },
        { "1; DROP TABLE users;--", "SQL injection 2" },
        { "1 UNION SELECT * FROM users", "UNION injection" },

        // XSS attempts
        { "<script>alert(1)</script>", "XSS basic" },
        { "javascript:alert(1)",       "XSS protocol" },
        { "<img src=x onerror=alert(1)>", "XSS img" },

        // Path traversal
        { "../../etc/passwd",    "Path traversal" },
        { "..\\..\\windows\\system32", "Path traversal Win" },

        // Null byte injection
        { "admin%00suffix",     "Null byte" },
        { "very" + "A".repeat(10000), "Buffer overflow attempt" },
    };
}

@Test(dataProvider = "injectionPayloads")
public void inputValidation_rejectsInjectionAttempts(
    String payload, String description) {

    Response response = given().spec(withAuth("customer"))
        .body(Map.of("name", payload))
    .when().post("/api/users");

    // Must NOT return 500 (indicates code error/unhandled exception)
    assertThat(response.getStatusCode())
        .as("Injection test [%s] must not return 500", description)
        .isNotEqualTo(500);

    // Must return 400 or 422 (validation rejected)
    assertThat(response.getStatusCode())
        .as("Injection test [%s]", description)
        .isIn(400, 422);
}

Sensitive Data in Responses

// API must NEVER return sensitive fields in responses
@Test
public void userResponse_doesNotExposePasswordOrSecrets() {
    Response response = given().spec(withAuth("admin"))
    .when().get("/api/users/42")
    .then().statusCode(200).extract().response();

    String responseBody = response.getBody().asString().toLowerCase();

    // These fields must NEVER appear in any API response
    assertThat(responseBody).doesNotContain("password");
    assertThat(responseBody).doesNotContain("secret");
    assertThat(responseBody).doesNotContain("private_key");
    assertThat(responseBody).doesNotContain("credit_card");
    assertThat(responseBody).doesNotContain("cvv");
    assertThat(responseBody).doesNotContain("ssn");
}

// PAN masking test (payment cards)
@Test
public void paymentResponse_cardNumberIsMasked() {
    Response response = given().spec(withAuth("customer"))
    .when().get("/api/users/42/payment-methods")
    .then().statusCode(200).extract().response();

    List<String> cardNumbers = response.jsonPath().getList("data.cardNumber");
    for (String card : cardNumbers) {
        // Must be masked: **** **** **** 1234
        assertThat(card).matches("\\*{4} \\*{4} \\*{4} [0-9]{4}");
    }
}

FAQs

Can REST Assured be used for security testing?

Yes, for functional security checks such as authorisation rules, token handling, BOLA and input validation. Deep scanning still needs tools such as OWASP ZAP or Burp Suite.

What is BOLA in API security?

Broken Object Level Authorization: an API returns or changes an object (an order, an account) without checking that the caller owns it, usually by changing an ID in the request.