Authentication in REST Assured
Basic, Digest, API keys, bearer tokens, a login that feeds the next request, and OAuth 2.0.
Most real endpoints need credentials. REST Assured has built-in support for the common schemes through auth(), and for anything else you set the header yourself. This lesson goes through each, using the published demo credentials of the authentication test API, and ends with the pattern most real suites need: log in once, then use the token. The schemes themselves are explained in depth in the authentication tutorial; here the focus is how REST Assured sends them.
Basic and Digest
@Test
void basicAuth() {
given()
.spec(api())
.auth().preemptive().basic("apibee", "password123")
.when()
.get("/api/auth/basic")
.then()
.statusCode(200)
.body("user", equalTo("apibee"));
}The word that matters is preemptive(). Without it, REST Assured follows the HTTP rules strictly: it sends the request with no credentials, waits for a 401 with a WWW-Authenticatechallenge, and only then sends them. That costs a second request on every call, and on servers that don't send a challenge it never sends the credentials at all:
auth().basic(...) on /api/auth/basic 200
auth().basic(...) on /api/auth/basic-hidden/foo/bar 404
auth().preemptive().basic(...) on /api/auth/basic-hidden/foo/bar 200The hidden endpoint answers bad credentials with 404 and no challenge, as some servers do to hide that a resource exists. The non-preemptive request got 404 because REST Assured never sent the password. With preemptive() it sent it on the first request and got in. Use preemptive()for Basic auth unless you're specifically testing the challenge.
Digest is different by design: it needs the server's challenge to compute its answer, so it can't be preemptive.
@Test
void digestAuth() {
given()
.spec(api())
.auth().digest("apibee", "password123")
.when()
.get("/api/auth/digest")
.then()
.statusCode(200)
.body("scheme", equalTo("digest"));
}REST Assured handles the round trip, the nonce and the hashing; the test only gives the username and password.
API keys
An API key is usually just a header, set with header():
@Test
void apiKeyInAHeader() {
given()
.spec(api())
.header("X-API-Key", "apibee-key-123")
.when()
.get("/api/auth/api-key/header")
.then()
.statusCode(200);
}@Test
void aWrongApiKeyIsRefused() {
given()
.spec(api())
.header("X-API-Key", "wrong")
.when()
.get("/api/auth/api-key/header")
.then()
.statusCode(401);
}A wrong key gets 401, the same as a missing one, which is what the HTTP specification intends for credentials that don't check out. Some APIs answer 403 instead; whichever yours does, write it down in a test like this one so a change is noticed. Keys sent as a query parameter use queryParam("api_key", ...) instead.
Log in, then use the token
The pattern behind most real suites: one request gets a token, and the requests after it send that token. extract().path() carries it from one to the next:
@Test
void loginThenUseTheToken() {
String token = given()
.spec(api())
.contentType(ContentType.JSON)
.body(Map.of("username", "user", "password", "user123"))
.when()
.post("/api/auth/jwt/login")
.then()
.statusCode(200)
.extract().path("accessToken");
given()
.spec(api())
.auth().oauth2(token) // sends Authorization: Bearer <token>
.when()
.get("/api/auth/jwt/me")
.then()
.statusCode(200)
.body("username", equalTo("user"));
given()
.spec(api())
.auth().oauth2(token)
.when()
.get("/api/auth/jwt/admin")
.then()
.statusCode(403); // a genuine token without the admin role
}auth().oauth2(token) sends Authorization: Bearer <token>. The name is a little misleading: it works for any bearer token, JWTs included, not only those from an OAuth server. Writing header("Authorization", "Bearer " + token) does the same. The third request matters as much as the second. A genuine user token on the admin endpoint must get 403: authenticated, but not allowed. Testing only that the right token works misses a missing role check.
OAuth 2.0 client credentials
For service-to-service calls, an OAuth token is a form-encoded POST to the token endpoint, with the client's ID and secret as Basic auth:
@Test
void oauthClientCredentials() {
String accessToken = given()
.spec(api())
.auth().preemptive().basic("apibee-client", "apibee-secret")
.formParam("grant_type", "client_credentials")
.formParam("scope", "read")
.when()
.post("/api/auth/oauth/token")
.then()
.statusCode(200)
.header("Cache-Control", "no-store")
.body("scope", equalTo("read"))
.extract().path("access_token");
given()
.spec(api())
.auth().oauth2(accessToken)
.when()
.get("/api/auth/oauth/protected")
.then()
.statusCode(200)
.body("client_id", equalTo("apibee-client"));
}formParam builds an application/x-www-form-urlencoded body and sets the content type to match. The test also checks Cache-Control: no-store, which the OAuth specification requires on token responses, so no cache keeps a copy. For the browser-based flows, authorization code with PKCE, a test usually gets its token another way, as the PKCE lesson shows request by request.
Which method for which scheme
| Scheme | In REST Assured |
|---|---|
| Basic | auth().preemptive().basic(user, password) |
| Digest | auth().digest(user, password) |
| API key in a header | header("X-API-Key", key) |
| API key in the query | queryParam("api_key", key) |
| Bearer token, including JWTs | auth().oauth2(token) |
| OAuth 2.0 client credentials | formParams to the token endpoint, then auth().oauth2(...) |
- Keep real credentials out of the code; read them from the environment, as lesson 6 does with the API key.
- For every scheme, test missing and wrong credentials as well as right ones.
Exercise
Add a test that logs in as admin / admin123 and checks /api/auth/jwt/adminsign in to see returns 200 for that token. Then fetch the deliberately expired token from GET /api/auth/jwt/expiredsign in to see, send it to /api/auth/jwt/mesign in to see, and check for a 401 with a WWW-Authenticate header containing invalid_token.