mirror of https://github.com/grpc/grpc-java.git
examples: Add a JWT authentication example (#5915)
This commit is contained in:
parent
58e6ad71cc
commit
b7859e73a0
|
|
@ -45,6 +45,8 @@ $ VERSION_FILES=(
|
||||||
examples/example-alts/build.gradle
|
examples/example-alts/build.gradle
|
||||||
examples/example-gauth/build.gradle
|
examples/example-gauth/build.gradle
|
||||||
examples/example-gauth/pom.xml
|
examples/example-gauth/pom.xml
|
||||||
|
examples/example-jwt-auth/build.gradle
|
||||||
|
examples/example-jwt-auth/pom.xml
|
||||||
examples/example-hostname/build.gradle
|
examples/example-hostname/build.gradle
|
||||||
examples/example-hostname/pom.xml
|
examples/example-hostname/pom.xml
|
||||||
examples/example-kotlin/build.gradle
|
examples/example-kotlin/build.gradle
|
||||||
|
|
|
||||||
|
|
@ -156,6 +156,8 @@ $ bazel-bin/hello-world-client
|
||||||
|
|
||||||
- [Google Authentication](example-gauth)
|
- [Google Authentication](example-gauth)
|
||||||
|
|
||||||
|
- [JWT-based Authentication](example-jwt-auth)
|
||||||
|
|
||||||
- [Kotlin examples](example-kotlin)
|
- [Kotlin examples](example-kotlin)
|
||||||
|
|
||||||
- [Kotlin Android examples](example-kotlin/android)
|
- [Kotlin Android examples](example-kotlin/android)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,70 @@
|
||||||
|
Authentication Example
|
||||||
|
==============================================
|
||||||
|
|
||||||
|
This example illustrates a simple JWT-based authentication implementation in gRPC using
|
||||||
|
server interceptor. It uses the JJWT library to create and verify JSON Web Tokens (JWTs).
|
||||||
|
|
||||||
|
The example requires grpc-java to be pre-built. Using a release tag will download the relevant binaries
|
||||||
|
from a maven repository. But if you need the latest SNAPSHOT binaries you will need to follow
|
||||||
|
[COMPILING](../../COMPILING.md) to build these.
|
||||||
|
|
||||||
|
The source code is [here](src/main/java/io/grpc/examples/jwtauth).
|
||||||
|
To build the example, run in this directory:
|
||||||
|
```
|
||||||
|
$ ../gradlew installDist
|
||||||
|
```
|
||||||
|
The build creates scripts `auth-server` and `auth-client` in the `build/install/example-jwt-auth/bin/` directory
|
||||||
|
which can be used to run this example. The example requires the server to be running before starting the
|
||||||
|
client.
|
||||||
|
|
||||||
|
Running auth-server is similar to the normal hello world example and there are no arguments to supply:
|
||||||
|
|
||||||
|
**auth-server**:
|
||||||
|
|
||||||
|
The auth-server accepts optional argument for port on which the server should run:
|
||||||
|
|
||||||
|
```text
|
||||||
|
USAGE: auth-server [port]
|
||||||
|
```
|
||||||
|
|
||||||
|
The auth-client accepts optional arguments for server-host, server-port, user-name and client-id:
|
||||||
|
|
||||||
|
**auth-client**:
|
||||||
|
|
||||||
|
```text
|
||||||
|
USAGE: auth-client [server-host [server-port [user-name [client-id]]]]
|
||||||
|
```
|
||||||
|
|
||||||
|
The `user-name` value is simply passed in the `HelloRequest` message as payload and the value of
|
||||||
|
`client-id` is included in the JWT claims passed in the metadata header.
|
||||||
|
|
||||||
|
|
||||||
|
#### How to run the example:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run the server:
|
||||||
|
./build/install/example-jwt-auth/bin/auth-server 50051
|
||||||
|
# In another terminal run the client
|
||||||
|
./build/install/example-jwt-auth/bin/auth-client localhost 50051 userA clientB
|
||||||
|
```
|
||||||
|
|
||||||
|
That's it! The client will show the user-name reflected back in the message from the server as follows:
|
||||||
|
```
|
||||||
|
INFO: Greeting: Hello, userA
|
||||||
|
```
|
||||||
|
|
||||||
|
And on the server side you will see the message with the client's identifier:
|
||||||
|
```
|
||||||
|
Processing request from clientB
|
||||||
|
```
|
||||||
|
|
||||||
|
## Maven
|
||||||
|
|
||||||
|
If you prefer to use Maven follow these [steps](../README.md#maven). You can run the example as follows:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ # Run the server
|
||||||
|
$ mvn exec:java -Dexec.mainClass=io.grpc.examples.authentication.AuthServer -Dexec.args="50051"
|
||||||
|
$ # In another terminal run the client
|
||||||
|
$ mvn exec:java -Dexec.mainClass=io.grpc.examples.authentication.AuthClient -Dexec.args="localhost 50051 userA clientB"
|
||||||
|
```
|
||||||
|
|
@ -0,0 +1,84 @@
|
||||||
|
plugins {
|
||||||
|
// Provide convenience executables for trying out the examples.
|
||||||
|
id 'application'
|
||||||
|
// ASSUMES GRADLE 2.12 OR HIGHER. Use plugin version 0.7.5 with earlier gradle versions
|
||||||
|
id 'com.google.protobuf' version '0.8.8'
|
||||||
|
// Generate IntelliJ IDEA's .idea & .iml project files
|
||||||
|
id 'idea'
|
||||||
|
}
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
maven { // The google mirror is less flaky than mavenCentral()
|
||||||
|
url "https://maven-central.storage-download.googleapis.com/repos/central/data/"
|
||||||
|
}
|
||||||
|
mavenLocal()
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceCompatibility = 1.7
|
||||||
|
targetCompatibility = 1.7
|
||||||
|
|
||||||
|
// IMPORTANT: You probably want the non-SNAPSHOT version of gRPC. Make sure you
|
||||||
|
// are looking at a tagged version of the example and not "master"!
|
||||||
|
|
||||||
|
// Feel free to delete the comment at the next line. It is just for safely
|
||||||
|
// updating the version in our release process.
|
||||||
|
def grpcVersion = '1.29.0-SNAPSHOT' // CURRENT_GRPC_VERSION
|
||||||
|
def protobufVersion = '3.11.0'
|
||||||
|
def protocVersion = protobufVersion
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation "io.grpc:grpc-protobuf:${grpcVersion}"
|
||||||
|
implementation "io.grpc:grpc-stub:${grpcVersion}"
|
||||||
|
implementation "io.jsonwebtoken:jjwt:0.9.1"
|
||||||
|
implementation "javax.xml.bind:jaxb-api:2.3.1"
|
||||||
|
|
||||||
|
compileOnly "javax.annotation:javax.annotation-api:1.2"
|
||||||
|
|
||||||
|
runtimeOnly "io.grpc:grpc-netty-shaded:${grpcVersion}"
|
||||||
|
|
||||||
|
testImplementation "io.grpc:grpc-testing:${grpcVersion}"
|
||||||
|
testImplementation "junit:junit:4.12"
|
||||||
|
testImplementation "org.mockito:mockito-core:2.28.2"
|
||||||
|
}
|
||||||
|
|
||||||
|
protobuf {
|
||||||
|
protoc { artifact = "com.google.protobuf:protoc:${protocVersion}" }
|
||||||
|
plugins {
|
||||||
|
grpc { artifact = "io.grpc:protoc-gen-grpc-java:${grpcVersion}" }
|
||||||
|
}
|
||||||
|
generateProtoTasks {
|
||||||
|
all()*.plugins { grpc {} }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inform IDEs like IntelliJ IDEA, Eclipse or NetBeans about the generated code.
|
||||||
|
sourceSets {
|
||||||
|
main {
|
||||||
|
java {
|
||||||
|
srcDirs 'build/generated/source/proto/main/grpc'
|
||||||
|
srcDirs 'build/generated/source/proto/main/java'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
startScripts.enabled = false
|
||||||
|
|
||||||
|
task hellowWorldJwtAuthServer(type: CreateStartScripts) {
|
||||||
|
mainClassName = 'io.grpc.examples.jwtauth.AuthServer'
|
||||||
|
applicationName = 'auth-server'
|
||||||
|
outputDir = new File(project.buildDir, 'tmp')
|
||||||
|
classpath = startScripts.classpath
|
||||||
|
}
|
||||||
|
|
||||||
|
task hellowWorldJwtAuthClient(type: CreateStartScripts) {
|
||||||
|
mainClassName = 'io.grpc.examples.jwtauth.AuthClient'
|
||||||
|
applicationName = 'auth-client'
|
||||||
|
outputDir = new File(project.buildDir, 'tmp')
|
||||||
|
classpath = startScripts.classpath
|
||||||
|
}
|
||||||
|
|
||||||
|
applicationDistribution.into('bin') {
|
||||||
|
from(hellowWorldJwtAuthServer)
|
||||||
|
from(hellowWorldJwtAuthClient)
|
||||||
|
fileMode = 0755
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,136 @@
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
<groupId>io.grpc</groupId>
|
||||||
|
<artifactId>example-jwt-auth</artifactId>
|
||||||
|
<packaging>jar</packaging>
|
||||||
|
<!-- Feel free to delete the comment at the end of these lines. It is just
|
||||||
|
for safely updating the version in our release process. -->
|
||||||
|
<version>1.29.0-SNAPSHOT</version><!-- CURRENT_GRPC_VERSION -->
|
||||||
|
<name>example-jwt-auth</name>
|
||||||
|
<url>https://github.com/grpc/grpc-java</url>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
|
<grpc.version>1.29.0-SNAPSHOT</grpc.version><!-- CURRENT_GRPC_VERSION -->
|
||||||
|
<protobuf.version>3.11.0</protobuf.version>
|
||||||
|
<protoc.version>3.11.0</protoc.version>
|
||||||
|
<!-- required for jdk9 -->
|
||||||
|
<maven.compiler.source>1.7</maven.compiler.source>
|
||||||
|
<maven.compiler.target>1.7</maven.compiler.target>
|
||||||
|
</properties>
|
||||||
|
|
||||||
|
<dependencyManagement>
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.grpc</groupId>
|
||||||
|
<artifactId>grpc-bom</artifactId>
|
||||||
|
<version>${grpc.version}</version>
|
||||||
|
<type>pom</type>
|
||||||
|
<scope>import</scope>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
</dependencyManagement>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.grpc</groupId>
|
||||||
|
<artifactId>grpc-netty-shaded</artifactId>
|
||||||
|
<scope>runtime</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.grpc</groupId>
|
||||||
|
<artifactId>grpc-protobuf</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.grpc</groupId>
|
||||||
|
<artifactId>grpc-stub</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.jsonwebtoken</groupId>
|
||||||
|
<artifactId>jjwt</artifactId>
|
||||||
|
<version>0.9.1</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>javax.xml.bind</groupId>
|
||||||
|
<artifactId>jaxb-api</artifactId>
|
||||||
|
<version>2.3.1</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>javax.annotation</groupId>
|
||||||
|
<artifactId>javax.annotation-api</artifactId>
|
||||||
|
<version>1.2</version>
|
||||||
|
<scope>provided</scope> <!-- not needed at runtime -->
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.grpc</groupId>
|
||||||
|
<artifactId>grpc-testing</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>junit</groupId>
|
||||||
|
<artifactId>junit</artifactId>
|
||||||
|
<version>4.12</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.mockito</groupId>
|
||||||
|
<artifactId>mockito-core</artifactId>
|
||||||
|
<version>2.28.2</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
|
<build>
|
||||||
|
<extensions>
|
||||||
|
<extension>
|
||||||
|
<groupId>kr.motd.maven</groupId>
|
||||||
|
<artifactId>os-maven-plugin</artifactId>
|
||||||
|
<version>1.5.0.Final</version>
|
||||||
|
</extension>
|
||||||
|
</extensions>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.xolstice.maven.plugins</groupId>
|
||||||
|
<artifactId>protobuf-maven-plugin</artifactId>
|
||||||
|
<version>0.5.1</version>
|
||||||
|
<configuration>
|
||||||
|
<protocArtifact>
|
||||||
|
com.google.protobuf:protoc:${protoc.version}:exe:${os.detected.classifier}
|
||||||
|
</protocArtifact>
|
||||||
|
<pluginId>grpc-java</pluginId>
|
||||||
|
<pluginArtifact>
|
||||||
|
io.grpc:protoc-gen-grpc-java:${grpc.version}:exe:${os.detected.classifier}
|
||||||
|
</pluginArtifact>
|
||||||
|
</configuration>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<goals>
|
||||||
|
<goal>compile</goal>
|
||||||
|
<goal>compile-custom</goal>
|
||||||
|
</goals>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
</plugin>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-enforcer-plugin</artifactId>
|
||||||
|
<version>1.4.1</version>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<id>enforce</id>
|
||||||
|
<goals>
|
||||||
|
<goal>enforce</goal>
|
||||||
|
</goals>
|
||||||
|
<configuration>
|
||||||
|
<rules>
|
||||||
|
<requireUpperBoundDeps/>
|
||||||
|
</rules>
|
||||||
|
</configuration>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
</project>
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
pluginManagement {
|
||||||
|
repositories {
|
||||||
|
maven { // The google mirror is less flaky than mavenCentral()
|
||||||
|
url "https://maven-central.storage-download.googleapis.com/repos/central/data/"
|
||||||
|
}
|
||||||
|
gradlePluginPortal()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,120 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2019 The gRPC Authors
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.grpc.examples.jwtauth;
|
||||||
|
|
||||||
|
import io.grpc.CallCredentials;
|
||||||
|
import io.grpc.ManagedChannel;
|
||||||
|
import io.grpc.ManagedChannelBuilder;
|
||||||
|
import io.grpc.examples.helloworld.GreeterGrpc;
|
||||||
|
import io.grpc.examples.helloworld.HelloReply;
|
||||||
|
import io.grpc.examples.helloworld.HelloRequest;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An authenticating client that requests a greeting from the {@link AuthServer}.
|
||||||
|
*/
|
||||||
|
public class AuthClient {
|
||||||
|
|
||||||
|
private static final Logger logger = Logger.getLogger(AuthClient.class.getName());
|
||||||
|
|
||||||
|
private final ManagedChannel channel;
|
||||||
|
private final GreeterGrpc.GreeterBlockingStub blockingStub;
|
||||||
|
private final CallCredentials callCredentials;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct client for accessing GreeterGrpc server.
|
||||||
|
*/
|
||||||
|
AuthClient(CallCredentials callCredentials, String host, int port) {
|
||||||
|
this(
|
||||||
|
callCredentials,
|
||||||
|
ManagedChannelBuilder
|
||||||
|
.forAddress(host, port)
|
||||||
|
// Channels are secure by default (via SSL/TLS). For this example we disable TLS
|
||||||
|
// to avoid needing certificates, but it is recommended to use a secure channel
|
||||||
|
// while passing credentials.
|
||||||
|
.usePlaintext()
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct client for accessing GreeterGrpc server using the existing channel.
|
||||||
|
*/
|
||||||
|
AuthClient(CallCredentials callCredentials, ManagedChannel channel) {
|
||||||
|
this.callCredentials = callCredentials;
|
||||||
|
this.channel = channel;
|
||||||
|
this.blockingStub = GreeterGrpc.newBlockingStub(channel);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void shutdown() throws InterruptedException {
|
||||||
|
channel.shutdown().awaitTermination(5, TimeUnit.SECONDS);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Say hello to server.
|
||||||
|
*
|
||||||
|
* @param name name to set in HelloRequest
|
||||||
|
* @return the message in the HelloReply from the server
|
||||||
|
*/
|
||||||
|
public String greet(String name) {
|
||||||
|
logger.info("Will try to greet " + name + " ...");
|
||||||
|
HelloRequest request = HelloRequest.newBuilder().setName(name).build();
|
||||||
|
|
||||||
|
// Use a stub with the given call credentials applied to invoke the RPC.
|
||||||
|
HelloReply response =
|
||||||
|
blockingStub
|
||||||
|
.withCallCredentials(callCredentials)
|
||||||
|
.sayHello(request);
|
||||||
|
|
||||||
|
logger.info("Greeting: " + response.getMessage());
|
||||||
|
return response.getMessage();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Greet server. If provided, the first element of {@code args} is the name to use in the greeting
|
||||||
|
* and the second is the client identifier to set in JWT
|
||||||
|
*/
|
||||||
|
public static void main(String[] args) throws Exception {
|
||||||
|
|
||||||
|
String host = "localhost";
|
||||||
|
int port = 50051;
|
||||||
|
String user = "world";
|
||||||
|
String clientId = "default-client";
|
||||||
|
|
||||||
|
if (args.length > 0) {
|
||||||
|
host = args[0]; // Use the arg as the server host if provided
|
||||||
|
}
|
||||||
|
if (args.length > 1) {
|
||||||
|
port = Integer.parseInt(args[1]); // Use the second argument as the server port if provided
|
||||||
|
}
|
||||||
|
if (args.length > 2) {
|
||||||
|
user = args[2]; // Use the the third argument as the name to greet if provided
|
||||||
|
}
|
||||||
|
if (args.length > 3) {
|
||||||
|
clientId = args[3]; // Use the fourth argument as the client identifier if provided
|
||||||
|
}
|
||||||
|
|
||||||
|
CallCredentials credentials = new JwtCredential(clientId);
|
||||||
|
AuthClient client = new AuthClient(credentials, host, port);
|
||||||
|
|
||||||
|
try {
|
||||||
|
client.greet(user);
|
||||||
|
} finally {
|
||||||
|
client.shutdown();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,103 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2019 The gRPC Authors
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.grpc.examples.jwtauth;
|
||||||
|
|
||||||
|
import io.grpc.Server;
|
||||||
|
import io.grpc.ServerBuilder;
|
||||||
|
import io.grpc.examples.helloworld.GreeterGrpc;
|
||||||
|
import io.grpc.examples.helloworld.HelloReply;
|
||||||
|
import io.grpc.examples.helloworld.HelloRequest;
|
||||||
|
import io.grpc.stub.StreamObserver;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Server that manages startup/shutdown of a {@code Greeter} server. This also uses a {@link
|
||||||
|
* JwtServerInterceptor} to intercept the JWT token passed
|
||||||
|
*/
|
||||||
|
public class AuthServer {
|
||||||
|
|
||||||
|
private static final Logger logger = Logger.getLogger(AuthServer.class.getName());
|
||||||
|
|
||||||
|
private Server server;
|
||||||
|
private int port;
|
||||||
|
|
||||||
|
public AuthServer(int port) {
|
||||||
|
this.port = port;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void start() throws IOException {
|
||||||
|
server = ServerBuilder.forPort(port)
|
||||||
|
.addService(new GreeterImpl())
|
||||||
|
.intercept(new JwtServerInterceptor()) // add the JwtServerInterceptor
|
||||||
|
.build()
|
||||||
|
.start();
|
||||||
|
logger.info("Server started, listening on " + port);
|
||||||
|
Runtime.getRuntime().addShutdownHook(new Thread() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
// Use stderr here since the logger may have been reset by its JVM shutdown hook.
|
||||||
|
System.err.println("*** shutting down gRPC server since JVM is shutting down");
|
||||||
|
AuthServer.this.stop();
|
||||||
|
System.err.println("*** server shut down");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void stop() {
|
||||||
|
if (server != null) {
|
||||||
|
server.shutdown();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Await termination on the main thread since the grpc library uses daemon threads.
|
||||||
|
*/
|
||||||
|
private void blockUntilShutdown() throws InterruptedException {
|
||||||
|
if (server != null) {
|
||||||
|
server.awaitTermination();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main launches the server from the command line.
|
||||||
|
*/
|
||||||
|
public static void main(String[] args) throws IOException, InterruptedException {
|
||||||
|
|
||||||
|
// The port on which the server should run
|
||||||
|
int port = 50051; // default
|
||||||
|
if (args.length > 0) {
|
||||||
|
port = Integer.parseInt(args[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
final AuthServer server = new AuthServer(port);
|
||||||
|
server.start();
|
||||||
|
server.blockUntilShutdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
static class GreeterImpl extends GreeterGrpc.GreeterImplBase {
|
||||||
|
@Override
|
||||||
|
public void sayHello(HelloRequest req, StreamObserver<HelloReply> responseObserver) {
|
||||||
|
// get client id added to context by interceptor
|
||||||
|
String clientId = Constant.CLIENT_ID_CONTEXT_KEY.get();
|
||||||
|
logger.info("Processing request from " + clientId);
|
||||||
|
HelloReply reply = HelloReply.newBuilder().setMessage("Hello, " + req.getName()).build();
|
||||||
|
responseObserver.onNext(reply);
|
||||||
|
responseObserver.onCompleted();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2019 The gRPC Authors
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.grpc.examples.jwtauth;
|
||||||
|
|
||||||
|
import io.grpc.Context;
|
||||||
|
import io.grpc.Metadata;
|
||||||
|
|
||||||
|
import static io.grpc.Metadata.ASCII_STRING_MARSHALLER;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constants definition
|
||||||
|
*/
|
||||||
|
final class Constant {
|
||||||
|
static final String JWT_SIGNING_KEY = "L8hHXsaQOUjk5rg7XPGv4eL36anlCrkMz8CJ0i/8E/0=";
|
||||||
|
static final String BEARER_TYPE = "Bearer";
|
||||||
|
|
||||||
|
static final Metadata.Key<String> AUTHORIZATION_METADATA_KEY = Metadata.Key.of("Authorization", ASCII_STRING_MARSHALLER);
|
||||||
|
static final Context.Key<String> CLIENT_ID_CONTEXT_KEY = Context.key("clientId");
|
||||||
|
|
||||||
|
private Constant() {
|
||||||
|
throw new AssertionError();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,68 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2019 The gRPC Authors
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.grpc.examples.jwtauth;
|
||||||
|
|
||||||
|
import io.grpc.CallCredentials;
|
||||||
|
import io.grpc.Metadata;
|
||||||
|
import io.grpc.Status;
|
||||||
|
import io.jsonwebtoken.Jwts;
|
||||||
|
import io.jsonwebtoken.SignatureAlgorithm;
|
||||||
|
import java.util.concurrent.Executor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CallCredentials implementation, which carries the JWT value that will be propagated to the
|
||||||
|
* server in the request metadata with the "Authorization" key and the "Bearer" prefix.
|
||||||
|
*/
|
||||||
|
public class JwtCredential extends CallCredentials {
|
||||||
|
|
||||||
|
private final String subject;
|
||||||
|
|
||||||
|
JwtCredential(String subject) {
|
||||||
|
this.subject = subject;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void applyRequestMetadata(final RequestInfo requestInfo, final Executor executor,
|
||||||
|
final MetadataApplier metadataApplier) {
|
||||||
|
// Make a JWT compact serialized string.
|
||||||
|
// This example omits setting the expiration, but a real application should do it.
|
||||||
|
final String jwt =
|
||||||
|
Jwts.builder()
|
||||||
|
.setSubject(subject)
|
||||||
|
.signWith(SignatureAlgorithm.HS256, Constant.JWT_SIGNING_KEY)
|
||||||
|
.compact();
|
||||||
|
|
||||||
|
executor.execute(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
try {
|
||||||
|
Metadata headers = new Metadata();
|
||||||
|
headers.put(Constant.AUTHORIZATION_METADATA_KEY,
|
||||||
|
String.format("%s %s", Constant.BEARER_TYPE, jwt));
|
||||||
|
metadataApplier.apply(headers);
|
||||||
|
} catch (Throwable e) {
|
||||||
|
metadataApplier.fail(Status.UNAUTHENTICATED.withCause(e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void thisUsesUnstableApi() {
|
||||||
|
// noop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,76 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2019 The gRPC Authors
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.grpc.examples.jwtauth;
|
||||||
|
|
||||||
|
import io.grpc.Context;
|
||||||
|
import io.grpc.Contexts;
|
||||||
|
import io.grpc.Metadata;
|
||||||
|
import io.grpc.ServerCall;
|
||||||
|
import io.grpc.ServerCallHandler;
|
||||||
|
import io.grpc.ServerInterceptor;
|
||||||
|
import io.grpc.Status;
|
||||||
|
import io.jsonwebtoken.Claims;
|
||||||
|
import io.jsonwebtoken.Jws;
|
||||||
|
import io.jsonwebtoken.JwtException;
|
||||||
|
import io.jsonwebtoken.JwtParser;
|
||||||
|
import io.jsonwebtoken.Jwts;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This interceptor gets the JWT from the metadata, verifies it and sets the client identifier
|
||||||
|
* obtained from the token into the context. In order not to complicate the example with additional
|
||||||
|
* checks (expiration date, issuer and etc.), it relies only on the signature of the token for
|
||||||
|
* verification.
|
||||||
|
*/
|
||||||
|
public class JwtServerInterceptor implements ServerInterceptor {
|
||||||
|
|
||||||
|
private JwtParser parser = Jwts.parser().setSigningKey(Constant.JWT_SIGNING_KEY);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(ServerCall<ReqT, RespT> serverCall,
|
||||||
|
Metadata metadata, ServerCallHandler<ReqT, RespT> serverCallHandler) {
|
||||||
|
String value = metadata.get(Constant.AUTHORIZATION_METADATA_KEY);
|
||||||
|
|
||||||
|
Status status = Status.OK;
|
||||||
|
if (value == null) {
|
||||||
|
status = Status.UNAUTHENTICATED.withDescription("Authorization token is missing");
|
||||||
|
} else if (!value.startsWith(Constant.BEARER_TYPE)) {
|
||||||
|
status = Status.UNAUTHENTICATED.withDescription("Unknown authorization type");
|
||||||
|
} else {
|
||||||
|
Jws<Claims> claims = null;
|
||||||
|
// remove authorization type prefix
|
||||||
|
String token = value.substring(Constant.BEARER_TYPE.length()).trim();
|
||||||
|
try {
|
||||||
|
// verify token signature and parse claims
|
||||||
|
claims = parser.parseClaimsJws(token);
|
||||||
|
} catch (JwtException e) {
|
||||||
|
status = Status.UNAUTHENTICATED.withDescription(e.getMessage()).withCause(e);
|
||||||
|
}
|
||||||
|
if (claims != null) {
|
||||||
|
// set client id into current context
|
||||||
|
Context ctx = Context.current()
|
||||||
|
.withValue(Constant.CLIENT_ID_CONTEXT_KEY, claims.getBody().getSubject());
|
||||||
|
return Contexts.interceptCall(ctx, serverCall, metadata, serverCallHandler);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
serverCall.close(status, new Metadata());
|
||||||
|
return new ServerCall.Listener<ReqT>() {
|
||||||
|
// noop
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
// Copyright 2019 The gRPC Authors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
syntax = "proto3";
|
||||||
|
|
||||||
|
option java_multiple_files = true;
|
||||||
|
option java_package = "io.grpc.examples.helloworld";
|
||||||
|
option java_outer_classname = "HelloWorldProto";
|
||||||
|
option objc_class_prefix = "HLW";
|
||||||
|
|
||||||
|
package helloworld;
|
||||||
|
|
||||||
|
// The greeting service definition.
|
||||||
|
service Greeter {
|
||||||
|
// Sends a greeting
|
||||||
|
rpc SayHello (HelloRequest) returns (HelloReply) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The request message containing the user's name.
|
||||||
|
message HelloRequest {
|
||||||
|
string name = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The response message containing the greetings
|
||||||
|
message HelloReply {
|
||||||
|
string message = 1;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,118 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2019 The gRPC Authors
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.grpc.examples.jwtauth;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertNotNull;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
import static org.mockito.AdditionalAnswers.delegatesTo;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
|
||||||
|
import io.grpc.CallCredentials;
|
||||||
|
import io.grpc.ManagedChannel;
|
||||||
|
import io.grpc.Metadata;
|
||||||
|
import io.grpc.ServerCall;
|
||||||
|
import io.grpc.ServerCallHandler;
|
||||||
|
import io.grpc.ServerInterceptors;
|
||||||
|
import io.grpc.examples.helloworld.GreeterGrpc;
|
||||||
|
import io.grpc.examples.helloworld.HelloReply;
|
||||||
|
import io.grpc.examples.helloworld.HelloRequest;
|
||||||
|
import io.grpc.inprocess.InProcessChannelBuilder;
|
||||||
|
import io.grpc.inprocess.InProcessServerBuilder;
|
||||||
|
import io.grpc.ServerCall.Listener;
|
||||||
|
import io.grpc.ServerInterceptor;
|
||||||
|
import io.grpc.stub.StreamObserver;
|
||||||
|
import io.grpc.testing.GrpcCleanupRule;
|
||||||
|
import java.io.IOException;
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.Rule;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
import org.junit.runners.JUnit4;
|
||||||
|
import org.mockito.ArgumentCaptor;
|
||||||
|
import org.mockito.ArgumentMatchers;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unit tests for {@link AuthClient} testing the default and non-default tokens
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
@RunWith(JUnit4.class)
|
||||||
|
public class AuthClientTest {
|
||||||
|
/**
|
||||||
|
* This rule manages automatic graceful shutdown for the registered servers and channels at the
|
||||||
|
* end of test.
|
||||||
|
*/
|
||||||
|
@Rule
|
||||||
|
public final GrpcCleanupRule grpcCleanup = new GrpcCleanupRule();
|
||||||
|
|
||||||
|
private final ServerInterceptor mockServerInterceptor = mock(ServerInterceptor.class, delegatesTo(
|
||||||
|
new ServerInterceptor() {
|
||||||
|
@Override
|
||||||
|
public <ReqT, RespT> Listener<ReqT> interceptCall(
|
||||||
|
ServerCall<ReqT, RespT> call, Metadata headers, ServerCallHandler<ReqT, RespT> next) {
|
||||||
|
return next.startCall(call, headers);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
private AuthClient client;
|
||||||
|
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void setUp() throws IOException {
|
||||||
|
// Generate a unique in-process server name.
|
||||||
|
String serverName = InProcessServerBuilder.generateName();
|
||||||
|
|
||||||
|
// Create a server, add service, start, and register for automatic graceful shutdown.
|
||||||
|
grpcCleanup.register(InProcessServerBuilder.forName(serverName).directExecutor()
|
||||||
|
.addService(ServerInterceptors.intercept(
|
||||||
|
new GreeterGrpc.GreeterImplBase() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void sayHello(
|
||||||
|
HelloRequest request, StreamObserver<HelloReply> responseObserver) {
|
||||||
|
HelloReply reply = HelloReply.newBuilder()
|
||||||
|
.setMessage("AuthClientTest user=" + request.getName()).build();
|
||||||
|
responseObserver.onNext(reply);
|
||||||
|
responseObserver.onCompleted();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mockServerInterceptor))
|
||||||
|
.build().start());
|
||||||
|
|
||||||
|
CallCredentials credentials = new JwtCredential("test-client");
|
||||||
|
ManagedChannel channel = InProcessChannelBuilder.forName(serverName).directExecutor().build();
|
||||||
|
client = new AuthClient(credentials, channel);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void greet() {
|
||||||
|
ArgumentCaptor<Metadata> metadataCaptor = ArgumentCaptor.forClass(Metadata.class);
|
||||||
|
String retVal = client.greet("John");
|
||||||
|
|
||||||
|
verify(mockServerInterceptor).interceptCall(
|
||||||
|
ArgumentMatchers.<ServerCall<HelloRequest, HelloReply>>any(),
|
||||||
|
metadataCaptor.capture(),
|
||||||
|
ArgumentMatchers.<ServerCallHandler<HelloRequest, HelloReply>>any());
|
||||||
|
|
||||||
|
String token = metadataCaptor.getValue().get(Constant.AUTHORIZATION_METADATA_KEY);
|
||||||
|
assertNotNull(token);
|
||||||
|
assertTrue(token.startsWith("Bearer"));
|
||||||
|
assertEquals("AuthClientTest user=John", retVal);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue