Implementing HTTP Authentication with Authenticators in Java
Part 5 of 15 in Mastering Java Network Programming
When a Java HTTP client hits a protected endpoint that requires authentication — a 401 Unauthorized response with a WWW-Authenticate header — the JDK’s HttpURLConnection automatically delegates credential gathering to a pluggable Authenticator. By registering a subclass via java.net.Authenticator.setDefault(), you control exactly which credentials get sent, whether they come from a file, an environment variable, or a secrets vault.
This post walks through two patterns: a named authenticator class that prints the full request context it receives, and an inline conditional authenticator that picks different credentials based on the authentication realm.
The code
We use the JDK’s built-in com.sun.net.httpserver.HttpServer to spin up a local server that enforces Basic auth, making the demo self-contained without external dependencies.
import com.sun.net.httpserver.*;
import java.io.*;
import java.net.*;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Base64;
import java.util.concurrent.Executors;
public class AuthDemo {
// ── SecureAuthenticator: stores password as char[] ──
static class SecuredAuthenticator extends java.net.Authenticator {
private final String user;
private final char[] password;
public SecuredAuthenticator(String user, char[] password) {
this.user = user;
// Defensive copy — prevents the caller from mutating the
// stored credential after construction.
this.password = Arrays.copyOf(password, password.length);
}
@Override
protected PasswordAuthentication getPasswordAuthentication() {
System.out.println(\"── Authenticator.getPasswordAuthentication() called ──\");
System.out.printf(\" requestingHost : %s%n\", getRequestingHost());
System.out.printf(\" requestingPort : %d%n\", getRequestingPort());
System.out.printf(\" requestingScheme : %s%n\", getRequestingScheme());
System.out.printf(\" requestingPrompt : \\"%s\\"%n\", getRequestingPrompt());
System.out.printf(\" requestingProtocol: %s%n\", getRequestingProtocol());
// PasswordAuthentication holds the username and a char[] password.
// Using char[] (not String) means the password stays in mutable
// memory that can be wiped after use, rather than lingering on
// the String constant pool / heap until GC collects it.
return new PasswordAuthentication(user, password);
}
/** Wipe the stored credential from memory when done. */
public void destroy() {
Arrays.fill(password, '\0');
}
}
static final int SERVER_PORT = 0; // let kernel pick a free port
public static void main(String[] args) throws Exception {
// ════════════════════════════════════════════════════════════
// Example 1: Custom Authenticator with embedded server
// Demonstrates: setDefault(), context access, char[] passwords
// ════════════════════════════════════════════════════════════
System.out.println(\"=== Example 1: Custom Authenticator Pattern ===\n\");
HttpServer server = startServer(\"/auth\", \"app\", \"alice\", \"s3cureP@ss!\",
() -> \"Welcome, alice!\n\");
int port = server.getAddress().getPort();
System.out.printf(\"[Server started on port %d]%n%n\", port);
char[] rawPassword = \"s3cureP@ss!\".toCharArray();
// Register the authenticator globally — it will be used by every
// HttpURLConnection that encounters a 401.
java.net.Authenticator.setDefault(
new SecuredAuthenticator(\"alice\", rawPassword));
// Clear the original array now that SecuredAuthenticator has its own copy.
Arrays.fill(rawPassword, '\0');
System.out.println(\"[Original password char[] cleared]\n\");
URL url = new URL(String.format(\"http://localhost:%d/auth\", port));
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
try {
int code = conn.getResponseCode();
System.out.printf(\"\nHTTP response: %d%n\", code);
if (code == 200) {
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(conn.getInputStream(),
StandardCharsets.UTF_8))) {
System.out.println(\"Body: \" + reader.readLine().trim());
}
} else if (code == 401) {
System.out.println(\"Auth failed — check credentials.\");
}
} finally {
conn.disconnect();
}
// ── Cleanup the default authenticator ──
java.net.Authenticator.setDefault(null);
server.stop(0);
// ════════════════════════════════════════════════════════════
// Example 2: Conditional auth — different credentials by realm
// Demonstrates: using context methods to select credentials,
// returning null to abort requests
// ════════════════════════════════════════════════════════════
System.out.println(\"\n\n=== Example 2: Conditional Authentication ===\n\");
HttpServer apiServer = startServer(\"/api\", \"api\", \"apiuser\", \"apip@ss\",
() -> \"{\\"detail\\": \\"authorized\\"}\n\");
int apiPort = apiServer.getAddress().getPort();
System.out.printf(\"[API server started on port %d]%n%n\", apiPort);
char[] knownPass = \"knownP@ss\".toCharArray();
char[] apiPass = \"apip@ss\".toCharArray();
java.net.Authenticator.setDefault(new java.net.Authenticator() {
@Override
protected PasswordAuthentication getPasswordAuthentication() {
String host = getRequestingHost();
int port = getRequestingPort();
String realm = getRequestingPrompt();
String scheme = getRequestingScheme();
System.out.printf(\"── Called for %s:%d (realm=%s, scheme=%s) ──%n\",
host, port, realm, scheme);
// In real code you'd look up a vault / env var / keystore here.
if (\"known\".equalsIgnoreCase(realm)) {
return new PasswordAuthentication(\"alice\", knownPass);
} else if (\"api\".equalsIgnoreCase(realm)) {
return new PasswordAuthentication(\"apiuser\", apiPass);
}
// Returning null = \"don't retry with auth\" (abort the request).
System.out.println(\" → no credentials for this realm — aborting\");
return null;
}
});
URL apiUrl = new URL(String.format(\"http://localhost:%d/api\", apiPort));
HttpURLConnection apiConn = (HttpURLConnection) apiUrl.openConnection();
try {
int code = apiConn.getResponseCode();
System.out.printf(\"\nHTTP response: %d%n\", code);
if (code == 200) {
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(apiConn.getInputStream(),
StandardCharsets.UTF_8))) {
System.out.println(\"Body: \" + reader.readLine().trim());
}
} else if (code == 401) {
System.out.println(\"Auth failed — credentials not sent.\");
}
} finally {
apiConn.disconnect();
}
// Wipe the char[]s when no longer needed.
Arrays.fill(knownPass, '\0');
Arrays.fill(apiPass, '\0');
apiServer.stop(0);
}
// ── Helpers ────────────────────────────────────────────
/** Start an embedded HTTP server on a random port requiring Basic auth. */
static HttpServer startServer(String path, String realm,
String userOk, String passOk,
java.util.function.Supplier<String> successBody) throws IOException {
HttpServer server = HttpServer.create(
new InetSocketAddress(SERVER_PORT), 0);
server.createContext(path, exchange -> handleAuth(exchange, realm,
userOk, passOk, successBody));
server.setExecutor(Executors.newCachedThreadPool());
server.start();
return server;
}
static void handleAuth(HttpExchange exchange, String realm,
String userOk, String passOk,
java.util.function.Supplier<String> successBody) throws IOException {
String auth = exchange.getRequestHeaders()
.getFirst(\"Authorization\");
if (auth == null || !auth.startsWith(\"Basic \")) {
sendUnauthorized(exchange, realm);
return;
}
// Decode Basic credentials
byte[] decoded = Base64.getDecoder().decode(
auth.substring(6));
String credStr = new String(decoded, StandardCharsets.UTF_8);
String[] parts = credStr.split(\":\", 2);
if (parts.length == 2 && userOk.equals(parts[0]) && passOk.equals(parts[1])) {
byte[] bodyBytes = successBody.get().getBytes(StandardCharsets.UTF_8);
exchange.getResponseHeaders().set(\"Content-Type\", \"text/plain\");
exchange.sendResponseHeaders(200, bodyBytes.length);
try (OutputStream os = exchange.getResponseBody()) {
os.write(bodyBytes);
}
} else {
sendUnauthorized(exchange, realm);
}
}
static void sendUnauthorized(HttpExchange exchange, String realm) throws IOException {
exchange.getResponseHeaders().set(\"WWW-Authenticate\",
\"Basic realm=\\"\" + realm + \"\\"\");
byte[] err = (\"401 Unauthorized\n\").getBytes(StandardCharsets.UTF_8);
exchange.sendResponseHeaders(401, err.length);
try (OutputStream os = exchange.getResponseBody()) {
os.write(err);
}
}
}
Running it
The first thing you’ll notice in the output is that Example 1’s Authenticator.getPasswordAuthentication() gets invoked automatically — you never call it yourself. Here’s what happens behind the scenes:
- The embedded server returns
401 UnauthorizedwithWWW-Authenticate: Basic realm="app". HttpURLConnectionreceives the response and sees that the defaultAuthenticatoris set.- It calls
getPasswordAuthentication()on that authenticator, passing no arguments — all context comes from the five getter methods you see printed (getRequestingHost(),getRequestingPort(), etc.). - The returned
PasswordAuthenticationis encoded as aBasic Authorization: Basic <base64>header and the request retries. - The server decodes the credentials, verifies them, and returns 200 with the protected body.
Notice that getRequestingPrompt() returned app — this is the realm name from the WWW-Authenticate header, not the user’s input field label (Basic auth has no prompt text; only Digest auth supplies one). The scheme correctly shows basic.
In Example 2, the authenticator inspects the realm value to decide which credentials to return. This is how you’d route credentials in a real application: check the realm or host, then look up the matching username/password from a secrets manager, environment variable, or encrypted keystore.
When getPasswordAuthentication() returns null, the client interprets this as “no credentials available” and aborts — the request is retried without an Authorization header, which results in another 401. This is the correct way to signal that you don’t have credentials for a given realm rather than sending wrong ones.
Why char arrays?
PasswordAuthentication’s constructor takes char[], not String. The reason is security: once a password becomes a String, it lives on the heap until the garbage collector collects it — and during that time it’s potentially visible in heap dumps, swap files, or debug output. A char[] can be explicitly zeroed with Arrays.fill(arr, '\0') as soon as you’re done with it, minimizing the window of exposure.
The SecuredAuthenticator class demonstrates both patterns: it makes a defensive copy of the incoming password array in its constructor (Arrays.copyOf(...)), and provides a destroy() method that wipes the stored credentials. The main method also clears the original rawPassword array after passing it to the authenticator.
Takeaway
The JDK’s Authenticator is a single-method interface that hooks into every HttpURLConnection’s authentication flow. By subclassing it and registering with setDefault(), you gain full control over how credentials are provided — including reading them from context (host, port, realm) and storing them securely as zeroable char arrays rather than immutable strings.