How do you test a secured endpoint with TestRestTemplate — for example sending HTTP Basic credentials?
answer
- withBasicAuth returns a NEW instance — use it
- real filter chain runs: 200 / 401 / 403
- 401 = auth fails, 403 = authorized fails
- bearer/custom → set HttpHeaders + exchange
- ENABLE_COOKIES for session flows
basics
~20 sCall rest.withBasicAuth("user", "password") — it returns a new TestRestTemplate that adds an HTTP Basic Authorization header to every request. Use the returned instance. Because it makes a real request, the whole Spring Security filter chain runs and you assert 200 vs 401/403.
solid answer
~40 sTestRestTemplate has withBasicAuth(username, password), which returns a NEW TestRestTemplate that attaches a Basic Authorization header to each call — you must use the returned instance, since the original is unchanged. Because the request is real HTTP through the running server, the full Spring Security filter chain executes: authentication, authorization, method security. So you can assert 200 for valid credentials, 401 for missing/bad credentials, and 403 for authenticated-but-unauthorized. For token/cookie schemes instead of Basic, set the Authorization or Cookie header yourself via HttpHeaders and exchange with an HttpEntity. For session-based flows you may also need HttpClientOption.ENABLE_COOKIES so the session cookie is carried across calls. This end-to-end execution of security is a major reason to use TestRestTemplate over slice tests.
code
java · 26 lines@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class SecuredEndpointTest {
@Autowired TestRestTemplate rest;
@Test
void validCredentials_200() {
// MUST use the returned instance; withBasicAuth does not mutate 'rest'.
ResponseEntity<String> r = rest.withBasicAuth("admin", "secret")
.getForEntity("/api/admin/stats", String.class);
assertThat(r.getStatusCode()).isEqualTo(HttpStatus.OK);
}
@Test
void noCredentials_401() {
assertThat(rest.getForEntity("/api/admin/stats", String.class)
.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED);
}
@Test
void wrongRole_403() {
assertThat(rest.withBasicAuth("viewer", "pw")
.getForEntity("/api/admin/stats", String.class)
.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN);
}
}go deeper
Know withBasicAuth adds Basic credentials and you assert 200 vs 401.
Explain the new-instance semantics, 401 vs 403, and bearer-token via HttpHeaders+exchange.
Cover ENABLE_COOKIES for session flows and why real-filter execution beats @WithMockUser slice tests.
Weigh full end-to-end security tests vs faster slice tests in the overall test pyramid and CI cost.
### The problem A secured endpoint requires credentials. With a real HTTP call, you must present them exactly as a client would — and the running server's **entire Spring Security filter chain** will evaluate them. ### withBasicAuth — the idiomatic helper `TestRestTemplate.withBasicAuth(String username, String password)` returns a **new** `TestRestTemplate` configured to add an `Authorization: Basic <base64(user:pass)>` header to every request. Two important properties: 1. It is **non-mutating** — it returns a new instance and leaves the original untouched. A classic bug is calling `rest.withBasicAuth(...)` and then using `rest` (unauthenticated). Always use the returned value. 2. It works with the auto-configured, port-aware instance, so you keep using relative URLs. ```java ResponseEntity<String> ok = rest .withBasicAuth("admin", "secret") .getForEntity("/api/admin/stats", String.class); assertThat(ok.getStatusCode()).isEqualTo(HttpStatus.OK); ``` ### The three status codes you typically assert - **200/2xx** — valid credentials, authorized. - **401 Unauthorized** — no credentials or bad credentials (authentication failed). Test by calling with no auth or wrong password. - **403 Forbidden** — authenticated but lacking the required role/authority (authorization failed). Test with a valid but under-privileged user. Because the real filter chain runs (including `@PreAuthorize` method security if enabled), these outcomes reflect production behavior. ### Non-Basic schemes (bearer tokens, custom headers) `withBasicAuth` only covers Basic. For a JWT/bearer token or custom header, build the header yourself and use `exchange`: ```java HttpHeaders headers = new HttpHeaders(); headers.setBearerAuth(jwt); // Authorization: Bearer <jwt> HttpEntity<Void> req = new HttpEntity<>(headers); ResponseEntity<String> r = rest.exchange( "/api/me", HttpMethod.GET, req, String.class); ``` ### Cookie/session flows By default TestRestTemplate does **not** persist cookies between calls. For a login-then-call session flow, construct it with `TestRestTemplate.HttpClientOption.ENABLE_COOKIES` so the `JSESSIONID`/session cookie set by the first response is sent on subsequent calls. Alternatively capture `Set-Cookie` from the first response and replay it manually via headers. ### Constructor-based Basic auth You can also bake credentials in at construction: `new TestRestTemplate("user", "pass")`. `withBasicAuth` is usually cleaner because it reuses the auto-configured, port-aware bean. ### Why this beats slice tests for security `@WebMvcTest` with `@WithMockUser` short-circuits real authentication (it injects a fake principal). A `TestRestTemplate` call exercises the **actual** authentication mechanism — real password/token verification, real filters — so it catches misconfigured security that a mocked principal would hide.
- What's the difference between a 401 and a 403 in these tests?401 Unauthorized means authentication failed — no or invalid credentials. 403 Forbidden means the caller authenticated successfully but lacks the required role/authority for that resource.
- How would you send a JWT bearer token instead of Basic auth?withBasicAuth only does Basic. Build HttpHeaders with headers.setBearerAuth(token), wrap in an HttpEntity, and call rest.exchange(url, method, entity, Type.class).
saying these in an interview costs you the question
- Calling rest.withBasicAuth(...) but then using the original rest (it returns a new instance).
- Confusing 401 (authentication) with 403 (authorization).
- Assuming cookies/session persist across calls without ENABLE_COOKIES.