How does REST Assured let you authenticate requests, and what is the difference between its basic() and preemptive().basic() authentication options?
answer
- preemptive().basic = header on first request
- basic() = wait for 401 + WWW-Authenticate, then retry
- oauth2(token) == Authorization: Bearer
- auth().none() to test the unauthenticated path
- blacklistHeader("Authorization") before log().all()
basics
~20 sgiven().auth() offers basic, preemptive basic, digest, form, OAuth 1/2 and certificate auth. preemptive().basic() sends the Authorization header on the first request; plain basic() waits for a 401 challenge before resending with credentials — so it costs an extra round trip and fails against servers that never challenge.
solid answer
~50 sAuthentication lives on the request spec: `given().auth()...`. - `auth().preemptive().basic(user, pass)` — attaches the `Authorization: Basic ...` header to the very first request. This is what you almost always want. - `auth().basic(user, pass)` — *challenged* basic: the first request goes out without credentials; only if the server answers HTTP 401 with a `WWW-Authenticate` header does REST Assured resend with credentials. Costs a round trip, and silently fails against servers that return 401/403 without a challenge header, or 302-redirect to a login page. - `auth().oauth2(token)` — sets `Authorization: Bearer <token>`; `auth().oauth1(...)` covers OAuth 1. - `auth().form(user, pass, config)` and `auth().digest(...)`, `auth().certificate(keystore, password)` for mTLS. - For anything custom (a JWT you fetched, an API key), just set the header: `header("Authorization", "Bearer " + token)`. Global defaults go in `RestAssured.authentication = preemptive().basic(...)` or a shared `RequestSpecification`; `auth().none()` opts a single test out to test the unauthenticated path.
code
java · 19 linesimport static io.restassured.RestAssured.given;
import static io.restassured.RestAssured.preemptive;
// sends Authorization on the FIRST request
given().auth().preemptive().basic("user", "pass")
.when().get("/admin/reports")
.then().statusCode(200);
// bearer token
given().auth().oauth2(accessToken)
.when().get("/me")
.then().statusCode(200);
// suite-wide default, then a test that checks the anonymous path
io.restassured.RestAssured.authentication = preemptive().basic("user", "pass");
given().auth().none()
.when().get("/admin/reports")
.then().statusCode(401);go deeper
Know that auth lives in given().auth(), and that preemptive basic sends the header immediately while plain basic waits for a 401 challenge.
Explain the extra round trip, the servers that never challenge, and how bearer tokens are just a header set via oauth2(token).
Cover suite-level design: token fetched once, injected via a filter or shared spec, refreshed on 401, secrets from the environment, Authorization blacklisted from logs.
Weigh how much auth machinery an API suite should own versus a dedicated auth harness, and how negative authorization cases are covered systematically rather than ad hoc.
## Where auth lives in the DSL Authentication is part of the **request specification**, so it is configured in the `given()` stage via `auth()`. Everything it does ultimately amounts to adding headers (or configuring TLS material) — REST Assured has no privileged access to your server; it is an HTTP client. ## Challenged vs preemptive Basic authentication HTTP Basic authentication, as specified for HTTP, is a **challenge-response** scheme: the client requests a protected resource without credentials, the server replies `401 Unauthorized` with a `WWW-Authenticate: Basic realm="..."` header, and the client retries with `Authorization: Basic base64(user:password)`. `given().auth().basic(user, pass)` implements exactly that: the first request carries **no** credentials. REST Assured only attaches them after seeing a proper 401 challenge. `given().auth().preemptive().basic(user, pass)` skips the negotiation and sends `Authorization: Basic ...` on the first request. Why the distinction matters in practice: 1. **Round trips.** Challenged basic doubles the request count on every protected call — noticeable in a suite of hundreds of tests, and it pollutes server logs and metrics with 401s. 2. **Servers that do not challenge.** Many APIs return `401` or `403` with no `WWW-Authenticate` header, or redirect to an HTML login page, because they are designed for token auth or for clients that always send credentials. Against those, challenged basic never sends the credentials at all, and the test fails with a 401/403 that looks like a credential problem when it is really a negotiation problem. "My username and password are definitely right but I get 401" is the classic symptom, and switching to `preemptive()` is the classic fix. 3. **Non-idempotent requests.** With a POST, the challenged flow may send the body twice (once rejected, once accepted), which is wasteful and occasionally surprising if a proxy or filter counts requests. The rule of thumb: **use `preemptive().basic(...)` unless you are specifically testing the challenge behaviour itself.** ## Token-based auth Most modern APIs use bearer tokens. Two equivalent routes: ```java given().auth().oauth2(accessToken) // sets Authorization: Bearer <token> given().header("Authorization", "Bearer " + accessToken) ``` `auth().oauth2(...)` is a convenience over the header; there is no OAuth flow being executed. Acquiring the token is your job — typically one REST Assured call to the token endpoint whose result you extract and cache: ```java String token = given() .contentType(ContentType.URLENC) .formParam("grant_type", "client_credentials") .formParam("client_id", id).formParam("client_secret", secret) .when().post("/oauth/token") .then().statusCode(200) .extract().path("access_token"); ``` Cache it for the suite rather than re-fetching per test, but respect expiry — a suite that runs longer than the token lifetime will start failing halfway through with 401s, which looks like flakiness. A `Filter` that refreshes the token when a response is 401 is a clean way to handle it. ## Other supported schemes - **Digest**: `auth().digest(user, pass)` — challenge-based by design. - **Form**: `auth().form(user, pass, new FormAuthConfig("/login", "username", "password"))`, which posts the login form, keeps the session cookie and reuses it. `FormAuthConfig.springSecurity()` is a preset for the conventional Spring Security login endpoint. `auth().form(...)` without a config attempts to autodetect the form fields by parsing the login page, which is brittle. - **Cookie/session**: after any login call, `extract().sessionId()` or `extract().cookies()` and then `given().sessionId(id)` / `given().cookies(map)`. - **Certificate / mTLS**: `auth().certificate(keystorePath, password, certAuthSettings)`, plus `RestAssured.config().sslConfig(...)` for trust stores. `relaxedHTTPSValidation()` disables certificate checks — acceptable against a throwaway test environment with self-signed certs, never something to leave in a suite that points at a real one. ## Applying it across a suite Repeating credentials in every test is noise. Options, in increasing order of discipline: - `RestAssured.authentication = preemptive().basic(user, pass);` in suite setup — a static global, so reset it afterwards. - A shared `RequestSpecification` built once with `RequestSpecBuilder().setAuth(...)` and passed to `given().spec(authedSpec)`. - A `Filter` that injects the current token, so refresh logic lives in one place. And a single test that needs to check the unauthenticated path uses `given().auth().none()` to opt out of whatever global default is in force. Testing the negative cases — no credentials, wrong credentials, valid credentials but insufficient role — is exactly the kind of coverage an API-level suite is good at, and it depends on being able to turn the default off. ## Secrets hygiene Credentials belong in environment variables or a secret store read at runtime, not hardcoded in test sources that live in version control. And remember that `log().all()` prints the `Authorization` header: use `log().ifValidationFails()` plus `config().logConfig(logConfig().blacklistHeader("Authorization"))` so a CI log does not leak a token.
- A test using given().auth().basic(user, pass) gets 401 even though the credentials are correct. What is your first hypothesis?That the server never issues a proper Basic challenge. Plain `basic()` sends the first request without credentials and only retries after a `401` carrying a `WWW-Authenticate: Basic` header; APIs that return a bare 401/403, or redirect to a login page, never trigger that retry, so the credentials are never sent. Switching to `auth().preemptive().basic(user, pass)` puts the Authorization header on the first request and usually fixes it immediately.
- How would you handle a bearer token across a suite that runs longer than the token's lifetime?Fetch the token once in shared setup rather than per test, but attach it through a REST Assured `Filter` or a shared `RequestSpecification` so there is a single place that owns it. The filter can inspect the response and, on a 401, refresh the token and replay the request. That keeps expiry handling out of individual tests, which otherwise start failing partway through a long run and look like random flakiness.
saying these in an interview costs you the question
- Believing basic() and preemptive().basic() are interchangeable, then treating the resulting 401 as a credentials problem.
- Thinking auth().oauth2(token) performs an OAuth flow rather than just setting the Bearer header.
- Fetching a fresh token in every test method, tripling the suite's runtime and hammering the auth server.
- Hardcoding credentials in test sources committed to the repository.
- Leaving log().all() on so the Authorization header lands in CI logs, or using relaxedHTTPSValidation() against a real environment.