From 674dec24b9f28789c9b6a2c3443a9cc9fa2f00e9 Mon Sep 17 00:00:00 2001 From: Charles Engelke Date: Sun, 1 Sep 2019 07:46:54 -0700 Subject: [PATCH] Sample for user auth with IAP (#334) --- authenticating-users/README.md | 5 ++ authenticating-users/app.js | 108 ++++++++++++++++++++++++++ authenticating-users/app.yaml | 14 ++++ authenticating-users/package.json | 41 ++++++++++ authenticating-users/test/app.test.js | 16 ++++ 5 files changed, 184 insertions(+) create mode 100644 authenticating-users/README.md create mode 100644 authenticating-users/app.js create mode 100644 authenticating-users/app.yaml create mode 100644 authenticating-users/package.json create mode 100644 authenticating-users/test/app.test.js diff --git a/authenticating-users/README.md b/authenticating-users/README.md new file mode 100644 index 000000000..802e4ecba --- /dev/null +++ b/authenticating-users/README.md @@ -0,0 +1,5 @@ +# Authenticating users + +This folder contains the sample code for the *Authenticating users* +tutorial. Please refer to the tutorial for instructions on configuring, running, +and deploying this sample. diff --git a/authenticating-users/app.js b/authenticating-users/app.js new file mode 100644 index 000000000..f94985d68 --- /dev/null +++ b/authenticating-users/app.js @@ -0,0 +1,108 @@ +/** + * Copyright 2019, Google LLC + * 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. + */ + +'use strict'; + +const express = require('express'); +const got = require('got'); +const jwt = require('jsonwebtoken'); + +const app = express(); + +// Cache externally fetched information for future invocations +let certs; +let aud; + +async function certificates() { + if (!certs) { + let response = await got('https://www.gstatic.com/iap/verify/public_key'); + certs = JSON.parse(response.body); + } + + return certs; +} + +async function get_metadata(itemName) { + const endpoint = 'http://metadata.google.internal'; + const path = '/computeMetadata/v1/project/'; + const url = endpoint + path + itemName; + + let response = await got(url, { + headers: {'Metadata-Flavor': 'Google'} + }); + return response.body; +} + +async function audience() { + if (!aud) { + let project_number = await get_metadata('numeric-project-id'); + let project_id = await get_metadata('project-id'); + + aud = '/projects/' + project_number + '/apps/' + project_id; + } + + return aud; +} + +async function validate_assertion(assertion) { + // Decode the header to determine which certificate signed the assertion + const encodedHeader = assertion.split('.')[0]; + const decodedHeader = Buffer.from(encodedHeader, 'base64').toString('utf8'); + const header = JSON.parse(decodedHeader); + const keyId = header.kid; + + // Fetch the current certificates and verify the signature on the assertion + const certs = await certificates(); + const payload = jwt.verify(assertion, certs[keyId]); + + // Check that the assertion's audience matches ours + const aud = await audience(); + if (payload.aud !== aud) { + throw new Error('Audience mismatch. {$payload.aud} should be {$aud}.'); + } + + // Return the two relevant pieces of information + return { + 'email': payload.email, + 'sub': payload.sub, + }; + +} + +app.get('/', (req, res) => { + const assertion = req.header('X-Goog-IAP-JWT-Assertion'); + validate_assertion(assertion).then((info) => { + res + .status(200) + .send('Hello ' + info.email) + .end(); + }).catch((error) => { + console.log(error); + + res + .status(200) + .send('Hello None') + .end(); + }); +}); + +// Start the server +const PORT = process.env.PORT || 8080; +app.listen(PORT, () => { + console.log(`App listening on port ${PORT}`); + console.log('Press Ctrl+C to quit.'); +}); + +module.exports = app; diff --git a/authenticating-users/app.yaml b/authenticating-users/app.yaml new file mode 100644 index 000000000..24d25d8e5 --- /dev/null +++ b/authenticating-users/app.yaml @@ -0,0 +1,14 @@ +# Copyright 2019 Google LLC +# 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. + +runtime: nodejs10 diff --git a/authenticating-users/package.json b/authenticating-users/package.json new file mode 100644 index 000000000..09a3e43f1 --- /dev/null +++ b/authenticating-users/package.json @@ -0,0 +1,41 @@ +{ + "name": "iap-authentication", + "description": "Minimal app to use authentication information from IAP.", + "version": "0.0.1", + "private": true, + "license": "Apache-2.0", + "author": "Google LLC", + "repository": { + "type": "git", + "url": "https://github.com/GoogleCloudPlatform/getting-started-nodejs.git" + }, + "engines": { + "node": ">=10.0.0" + }, + "scripts": { + "deploy": "gcloud app deploy", + "start": "node app.js", + "system-test": "mocha --exit test/*.test.js", + "test": "npm run system-test", + "e2e-test": "repo-tools test deploy" + }, + "dependencies": { + "express": "^4.17.1", + "got": "^9.6.0", + "jsonwebtoken": "^8.5.1" + }, + "devDependencies": { + "@google-cloud/nodejs-repo-tools": "^3.3.0", + "mocha": "^6.1.4", + "supertest": "^4.0.2" + }, + "cloud-repo-tools": { + "test": { + "app": { + "msg": "Hello, world!" + } + }, + "requiresKeyFile": true, + "requiresProjectId": true + } +} diff --git a/authenticating-users/test/app.test.js b/authenticating-users/test/app.test.js new file mode 100644 index 000000000..097e2fd23 --- /dev/null +++ b/authenticating-users/test/app.test.js @@ -0,0 +1,16 @@ +const app = require('../app'); + +const request = require('supertest'); + +describe('GET /', () => { + it('should get 200', done => { + request(app) + .get('/') + .expect(200, done); + }), + it('should get Hello None', done => { + request(app) + .get('/') + .expect('Hello None', done); + }); +});