jersey icon indicating copy to clipboard operation
jersey copied to clipboard

Make it possible to control the Digest Authentication cacheKey to consider the domain attribute of the response header

Open chrisrueger opened this issue 1 year ago • 0 comments

Motivation

We are migrating from jersey client 1.19 to 2.40 and we noticed that the Http Digest authentication behaves differently than in 1.19.

Steps to reproduce

  1. Setup your client like this:
HttpAuthenticationFeature authFeature = HttpAuthenticationFeature.universal(this.apiUsername, this.apiKey);
config.register(authFeature);

or

HttpAuthenticationFeature authFeature = HttpAuthenticationFeature.digest(this.apiUsername, this.apiKey);
config.register(authFeature);
  1. Do at least two requests like this: GET https://example.com:8080/api/articles/1 GET https://example.com:8080/api/articles/2

Expected behavior (as of Jersey Client 1.19)

2023-08-25 10:09:24,023 18 * Sending client request on thread pool-9-thread-5
18 > GET http://example.com:8080/api/articles/1
18 > Accept: application/json,application/json
18 > User-Agent: My App Client

2023-08-25 10:09:24,091 18 * Client response received on thread pool-9-thread-5
18 < 401
18 < Cache-Control: nocache, private
18 < Connection: keep-alive
18 < Content-Type: application/json
18 < Date: Fri, 25 Aug 2023 08:09:24 GMT
18 < Server: nginx/1.18.0
18 < Transfer-Encoding: chunked
18 < Www-Authenticate: Digest realm="Shopware REST-API", domain="/", nonce="db57680d872101ef1f94aacf8e50e9c4", opaque="d75db7b160fe72d1346d2bd1f67bfd10", algorithm="MD5", qop="auth"
{"success":false,"message":"Invalid or missing auth"}

2023-08-25 10:09:24,092 19 * Sending client request on thread pool-9-thread-5
19 > GET http://example.com:8080/api/articles/1
19 > Accept: application/json,application/json
19 > Authorization: [redacted]
19 > User-Agent: My App Client

2023-08-25 10:09:24,218 19 * Client response received on thread pool-9-thread-5
19 < 200
19 < Cache-Control: nocache, private
19 < Connection: keep-alive
19 < Content-Type: application/json
19 < Date: Fri, 25 Aug 2023 08:09:24 GMT
19 < Server: nginx/1.18.0
19 < Transfer-Encoding: chunked
{"data":"foobar1"}

2023-08-25 10:09:24,234 20 * Sending client request on thread pool-9-thread-5
20 > GET http://example.com:8080/api/articles/2
20 > Accept: application/json,application/json
20 > Authorization: [redacted]
20 > User-Agent: My App Client

2023-08-25 10:09:24,306 21 * Sending client request on thread pool-9-thread-5
21 > GET http://example.com:8080/api/articles/2
21 > Accept: application/json,application/json
21 > Authorization: [redacted]
21 > User-Agent: My App Client

2023-08-25 10:09:24,434 21 * Client response received on thread pool-9-thread-5
21 < 200
21 < Cache-Control: nocache, private
21 < Connection: keep-alive
21 < Content-Type: application/json
21 < Date: Fri, 25 Aug 2023 08:09:24 GMT
21 < Server: nginx/1.18.0
21 < Transfer-Encoding: chunked
{"data":"foobar2"}

Notice just one 401 response.

Actual Result (in Jersey client 2.x / 2.40)

2023-08-25 10:09:24,023 18 * Sending client request on thread pool-9-thread-5
18 > GET http://example.com:8080/api/articles/1
18 > Accept: application/json,application/json
18 > User-Agent: My App Client

2023-08-25 10:09:24,091 18 * Client response received on thread pool-9-thread-5
18 < 401
18 < Cache-Control: nocache, private
18 < Connection: keep-alive
18 < Content-Type: application/json
18 < Date: Fri, 25 Aug 2023 08:09:24 GMT
18 < Server: nginx/1.18.0
18 < Transfer-Encoding: chunked
18 < Www-Authenticate: Digest realm="Shopware REST-API", domain="/", nonce="db57680d872101ef1f94aacf8e50e9c4", opaque="d75db7b160fe72d1346d2bd1f67bfd10", algorithm="MD5", qop="auth"
{"success":false,"message":"Invalid or missing auth"}

2023-08-25 10:09:24,092 19 * Sending client request on thread pool-9-thread-5
19 > GET http://example.com:8080/api/articles/1
19 > Accept: application/json,application/json
19 > Authorization: [redacted]
19 > User-Agent: My App Client

2023-08-25 10:09:24,218 19 * Client response received on thread pool-9-thread-5
19 < 200
19 < Cache-Control: nocache, private
19 < Connection: keep-alive
19 < Content-Type: application/json
19 < Date: Fri, 25 Aug 2023 08:09:24 GMT
19 < Server: nginx/1.18.0
19 < Transfer-Encoding: chunked
{"data":"foobar1"}

2023-08-25 10:09:24,234 20 * Sending client request on thread pool-9-thread-5
20 > GET http://example.com:8080/api/articles/2
20 > Accept: application/json,application/json
20 > User-Agent: My App Client

2023-08-25 10:09:24,304 20 * Client response received on thread pool-9-thread-5
20 < 401
20 < Cache-Control: nocache, private
20 < Connection: keep-alive
20 < Content-Type: application/json
20 < Date: Fri, 25 Aug 2023 08:09:24 GMT
20 < Server: nginx/1.18.0
20 < Transfer-Encoding: chunked
20 < Www-Authenticate: Digest realm="Shopware REST-API", domain="/", nonce="db57680d872101ef1f94aacf8e50e9c4", opaque="d75db7b160fe72d1346d2bd1f67bfd10", algorithm="MD5", qop="auth"
{"success":false,"message":"Invalid or missing auth"}

2023-08-25 10:09:24,306 21 * Sending client request on thread pool-9-thread-5
21 > GET http://example.com:8080/api/articles/2
21 > Accept: application/json,application/json
21 > Authorization: [redacted]
21 > User-Agent: My App Client

2023-08-25 10:09:24,434 21 * Client response received on thread pool-9-thread-5
21 < 200
21 < Cache-Control: nocache, private
21 < Connection: keep-alive
21 < Content-Type: application/json
21 < Date: Fri, 25 Aug 2023 08:09:24 GMT
21 < Server: nginx/1.18.0
21 < Transfer-Encoding: chunked
{"data":"foobar2"}

You notice that the expected result just gets one 401 on the first request with the Digest Header, and then resuses this information on the subsequent requests. In the actual result each request first gets a 401 and then a 200.

The new behavior causes a twice as much requests than than previously.

Findings

In 2.40 it looks like the cacheKey always uses the URI including the path: https://github.com/eclipse-ee4j/jersey/blob/2.40/core-client/src/main/java/org/glassfish/jersey/client/authentication/AuthenticationUtil.java#L49

In 1.19 it seems there was a different mechanism using a ThreadLocal: https://github.com/javaee/jersey-1.x/blob/864a01d7be490ab93d2424da3e446ad8eb84b1e8/jersey-client/src/main/java/com/sun/jersey/api/client/filter/HTTPDigestAuthFilter.java#L386

The RFC 7616 for digest auth says the following about the domain attribute:

from: https://datatracker.ietf.org/doc/html/rfc7616#section-3.3 domain

  A quoted, space-separated list of URIs, as specified in [[RFC3986](https://datatracker.ietf.org/doc/html/rfc3986)],
  that define the protection space.  If a URI is a path-absolute, it
  is relative to the canonical root URL.  (See Section 2.2 of
  [RFC7235].)  An absolute-URI in this list may refer to a different
  server than the web-origin [[RFC6454](https://datatracker.ietf.org/doc/html/rfc6454)].  The client can use this
  list to determine the set of URIs for which the same
  authentication information may be sent: any URI that has a URI in
  this list as a prefix (after both have been made absolute) MAY be
  assumed to be in the same protection space.  If this parameter is
  omitted or its value is empty, the client SHOULD assume that the
  protection space consists of all URIs on the web-origin.

  This parameter is not meaningful in Proxy-Authenticate header
  fields, for which the protection space is always the entire proxy;
  if present, it MUST be ignored.

The part

  any URI that has a URI in
  this list as a prefix (after both have been made absolute) MAY be
  assumed to be in the same protection space

makes me think, that the domain attribute should be considered for the cacheKey.

In my example above the domain="/" would basicaly mean that all request URIs are in the same space and the auth information should be reused for those urls matching this.

GET https://example.com:8080/api/articles/1 GET https://example.com:8080/api/articles/2

everything after the first slash (/)

Suggestion

Maybe it would be most helpful to let the developer customize how the CacheKey is calculated. Currently org.glassfish.jersey.client.authentication.AuthenticationUtil.getCacheKey(ClientRequestContext) is called in 3 places:

image

Example Pseudo code:

BiFunction cacheKeyCustomizer = new BiFunction<ClientRequestContext, DigestScheme, URI>(){

	@Override
	public URI apply(ClientRequestContext request,
	                 DigestScheme u) {
		// TODO do custom cacheKey calculation which considers 'domain' from DigestScheme and the current request
		return null;
	}
	
};
HttpAuthenticationFeature authFeature = HttpAuthenticationFeature.digest(this.apiUsername, this.apiKey, cacheKeyCustomizer);
config.register(authFeature);

Any thoughts?

chrisrueger avatar Aug 25 '23 09:08 chrisrueger