Skip to content

Commit 27c96a5

Browse files
committed
#8 - ENH: Add in support for Authorization bearer token - AuthTokenProvider
1 parent a52491f commit 27c96a5

12 files changed

+392
-7
lines changed
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package io.avaje.http.client;
2+
3+
import java.time.Instant;
4+
5+
/**
6+
* Represents an Authorization Bearer token that can be held on the context.
7+
* <p>
8+
* Typically the token will be valid for a period and then expire.
9+
*/
10+
public interface AuthToken {
11+
12+
/**
13+
* Return the Authorization bearer token.
14+
*/
15+
String token();
16+
17+
/**
18+
* Return true if the token has expired or is no longer valid.
19+
*/
20+
boolean isExpired();
21+
22+
/**
23+
* Create an return a AuthToken with the given token and time it is valid until.
24+
*/
25+
static AuthToken of(String token, Instant validUntil) {
26+
return new Basic(token, validUntil);
27+
}
28+
29+
/**
30+
* Standard AuthToken implementation.
31+
*/
32+
class Basic implements AuthToken {
33+
34+
private final String token;
35+
private final Instant validUntil;
36+
37+
/**
38+
* Create with token and valid until time.
39+
*/
40+
public Basic(String token, Instant validUntil) {
41+
this.token = token;
42+
this.validUntil = validUntil;
43+
}
44+
45+
@Override
46+
public String token() {
47+
return token;
48+
}
49+
50+
@Override
51+
public boolean isExpired() {
52+
return Instant.now().isAfter(validUntil);
53+
}
54+
}
55+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package io.avaje.http.client;
2+
3+
/**
4+
* Use to obtain an Authorization bearer token that is expected to be used.
5+
*
6+
* <pre>{@code
7+
*
8+
* class MyAuthTokenProvider implements AuthTokenProducer {
9+
*
10+
* @Override
11+
* public AuthToken obtainToken(HttpClientRequest tokenRequest) {
12+
*
13+
* MyTokenResponse tokenResponse = tokenRequest
14+
* .url("https://foo/auth/token")
15+
* .header("content-type", "application/json")
16+
* .body(authRequestAsJson())
17+
* .post()
18+
* .bean(MyTokenResponse.class);
19+
*
20+
* String token = tokenResponse.getToken();
21+
* long expiresSecs = tokenResponse.getExpiresInSecs();
22+
*
23+
* Instant validUntil = Instant.now().plusSeconds(expiresSecs).minusSeconds(60);
24+
*
25+
* return AuthToken.of(token, validUntil);
26+
* }
27+
* }
28+
*
29+
* }</pre>
30+
*/
31+
public interface AuthTokenProvider {
32+
33+
/**
34+
* Obtain a new Authorization token.
35+
*
36+
* @param tokenRequest A new request to obtain an Authorisation token
37+
*/
38+
AuthToken obtainToken(HttpClientRequest tokenRequest);
39+
40+
}

client/src/main/java/io/avaje/http/client/DHttpClientContext.java

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import java.time.Duration;
99
import java.util.List;
1010
import java.util.Map;
11+
import java.util.concurrent.atomic.AtomicReference;
1112

1213
class DHttpClientContext implements HttpClientContext {
1314

@@ -16,13 +17,20 @@ class DHttpClientContext implements HttpClientContext {
1617
private final Duration requestTimeout;
1718
private final BodyAdapter bodyAdapter;
1819
private final RequestListener requestListener;
20+
private final RequestIntercept requestIntercept;
21+
private final boolean withAuthToken;
22+
private final AuthTokenProvider authTokenProvider;
23+
private final AtomicReference<AuthToken> tokenRef = new AtomicReference<>();
1924

20-
DHttpClientContext(HttpClient httpClient, String baseUrl, Duration requestTimeout, BodyAdapter bodyAdapter, RequestListener requestListener) {
25+
DHttpClientContext(HttpClient httpClient, String baseUrl, Duration requestTimeout, BodyAdapter bodyAdapter, RequestListener requestListener, AuthTokenProvider authTokenProvider, RequestIntercept intercept) {
2126
this.httpClient = httpClient;
2227
this.baseUrl = baseUrl;
2328
this.requestTimeout = requestTimeout;
2429
this.bodyAdapter = bodyAdapter;
2530
this.requestListener = requestListener;
31+
this.authTokenProvider = authTokenProvider;
32+
this.withAuthToken = authTokenProvider != null;
33+
this.requestIntercept = intercept;
2634
}
2735

2836
@Override
@@ -130,5 +138,27 @@ void afterResponse(DHttpClientRequest request) {
130138
if (requestListener != null) {
131139
requestListener.response(request.listenerEvent());
132140
}
141+
if (requestIntercept != null) {
142+
requestIntercept.afterResponse(request.response(), request);
143+
}
144+
}
145+
146+
void beforeRequest(DHttpClientRequest request) {
147+
if (withAuthToken && !request.isSkipAuthToken()) {
148+
request.header("Authorization", "Bearer " + authToken());
149+
}
150+
if (requestIntercept != null) {
151+
requestIntercept.beforeRequest(request);
152+
}
133153
}
154+
155+
private String authToken() {
156+
AuthToken authToken = tokenRef.get();
157+
if (authToken == null || authToken.isExpired()) {
158+
authToken = authTokenProvider.obtainToken(request().skipAuthToken());
159+
tokenRef.set(authToken);
160+
}
161+
return authToken.token();
162+
}
163+
134164
}

client/src/main/java/io/avaje/http/client/DHttpClientContextBuilder.java

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
import java.net.CookieManager;
55
import java.net.http.HttpClient;
66
import java.time.Duration;
7+
import java.util.ArrayList;
8+
import java.util.List;
79
import java.util.concurrent.Executor;
810

911
import static java.util.Objects.requireNonNull;
@@ -27,6 +29,10 @@ class DHttpClientContextBuilder implements HttpClientContext.Builder {
2729
private HttpClient.Version version;
2830
private Executor executor;
2931

32+
private AuthTokenProvider authTokenProvider;
33+
34+
private final List<RequestIntercept> interceptors = new ArrayList<>();
35+
3036
DHttpClientContextBuilder() {
3137
}
3238

@@ -60,6 +66,18 @@ public HttpClientContext.Builder withRequestListener(RequestListener requestList
6066
return this;
6167
}
6268

69+
@Override
70+
public HttpClientContext.Builder withRequestIntercept(RequestIntercept requestIntercept) {
71+
this.interceptors.add(requestIntercept);
72+
return this;
73+
}
74+
75+
@Override
76+
public HttpClientContext.Builder withAuthTokenProvider(AuthTokenProvider authTokenProvider) {
77+
this.authTokenProvider = authTokenProvider;
78+
return this;
79+
}
80+
6381
@Override
6482
public HttpClientContext.Builder withCookieHandler(CookieHandler cookieHandler) {
6583
this.cookieHandler = cookieHandler;
@@ -71,11 +89,13 @@ public HttpClientContext.Builder withRedirect(HttpClient.Redirect redirect) {
7189
this.redirect = redirect;
7290
return this;
7391
}
92+
7493
@Override
7594
public HttpClientContext.Builder withVersion(HttpClient.Version version) {
7695
this.version = version;
7796
return this;
7897
}
98+
7999
@Override
80100
public HttpClientContext.Builder withExecutor(Executor executor) {
81101
this.executor = executor;
@@ -89,7 +109,17 @@ public HttpClientContext build() {
89109
if (client == null) {
90110
client = defaultClient();
91111
}
92-
return new DHttpClientContext(client, baseUrl, requestTimeout, bodyAdapter, requestListener);
112+
return new DHttpClientContext(client, baseUrl, requestTimeout, bodyAdapter, requestListener, authTokenProvider, buildIntercept());
113+
}
114+
115+
private RequestIntercept buildIntercept() {
116+
if (interceptors.isEmpty()) {
117+
return null;
118+
} else if (interceptors.size() == 1) {
119+
return interceptors.get(0);
120+
} else {
121+
return new DRequestInterceptors(interceptors);
122+
}
93123
}
94124

95125
private HttpClient defaultClient() {

client/src/main/java/io/avaje/http/client/DHttpClientRequest.java

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,13 +47,20 @@ class DHttpClientRequest implements HttpClientRequest, HttpClientResponse {
4747
private HttpResponse<?> httpResponse;
4848
private BodyContent encodedResponseBody;
4949
private boolean loggableResponseBody;
50+
private boolean skipAuthToken;
5051

5152
public DHttpClientRequest(DHttpClientContext context, Duration requestTimeout) {
5253
this.context = context;
5354
this.requestTimeout = requestTimeout;
5455
this.url = context.url();
5556
}
5657

58+
@Override
59+
public HttpClientRequest skipAuthToken() {
60+
this.skipAuthToken = true;
61+
return this;
62+
}
63+
5764
@Override
5865
public HttpClientRequest requestTimeout(Duration requestTimeout) {
5966
this.requestTimeout = requestTimeout;
@@ -220,28 +227,24 @@ private void addHeaders() {
220227

221228
public HttpClientResponse get() {
222229
httpRequest = newGet(url.build());
223-
addHeaders();
224230
return this;
225231
}
226232

227233
@Override
228234
public HttpClientResponse delete() {
229235
httpRequest = newDelete(url.build());
230-
addHeaders();
231236
return this;
232237
}
233238

234239
@Override
235240
public HttpClientResponse post() {
236241
httpRequest = newPost(url.build(), body());
237-
addHeaders();
238242
return this;
239243
}
240244

241245
@Override
242246
public HttpClientResponse put() {
243247
httpRequest = newPut(url.build(), body());
244-
addHeaders();
245248
return this;
246249
}
247250

@@ -278,6 +281,8 @@ public <T> List<T> list(Class<T> cls) {
278281

279282
@Override
280283
public <T> HttpResponse<T> withResponseHandler(HttpResponse.BodyHandler<T> responseHandler) {
284+
context.beforeRequest(this);
285+
addHeaders();
281286
final long startNanos = System.nanoTime();
282287
final HttpResponse<T> response = context.send(httpRequest, responseHandler);
283288
requestTimeNanos = System.nanoTime() - startNanos;
@@ -357,6 +362,14 @@ RequestListener.Event listenerEvent() {
357362
return new ListenerEvent();
358363
}
359364

365+
HttpResponse<?> response() {
366+
return httpResponse;
367+
}
368+
369+
boolean isSkipAuthToken() {
370+
return skipAuthToken;
371+
}
372+
360373
private class ListenerEvent implements RequestListener.Event {
361374

362375
@Override
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package io.avaje.http.client;
2+
3+
import java.net.http.HttpResponse;
4+
import java.util.ArrayList;
5+
import java.util.Collections;
6+
import java.util.List;
7+
8+
/**
9+
* Processing of multiple RequestIntercept.
10+
* <p>
11+
* Noting that afterResponse interceptors are processed in reverse order.
12+
*/
13+
class DRequestInterceptors implements RequestIntercept {
14+
15+
private final List<RequestIntercept> before;
16+
private final List<RequestIntercept> after;
17+
18+
DRequestInterceptors(List<RequestIntercept> interceptors) {
19+
this.before = new ArrayList<>(interceptors);
20+
Collections.reverse(interceptors);
21+
this.after = new ArrayList<>(interceptors);
22+
}
23+
24+
@Override
25+
public void beforeRequest(HttpClientRequest request) {
26+
for (RequestIntercept interceptor : before) {
27+
interceptor.beforeRequest(request);
28+
}
29+
}
30+
31+
@Override
32+
public void afterResponse(HttpResponse<?> response, HttpClientRequest request) {
33+
for (RequestIntercept interceptor : after) {
34+
interceptor.afterResponse(response, request);
35+
}
36+
}
37+
}

client/src/main/java/io/avaje/http/client/HttpClientContext.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,23 @@ interface Builder {
157157
*/
158158
Builder withRequestListener(RequestListener requestListener);
159159

160+
/**
161+
* Add a request interceptor.
162+
*/
163+
Builder withRequestIntercept(RequestIntercept requestIntercept);
164+
165+
/**
166+
* Add a Authorization token provider.
167+
* <p>
168+
* When set all requests are expected to use a Authorization Bearer token
169+
* unless they are marked via {@link HttpClientRequest#skipAuthToken()}.
170+
* <p>
171+
* The AuthTokenProvider obtains a new token typically with an expiry. This
172+
* is automatically called as needed and the Authorization Bearer header set
173+
* on all requests (not marked with skipAuthToken()).
174+
*/
175+
Builder withAuthTokenProvider(AuthTokenProvider authTokenProvider);
176+
160177
/**
161178
* Specify a cookie handler to use on the HttpClient. This would override the default cookie handler.
162179
*

client/src/main/java/io/avaje/http/client/HttpClientRequest.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,14 @@
2626
*/
2727
public interface HttpClientRequest {
2828

29+
/**
30+
* For this request skip using an Authorization token.
31+
* <p>
32+
* This is automatically set on the request passed to
33+
* {@link AuthTokenProvider#obtainToken(HttpClientRequest)}.
34+
*/
35+
HttpClientRequest skipAuthToken();
36+
2937
/**
3038
* Set the request timeout to use for this request. When not set the default
3139
* request timeout will be used.

0 commit comments

Comments
 (0)