eclipse-ee4j / eclipse-ee4j/jersey
Make it possible to control the Digest Authentication cacheKey to consider the domain attribute of the response header
- Dominant language
- Java
- Stars
- 730
- Forks
- 382
- PR merge metrics
- No merged PRs in 30d
Description
## 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);
```
2. 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(){
@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?
Contributor guide
Assessment
This issue has not been assessed yet.