Testing Authenticated Endpoints

Welcome to the final lesson of our journey through automated API testing with Java. In this lesson, we'll focus on testing authenticated API endpoints. While you've already learned to organize tests and handle CRUD operations using JUnit, today we'll delve into how APIs manage secure access through different authentication methods — API Keys, Sessions, and JWT (JSON Web Tokens).

Authentication is crucial for securing API endpoints against unauthorized access. By understanding how to test these mechanisms, you'll ensure that your API maintains its integrity and protects sensitive data. We'll explore practical examples for each authentication method, giving you the tools to verify that only authorized users can interact with protected resources.

API Key Authentication

API Key authentication is one of the simplest ways to secure an API. It involves sending a unique key as part of the request headers, allowing access to protected endpoints. Let's look at an example of how you can set up and test API Key authentication using Java's HttpClient and JUnit:

public class TestAPIKeyAuthentication {
    private static final String BASE_URL = "http://localhost:8000";
    private static final String API_KEY = "123e4567-e89b-12d3-a456-426614174000";

    @Test
    public void testAPIKeyAuthentication() throws Exception {
        // Arrange
        HttpClient client = HttpClient.newHttpClient();
        HttpRequest request = HttpRequest.newBuilder()
                .uri(new URI(BASE_URL + "/todos"))
                .header("X-API-Key", API_KEY)
                .GET()
                .build();

        // Act
        HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());

        // Assert
        assertEquals(200, response.statusCode());
    }
}

Here, we define a test class TestAPIKeyAuthentication. Within the test method, testAPIKeyAuthentication, we specify the headers, including the X-API-Key. We use Java's HttpClient to make a call to the /todos endpoint and pass the headers, ensuring the API key is included in the request. The test then asserts that the response status code is 200, indicating successful authentication and access to the endpoint.

Session Authentication: Setup

We begin our session-based authentication by establishing a method for user credentials and a helper method for login. This enables each test to authenticate independently, providing a consistent environment to test both login functionality and access to protected endpoints:

public class TestSessionAuthentication {
    private static final String BASE_URL = "http://localhost:8000";
    private static final Gson gson = new Gson();

    private Map<String, String> getAuthDetails() {
        // Arrange - Providing user credentials
        Map<String, String> authDetails = new HashMap<>();
        authDetails.put("username", "testuser");
        authDetails.put("password", "testpass123");
        return authDetails;
    }

    // Helper method for sending a login POST request
    public HttpResponse<String> login(HttpClient client, Map<String, String> authDetails) throws Exception {
        String requestBody = gson.toJson(authDetails);
        HttpRequest request = HttpRequest.newBuilder()
                .uri(new URI(BASE_URL + "/auth/login"))
                .header("Content-Type", "application/json")
                .POST(HttpRequest.BodyPublishers.ofString(requestBody))
                .build();
        return client.send(request, HttpResponse.BodyHandlers.ofString());
    }
}

The getAuthDetails method supplies the required credentials, and the login helper method facilitates independent authentication for each test.

Session Authentication: Testing Login

This test leverages the login helper method to authenticate independently, validating that user credentials create a session successfully:

@Test
public void testLogin() throws Exception {
    // Arrange - Initialize HttpClient
    HttpClient client = HttpClient.newHttpClient();

    // Act - Execute the login process using the helper method
    HttpResponse<String> loginResponse = login(client, authDetails);

    // Assert - Verify that the login was successful
    assertEquals(200, loginResponse.statusCode());
}

The use of the helper ensures that this test does not rely on any previous operations, providing isolated verification of login success via status code.

Session Authentication: Testing Protected Endpoint

In this test, we independently authenticate using the helper method, extract the session cookie from the login response, and confirm that an established session grants access to a protected endpoint:

@Test
void testAccessWithSession() throws Exception {
    // Arrange - Use the login helper method to authenticate
    HttpClient client = HttpClient.newHttpClient();
    Map<String, String> authDetails = getAuthDetails();
    HttpResponse<String> loginResponse = login(client, authDetails);

    // Extract session cookie from login response
    String setCookieHeader = loginResponse.headers().firstValue("Set-Cookie").orElse("");
    String sessionCookie = setCookieHeader.split(";", 2)[0]; // e.g., "sessionid=..."

    // Act
    HttpRequest request = HttpRequest.newBuilder()
            .uri(new URI(BASE_URL + "/todos"))
            .header("Cookie", sessionCookie)
            .GET()
            .build();
    HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());

    // Assert - Confirm access is granted with a successful status code
    assertEquals(200, response.statusCode());
}

This approach ensures that the session cookie is included in the request, allowing access to the protected endpoint.

Session Authentication: Testing Logout

By using the login helper method, this test independently authenticates, extracts the session cookie, and then verifies the ability to terminate a session, ensuring logout functionality is effective:

@Test
void testLogoutWithSession() throws Exception {
    // Arrange - Use the login helper method to authenticate
    HttpClient client = HttpClient.newHttpClient();
    Map<String, String> authDetails = getAuthDetails();
    HttpResponse<String> loginResponse = login(client, authDetails);

    // Extract session cookie from login response
    String setCookieHeader = loginResponse.headers().firstValue("Set-Cookie").orElse("");
    String sessionCookie = setCookieHeader.split(";", 2)[0]; // e.g., "sessionid=..."

    // Act
    HttpRequest request = HttpRequest.newBuilder()
            .uri(new URI(BASE_URL + "/auth/logout"))
            .header("Content-Type", "application/json")
            .header("Cookie", sessionCookie)
            .POST(HttpRequest.BodyPublishers.noBody())
            .build();
    HttpResponse<String> logoutResponse = client.send(request, HttpResponse.BodyHandlers.ofString());

    // Assert - Verify logout success via status code
    assertEquals(200, logoutResponse.statusCode());
}

This approach confirms isolation, as each session is independently managed, ensuring logout integrity without relying on previous tests.

JWT Authentication: Setup

For JWT-based authentication, we replicate this strategy with a method for credentials and a login helper method, enabling each test to independently obtain and manage JWTs:

public class TestJWTAuthentication {
    private static final String BASE_URL = "http://localhost:8000";
    private static final Gson gson = new Gson();

    private Map<String, String> getAuthDetails() {
        // Arrange - User credentials for JWT
        Map<String, String> authDetails = new HashMap<>();
        authDetails.put("username", "testuser");
        authDetails.put("password", "testpass123");
        return authDetails;
    }

    // Helper method for sending a login POST request
    public HttpResponse<String> login(Map<String, String> authDetails) throws Exception {
        HttpClient client = HttpClient.newHttpClient();
        String requestBody = gson.toJson(authDetails);
        HttpRequest request = HttpRequest.newBuilder()
                .uri(new URI(BASE_URL + "/auth/login"))
                .header("Content-Type", "application/json")
                .POST(HttpRequest.BodyPublishers.ofString(requestBody))
                .build();
        return client.send(request, HttpResponse.BodyHandlers.ofString());
    }
}

This setup empowers each test to authenticate independently, retrieving tokens without relying on the order or success of other tests.

JWT Authentication: Testing Login

Using the login helper method, this test independently verifies that successful authentication results in token issuance, forming the basis for accessing protected resources:

@Test
public void testLogin() throws Exception {
    // Act - Authenticate and retrieve JWT tokens
    HttpResponse<String> loginResponse = login(getAuthDetails());

    // Assert - Confirm receipt of tokens and check their existence
    assertEquals(200, loginResponse.statusCode());
    Map<String, String> tokens = gson.fromJson(loginResponse.body(), Map.class);
    assertTrue(tokens.containsKey("access_token"));
    assertTrue(tokens.containsKey("refresh_token"));
}

By independently logging in, the test ensures token validity regardless of prior tests, establishing the foundation for secure access.

JWT Authentication: Testing Protected Endpoint

This test independently authenticates using the helper method and verifies that a JWT access token provides entry to a protected endpoint:

@Test
public void testAccessWithJWT() throws Exception {
    // Arrange - Authenticate and obtain an access token using the helper method
    HttpResponse<String> loginResponse = login(getAuthDetails());
    Map<String, String> tokens = gson.fromJson(loginResponse.body(), Map.class);
    String accessToken = tokens.get("access_token");

    // Act - Use the access token to request a protected endpoint
    HttpClient client = HttpClient.newHttpClient();
    HttpRequest request = HttpRequest.newBuilder()
            .uri(new URI(BASE_URL + "/todos"))
            .header("Authorization", "Bearer " + accessToken)
            .GET()
            .build();
    HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());

    // Assert - Confirm the request was successful by checking the status code
    assertEquals(200, response.statusCode());
}

By ensuring the test is independent, this approach validates the token's effectiveness in protecting resources, confirming stateless authentication.

JWT Authentication: Testing Logout

Finally, this test independently authenticates to verify that access and refresh tokens can be invalidated, securing the system against unauthorized reuse:

@Test
public void testJWTLogout() throws Exception {
    // Arrange - Authenticate and prepare tokens using the helper method
    HttpResponse<String> loginResponse = login(getAuthDetails());
    Map<String, String> tokens = gson.fromJson(loginResponse.body(), Map.class);
    String accessToken = tokens.get("access_token");
    String refreshToken = tokens.get("refresh_token");

    // Act - Use access and refresh tokens to process logout
    HttpClient client = HttpClient.newHttpClient();
    HttpRequest request = HttpRequest.newBuilder()
            .uri(new URI(BASE_URL + "/auth/logout"))
            .header("Authorization", "Bearer " + accessToken)
            .POST(HttpRequest.BodyPublishers.ofString("{\"refresh_token\":\"" + refreshToken + "\"}"))
            .build();
    HttpResponse<String> logoutResponse = client.send(request, HttpResponse.BodyHandlers.ofString());

    // Assert - Verify logout success through confirmation of status code
    assertEquals(200, logoutResponse.statusCode());
}

The refresh token is sent with the logout request so the server knows exactly which refresh token to invalidate. This ensures that the refresh token cannot be used to obtain new access tokens after logout, fully terminating the user's session and preventing token reuse.

Summary and Practices

Throughout this lesson, we've explored different methods of API authentication: API Keys, Sessions, and JWTs. Each method has distinct ways to test and secure access, and you've seen how to implement tests for them effectively using Java, HttpClient, Gson, and JUnit.

When testing any form of authentication, it’s crucial to handle API credentials with care. Avoid hardcoding sensitive information in your codebase, and consider using environment variables or secure vaults in production environments.

As we conclude this lesson and the course, take pride in the journey you've completed. You've gained a comprehensive understanding of how to automate API tests using Java, mastering basic requests through to secure, authenticated endpoints. Continue to practice and explore, applying these skills to ensure robust, reliable APIs in your projects. Congratulations on reaching the end of this course!

Sign up
Join the 1M+ learners on CodeSignal
Be a part of our community of 1M+ users who develop and demonstrate their skills on CodeSignal