From 66ab0fb692a42fc0c27da6d9f4b016b8627eaa03 Mon Sep 17 00:00:00 2001 From: Dustin Carlino Date: Thu, 14 Sep 2023 14:47:11 +0100 Subject: [PATCH] Verify IAP headers, and figure out who's logged in --- backend/README.md | 7 ++++--- backend/app.yaml | 1 + backend/package-lock.json | 3 ++- backend/package.json | 3 ++- backend/server.js | 38 +++++++++++++++++++++++++++++++++- package-lock.json | 9 ++++++++ package.json | 1 + src/lib/browse/LoggedIn.svelte | 14 +++++++++++++ src/pages/BrowseSchemes.svelte | 2 ++ 9 files changed, 72 insertions(+), 6 deletions(-) create mode 100644 src/lib/browse/LoggedIn.svelte diff --git a/backend/README.md b/backend/README.md index 7aca0e488..4e9cf0604 100644 --- a/backend/README.md +++ b/backend/README.md @@ -32,11 +32,12 @@ Names and regions should match above. ### Deploy 1. Update `GCS_BUCKET` in `backend/app.yaml` -2. Create the files to deploy: `VITE_ON_GCP="true" VITE_RESOURCE_BASE="https://atip-test-2.ew.r.appspot.com/data" npm run build && cd backend && rm -rf dist && cp -R ../dist .` +2. Run `gcloud projects describe atip-test-2 | grep projectNumber` and use the result to update `PROJECT_NUMBER` in `backend/app.yaml` +3. Create the files to deploy: `VITE_ON_GCP="true" VITE_RESOURCE_BASE="https://atip-test-2.ew.r.appspot.com/data" npm run build && cd backend && rm -rf dist && cp -R ../dist .` - Note we could make Cloud Build do this, but we'd have to get `wasm-pack` and other things set up there first - GH Actions will eventually trigger CI deployments for our test environment, and we've already done the work of configuring that build environment -3. `gcloud app --project=atip-test-2 deploy --quiet` (takes a minute or two) -4. Try the result: `gcloud app browse --project=atip-test-2` or +4. `gcloud app --project=atip-test-2 deploy --quiet` (takes a minute or two) +5. Try the result: `gcloud app browse --project=atip-test-2` or Useful debugging: diff --git a/backend/app.yaml b/backend/app.yaml index 0a595f31f..f89b09d4f 100644 --- a/backend/app.yaml +++ b/backend/app.yaml @@ -1,3 +1,4 @@ runtime: nodejs18 env_variables: GCS_BUCKET: "atip-test-2" + PROJECT_NUMBER: "29375903718" diff --git a/backend/package-lock.json b/backend/package-lock.json index 0f5da47d3..995f0ec49 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -9,7 +9,8 @@ "version": "0.0.0", "dependencies": { "@google-cloud/storage": "^7.0.1", - "express": "^4.18.2" + "express": "^4.18.2", + "google-auth-library": "^9.0.0" }, "devDependencies": { "@ianvs/prettier-plugin-sort-imports": "^4.0.2", diff --git a/backend/package.json b/backend/package.json index c5a37f74e..d1abb3df5 100644 --- a/backend/package.json +++ b/backend/package.json @@ -12,6 +12,7 @@ }, "dependencies": { "@google-cloud/storage": "^7.0.1", - "express": "^4.18.2" + "express": "^4.18.2", + "google-auth-library": "^9.0.0" } } diff --git a/backend/server.js b/backend/server.js index de6be3566..7e7fb6485 100644 --- a/backend/server.js +++ b/backend/server.js @@ -3,16 +3,22 @@ // API reference: https://googleapis.dev/nodejs/storage/latest/ import { Storage } from "@google-cloud/storage"; import express from "express"; +import { OAuth2Client } from "google-auth-library"; // This automatically finds gcloud credentials when running locally or the -// service account on GAE +// service account on GAE let storage = new Storage(); // TODO Upfront check the bucket is accessible and has some expected files, so // we could failfail for a bad deployment let bucket = process.env.GCS_BUCKET; +let oauthClient = new OAuth2Client(); + +let expectedAudience = `/projects/${process.env.PROJECT_NUMBER}/apps/${process.env.GOOGLE_CLOUD_PROJECT}`; let app = express(); +app.use(checkIap); + // Serve the ATIP frontend, which is just statically built HTML, CSS, JS, WASM // files bundled in the App Engine deployment directly. app.use(express.static("dist")); @@ -67,6 +73,36 @@ app.get("/data/*", async (req, resp) => { } }); +// See https://cloud.google.com/iap/docs/signed-headers-howto +async function checkIap(req, resp, next) { + let iapJwt = req.header("x-goog-iap-jwt-assertion"); + if (!iapJwt) { + resp.status(401).send("Missing x-goog-iap-jwt-assertion header"); + return; + } + + try { + // TODO Can we cache this between requests? + let iapPublicKeys = await oauthClient.getIapPublicKeys(); + let ticket = await oauthClient.verifySignedJwtWithCertsAsync( + iapJwt, + iapPublicKeys.pubkeys, + expectedAudience, + ["https://cloud.google.com/iap"] + ); + // Plumb back the email to display in Svelte using session cookies + // NOTE! This shouldn't be considered secure; the user can modify it. Only + // use it for client-side display. Always use this IAP token to determine + // who the user is for permissions. + resp.cookie("email", ticket.payload.email); + + next(); + } catch (err) { + console.log(`IAP auth broke: ${err}`); + resp.status(401).send(err); + } +} + let port = process.env.PORT || 8080; app.listen(port, () => { console.log(`Server is running at http://localhost:${port}`); diff --git a/package-lock.json b/package-lock.json index 5d9c1668b..e7831488e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,6 +22,7 @@ "comlink": "^4.4.1", "govuk-frontend": "^4.6.0", "humanize-string": "^3.0.0", + "js-cookie": "^3.0.5", "maplibre-gl": "^3.1.0", "pmtiles": "^2.10.0-beta.0", "read-excel-file": "^5.6.1", @@ -2299,6 +2300,14 @@ "node": ">=0.10.0" } }, + "node_modules/js-cookie": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz", + "integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==", + "engines": { + "node": ">=14" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", diff --git a/package.json b/package.json index ee8470cc9..6c128d60e 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "comlink": "^4.4.1", "govuk-frontend": "^4.6.0", "humanize-string": "^3.0.0", + "js-cookie": "^3.0.5", "maplibre-gl": "^3.1.0", "pmtiles": "^2.10.0-beta.0", "read-excel-file": "^5.6.1", diff --git a/src/lib/browse/LoggedIn.svelte b/src/lib/browse/LoggedIn.svelte new file mode 100644 index 000000000..5043bcf1a --- /dev/null +++ b/src/lib/browse/LoggedIn.svelte @@ -0,0 +1,14 @@ + + +{#if email} +

Logged in as {email}

+{:else} + +{/if} diff --git a/src/pages/BrowseSchemes.svelte b/src/pages/BrowseSchemes.svelte index 7ed688d6f..6cee62f6d 100644 --- a/src/pages/BrowseSchemes.svelte +++ b/src/pages/BrowseSchemes.svelte @@ -6,6 +6,7 @@ import Filters from "lib/browse/Filters.svelte"; import LayerControls from "lib/browse/LayerControls.svelte"; import LoadRemoteSchemeData from "lib/browse/LoadRemoteSchemeData.svelte"; + import LoggedIn from "lib/browse/LoggedIn.svelte"; import SchemeCard from "lib/browse/SchemeCard.svelte"; import authorityNamesList from "../../assets/authority_names.json"; import "../style/main.css"; @@ -95,6 +96,7 @@ {#if import.meta.env.VITE_ON_GCP === "true"} + {/if}