From b6eb5ec5466686b3499599202ff2c0cb8acb6859 Mon Sep 17 00:00:00 2001 From: Ace Nassri Date: Mon, 10 Jul 2017 16:02:21 -0700 Subject: [PATCH] Add Endpoints gRPC + JWT sample (#419) * Add Endpoints gRPC + JWT sample * Report parameter error through yargs instead of throwing it * Fix lint * Address comments * Address jeffmendoza's comments --- endpoints/getting-started-grpc/README.md | 72 ++++++++++++++----- .../getting-started-grpc/api_config.jwt.yaml | 61 ++++++++++++++++ .../{api_config.yaml => api_config.key.yaml} | 0 endpoints/getting-started-grpc/client.js | 28 ++++++-- endpoints/getting-started-grpc/package.json | 12 ++-- .../system-test/endpoints.test.js | 61 ++++++++++++++-- 6 files changed, 203 insertions(+), 31 deletions(-) create mode 100644 endpoints/getting-started-grpc/api_config.jwt.yaml rename endpoints/getting-started-grpc/{api_config.yaml => api_config.key.yaml} (100%) diff --git a/endpoints/getting-started-grpc/README.md b/endpoints/getting-started-grpc/README.md index fb02a819f2..e03845d69d 100644 --- a/endpoints/getting-started-grpc/README.md +++ b/endpoints/getting-started-grpc/README.md @@ -3,7 +3,7 @@ This sample demonstrates how to use Google Cloud Endpoints with Node.js. For a complete walkthrough showing how to run this sample in different -environments, see the [Google Cloud Endpoints Quickstarts](https://cloud.google.com/endpoints/docs/quickstarts). +environments, see the [Google Cloud Endpoints Quickstarts][docs_quickstart]. ## Running locally @@ -19,19 +19,26 @@ $ node client.js -h localhost:50051 ## Running on Google Cloud Platform ### Setup -Make sure you have [gcloud](https://cloud.google.com/sdk/gcloud/) and [Node.js](https://nodejs.org/) installed. +Make sure you have [gcloud][gcloud] and [Node.js][nodejs] installed. To update `gcloud`, use the `gcloud components update` command. +### Selecting an authentication method +1. Determine the appropriate API configuration file to use based on your authentication method. +- [JSON Web Tokens][jwt_io]: use `api_config.jwt.yaml` +- [API keys][gcp_api_key]: use `api_config.key.yaml` + +2. Rename the `api_config.*.yaml` file you chose in Step 1 to `api_config.yaml`. + ### Deploying to Endpoints -1. Install [protoc](https://github.com/google/protobuf/#protocol-compiler-installation). +1. Install [protoc][protoc]. 1. Compile the proto file using protoc. ``` $ protoc --include_imports --include_source_info protos/helloworld.proto --descriptor_set_out out.pb ``` -1. In `api_config.yaml`, replace `MY_PROJECT_ID` with your Project ID. +1. In `api_config.yaml`, replace `MY_PROJECT_ID` and `SERVICE-ACCOUNT-ID` with your Project ID and your service account's email address respectively. 1. Deploy your service's configuration to Endpoints. Take note of your service's config ID and name once the deployment completes. ``` @@ -47,11 +54,11 @@ $ gcloud container builds submit --tag gcr.io/[YOUR_PROJECT_ID]/endpoints-exampl ### Running your service #### Compute Engine -1. [Create](https://console.cloud.google.com/compute/instancesAdd) a Compute Engine instance. Be sure to check **Allow HTTP traffic** and **Allow HTTPS traffic** when creating the instance. +1. [Create][console_gce_create] a Compute Engine instance. Be sure to check **Allow HTTP traffic** and **Allow HTTPS traffic** when creating the instance. 1. Once your instance is created, take note of its IP address. -Note: this IP address is _ephemeral_ by default, and may change unexpectedly. If you plan to use this instance in the future, [reserve a static IP address](https://cloud.google.com/compute/docs/configure-ip-addresses#reserve_new_static) instead. +Note: this IP address is _ephemeral_ by default, and may change unexpectedly. If you plan to use this instance in the future, [reserve a static IP address][docs_gce_static_ip] instead. 1. SSH into your instance, and install Docker. ``` @@ -75,7 +82,7 @@ $ sudo docker run --detach --name=esp \ -a grpc://helloworld:50051 ``` -1. On your local machine, use the client to test your Endpoints deployment. Replace `[YOUR_INSTANCE_IP_ADDRESS]` with your instance's external IP address, and `[YOUR_API_KEY]` with a [valid Google Cloud Platform API key](https://support.google.com/cloud/answer/6158862?hl=en). +1. On your local machine, use the client to test your Endpoints deployment. Replace `[YOUR_INSTANCE_IP_ADDRESS]` with your instance's external IP address, and `[YOUR_API_KEY]` with a [valid Google Cloud Platform API key][gcp_api_key]. ``` $ node client.js -h [YOUR_INSTANCE_IP_ADDRESS]:80 -k [YOUR_API_KEY] ``` @@ -86,7 +93,7 @@ $ node client.js -h [YOUR_INSTANCE_IP_ADDRESS]:80 -k [YOUR_API_KEY] $ gcloud components install kubectl ``` -1. [Create](https://console.cloud.google.com/kubernetes/add) a container cluster with the default settings. Remember the cluster's name and zone, as you will need these later. +1. [Create][console_gke_create] a container cluster with the default settings. Remember the cluster's name and zone, as you will need these later. 1. Configure `kubectl` to have access to the cluster. Replace `[YOUR_CLUSTER_NAME]` and `[YOUR_CLUSTER_ZONE]` with your cluster's name and zone respectively. @@ -96,7 +103,7 @@ $ gcloud container clusters get-credentials [YOUR_CLUSTER_NAME] --zone [YOUR_CLU 1. Edit the `container_engine.yaml` file, and replace `GCLOUD_PROJECT`, `SERVICE_NAME`, and `SERVICE_CONFIG` with your Project ID and your Endpoints service's name and config ID respectively. -1. Add a [Kubernetes service](https://kubernetes.io/docs/user-guide/services/) to the cluster you created. Note that Kubernetes services should not be confused with [Endpoints services](https://cloud.google.com/endpoints/docs/grpc). +1. Add a [Kubernetes service][docs_k8s_services] to the cluster you created. Note that Kubernetes services should not be confused with [Endpoints services][docs_endpoints_services]. ``` $ kubectl create -f container-engine.yaml ``` @@ -106,17 +113,50 @@ $ kubectl create -f container-engine.yaml $ kubectl get service ``` -1. Use the client to test your Endpoints deployment. Replace `[YOUR_CLUSTER_IP_ADDRESS]` with your service's external IP address, and `[YOUR_API_KEY]` with a [valid Google Cloud Platform API key](https://support.google.com/cloud/answer/6158862?hl=en). -``` -$ node client.js -h [YOUR_CLUSTER_IP_ADDRESS]:80 -k [YOUR_API_KEY] -``` +### Testing your service +You can use the included client to test your Endpoints deployment. + +1. Determine your service's IP address. + +* If your service is hosted on Compute Engine, this will be your _instance's_ external IP address. + +* If your service is hosted on Container Engine, this will be your _service's_ external IP address. + +2. Run the client to connect to your service. When running the following commands, replace `[YOUR_IP_ADDRESS]` with the IP address you found in Step 1. + + * If you're using an API key, run the following command and replace `[YOUR_API_KEY]` with the appropriate [API key][gcp_api_key]. + ``` + $ node client.js -h [YOUR_CLUSTER_IP_ADDRESS]:80 -k [YOUR_API_KEY] + ``` + + * If you're using a [JSON Web Token][jwt_io], run the following command and replace `[YOUR_JWT_AUTHTOKEN]` with a valid JSON Web Token. + ``` + $ node client.js -h [YOUR_CLUSTER_IP_ADDRESS]:80 -j [YOUR_JWT_AUTHTOKEN] + ``` ## Cleanup -If you do not intend to use the resources you created for this tutorial in the future, delete your [VM instances](https://console.cloud.google.com/compute/instances) and/or [container clusters](https://console.cloud.google.com/kubernetes/list) to prevent additional charges. +If you do not intend to use the resources you created for this tutorial in the future, delete your [VM instances][console_gce_instances] and/or [container clusters][console_gke_instances] to prevent additional charges. ## Troubleshooting If you're having issues with this tutorial, here are some things to try: -- [Check](https://console.cloud.google.com/logs/viewer) your VM instance's/cluster's logs +- [Check][console_logs] your GCE/GKE instance's logs - Make sure your Compute Engine instance's [firewall](https://console.cloud.google.com/networking/firewalls/list) permits TCP access to port 80 -If those suggestions don't solve your problem, please [let us know](https://github.com/GoogleCloudPlatform/nodejs-docs-samples/issues) or [submit a PR](https://github.com/GoogleCloudPlatform/nodejs-docs-samples/pulls). \ No newline at end of file +If those suggestions don't solve your problem, please [let us know][github_issues] or [submit a pull request][github_pulls]. + +[nodejs]: https://nodejs.org/ +[gcloud]: https://cloud.google.com/sdk/gcloud/ +[jwt_io]: https://jwt.io +[protoc]: https://github.com/google/protobuf/#protocol-compiler-installation +[gcp_api_key]: https://support.google.com/cloud/answer/6158862?hl=en +[github_issues]: https://github.com/GoogleCloudPlatform/nodejs-docs-samples/issues +[github_pulls]: https://github.com/GoogleCloudPlatform/nodejs-docs-samples/pulls +[console_gce_instances]: https://console.cloud.google.com/compute/instances +[console_gce_create]: https://console.cloud.google.com/compute/instancesAdd +[console_gke_instances]: https://console.cloud.google.com/kubernetes/list +[console_gke_create]: https://console.cloud.google.com/kubernetes/add +[console_logs]: https://console.cloud.google.com/logs/viewer +[docs_k8s_services]: https://kubernetes.io/docs/user-guide/services/ +[docs_endpoints_services]: https://cloud.google.com/endpoints/docs/grpc +[docs_gce_static_ip]: https://cloud.google.com/compute/docs/configure-ip-addresses#reserve_new_static +[docs_quickstart]: https://cloud.google.com/endpoints/docs/quickstarts diff --git a/endpoints/getting-started-grpc/api_config.jwt.yaml b/endpoints/getting-started-grpc/api_config.jwt.yaml new file mode 100644 index 0000000000..425cd888e8 --- /dev/null +++ b/endpoints/getting-started-grpc/api_config.jwt.yaml @@ -0,0 +1,61 @@ +# Copyright 2017 Google Inc. +# +# 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. + +# +# An example API configuration. +# +# Below, replace MY_PROJECT_ID with your Google Cloud Project ID. +# + +# The configuration schema is defined by service.proto file +# https://github.com/googleapis/googleapis/blob/master/google/api/service.proto +type: google.api.Service +config_version: 3 + +# +# Name of the service configuration. +# +name: hellogrpc.endpoints.MY_PROJECT_ID.cloud.goog + +# +# API title to appear in the user interface (Google Cloud Console). +# +title: Hello gRPC API +apis: +- name: helloworld.Greeter + +# +# API usage restrictions +# +usage: + rules: + # None of these API methods require an API key + # N.B: JWTs are not a substitute for API keys + - selector: "*" + allow_unregistered_calls: true + +# +# Request authentication (in this case, a JWT) +# +authentication: + providers: + - id: google_service_account + # Replace SERVICE-ACCOUNT-ID with your service account's email address. + issuer: SERVICE-ACCOUNT-ID + jwks_uri: https://www.googleapis.com/robot/v1/metadata/x509/SERVICE-ACCOUNT-ID + rules: + # This auth rule will apply to all methods. + - selector: "*" + requirements: + - provider_id: google_service_account \ No newline at end of file diff --git a/endpoints/getting-started-grpc/api_config.yaml b/endpoints/getting-started-grpc/api_config.key.yaml similarity index 100% rename from endpoints/getting-started-grpc/api_config.yaml rename to endpoints/getting-started-grpc/api_config.key.yaml diff --git a/endpoints/getting-started-grpc/client.js b/endpoints/getting-started-grpc/client.js index 431703566b..da490842d8 100644 --- a/endpoints/getting-started-grpc/client.js +++ b/endpoints/getting-started-grpc/client.js @@ -15,8 +15,9 @@ 'use strict'; -function makeGrpcRequest (API_KEY, HOST, GREETEE) { +function makeGrpcRequest (JWT_AUTH_TOKEN, API_KEY, HOST, GREETEE) { // Uncomment these lines to set their values + // const JWT_AUTH_TOKEN = 'YOUR_JWT_AUTH_TOKEN'; // const API_KEY = 'YOUR_API_KEY'; // const HOST = 'localhost:50051'; // The IP address of your endpoints host // const GREETEE = 'world'; @@ -34,7 +35,11 @@ function makeGrpcRequest (API_KEY, HOST, GREETEE) { // Build gRPC request const metadata = new grpc.Metadata(); - metadata.add('x-api-key', API_KEY); + if (API_KEY) { + metadata.add('x-api-key', API_KEY); + } else if (JWT_AUTH_TOKEN) { + metadata.add('authorization', `Bearer ${JWT_AUTH_TOKEN}`); + } // Execute gRPC request client.sayHello({ name: GREETEE }, metadata, (err, response) => { @@ -50,7 +55,13 @@ function makeGrpcRequest (API_KEY, HOST, GREETEE) { // The command-line program const argv = require('yargs') - .usage('Usage: node $0 -k YOUR_API_KEY [-h YOUR_ENDPOINTS_HOST] [-g GREETEE_NAME]') + .usage('Usage: node $0 {-k YOUR_API_KEY>, <-j YOUR_JWT_AUTH_TOKEN} [-h YOUR_ENDPOINTS_HOST] [-g GREETEE_NAME]') + .option('jwtAuthToken', { + alias: 'j', + type: 'string', + global: true, + default: '' + }) .option('apiKey', { alias: 'k', type: 'string', @@ -69,8 +80,17 @@ const argv = require('yargs') default: 'world', global: true }) + .check((argv) => { + const valid = !!(argv.jwtAuthToken || argv.apiKey); + if (!valid) { + console.error('One of API_KEY or JWT_AUTH_TOKEN must be set.'); + } + return valid; + }) .wrap(120) + .help() + .strict() .epilogue(`For more information, see https://cloud.google.com/endpoints/docs`) .argv; -makeGrpcRequest(argv.apiKey, argv.host, argv.greetee); +makeGrpcRequest(argv.jwtAuthToken, argv.apiKey, argv.host, argv.greetee); diff --git a/endpoints/getting-started-grpc/package.json b/endpoints/getting-started-grpc/package.json index e85c5f1337..6f046211b5 100644 --- a/endpoints/getting-started-grpc/package.json +++ b/endpoints/getting-started-grpc/package.json @@ -15,16 +15,18 @@ "scripts": { "start": "node server.js", "system-test": "ava -T 30s --verbose system-test/*.test.js", - "test": "npm run system-test" + "test": "samples lint && npm run system-test" }, "dependencies": { "body-parser": "1.17.2", "express": "4.15.3", "grpc": "1.3.8", - "yargs": "8.0.2" + "yargs": "8.0.2", + "google-auth-library": "^0.10.0", + "jsonwebtoken": "^7.4.1" }, "devDependencies": { - "@google-cloud/nodejs-repo-tools": "1.4.15", + "@google-cloud/nodejs-repo-tools": "^1.4.15", "ava": "0.19.1" }, "cloud-repo-tools": { @@ -33,7 +35,9 @@ "requiredEnvVars": [ "ENDPOINTS_API_KEY", "ENDPOINTS_GCE_HOST", - "ENDPOINTS_GKE_HOST" + "ENDPOINTS_GKE_HOST", + "ENDPOINTS_SERVICE_NAME", + "GOOGLE_APPLICATION_CREDENTIALS" ] } } diff --git a/endpoints/getting-started-grpc/system-test/endpoints.test.js b/endpoints/getting-started-grpc/system-test/endpoints.test.js index f895ca42af..a446ede08b 100644 --- a/endpoints/getting-started-grpc/system-test/endpoints.test.js +++ b/endpoints/getting-started-grpc/system-test/endpoints.test.js @@ -18,6 +18,8 @@ const childProcess = require('child_process'); const path = require('path'); const test = require('ava'); +const fs = require(`fs`); +const jwt = require('jsonwebtoken'); const tools = require('@google-cloud/nodejs-repo-tools'); const clientCmd = `node client.js`; @@ -26,36 +28,81 @@ const serverCmd = `node server.js`; const cwd = path.join(__dirname, `..`); const API_KEY = process.env.ENDPOINTS_API_KEY; +const GOOGLE_KEYFILE = JSON.parse(fs.readFileSync(process.env.GOOGLE_APPLICATION_CREDENTIALS, 'utf8')); +const SERVICE_NAME = process.env.ENDPOINTS_SERVICE_NAME; const GCE_HOST = process.env.ENDPOINTS_GCE_HOST; const GKE_HOST = process.env.ENDPOINTS_GKE_HOST; test.before((t) => { - t.truthy(API_KEY, 'Must set API_KEY environment variable!'); - t.truthy(GCE_HOST, 'Must set GCE_HOST environment variable!'); - t.truthy(GKE_HOST, 'Must set GKE_HOST environment variable!'); + t.truthy(API_KEY, 'Must set ENDPOINTS_API_KEY environment variable!'); + t.truthy(GCE_HOST, 'Must set ENDPOINTS_GCE_HOST environment variable!'); + t.truthy(GKE_HOST, 'Must set ENDPOINTS_GKE_HOST environment variable!'); + t.truthy(SERVICE_NAME, 'Must set ENDPOINTS_SERVICE_NAME environment variable!'); + t.truthy(GOOGLE_KEYFILE, 'GOOGLE_APPLICATION_CREDENTIALS environment variable must point to a service account keyfile!'); + t.truthy(GOOGLE_KEYFILE.client_email, 'Service account keyfile must contain a "client_email" field!'); + t.truthy(GOOGLE_KEYFILE.private_key, 'Service account keyfile must contain a "private_key" field!'); }); +// Generate JWT based on GOOGLE_APPLICATION_CREDENTIALS and ENDPOINTS_SERVICE_NAME +const JWT_AUTH_TOKEN = jwt.sign({ + 'aud': SERVICE_NAME, + 'iss': GOOGLE_KEYFILE.client_email, + 'iat': parseInt(Date.now() / 1000), + 'exp': parseInt(Date.now() / 1000) + (20 * 60), // 20 minutes + 'email': GOOGLE_KEYFILE.client_email, + 'sub': GOOGLE_KEYFILE.client_email +}, GOOGLE_KEYFILE.private_key, { algorithm: 'RS256' }); + const delay = (mSec) => { return new Promise((resolve) => setTimeout(resolve, mSec)); }; -test.serial(`should request a greeting from a remote Compute Engine instance`, async (t) => { +// API key +test(`should request a greeting from a remote Compute Engine instance using an API key`, async (t) => { const output = await tools.runAsync(`${clientCmd} -h ${GCE_HOST} -k ${API_KEY}`, cwd); t.regex(output, /Hello world/); }); -test.serial(`should request a greeting from a remote Container Engine cluster`, async (t) => { +test(`should request a greeting from a remote Container Engine cluster using an API key`, async (t) => { const output = await tools.runAsync(`${clientCmd} -h ${GKE_HOST} -k ${API_KEY}`, cwd); t.regex(output, /Hello world/); }); -test.serial(`should request and handle a greeting locally`, async (t) => { +test.serial(`should request and handle a greeting locally using an API key`, async (t) => { const PORT = 50051; const server = childProcess.exec(`${serverCmd} -p ${PORT}`, { cwd: cwd }); await delay(1000); - console.log(`${clientCmd} -h localhost:${PORT} -k ${API_KEY}`); const clientOutput = await tools.runAsync(`${clientCmd} -h localhost:${PORT} -k ${API_KEY}`, cwd); t.regex(clientOutput, /Hello world/); server.kill(); }); + +// Authtoken +test(`should request a greeting from a remote Compute Engine instance using a JWT Auth Token`, async (t) => { + const output = await tools.runAsync(`${clientCmd} -h ${GCE_HOST} -j ${JWT_AUTH_TOKEN}`, cwd); + t.regex(output, /Hello world/); +}); + +test(`should request a greeting from a remote Container Engine cluster using a JWT Auth Token`, async (t) => { + const output = await tools.runAsync(`${clientCmd} -h ${GKE_HOST} -j ${JWT_AUTH_TOKEN}`, cwd); + t.regex(output, /Hello world/); +}); + +test.serial(`should request and handle a greeting locally using a JWT Auth Token`, async (t) => { + const PORT = 50051; + const server = childProcess.exec(`${serverCmd} -p ${PORT}`, { cwd: cwd }); + + await delay(1000); + const clientOutput = await tools.runAsync(`${clientCmd} -h localhost:${PORT} -j ${JWT_AUTH_TOKEN}`, cwd); + t.regex(clientOutput, /Hello world/); + server.kill(); +}); + +// Misc +test('should require either an API key or a JWT Auth Token', async (t) => { + await t.throws( + tools.runAsync(`${clientCmd} -h ${GCE_HOST}`, cwd), + /One of API_KEY or JWT_AUTH_TOKEN must be set/ + ); +});