Skip to content

Commit 357286c

Browse files
committed
initial commit
1 parent d6afece commit 357286c

31 files changed

+1093
-10
lines changed

.gitignore

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,6 @@
11
.gradle
2+
.idea
23
/build/
3-
4-
# Ignore Gradle GUI config
5-
gradle-app.setting
6-
4+
/out/
75
# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored)
86
!gradle-wrapper.jar
9-
10-
# Cache of project
11-
.gradletasknamecache
12-
13-
# # Work around https://youtrack.jetbrains.com/issue/IDEA-116898
14-
# gradle/wrapper/gradle-wrapper.properties

.travis.yml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
language: java
2+
services:
3+
- docker
4+
5+
after_success:
6+
- test $TRAVIS_PULL_REQUEST == "false" && test "$TRAVIS_TAG" != "" && test $TRAVIS_REPO_SLUG == "avast/grpc-java-jwt" ./gradlew bintrayUpload -Pversion="$TRAVIS_TAG" --info --no-daemon
7+
8+
before_cache:
9+
- rm -f $HOME/.gradle/caches/modules-2/modules-2.lock
10+
- rm -fr $HOME/.gradle/caches/*/plugin-resolution/
11+
cache:
12+
directories:
13+
- $HOME/.gradle/caches/
14+
- $HOME/.gradle/wrapper/
15+
- $HOME/.m2

README.md

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
# gRPC Java JWT support
2+
[![Build Status](https://travis-ci.org/avast/grpc-java-jwt.svg?branch=master)](https://travis-ci.org/avast/grpc-java-jwt) [![Download](https://api.bintray.com/packages/avast/maven/grpc-java-jwt/images/download.svg) ](https://bintray.com/avast/maven/grpc-java-jwt/_latestVersion)
3+
4+
Library that helps with authenticated communication in gRPC-Java based applications. It uses [JSON Web Token](https://jwt.io/) transported in `Authorization` header (as `Bearer rawJWT`).
5+
6+
Implementation of standard `CallCredentials` ensures that the header is sent, and `ServerInterceptor` ensures that the incoming header is valid and makes the parsed JWT available for the underlying code.
7+
8+
```maven
9+
<dependency>
10+
<groupId>com.avast.grpc</groupId>
11+
<artifactId>grpc-java-jwt</artifactId>
12+
<version>$latestVersion</version>
13+
</dependency>
14+
```
15+
```gradle
16+
compile "com.avast.grpc:grpc-java-jwt:$latestVersion"
17+
````
18+
19+
## Keycloak support
20+
There are implementations of the core interfaces for [Keycloak](https://www.keycloak.org/).
21+
22+
```maven
23+
<dependency>
24+
<groupId>com.avast.grpc</groupId>
25+
<artifactId>grpc-java-jwt-keycloak</artifactId>
26+
<version>$latestVersion</version>
27+
</dependency>
28+
```
29+
```gradle
30+
compile "com.avast.grpc:grpc-java-jwt-keycloak:$latestVersion"
31+
````
32+
33+
### Client usage
34+
This ensures that each call contains `Authorization` header with `Bearer ` prefixed Keycloak access token (as JWT).
35+
```java
36+
import com.avast.grpc.jwt.keycloak.client.KeycloakJwtCallCredentials;
37+
38+
KeycloakJwtCallCredentials callCredentials = KeycloakJwtCallCredentials.fromConfig(yourConfig);
39+
YourService.newStub(aChannel).withCallCredentials(callCredentials);
40+
```
41+
42+
### Server usage
43+
This ensures that only requests with valid `JWT` in `Authorization` header are processed.
44+
The `fromConfig` method automatically downloads the Keycloak public key before the instance is actually created.
45+
```java
46+
import io.grpc.ServerServiceDefinition;
47+
import com.avast.grpc.jwt.keycloak.server.KeycloakJwtServerInterceptor;
48+
49+
KeycloakJwtServerInterceptor serverInterceptor = KeycloakJwtServerInterceptor.fromConfig(yourConfig);
50+
ServerServiceDefinition interceptedService = ServerInterceptors.intercept(yourService, serverInterceptor);
51+
52+
// read token in a gRPC method implementation
53+
import org.keycloak.representations.AccessToken;
54+
AccessToken accessToken = serverInterceptor.AccessTokenContextKey.get();
55+
```
56+
57+
There is also [this integration test](keycloak/src/test/java/com/avast/grpc/jwt/keycloak/KeycloakTest.java) that can serve as nice example.
58+
59+
## Implementation notes
60+
61+
On the client side, there is implementation of `CallCredentials` that ensures the JWT token is correctly stored to the headers. Just call a static method on [JwtCallCredentials](core/src/main/java/com/avast/grpc/jwt/client/JwtCallCredentials.java) - it will require an instance of a _JwtTokenProvider_ (an interface that returns encoded JWT).
62+
63+
On server side, there is `ServerInterceptor` implementation that parses the incoming JWT and verifies it. [JwtServerInterceptor](core/src/main/java/com/avast/grpc/jwt/server/JwtServerInterceptor.java) requires an instance of [JwtTokenParser](core/src/main/java/com/avast/grpc/jwt/server/JwtTokenParser.java) - it's an interface that parses and verifies the JWT.
64+
65+
## About gRPC internals
66+
gRCP uses terms `Metadata` and `Context keys`. `Metadata` is set of key-value pairs that are transported between client and server, et vice versa. So it's like HTTP headers.
67+
68+
On other hand, `Context key` is set of values that are available during request processing.
69+
By default, a `Storage` implementation based on `ThreadLocal` is used.
70+
Thanks to this, you can just call `get()` method on a Context key and you immediately get the value because it read the value from `Context.current()`.
71+
72+
So when implementing interceptors, you must be sure that you read Context values from the right thread. It's actually no issue for us because:
73+
1. The right thread is automatically handled by gRPC-core when using`CallCredentials`. So you can call `applier.apply()` method on any thread.
74+
2. Our `ServerInterceptor` implementation is fully synchronous.

build.gradle

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
plugins {
2+
id 'com.github.sherter.google-java-format' version '0.8' apply false
3+
id 'com.google.protobuf' version '0.8.8' apply false
4+
id 'com.avast.gradle.docker-compose' version '0.9.2' apply false
5+
id 'com.jfrog.bintray' version '1.8.4' apply false
6+
}
7+
8+
allprojects {
9+
group 'com.avast.grpc'
10+
version = version == 'unspecified' ? 'DEVELOPER-SNAPSHOT' : version
11+
}
12+
13+
ext {
14+
grpcVersion = '1.20.0'
15+
protobufVersion = '3.7.1'
16+
}
17+
18+
subprojects {
19+
apply plugin: 'java'
20+
apply plugin: 'maven'
21+
apply plugin: 'com.github.sherter.google-java-format'
22+
apply plugin: 'com.jfrog.bintray'
23+
24+
sourceCompatibility = JavaVersion.VERSION_1_8
25+
26+
repositories {
27+
mavenCentral()
28+
}
29+
30+
dependencies {
31+
testCompile 'junit:junit:4.12'
32+
testCompile 'org.mockito:mockito-core:2.27.0'
33+
}
34+
35+
task sourcesJar(type: Jar) {
36+
from sourceSets.main.allSource
37+
archiveClassifier = 'sources'
38+
}
39+
40+
artifacts {
41+
archives sourcesJar
42+
}
43+
44+
bintray {
45+
user = System.getenv('BINTRAY_USER')
46+
key = System.getenv('BINTRAY_KEY')
47+
configurations = ['archives']
48+
publish = true
49+
dryRun = true
50+
pkg {
51+
repo = 'maven'
52+
name = 'grpc-java-jwt'
53+
desc = 'Library that helps with authenticated communication in gRPC-Java based applications. It uses JSON Web Token.'
54+
userOrg = 'avast'
55+
licenses = ['MIT']
56+
vcsUrl = 'https://github.com/avast/grpc-java-jwt.git'
57+
websiteUrl = 'https://github.com/avast/grpc-java-jwt'
58+
issueTrackerUrl = 'https://github.com/avast/grpc-java-jwt/issues'
59+
githubRepo = 'avast/grpc-java-jwt'
60+
labels = ['grpc', 'java', 'jwt', 'keycloak']
61+
version {
62+
name = project.version
63+
vcsTag = project.version
64+
}
65+
}
66+
}
67+
68+
}

core/build.gradle

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
archivesBaseName = 'grpc-java-jwt'
2+
3+
dependencies {
4+
compile "io.grpc:grpc-core:$grpcVersion"
5+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package com.avast.grpc.jwt;
2+
3+
import io.grpc.Metadata;
4+
5+
public final class Constants {
6+
private Constants() {}
7+
8+
public static io.grpc.Metadata.Key<String> AuthorizationMetadataKey =
9+
Metadata.Key.of("Authorization", Metadata.ASCII_STRING_MARSHALLER);
10+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package com.avast.grpc.jwt.client;
2+
3+
import java.util.concurrent.CompletableFuture;
4+
5+
@FunctionalInterface
6+
public interface AsyncJwtTokenProvider {
7+
/* Gets encoded JWT token. */
8+
CompletableFuture<String> get();
9+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package com.avast.grpc.jwt.client;
2+
3+
@FunctionalInterface
4+
public interface BlockingJwtTokenProvider {
5+
/* Gets encoded JWT token, can block (e.g. perform blocking I/O). */
6+
String get();
7+
}
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
package com.avast.grpc.jwt.client;
2+
3+
import com.avast.grpc.jwt.Constants;
4+
import io.grpc.CallCredentials;
5+
import io.grpc.Metadata;
6+
import io.grpc.Status;
7+
import java.util.concurrent.Executor;
8+
9+
public abstract class JwtCallCredentials extends CallCredentials {
10+
11+
public static JwtCallCredentials synchronous(SynchronousJwtTokenProvider tokenProvider) {
12+
return new Synchronous(tokenProvider);
13+
}
14+
15+
public static JwtCallCredentials blocking(BlockingJwtTokenProvider tokenProvider) {
16+
return new Blocking(tokenProvider);
17+
}
18+
19+
public static JwtCallCredentials asynchronous(AsyncJwtTokenProvider tokenProvider) {
20+
return new Asynchronous(tokenProvider);
21+
}
22+
23+
@Override
24+
public void thisUsesUnstableApi() {}
25+
26+
protected void applyToken(MetadataApplier applier, String jwtToken) {
27+
Metadata metadata = new Metadata();
28+
metadata.put(Constants.AuthorizationMetadataKey, "Bearer " + jwtToken);
29+
applier.apply(metadata);
30+
}
31+
32+
protected void applyFailure(MetadataApplier applier, Throwable e) {
33+
applier.fail(
34+
Status.UNAUTHENTICATED
35+
.withDescription("An exception when obtaining JWT token")
36+
.withCause(e));
37+
}
38+
39+
public static class Synchronous extends JwtCallCredentials {
40+
41+
private final SynchronousJwtTokenProvider jwtTokenProvider;
42+
43+
public Synchronous(SynchronousJwtTokenProvider jwtTokenProvider) {
44+
this.jwtTokenProvider = jwtTokenProvider;
45+
}
46+
47+
@Override
48+
public void applyRequestMetadata(
49+
RequestInfo requestInfo, Executor appExecutor, MetadataApplier applier) {
50+
try {
51+
applyToken(applier, jwtTokenProvider.get());
52+
} catch (RuntimeException e) {
53+
applyFailure(applier, e);
54+
}
55+
}
56+
}
57+
58+
public static class Blocking extends JwtCallCredentials {
59+
60+
private final BlockingJwtTokenProvider jwtTokenProvider;
61+
62+
public Blocking(BlockingJwtTokenProvider jwtTokenProvider) {
63+
this.jwtTokenProvider = jwtTokenProvider;
64+
}
65+
66+
@Override
67+
public void applyRequestMetadata(
68+
RequestInfo requestInfo, Executor appExecutor, MetadataApplier applier) {
69+
appExecutor.execute(
70+
() -> {
71+
try {
72+
applyToken(applier, jwtTokenProvider.get());
73+
} catch (RuntimeException e) {
74+
applyFailure(applier, e);
75+
}
76+
});
77+
}
78+
}
79+
80+
public static class Asynchronous extends JwtCallCredentials {
81+
82+
private final AsyncJwtTokenProvider jwtTokenProvider;
83+
84+
public Asynchronous(AsyncJwtTokenProvider jwtTokenProvider) {
85+
this.jwtTokenProvider = jwtTokenProvider;
86+
}
87+
88+
@Override
89+
public void applyRequestMetadata(
90+
RequestInfo requestInfo, Executor appExecutor, MetadataApplier applier) {
91+
jwtTokenProvider
92+
.get()
93+
.whenComplete(
94+
(token, e) -> {
95+
if (token != null) applyToken(applier, token);
96+
else applyFailure(applier, e);
97+
});
98+
}
99+
}
100+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package com.avast.grpc.jwt.client;
2+
3+
@FunctionalInterface
4+
public interface SynchronousJwtTokenProvider {
5+
/* Gets encoded JWT token without blocking. */
6+
String get();
7+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package com.avast.grpc.jwt.server;
2+
3+
import com.avast.grpc.jwt.Constants;
4+
import io.grpc.*;
5+
6+
public class JwtServerInterceptor<T> implements ServerInterceptor {
7+
public final io.grpc.Context.Key<T> AccessTokenContextKey = Context.key("AccessToken");
8+
9+
private static final String AUTH_HEADER_PREFIX = "Bearer ";
10+
11+
private final JwtTokenParser<T> tokenParser;
12+
13+
public JwtServerInterceptor(JwtTokenParser<T> tokenParser) {
14+
this.tokenParser = tokenParser;
15+
}
16+
17+
@Override
18+
public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(
19+
ServerCall<ReqT, RespT> call, Metadata headers, ServerCallHandler<ReqT, RespT> next) {
20+
String authHeader = headers.get(Constants.AuthorizationMetadataKey);
21+
if (authHeader == null) {
22+
call.close(
23+
Status.UNAUTHENTICATED.withDescription(
24+
Constants.AuthorizationMetadataKey.name() + " header not found"),
25+
new Metadata());
26+
return new ServerCall.Listener<ReqT>() {};
27+
}
28+
if (!authHeader.startsWith(AUTH_HEADER_PREFIX)) {
29+
call.close(
30+
Status.UNAUTHENTICATED.withDescription(
31+
Constants.AuthorizationMetadataKey.name()
32+
+ " header does not start with "
33+
+ AUTH_HEADER_PREFIX),
34+
new Metadata());
35+
return new ServerCall.Listener<ReqT>() {};
36+
}
37+
try {
38+
T token = tokenParser.parseToValid(authHeader.substring(AUTH_HEADER_PREFIX.length()));
39+
return Contexts.interceptCall(
40+
Context.current().withValue(AccessTokenContextKey, token), call, headers, next);
41+
} catch (Exception e) {
42+
call.close(
43+
Status.UNAUTHENTICATED.withDescription(
44+
Constants.AuthorizationMetadataKey.name()
45+
+ " header does not start with "
46+
+ AUTH_HEADER_PREFIX),
47+
new Metadata());
48+
return new ServerCall.Listener<ReqT>() {};
49+
}
50+
}
51+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package com.avast.grpc.jwt.server;
2+
3+
@FunctionalInterface
4+
public interface JwtTokenParser<T> {
5+
6+
/** Get valid JWT token, throws an exception otherwise. */
7+
T parseToValid(String jwtToken) throws Exception;
8+
}

0 commit comments

Comments
 (0)