In REST Assured, which objects do auth().basic(...) and auth().preemptive().basic(...) install on a request?
answer
- one installs a scheme, one a header
- credentials wallet versus credentials on the wire
- AuthScope keyed by host and port
- PreemptiveBasicAuthScheme.generateAuthToken()
- no AuthCache anywhere in the library
basics
~20 sauth().basic(user, pass) installs a BasicAuthScheme that hands the credentials to the underlying Apache HttpClient, so the Authorization header goes out only after a 401. auth().preemptive().basic(...) installs no scheme at all and writes the header directly.
solid answer
~40 s`given().auth().basic(u, p)` sets the request's `authenticationScheme` to a `BasicAuthScheme`. At send time that scheme registers a `UsernamePasswordCredentials` with the underlying Apache HttpClient under an `AuthScope` built from the target host and port — it does not write a header. So the first call to `/v1/sailings` goes out bare, the ferry API answers `401`, and the client replays the request with `Authorization`: two round trips per chain. `given().auth().preemptive()` returns a `PreemptiveAuthSpec`, whose `basic(...)` installs **no scheme**. It calls `auth().none()` to clear anything already set, then adds a literal `Authorization: Basic <base64>` header via `PreemptiveBasicAuthScheme.generateAuthToken()`. One round trip, and the credential is just a header from then on.
code
java · 28 linesimport io.restassured.RestAssured;
import static io.restassured.RestAssured.given;
public class FerryAuthExample {
public static void main(String[] args) {
RestAssured.baseURI = "https://ferry-api.internal";
// Challenged: first request carries no Authorization, server answers 401, client replays.
given().auth().basic("timetable-bot", "s3cret")
.queryParam("routeCode", "DOV-CAL")
.when()
.get("/v1/sailings")
.then()
.statusCode(200);
// Preemptive: Authorization rides on the very first request.
given().auth().preemptive().basic("timetable-bot", "s3cret")
.queryParam("routeCode", "DOV-CAL")
.when()
.get("/v1/sailings")
.then()
.statusCode(200);
RestAssured.reset();
}
}go deeper
Be ready to state that one of the two spellings sends credentials immediately and the other waits to be challenged, and to write both chains correctly with a static import of given().
Explain the mechanics: which scheme object each call assigns, that BasicAuthScheme registers credentials with the HTTP client rather than writing a header, and that preemptive basic adds a plain Authorization header instead.
Show the operational consequences — doubled request counts against a rate limiter, credentials scoped to a host and port that a redirect can invalidate, and when the challenge itself is the behaviour under test.
Own the convention: decide whether the suite standardises on preemptive credentials for speed and determinism, and where challenge-driven authentication stays as deliberate coverage of the security boundary.
## The scheme object, not the header `given().auth()` hands you an `io.restassured.specification.AuthenticationSpecification`. Every method on it — `basic`, `ntlm`, `digest`, `form`, `certificate`, `oauth`, `oauth2`, `none` — does the same kind of work: it assigns a strategy object to the request specification's `authenticationScheme` field and returns the `RequestSpecification` so the chain continues. **Nothing is encoded and nothing is sent at that moment.** `auth().basic("timetable-bot", "s3cret")` assigns a `BasicAuthScheme` holding the user name and password. That class is tiny — two fields and one method, `authenticate(HTTPBuilder)`. REST Assured invokes that method late, while it assembles the outgoing call to your ferry timetable service, and `BasicAuthScheme` responds by registering a `UsernamePasswordCredentials` in the underlying Apache HttpClient's credentials provider, keyed by an `AuthScope` derived from the target URI's host and port. Registering a credential is not sending one. The client keeps it in a wallet and produces it only when a server asks for it. ## What challenged basic costs on the wire Against a protected `GET /v1/sailings?routeCode=DOV-CAL`, one DSL chain produces this exchange: 1. REST Assured sends the request with **no** `Authorization` header. 2. The service answers `401` and names a scheme in its challenge. 3. The HttpClient finds the credential registered for that host and port and replays the identical request, this time carrying `Authorization`. 4. Your `then()` block only ever sees the final `200`; the 401 is invisible to your assertions. One chain, two round trips. Nothing in the DSL shortens it: REST Assured's main sources contain no `AuthCache` and no request interceptor that primes the header, so `basic(...)` structurally cannot speak first. `auth().ntlm(userName, password, workstation, domain)` is the same shape one class over — it installs an `NTLMAuthScheme` that registers `NTCredentials` in the same provider and waits for the same challenge. ## What `preemptive().basic(...)` installs instead `given().auth().preemptive()` returns an `io.restassured.specification.PreemptiveAuthSpec`, and its `basic(String, String)` installs **no scheme object at all**. It does three things in order: - It calls `auth().none()` on the request, which replaces the scheme with `ExplicitNoAuthScheme`, drops any `AuthFilter` from the filter list and removes an existing `Authorization` header. - It builds a throwaway `PreemptiveBasicAuthScheme` purely to call `generateAuthToken()`, which returns the literal string `Basic ` followed by the base64 of `user:password`. - It sets that string as an ordinary request header. From that point the credential is a header like `Accept` or `Content-Type`. The first request to `/v1/sailings` carries it, the server never has to challenge, and one chain is one round trip. Two consequences follow from it being a plain header: - Anything that inspects or rewrites headers sees it, including `LogConfig.blacklistDefaultSensitiveHeaders()`, which redacts `Authorization` in the request log. - `PreemptiveBasicAuthScheme.generateAuthToken()` hardcodes ISO-8859-1 when it turns `user:password` into bytes, so a password outside that character set is encoded by that charset whatever the server would prefer. ## The two `preemptive()`s are different types This is the mis-attribution that catches people, because both spellings are real: | call | returns | declares | | --- | --- | --- | | `given().auth().preemptive()` | `PreemptiveAuthSpec` | `basic(String, String)` and `oauth2(String)` | | `RestAssured.preemptive()` (static) | `PreemptiveAuthProvider` | `basic(String, String)` only | The static `RestAssured.preemptive().basic(u, p)` returns a `PreemptiveBasicAuthScheme` object, which you assign to `RestAssured.authentication` as a library-wide default; its `authenticate(HTTPBuilder)` writes the `Authorization` header at send time. The instance spec never produces a scheme for you to hold. And `preemptive().digest(...)` exists on neither surface — there is no such method anywhere in the library. ## Choosing between them on a real suite - Prefer `preemptive().basic(...)` for ordinary happy-path coverage of the ferry timetable API. It halves the request count and removes a whole class of environment-dependent flakiness. - Reach for `auth().basic(...)` when the challenge itself is the thing under test — you are asserting that `/v1/sailings/{sailingId}/manifest` actually refuses an anonymous caller, or that a gateway forwards the challenge intact. - Watch the request count when a rate limiter or an access-log assertion is involved: challenged basic doubles the traffic your test generates, and a per-minute quota will notice. - Remember that a redirect or a proxy hop can move the request off the host and port the `AuthScope` was built from, at which point the registered credential no longer matches and the challenge goes unanswered. A preemptive header does not depend on that matching at all. - Do not describe `digest(...)` as the preemptive family's third member. It is on `AuthenticationSpecification`, not on `PreemptiveAuthSpec`, and it installs a `BasicAuthScheme`. The short version to say out loud: `basic(...)` installs a scheme that hands credentials to the HTTP client and waits to be asked; `preemptive().basic(...)` skips the scheme layer entirely and writes the header itself.
- What breaks if the ferry API redirects an authenticated call to a different host?The `BasicAuthScheme` path registers credentials under an `AuthScope` built from the original host and port, so on a cross-host hop the client has no matching credential and leaves the new challenge unanswered. The preemptive path does not depend on scope matching, because the credential is already a header on the request.
- Does given().auth().preemptive().basic(...) leave anything in the request's authenticationScheme field?Yes, but not a basic scheme. It first calls `auth().none()`, which sets `ExplicitNoAuthScheme`, strips `AuthFilter` instances and removes any existing `Authorization` header; only then does it add the header it computed. So the field holds an explicit no-auth marker while the credential lives in the headers.
- Which preemptive surface would you use to authenticate every request in the suite?The static one: `RestAssured.authentication = RestAssured.preemptive().basic(user, pass)`. `RestAssured.preemptive()` returns a `PreemptiveAuthProvider` whose `basic(...)` hands back a `PreemptiveBasicAuthScheme` object you can assign. The instance `given().auth().preemptive()` returns a `PreemptiveAuthSpec` and gives you no scheme object to store.
Challenged basic is handing your pass to the doorman only after he stops you; preemptive basic is holding it up as you walk in. Same pass, one fewer conversation.
saying these in an interview costs you the question
- Claiming basic() and preemptive().basic() differ only in speed
- Saying preemptive().basic() installs a PreemptiveBasicAuthScheme on the request
- Believing basic() can be made preemptive by a config flag
- Thinking the 401 from the challenge is visible to then() assertions
- Assuming both preemptive() surfaces expose the same methods
- Listing digest as a member of the preemptive family