jersey
jersey copied to clipboard
Make it possible to control the Digest Authentication cacheKey to consider the domain attribute of the response header
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
- 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);
- 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:
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?