Skip to content

Commit 4f0b184

Browse files
authored
Merge pull request #596 from yidongnan/feature/dynamic-bearer-token
Add dynamic CallCredentials helpers
2 parents 3b299c6 + a96c561 commit 4f0b184

File tree

2 files changed

+149
-9
lines changed

2 files changed

+149
-9
lines changed

docs/en/client/security.md

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ This page describes how you connect to a grpc server and authenticate yourself.
1212
- [Trusting a Server](#trusting-a-server)
1313
- [Mutual Certificate Authentication](#mutual-certificate-authentication)
1414
- [Authentication](#authentication)
15+
- [Creating CallCredentials](#creating-callcredentials)
16+
- [Using CallCredentials](#using-callcredentials)
17+
- [Retry with new Authentication](#retry-with-new-authentication)
1518

1619
## Additional Topics <!-- omit in toc -->
1720

@@ -97,6 +100,8 @@ grpc.client.__name__.security.privateKey=file:certificates/client.key
97100
98101
## Authentication
99102
103+
### Creating CallCredentials
104+
100105
In addition to mutual certificate authentication, there are several other ways to authenticate yourself, such as
101106
`BasicAuth`.
102107
@@ -106,6 +111,20 @@ various libraries out there that provide implementations for grpc's
106111
`CallCredentials` are potentially active components because they can authenticate the request using a (third party)
107112
service and can manage and renew session tokens themselves.
108113

114+
````java
115+
@Bean
116+
CallCredentials basicAuthCredentials() {
117+
return CallCredentialsHelper.basicAuth("user", "password");
118+
}
119+
120+
@Bean
121+
CallCredentials bearerAuthForwardingCredentials() {
122+
return CallCredentialsHelper.bearerAuth(() -> KeycloakSecurityContext.getTokenString());
123+
}
124+
````
125+
126+
### Using CallCredentials
127+
109128
If you have exactly one `CallCredentials` in your application context, we'll automatically create a `StubTransformer`
110129
for you and configure all `Stub`s to use it. If you wish to configure different credentials per stub, then you use our
111130
helper methods in the
@@ -122,6 +141,45 @@ MyServiceBlockingStub myServiceForUser = myService.withCallCredentials(userCrede
122141
return myServiceForUser.send(request);
123142
````
124143
144+
### Retry with new Authentication
145+
146+
If you want to retry calls that failed due to an expired token (using grpc's built-in retry mechanism), you can use the
147+
following example `ClientInterceptor` as a guide to automatically report the failure to the token store.
148+
Please note that many popular token-based authentication systems (such as OAuth) also provide a token TTL that can be
149+
used to automatically update the token before the call is even sent for the first time, rendering this obsolete.
150+
151+
````java
152+
@Override
153+
public <ReqT, RespT> ClientCall<ReqT, RespT> interceptCall(
154+
MethodDescriptor<ReqT, RespT> method, CallOptions callOptions, Channel next) {
155+
156+
callOptions = callOptions
157+
.withCallCredentials(this.credentials)
158+
.withStreamTracerFactory(new ClientStreamTracer.Factory() {
159+
160+
@Override
161+
public ClientStreamTracer newClientStreamTracer(
162+
ClientStreamTracer.StreamInfo info, Metadata headers) {
163+
164+
// Make sure your implementations do _not_ block and return _immediately_
165+
final Object authToken = headers.get(AUTH_TOKEN_KEY);
166+
return new ClientStreamTracer() {
167+
168+
@Override
169+
public void streamClosed(final Status status) {
170+
this.credentials.invalidate(authToken);
171+
}
172+
};
173+
174+
}
175+
});
176+
177+
return next.newCall(method, callOptions);
178+
}
179+
````
180+
181+
For more details refer to [How to retry with new auth token using builtin retry?](https://github.com/grpc/grpc-java/issues/7345#issuecomment-679295003)
182+
125183
## Additional Topics <!-- omit in toc -->
126184

127185
- [Getting Started](getting-started.md)

grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/security/CallCredentialsHelper.java

Lines changed: 91 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import java.util.Base64;
2828
import java.util.Map;
2929
import java.util.concurrent.Executor;
30+
import java.util.function.Supplier;
3031

3132
import javax.annotation.Nullable;
3233

@@ -57,6 +58,8 @@
5758
* </p>
5859
* <ul>
5960
* <li>{@link #basicAuth(String, String) Basic-Auth}</li>
61+
* <li>{@link #bearerAuth(Supplier) Bearer-Auth}</li>
62+
* <li>Other variants using static or dynamic headers</li>
6063
* <li>{@link #requirePrivacy(CallCredentials) Require privacy for the connection} (Wrapper)</li>
6164
* <li>{@link #includeWhenPrivate(CallCredentials) Include credentials only if connection is private} (Wrapper)</li>
6265
* </ul>
@@ -72,7 +75,7 @@
7275
* <pre>
7376
* <code>@Bean
7477
* CallCredentials myCallCredentials() {
75-
* return CallCredentialsHelper#basicAuth("user", "password")}
78+
* return CallCredentialsHelper.basicAuth("user", "password");
7679
* }</code>
7780
* </pre>
7881
*
@@ -84,7 +87,7 @@
8487
* <pre>
8588
* <code>@Bean
8689
* StubTransformer myCallCredentialsTransformer() {
87-
* return CallCredentialsHelper#mappedCredentialsStubTransformer(Map.of(
90+
* return CallCredentialsHelper.mappedCredentialsStubTransformer(Map.of(
8891
* "myService1", basicAuth("user1", "password1"),
8992
* "theService2", basicAuth("foo", "bar"),
9093
* "publicApi", null // No credentials needed
@@ -96,7 +99,7 @@
9699
* <li>If you need different CallCredentials for each call, then you have to define it in the method yourself.
97100
*
98101
* <pre>
99-
* <code>stub.withCallCredentials(CallCredentialsHelper#basicAuth("user", "password")).doStuff(request);</code>
102+
* <code>stub.withCallCredentials(CallCredentialsHelper.basicAuth("user", "password")).doStuff(request);</code>
100103
* </pre>
101104
*
102105
* </li>
@@ -159,7 +162,8 @@ public static StubTransformer mappedCredentialsStubTransformer(
159162
}
160163

161164
/**
162-
* Creates new call credentials with the given token for bearer auth.
165+
* Creates new call credentials with the given token for bearer auth. Use this method if you have a permanent token
166+
* or only use the call credentials for a single call/while the token is valid.
163167
*
164168
* <p>
165169
* <b>Note:</b> This method uses experimental grpc-java-API features.
@@ -174,6 +178,23 @@ public static CallCredentials bearerAuth(final String token) {
174178
return authorizationHeader(BEARER_AUTH_PREFIX + token);
175179
}
176180

181+
/**
182+
* Creates new call credentials with the given token source for bearer auth. Use this method if you derive the token
183+
* from the active context (e.g. currently logged in user) or dynamically obtain it from the authentication server.
184+
*
185+
* <p>
186+
* <b>Note:</b> This method uses experimental grpc-java-API features.
187+
* </p>
188+
*
189+
* @param tokenSource the bearer token source to use
190+
* @return The newly created bearer auth credentials.
191+
* @see SecurityConstants#BEARER_AUTH_PREFIX
192+
* @see #authorizationHeader(Supplier)
193+
*/
194+
public static CallCredentials bearerAuth(final Supplier<String> tokenSource) {
195+
return authorizationHeader(() -> BEARER_AUTH_PREFIX + tokenSource);
196+
}
197+
177198
/**
178199
* Creates new call credentials with the given username and password for basic auth.
179200
*
@@ -228,32 +249,55 @@ public static String encodeBasicAuth(final String username, final String passwor
228249
* @see #authorizationHeaders(Metadata)
229250
*/
230251
public static CallCredentials authorizationHeader(final String authorization) {
231-
requireNonNull(authorization);
252+
requireNonNull(authorization, "authorization");
232253
final Metadata extraHeaders = new Metadata();
233254
extraHeaders.put(AUTHORIZATION_HEADER, authorization);
234255
return authorizationHeaders(extraHeaders);
235256
}
236257

258+
/**
259+
* Creates new call credentials with the given authorization information source.
260+
*
261+
* <p>
262+
* <b>Note:</b> This method uses experimental grpc-java-API features.
263+
* </p>
264+
*
265+
* @param authorizationSource The authorization source to use. The authorization usually starts with the scheme such
266+
* as as {@code "Basic "} or {@code "Bearer "} followed by the actual authentication information.
267+
* @return The newly created call credentials.
268+
* @see SecurityConstants#AUTHORIZATION_HEADER
269+
* @see #authorizationHeaders(Supplier)
270+
*/
271+
public static CallCredentials authorizationHeader(final Supplier<String> authorizationSource) {
272+
requireNonNull(authorizationSource, "authorizationSource");
273+
274+
return authorizationHeaders(() -> {
275+
final Metadata extraHeaders = new Metadata();
276+
extraHeaders.put(AUTHORIZATION_HEADER, authorizationSource.get());
277+
return extraHeaders;
278+
});
279+
}
280+
237281
/**
238282
* Creates new call credentials with the given static authorization headers.
239283
*
240284
* @param authorizationHeaders The authorization headers to use.
241285
* @return The newly created call credentials.
242286
*/
243287
public static CallCredentials authorizationHeaders(final Metadata authorizationHeaders) {
244-
return new StaticSecurityHeaderCallCredentials(requireNonNull(authorizationHeaders));
288+
return new StaticSecurityHeaderCallCredentials(authorizationHeaders);
245289
}
246290

247291
/**
248-
* The static security header {@link CallCredentials} simply add a set of predefined headers to the call. Their
292+
* The static security header {@link CallCredentials} simply adds a set of predefined headers to the call. Their
249293
* specific meaning is server specific. This implementation can be used, for example, for BasicAuth.
250294
*/
251295
private static final class StaticSecurityHeaderCallCredentials extends CallCredentials {
252296

253297
private final Metadata extraHeaders;
254298

255-
StaticSecurityHeaderCallCredentials(final Metadata extraHeaders) {
256-
this.extraHeaders = requireNonNull(extraHeaders, "extraHeaders");
299+
StaticSecurityHeaderCallCredentials(final Metadata authorizationHeaders) {
300+
this.extraHeaders = requireNonNull(authorizationHeaders, "authorizationHeaders");
257301
}
258302

259303
@Override
@@ -272,6 +316,44 @@ public String toString() {
272316

273317
}
274318

319+
/**
320+
* Creates new call credentials with the given authorization headers source.
321+
*
322+
* @param authorizationHeadersSupplier The authorization headers source to use.
323+
* @return The newly created call credentials.
324+
*/
325+
public static CallCredentials authorizationHeaders(final Supplier<Metadata> authorizationHeadersSupplier) {
326+
return new DynamicSecurityHeaderCallCredentials(authorizationHeadersSupplier);
327+
}
328+
329+
/**
330+
* The dynamic security header {@link CallCredentials} simply adds a set of dynamic headers to the call. Their
331+
* specific meaning is server specific. This implementation can be used, for example, for BasicAuth.
332+
*/
333+
private static final class DynamicSecurityHeaderCallCredentials extends CallCredentials {
334+
335+
private final Supplier<Metadata> extraHeadersSupplier;
336+
337+
DynamicSecurityHeaderCallCredentials(final Supplier<Metadata> authorizationHeadersSupplier) {
338+
this.extraHeadersSupplier = requireNonNull(authorizationHeadersSupplier, "authorizationHeadersSupplier");
339+
}
340+
341+
@Override
342+
public void applyRequestMetadata(final RequestInfo requestInfo, final Executor appExecutor,
343+
final MetadataApplier applier) {
344+
applier.apply(this.extraHeadersSupplier.get());
345+
}
346+
347+
@Override
348+
public void thisUsesUnstableApi() {} // API evolution in progress
349+
350+
@Override
351+
public String toString() {
352+
return "DynamicSecurityHeaderCallCredentials [extraHeadersSupplier=" + this.extraHeadersSupplier + "]";
353+
}
354+
355+
}
356+
275357
/**
276358
* Checks whether the given security level provides privacy for all data being send on the connection.
277359
*

0 commit comments

Comments
 (0)