Basic and Digest Authentication

7 min read

Most modern APIs use bearer tokens, but Basic auth is far from dead. Internal admin tools, scripted utilities, status endpoints, and a long tail of legacy systems still wear Authorization: Basic ... like a comfortable old jumper. Digest auth is rarer but you'll still meet it in some banking and telecom corners. The API Testing Masterclass lesson on API keys and basic auth covered the what; this lesson is the how in Rest Assured — including the preemptive variant that fixes the most common surprise, plus the negative tests every secured endpoint deserves.

Basic auth in one line

auth().basic(username, password) is the entire integration:

@Test
public void testValidBasicAuthAccess() {
    given()
        .auth().basic("alice", "password123")
    .when()
        .get("/secure/data")
    .then()
        .statusCode(200)
        .body("user", equalTo("alice"));
}

Rest Assured Base64-encodes the credentials and sends them in the Authorization: Basic header:

Authorization: Basic YWxpY2U6cGFzc3dvcmQxMjM=

The encoding is not encryption. Anyone who captures the header can decode it instantly. Always run Basic auth over HTTPS — and yes, your tests should hit https://..., not http://, even in dev.

Preemptive vs challenged

By default, auth().basic(...) follows the polite RFC dance: send the request without credentials first, wait for a 401 Unauthorized with a WWW-Authenticate: Basic challenge header, then resend with credentials. That's two round trips per call — fine in theory, but most modern APIs don't issue the challenge, they just return 401 and expect you to send credentials up front.

For those, use the preemptive variant:

given()
    .auth().preemptive().basic("alice", "password123")
.when()
    .get("/secure/data");

Preemptive sends the Authorization header on the first request — one round trip. Default to preemptive for any API that doesn't actively challenge. Most do.

Digest auth

Digest is a stronger relative of Basic — the password isn't sent in plaintext; instead, the client hashes a challenge from the server using the password as a key. Rest Assured supports it with the same shape:

given()
    .auth().digest("alice", "password123")
.when()
    .get("/secure/data");

Digest requires the server-side challenge (it's how the client gets the nonce to hash), so there's no preemptive form. You'll meet Digest in the wild on a few telecom and IoT APIs; it's largely been superseded by HTTPS + Bearer tokens.

The auth flow visualised

Step 1 of 5

Encode credentials

Rest Assured Base64-encodes 'username:password' (e.g., alice:pass → YWxpY2U6cGFzcw==). This is encoding, not encryption — HTTPS does the protecting.

The challenge response (step 4 returning 401) is what non-preemptive mode reacts to. Preemptive skips waiting for that and sends credentials immediately, halving the round trips.

The three negative tests every auth endpoint needs

Auth code paths fail in specific, repeatable ways. Cover them as a triplet:

@Test
public void testValidCredentialsReturn200() {
    given().auth().preemptive().basic("alice", "password123")
    .when().get("/secure/data")
    .then().statusCode(200);
}
 
@Test
public void testInvalidPasswordReturns401() {
    given().auth().preemptive().basic("alice", "wrong-password")
    .when().get("/secure/data")
    .then()
        .statusCode(401)
        .body("error", anyOf(equalTo("invalid_credentials"), equalTo("Unauthorized")));
}
 
@Test
public void testMissingCredentialsReturns401() {
    given()
    .when().get("/secure/data")
    .then()
        .statusCode(401)
        .header("WWW-Authenticate", containsString("Basic"));
}

The third test is the easiest one to skip and the most valuable when a server-side regression accidentally drops auth altogether — turning the secured endpoint into an open one. One line of test catches a CVE.

Auth for an entire test class

Hardcoding auth().basic(...) in every given() is repetitive. Lift it onto the shared RequestSpecification:

import io.restassured.builder.RequestSpecBuilder;
import io.restassured.specification.RequestSpecification;
 
public class SecureApiTest {
 
    private static RequestSpecification authSpec;
 
    @BeforeClass
    public void setup() {
        RestAssured.baseURI = "https://api.mycompany.com";
        authSpec = new RequestSpecBuilder()
            .setAuth(preemptive().basic(
                System.getenv("API_USER"),
                System.getenv("API_PASSWORD")
            ))
            .build();
    }
 
    @Test
    public void getSecureData() {
        given().spec(authSpec)
        .when().get("/secure/data")
        .then().statusCode(200);
    }
}

Two upgrades in one move: every test gets auth without the line; credentials come from environment variables instead of literal strings (so they don't end up in git). Chapter 6 builds on this with a full base test class.

Reading credentials safely

Hardcoded credentials in test code are how secrets leak into git history. The minimum bar:

String username = System.getenv("API_USER");
String password = System.getenv("API_PASSWORD");
 
if (username == null || password == null) {
    throw new IllegalStateException(
        "Set API_USER and API_PASSWORD env vars before running auth tests");
}

For local development, put the values in a .env file that's .gitignored, and use a library (like the java-dotenv package) or your IDE's run configuration to load them. CI environments inject the same env vars from secret stores. Never commit a password = "real-password" line, even temporarily.

Custom Authorization headers

Some APIs want their auth header in a non-standard form (Authorization: ApiKey abc123, X-Auth-Token: ...). Skip Rest Assured's auth() family and just set the header:

given()
    .header("Authorization", "ApiKey " + System.getenv("API_KEY"))
.when()
    .get("/data");
 
// or
given()
    .header("X-Auth-Token", System.getenv("API_TOKEN"))
.when()
    .get("/data");

The auth() methods are syntactic sugar over the same header-setting machinery. Anything they can do, header("Authorization", ...) can do — pick whichever reads better at the call site.

⚠️ Common mistakes

  • Using non-preemptive Basic against an API that doesn't challenge. Without the WWW-Authenticate response, Rest Assured's default mode never sends the credentials. The test fails with 401 and you assume the password is wrong. Reach for .preemptive() first; only drop it when you've confirmed the server actually issues challenges.
  • Hardcoding credentials in test source. A search of GitHub for auth().basic("admin" finds thousands of leaked production passwords. Always read from environment variables or a secret manager — and add a CI check that fails the build if a literal password sneaks in.
  • Not testing the missing-credentials case. It's the one negative test that catches "the auth filter accidentally got removed." If your suite doesn't have at least one test that asserts 401 without sending credentials, your secured endpoints might silently be open.

🎯 Practice task

httpbin.org has a Basic auth endpoint perfect for practice: /basic-auth/{user}/{passwd}. 20 minutes.

  1. Set RestAssured.baseURI = "https://httpbin.org".
  2. Write validCredentialsReturn200() against /basic-auth/alice/secret using .auth().preemptive().basic("alice", "secret"). Assert 200 and that the response body's authenticated field is true.
  3. Write wrongPasswordReturns401() — same endpoint, but pass "wrong" as the password. Assert 401.
  4. Write missingCredentialsReturns401() — no .auth() call at all. Assert 401 and that the WWW-Authenticate header contains "Basic".
  5. Compare preemptive and challenged. Repeat test #2 without .preemptive(). Note that it still works (httpbin does send a challenge), but the wire trace would show two round trips. In a real test, log .log().all() to see them.
  6. Read from env vars. Move the credentials into System.getenv("HTTPBIN_USER") and System.getenv("HTTPBIN_PASS"). Set them in your shell before running. Confirm the suite still passes.
  7. Lift to RequestSpec. Build a RequestSpecBuilder with the preemptive auth and use it via given().spec(...) in every test. Note how short each test becomes.
  8. Stretch: find a real, internal API of yours that uses Basic auth (an internal admin tool, a CI status page, a Docker registry). Write a smoke test against it — hit one endpoint, assert 200, assert one body field. This is now part of your regression suite.

Next lesson: OAuth 2.0 — how to get a bearer token, attach it to every request, and structure tests around the most common auth flow on the modern internet.

// tip to track lessons you complete and pick up where you left off across devices.