Examples: Add a JWT authentication example (#5154)

This commit is contained in:
sanjaypujare 2018-12-13 12:26:39 -08:00 committed by GitHub
parent 2ffc46d6fa
commit ac52e27b2a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 583 additions and 0 deletions

View File

@ -0,0 +1,78 @@
Authentication Example
==============================================
This example illustrates a simple JWT-like credential based authentication implementation in gRPC using
client and server interceptors. For simplicity a simple string value is used instead of an actual
string-encoded JWT.
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/authentication). Please follow the
[steps](./README.md#to-build-the-examples) to build the examples. The build creates scripts
`auth-server` and `auth-client` in the `build/install/examples/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**:
```text
USAGE: AuthServer
```
The auth-client accepts optional arguments for user-name and token-value:
**auth-client**:
```text
USAGE: AuthClient [user-name] [token-value]
```
The `user-name` value is simply passed in the `HelloRequest` message as payload and the value of
`payload` is passed in the metadata header as a string value signifying an authentication token.
#### How to run the example:
```bash
# Run the server:
./build/install/examples/bin/auth-server
# In another terminal run the client
./build/install/examples/bin/auth-client client-userA token-valueB
```
That's it! The client will show the user-name reflected back in the message from the server as follows:
```
INFO: Greeting: Hello Authenticated client-userA
```
And on the server side you will see the token value sent by the client:
```
Token: token-valueB
```
## 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
$ # In another terminal run the client
$ mvn exec:java -Dexec.mainClass=io.grpc.examples.authentication.AuthClient -Dexec.args="client-userA token-valueB"
```
## Bazel
If you prefer to use Bazel:
```
(With Bazel v0.8.0 or above.)
$ bazel build :auth-server :auth-client
$ # Run the server
$ bazel-bin/auth-server
$ # In another terminal run the client
$ bazel-bin/auth-client client-userA token-valueB
```

View File

@ -98,6 +98,24 @@ java_binary(
],
)
java_binary(
name = "auth-client",
testonly = 1,
main_class = "io.grpc.examples.authentication.AuthClient",
runtime_deps = [
":examples",
],
)
java_binary(
name = "auth-server",
testonly = 1,
main_class = "io.grpc.examples.authentication.AuthServer",
runtime_deps = [
":examples",
],
)
java_binary(
name = "route-guide-client",
testonly = 1,

View File

@ -25,6 +25,9 @@ before trying out the examples.
- [Json serialization](src/main/java/io/grpc/examples/advanced)
- [Authentication](AUTHENTICATION_EXAMPLE.md)
### To build the examples
1. **[Install gRPC Java library SNAPSHOT locally, including code generation plugin](../COMPILING.md) (Only need this step for non-released versions, e.g. master HEAD).**

View File

@ -91,6 +91,20 @@ task helloWorldClient(type: CreateStartScripts) {
classpath = startScripts.classpath
}
task authServer(type: CreateStartScripts) {
mainClassName = 'io.grpc.examples.authentication.AuthServer'
applicationName = 'auth-server'
outputDir = new File(project.buildDir, 'tmp')
classpath = startScripts.classpath
}
task authClient(type: CreateStartScripts) {
mainClassName = 'io.grpc.examples.authentication.AuthClient'
applicationName = 'auth-client'
outputDir = new File(project.buildDir, 'tmp')
classpath = startScripts.classpath
}
task compressingHelloWorldClient(type: CreateStartScripts) {
mainClassName = 'io.grpc.examples.experimental.CompressingHelloWorldClient'
applicationName = 'compressing-hello-world-client'
@ -103,6 +117,8 @@ applicationDistribution.into('bin') {
from(routeGuideClient)
from(helloWorldServer)
from(helloWorldClient)
from(authServer)
from(authClient)
from(compressingHelloWorldClient)
fileMode = 0755
}

View File

@ -0,0 +1,109 @@
/*
* Copyright 2018 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.authentication;
import io.grpc.ManagedChannel;
import io.grpc.ManagedChannelBuilder;
import io.grpc.StatusRuntimeException;
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.Level;
import java.util.logging.Logger;
/**
* An authenticating client that requests a greeting from the {@link AuthServer}.
* It uses a {@link JwtClientInterceptor} to inject a JWT credential.
*/
public class AuthClient {
private static final Logger logger = Logger.getLogger(AuthClient.class.getName());
private final ManagedChannel channel;
private final GreeterGrpc.GreeterBlockingStub blockingStub;
private final JwtClientInterceptor jwtClientInterceptor = new JwtClientInterceptor();
/** Construct client connecting to AuthServer at {@code host:port} using a client-interceptor
* to inject the JWT credentials
*/
public AuthClient(String host, int port) {
this(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());
}
/** Construct client for accessing GreeterGrpc server using the existing channel. */
AuthClient(ManagedChannelBuilder builder) {
this.channel = builder
.intercept(jwtClientInterceptor)
.build();
blockingStub = GreeterGrpc.newBlockingStub(channel);
}
public void shutdown() throws InterruptedException {
channel.shutdown().awaitTermination(5, TimeUnit.SECONDS);
}
public void setTokenValue(String tokenValue) {
jwtClientInterceptor.setTokenValue(tokenValue);
}
/**
* 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();
HelloReply response;
try {
response = blockingStub.sayHello(request);
} catch (StatusRuntimeException e) {
logger.log(Level.WARNING, "RPC failed: {0}", e.getStatus());
return e.toString();
}
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.
*/
public static void main(String[] args) throws Exception {
AuthClient client = new AuthClient("localhost", 50051);
try {
/* Access a service running on the local machine on port 50051 */
String user = "world";
if (args.length > 0) {
user = args[0]; /* Use the arg as the name to greet if provided */
}
if (args.length > 1) {
client.setTokenValue(args[1]);
}
client.greet(user);
} finally {
client.shutdown();
}
}
}

View File

@ -0,0 +1,91 @@
/*
* Copyright 2018 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.authentication;
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 void start() throws IOException {
/* The port on which the server should run */
int port = 50051;
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 {
final AuthServer server = new AuthServer();
server.start();
server.blockUntilShutdown();
}
static class GreeterImpl extends GreeterGrpc.GreeterImplBase {
@Override
public void sayHello(HelloRequest req, StreamObserver<HelloReply> responseObserver) {
HelloReply reply = HelloReply.newBuilder().setMessage("Hello Authenticated " + req.getName()).build();
responseObserver.onNext(reply);
responseObserver.onCompleted();
}
}
}

View File

@ -0,0 +1,32 @@
/*
* Copyright 2018 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.authentication;
import io.grpc.Metadata;
import static io.grpc.Metadata.ASCII_STRING_MARSHALLER;
/**
* Constants definition
*/
public final class Constant {
private Constant() {
}
public static final Metadata.Key<String> JWT_METADATA_KEY = Metadata.Key.of("jwt", ASCII_STRING_MARSHALLER);
}

View File

@ -0,0 +1,58 @@
/*
* Copyright 2018 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.authentication;
import io.grpc.CallOptions;
import io.grpc.Channel;
import io.grpc.ClientCall;
import io.grpc.ClientInterceptor;
import io.grpc.ForwardingClientCall;
import io.grpc.Metadata;
import io.grpc.MethodDescriptor;
/**
* Use a {@link ClientInterceptor} to insert a JWT in the client to outgoing calls, that's designed
* specifically to append credentials information. A token might expire, so for each call, if the
* token has expired, you can proactively refresh it.
*
* Every time a call takes place, the handler will execute, and the
* {@link io.grpc.CallCredentials2.MetadataApplier}
* is applied to add a header. The method should not block: to refresh or fetch a token from the
* network, it should be done asynchronously.
*/
public class JwtClientInterceptor implements ClientInterceptor {
private String tokenValue = "my-default-token";
public void setTokenValue(String tokenValue) {
this.tokenValue = tokenValue;
}
@Override
public <ReqT, RespT> ClientCall<ReqT, RespT> interceptCall(
MethodDescriptor<ReqT, RespT> methodDescriptor, CallOptions callOptions,
Channel channel) {
return new ForwardingClientCall.SimpleForwardingClientCall<ReqT, RespT>(
channel.newCall(methodDescriptor, callOptions)) {
@Override
public void start(Listener<RespT> responseListener, Metadata headers) {
headers.put(Constant.JWT_METADATA_KEY, tokenValue);
super.start(responseListener, headers);
}
};
}
}

View File

@ -0,0 +1,42 @@
/*
* Copyright 2018 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.authentication;
import io.grpc.Metadata;
import io.grpc.ServerCall;
import io.grpc.ServerCallHandler;
import io.grpc.ServerInterceptor;
/**
* Use a {@link ServerInterceptor} to capture metadata and retrieve any JWT token.
*
* This interceptor only captures the JWT token and prints it out.
* Normally the token will need to be validated against an identity provider.
*/
public class JwtServerInterceptor implements ServerInterceptor {
@Override
public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(ServerCall<ReqT, RespT> serverCall, Metadata metadata, ServerCallHandler<ReqT, RespT> serverCallHandler) {
// Get token from Metadata
// Capture the JWT token and just print it out.
String token = metadata.get(Constant.JWT_METADATA_KEY);
System.out.println("Token: " + token);
return serverCallHandler.startCall(serverCall, metadata);
}
}

View File

@ -0,0 +1,136 @@
/*
* Copyright 2015 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.authentication;
import io.grpc.*;
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.testing.GrpcCleanupRule;
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.Matchers;
import java.io.IOException;
import static org.junit.Assert.assertEquals;
import static org.mockito.AdditionalAnswers.delegatesTo;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
/**
* 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(io.grpc.examples.helloworld.HelloRequest request,
io.grpc.stub.StreamObserver<io.grpc.examples.helloworld.HelloReply> responseObserver) {
HelloReply reply = HelloReply.newBuilder().setMessage("AuthClientTest user=" + request.getName()).build();
responseObserver.onNext(reply);
responseObserver.onCompleted();
}
}, mockServerInterceptor))
.build().start());
// Create an AuthClient using the in-process channel;
client = new AuthClient(InProcessChannelBuilder.forName(serverName).directExecutor());
}
/**
* Test default JWT token used
*
* @throws Exception
*/
@Test
public void defaultTokenDeliveredToServer() throws Exception {
ArgumentCaptor<Metadata> metadataCaptor = ArgumentCaptor.forClass(Metadata.class);
ArgumentCaptor<HelloRequest> requestCaptor = ArgumentCaptor.forClass(HelloRequest.class);
String retVal = client.greet("default token test");
verify(mockServerInterceptor).interceptCall(
Matchers.<ServerCall<HelloRequest, HelloReply>>any(),
metadataCaptor.capture(),
Matchers.<ServerCallHandler<HelloRequest, HelloReply>>any());
assertEquals(
"my-default-token",
metadataCaptor.getValue().get(Constant.JWT_METADATA_KEY));
assertEquals("AuthClientTest user=default token test", retVal);
}
/**
* Test non-default JWT token used
*
* @throws Exception
*/
@Test
public void nonDefaultTokenDeliveredToServer() throws Exception {
ArgumentCaptor<Metadata> metadataCaptor = ArgumentCaptor.forClass(Metadata.class);
ArgumentCaptor<HelloRequest> requestCaptor = ArgumentCaptor.forClass(HelloRequest.class);
client.setTokenValue("non-default-token");
String retVal = client.greet("non default token test");
verify(mockServerInterceptor).interceptCall(
Matchers.<ServerCall<HelloRequest, HelloReply>>any(),
metadataCaptor.capture(),
Matchers.<ServerCallHandler<HelloRequest, HelloReply>>any());
assertEquals(
"non-default-token",
metadataCaptor.getValue().get(Constant.JWT_METADATA_KEY));
assertEquals("AuthClientTest user=non default token test", retVal);
}
}