From 9f38e433d40b1281e08b9358893e4e9cd26b121c Mon Sep 17 00:00:00 2001 From: Simone Bordet Date: Thu, 2 Apr 2020 12:19:23 +0200 Subject: [PATCH] Improvements to the Jetty client documentation, authentication section. Signed-off-by: Simone Bordet --- .../http/client-http-authentication.adoc | 100 ++++++++++-------- .../embedded/client/http/HTTPClientDocs.java | 56 ++++++++++ 2 files changed, 111 insertions(+), 45 deletions(-) diff --git a/jetty-documentation/src/main/asciidoc/embedded-guide/client/http/client-http-authentication.adoc b/jetty-documentation/src/main/asciidoc/embedded-guide/client/http/client-http-authentication.adoc index 4e3e48704ae..fcdd2fc3b01 100644 --- a/jetty-documentation/src/main/asciidoc/embedded-guide/client/http/client-http-authentication.adoc +++ b/jetty-documentation/src/main/asciidoc/embedded-guide/client/http/client-http-authentication.adoc @@ -17,75 +17,85 @@ // [[client-http-authentication]] -=== Authentication Support +=== HttpClient Authentication Support -Jetty's HTTP client supports the `BASIC` and `DIGEST` authentication mechanisms defined by link:https://tools.ietf.org/html/rfc7235[RFC 7235]. +Jetty's `HttpClient` supports the `BASIC` and `DIGEST` authentication +mechanisms defined by link:https://tools.ietf.org/html/rfc7235[RFC 7235], +as well as the SPNEGO authentication mechanism defined in +link:https://tools.ietf.org/html/rfc4559[RFC 4559]. -You can configure authentication credentials in the HTTP client instance as follows: +The HTTP _conversation_ - the sequence of related HTTP requests - for a +request that needs authentication is the following: -[source, java, subs="{sub-order}"] +[plantuml] ---- -URI uri = new URI("http://domain.com/secure"); -String realm = "MyRealm"; -String user = "username"; -String pass = "password"; +skinparam backgroundColor transparent +skinparam monochrome true +skinparam shadowing false -// Add authentication credentials -AuthenticationStore auth = httpClient.getAuthenticationStore(); -auth.addAuthentication(new BasicAuthentication(uri, realm, user, pass)); +participant Application +participant HttpClient +participant Server -ContentResponse response = httpClient - .newRequest(uri) - .send() - .get(5, TimeUnit.SECONDS); +Application -> Server: GET /path +Server -> HttpClient: 401 + WWW-Authenticate +HttpClient -> Server: GET + Authentication +Server -> Application: 200 OK ---- -Jetty's HTTP client tests authentication credentials against the challenge(s) the server issues (see our section here on link:#configuring-security-secure-passwords[secure password obfuscation]), and if they match it automatically sends the right authentication headers to the server for authentication. -If the authentication is successful, it caches the result and reuses it for subsequent requests for the same domain and matching URIs. +Upon receiving a HTTP 401 response code, `HttpClient` looks at the +`WWW-Authenticate` response header (the server _challenge_) and then tries to +match configured authentication credentials to produce an `Authentication` +header that contains the authentication credentials to access the resource. -The HTTP conversation for a successful match is the following: +You can configure authentication credentials in the `HttpClient` instance as +follows: +[source,java,indent=0] ---- -Application HttpClient Server - | | | - |--- GET ---|------------ GET ----------->| - | | | - | |<-- 401 + WWW-Authenticate --| - | | | - | |--- GET + Authentication --->| - | | | - |<-- 200 ---|------------ 200 ------------| +include::../../{doc_code}/embedded/client/http/HTTPClientDocs.java[tag=addAuthentication] ---- -The application does not receive events related to the response with code 401, they are handled internally by `HttpClient` which produces a request similar to the original but with the correct `Authorization` header, and then relays the response with code 200 to the application. +``Authentication``s are matched against the server challenge first by +mechanism (e.g. `BASIC` or `DIGEST`), then by realm and then by URI. -Successful authentications are cached, but it is possible to clear them in order to force authentication again: +If an `Authentication` match is found, the application does not receive events +related to the HTTP 401 response. These events are handled internally by +`HttpClient` which produces another (internal) request similar to the original +request but with an additional `Authorization` header. -[source, java, subs="{sub-order}"] +If the authentication is successful, the server responds with a HTTP 200 and +`HttpClient` caches the `Authentication.Result` so that subsequent requests +for a matching URI will not incur in the additional rountrip caused by the +HTTP 401 response. + +It is possible to clear ``Authentication.Result``s in order to force +authentication again: + +[source,java,indent=0] ---- -httpClient.getAuthenticationStore().clearAuthenticationResults(); +include::../../{doc_code}/embedded/client/http/HTTPClientDocs.java[tag=clearResults] ---- -Authentications may be preempted to avoid the additional roundtrip due to the server challenge in this way: +Authentication results may be preempted to avoid the additional roundtrip +due to the server challenge in this way: -[source, java, subs="{sub-order}"] +[source,java,indent=0] ---- -AuthenticationStore auth = httpClient.getAuthenticationStore(); -URI uri = URI.create("http://domain.com/secure"); -auth.addAuthenticationResult(new BasicAuthentication.BasicResult(uri, "username", "password")); +include::../../{doc_code}/embedded/client/http/HTTPClientDocs.java[tag=preemptedResult] ---- -In this way, requests for the given URI are enriched by `HttpClient` immediately with the `Authorization` header, and the server should respond with a 200 and the resource content rather than with the 401 and the challenge. +In this way, requests for the given URI are enriched immediately with the +`Authorization` header, and the server should respond with HTTP 200 (and the +resource content) rather than with the 401 and the challenge. -It is also possible to preempt the authentication for a single request only, in this way: +It is also possible to preempt the authentication for a single request only, +in this way: -[source, java, subs="{sub-order}"] +[source,java,indent=0] ---- -URI uri = URI.create("http://domain.com/secure"); -Authentication.Result authn = new BasicAuthentication.BasicResult(uri, "username", "password") -Request request = httpClient.newRequest(uri); -authn.apply(request); -request.send(); +include::../../{doc_code}/embedded/client/http/HTTPClientDocs.java[tag=requestPreemptedResult] ---- -See also the link:#client-http-proxy-authentication[proxy authentication section] for further information about how authentication works with HTTP proxies. +See also the link:#client-http-proxy-authentication[proxy authentication section] +for further information about how authentication works with HTTP proxies. diff --git a/jetty-documentation/src/main/java/embedded/client/http/HTTPClientDocs.java b/jetty-documentation/src/main/java/embedded/client/http/HTTPClientDocs.java index d36b9fa9618..3e44f7c6bb8 100644 --- a/jetty-documentation/src/main/java/embedded/client/http/HTTPClientDocs.java +++ b/jetty-documentation/src/main/java/embedded/client/http/HTTPClientDocs.java @@ -32,12 +32,15 @@ import java.util.concurrent.TimeUnit; import java.util.function.LongConsumer; import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.client.api.Authentication; +import org.eclipse.jetty.client.api.AuthenticationStore; import org.eclipse.jetty.client.api.ContentResponse; import org.eclipse.jetty.client.api.Request; import org.eclipse.jetty.client.api.Response; import org.eclipse.jetty.client.api.Result; import org.eclipse.jetty.client.dynamic.HttpClientTransportDynamic; import org.eclipse.jetty.client.util.AsyncRequestContent; +import org.eclipse.jetty.client.util.BasicAuthentication; import org.eclipse.jetty.client.util.BufferingResponseListener; import org.eclipse.jetty.client.util.BytesRequestContent; import org.eclipse.jetty.client.util.FutureResponseListener; @@ -563,4 +566,57 @@ public class HTTPClientDocs httpClient.setCookieStore(new GoogleOnlyCookieStore()); // end::filteringCookieStore[] } + + public void addAuthentication() throws Exception + { + HttpClient httpClient = new HttpClient(); + httpClient.start(); + + // tag::addAuthentication[] + // Add authentication credentials. + AuthenticationStore auth = httpClient.getAuthenticationStore(); + + URI uri1 = new URI("http://mydomain.com/secure"); + auth.addAuthentication(new BasicAuthentication(uri1, "MyRealm", "userName1", "password1")); + + URI uri2 = new URI("http://otherdomain.com/admin"); + auth.addAuthentication(new BasicAuthentication(uri1, "AdminRealm", "admin", "password")); + // end::addAuthentication[] + } + + public void clearResults() throws Exception + { + HttpClient httpClient = new HttpClient(); + httpClient.start(); + + // tag::clearResults[] + httpClient.getAuthenticationStore().clearAuthenticationResults(); + // end::clearResults[] + } + + public void preemptedResult() throws Exception + { + HttpClient httpClient = new HttpClient(); + httpClient.start(); + + // tag::preemptedResult[] + AuthenticationStore auth = httpClient.getAuthenticationStore(); + URI uri = URI.create("http://domain.com/secure"); + auth.addAuthenticationResult(new BasicAuthentication.BasicResult(uri, "username", "password")); + // end::preemptedResult[] + } + + public void requestPreemptedResult() throws Exception + { + HttpClient httpClient = new HttpClient(); + httpClient.start(); + + // tag::requestPreemptedResult[] + URI uri = URI.create("http://domain.com/secure"); + Authentication.Result authn = new BasicAuthentication.BasicResult(uri, "username", "password"); + Request request = httpClient.newRequest(uri); + authn.apply(request); + request.send(); + // end::requestPreemptedResult[] + } }