Skip to content

Commit

Permalink
Verify IAP headers, and figure out who's logged in
Browse files Browse the repository at this point in the history
  • Loading branch information
dabreegster committed Sep 14, 2023
1 parent 0ec189c commit 66ab0fb
Show file tree
Hide file tree
Showing 9 changed files with 72 additions and 6 deletions.
7 changes: 4 additions & 3 deletions backend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <https://atip-test-2.ew.r.appspot.com/browse.html>
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 <https://atip-test-2.ew.r.appspot.com/browse.html>

Useful debugging:

Expand Down
1 change: 1 addition & 0 deletions backend/app.yaml
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
runtime: nodejs18
env_variables:
GCS_BUCKET: "atip-test-2"
PROJECT_NUMBER: "29375903718"
3 changes: 2 additions & 1 deletion backend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
38 changes: 37 additions & 1 deletion backend/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -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"));
Expand Down Expand Up @@ -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}`);
Expand Down
9 changes: 9 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
14 changes: 14 additions & 0 deletions src/lib/browse/LoggedIn.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<script lang="ts">
import Cookies from "js-cookie";
import { ErrorMessage } from "lib/govuk";
let email = Cookies.get("email");
</script>

{#if email}
<p>Logged in as {email}</p>
{:else}
<ErrorMessage
errorMessage="You shouldn't be able to view this app without being logged in!"
/>
{/if}
2 changes: 2 additions & 0 deletions src/pages/BrowseSchemes.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -95,6 +96,7 @@
</div>
{#if import.meta.env.VITE_ON_GCP === "true"}
<LoadRemoteSchemeData {loadFile} />
<LoggedIn />
{/if}
<FileInput label="Load schemes from GeoJSON" id="load-geojson" {loadFile} />
<ErrorMessage {errorMessage} />
Expand Down

0 comments on commit 66ab0fb

Please sign in to comment.