skip to content

Your REST Assured suite sets RestAssured.authentication globally and a test expecting 401 gets 200. How do you fix it?

level: seniorimportance: should knowfreq 41%

answer

  1. the test never said anything about auth
  2. implicit versus explicit no-auth
  3. substitution only when nothing is set
  4. ExplicitNoAuthScheme is a different class
  5. check the spec, the builder and the static

basics

~10 s

The test never called auth(), so its scheme was still the implicit NoAuthScheme and REST Assured substituted the global default in. Add given().auth().none(), which installs ExplicitNoAuthScheme and blocks that substitution.

solid answer

~40 s

The call inherited the global credential. REST Assured copies `RestAssured.authentication` onto a request only while that request's own `authenticationScheme` is still the implicit `NoAuthScheme` — and a chain that never calls `auth()` is exactly that. Not asking for authentication is not the same as asking for none. The fix is `given().auth().none()`, which sets `ExplicitNoAuthScheme`, drops `AuthFilter` instances and removes any `Authorization` header. Because `ExplicitNoAuthScheme` is a different class from `NoAuthScheme`, the substitution is skipped. To confirm before fixing, log the request headers or query the built spec with `SpecificationQuerier.query(spec).getAuthenticationScheme()`. Then harden it: keep credentials in a `RequestSpecification` rather than a static, and `RestAssured.reset()` in teardown.

code

java · 22 lines
java
import io.restassured.RestAssured;

import static io.restassured.RestAssured.basic;
import static io.restassured.RestAssured.given;

public class FerryAnonymousCheck {

    public static void main(String[] args) {
        RestAssured.baseURI = "https://ferry-api.internal";
        RestAssured.authentication = basic("timetable-bot", "s3cret");

        // Without auth().none() the static default is substituted in and this returns 200.
        given().auth().none()
                .log().headers()
        .when()
                .get("/v1/sailings/9f31/manifest")
        .then()
                .statusCode(401);

        RestAssured.reset();
    }
}

go deeper

for a junior

Recall that a global default scheme exists and that a request without an explicit auth() call inherits it, so an anonymous test must say given().auth().none().

for a middle

Explain the substitution rule and why ExplicitNoAuthScheme defeats it, and show how to confirm the diagnosis by logging request headers rather than guessing.

for a senior

Demonstrate the full diagnosis: check the static, the shared specification, the builder's construction order and spec() overwrite semantics, then harden the suite so a security assertion cannot inherit a credential.

for a principal

Own the policy that credentials are attached explicitly rather than inherited from process-wide state, so tests asserting a security boundary state their posture and cannot silently pass for the wrong reason.

## What you are looking at Your ferry timetable suite has a base class that does this once: ```java RestAssured.baseURI = "https://ferry-api.internal"; RestAssured.authentication = RestAssured.basic("timetable-bot", "s3cret"); ``` and a negative test that reads, reasonably enough: ```java given() .when() .get("/v1/sailings/9f31/manifest") .then() .statusCode(401); ``` It returns `200`. The service is not broken and the assertion is not wrong — the request was authenticated on its way out, by the harness, without anything in the test saying so. ## The mechanism, precisely While REST Assured builds a request it performs exactly one substitution: **if the request's own `authenticationScheme` is still the implicit `NoAuthScheme`, and the static default is something other than a `NoAuthScheme`, the default is copied onto the request.** A chain that never calls `auth()` is precisely a request whose scheme is still the implicit `NoAuthScheme`, so it inherits the credential. This is the trap in one sentence: **not asking for authentication is not the same as asking for no authentication.** ## The fix Say it explicitly: ```java given().auth().none() .when() .get("/v1/sailings/9f31/manifest") .then() .statusCode(401); ``` `none()` does three things — sets the scheme to `ExplicitNoAuthScheme`, removes every `AuthFilter` from the request's filter list, and removes any `Authorization` header already present. The first is what defeats the substitution: `ExplicitNoAuthScheme` implements `AuthenticationScheme` directly and is **not** a `NoAuthScheme`, so REST Assured sees a request that has already chosen a scheme and leaves it alone. That class distinction is the whole mechanism. ## How to diagnose it rather than guess Work down the layers, cheapest first: 1. **Log the request.** `given().log().headers()` on the failing call, or `RestAssured.enableLoggingOfRequestAndResponseIfValidationFails()` at suite level. If an `Authorization` header is present on a call that never set one, you have your answer. Note that `LogConfig.blacklistDefaultSensitiveHeaders()` masks `Authorization` as `[ BLACKLISTED ]` in the request log — the header still shows as present, which is all you need here. 2. **Inspect the built specification.** `SpecificationQuerier.query(spec)` exposes `getAuthenticationScheme()` on a `QueryableRequestSpecification`, so you can assert in a unit test which scheme a shared spec actually carries. 3. **Check the other places a credential hides.** The static field is one source; the others are `RestAssured.requestSpecification`, a `RequestSpecBuilder` that was given `setAuth(...)`, and any spec you attach with `given().spec(...)` — which **overwrites** the request's scheme rather than merging it, so call order decides. 4. **Check when the builder was constructed.** `new RequestSpecBuilder()` snapshots the statics — including `authentication` — in its constructor. A builder created before the base class assigns the static will not carry the credential, which produces the mirror-image symptom: an authenticated test getting a surprise `401`. ## Hardening the suite so it cannot recur - Make every negative authentication test say `auth().none()` explicitly, and treat a bare `given()` in such a test as a review finding. - Prefer carrying credentials in a `RequestSpecification` attached with `given().spec(authSpec)` over the process-wide static. Then the anonymous case is the default and the authenticated case is the one you opt into, which is the safer direction for a security assertion. - Call `RestAssured.reset()` in teardown if you do set statics. It restores `authentication` to the default `NoAuthScheme`, along with `baseURI`, `basePath`, `rootPath`, the filter list, `config`, `sessionId`, `proxy` and both static specifications — and it puts `port` back to `UNDEFINED_PORT` (`-1`), not to `DEFAULT_PORT`, which surprises people. - Assert on more than the status code where the endpoint allows it. A manifest response that returns `200` with a real `vesselName` and `crossingId` proves the call was authenticated, so pairing the positive test with a body assertion makes the negative test's meaning unambiguous. - Keep the static assignment in exactly one place. Scattered assignments across test classes make the effective credential depend on execution order, and the symptom then looks like flakiness rather than configuration. - Distinguish the two failure directions when you triage. An unexpected `200` on a negative test means a credential arrived that nobody asked for; an unexpected `401` on a positive test usually means a specification was built before the credential existed, or a later `spec(...)` overwrote it. Both come from the same substitution and overwrite rules, read in opposite directions. ## The generalisation worth carrying Any authentication default that is applied implicitly can make a negative test pass for the wrong reason. In REST Assured the specific shape is: the default is a static field, the substitution rule is "only if nothing else is set", and the explicit opt-out is a distinct scheme class. A test that asserts a security boundary must state its credential posture — including the absence of one — rather than inherit it.

  • How would you prove which scheme a shared request specification is actually carrying?
    Pass it through `SpecificationQuerier.query(spec)` to get a `QueryableRequestSpecification` and read `getAuthenticationScheme()`. That reads the built specification rather than the DSL you think you wrote, so it also catches a credential arriving from `RestAssured.requestSpecification` or from a `RequestSpecBuilder.setAuth(...)` you had forgotten.
  • What is the mirror-image failure of this same substitution rule?
    A `RequestSpecBuilder` snapshots the statics — including `authentication` — in its constructor. A builder constructed before the base class assigns `RestAssured.authentication` carries no credential, so a test that should be authenticated gets a surprise `401`. The symptom flips with construction order, which reads as flakiness.
  • Would given().spec(authSpec) after a per-request auth() call keep the per-request credential?
    No. Specification merging overwrites the `authenticationScheme` outright rather than accumulating it, so the incoming spec wins at the point of the call. Only `given().spec(authSpec).auth().none()` — the opt-out after the attachment — leaves the request anonymous. Call order decides.

saying these in an interview costs you the question

  • Blaming the service instead of checking the outgoing request
  • Treating a bare given() as an anonymous request
  • Setting RestAssured.authentication to null to clear it
  • Relying on test execution order to unset the static
  • Assuming spec() merges the authentication scheme rather than overwriting it
  • Loosening the assertion to anyOf(200, 401) to make it pass