From 52631e736bf0de2ebc56038a5ac0fa8db05cf69e Mon Sep 17 00:00:00 2001
From: jonmeow <46229924+jonmeow@users.noreply.github.com>
Date: Thu, 28 May 2020 13:36:38 -0700
Subject: [PATCH 1/3] Trying to get cookie working
---
src/firebase/functions/index.js | 30 ++++++++++++++++++++++++++----
src/firebase/public/login.html | 23 ++++++++++++++++++-----
src/firebase/public/logout.html | 4 ++--
3 files changed, 46 insertions(+), 11 deletions(-)
diff --git a/src/firebase/functions/index.js b/src/firebase/functions/index.js
index 73a0f61f78feb..72c793e9fed7a 100644
--- a/src/firebase/functions/index.js
+++ b/src/firebase/functions/index.js
@@ -22,21 +22,39 @@ const { Octokit } = require('@octokit/rest');
const {Storage} = require('@google-cloud/storage');
const gcs = new Storage();
+app.post('/loginSession', (req, res) => {
+ // Get the ID token passed and the CSRF token.
+ const idToken = req.body.idToken.toString();
+ // Create a 14-day session (the limit).
+ const expiresIn = 14 * 24 * 60 * 60 * 1000;
+ admin.auth().createSessionCookie(idToken, {expiresIn})
+ .then((sessionCookie) => {
+ console.log("loginSession: Created session cookie");
+ const options = {maxAge: expiresIn, httpOnly: true, secure: true};
+ res.cookie('session', sessionCookie, options);
+ res.end(JSON.stringify({status: 'success'}));
+ }, error => {
+ console.log("loginSession: Rejecting due to: " + error);
+ res.redirect(302, "/logout.html");
+ });
+});
+
// Checks for a session cookie. If present, the content will be added as
// req.user.
const validateFirebaseIdToken = async (req, res, next) => {
- if (!req.cookies || !req.cookies.__session) {
+ if (!req.cookies || !req.cookies.session) {
// Not logged in.
+ console.log("validateFirebaseIdToken: No cookie");
res.redirect(302, "/login.html");
return;
}
- // Validate the cookie, and get user info from it.
- const idToken = req.cookies.__session;
try {
- req.user = await admin.auth().verifyIdToken(idToken);
+ req.user =
+ await admin.auth().verifySessionCookie(req.cookies.session, true);
} catch (error) {
// Invalid login, use logout to clear it.
+ console.log("validateFirebaseIdToken: Invalid session: " + error);
res.redirect(302, "/logout.html");
return;
}
@@ -58,6 +76,7 @@ const validateFirebaseIdToken = async (req, res, next) => {
});
if (ghUser.length < 1 || ghUser[0].id != wantId) {
// An issue with the GitHub ID.
+ console.log("validateFirebaseIdToken: GitHub ID issue: " + wantId);
res.redirect(302, "/logout.html");
}
const username = ghUser[0].login;
@@ -70,12 +89,15 @@ const validateFirebaseIdToken = async (req, res, next) => {
});
if (member && member.state == 'active' && member.user.id == wantId) {
next();
+ console.log("validateFirebaseIdToken: Accepted");
return;
}
// No access, force logout.
+ console.log("validateFirebaseIdToken: Not an active member: " + wantId);
res.redirect(302, "/logout.html");
} catch (err) {
// Not a member, force logout.
+ console.log("validateFirebaseIdToken: Not a member: " + wantId);
res.redirect(302, "/logout.html");
}
};
diff --git a/src/firebase/public/login.html b/src/firebase/public/login.html
index 547ef522b16cb..c4dc1d52d79af 100644
--- a/src/firebase/public/login.html
+++ b/src/firebase/public/login.html
@@ -9,6 +9,7 @@
Carbon: Login
+
diff --git a/src/firebase/public/logout.html b/src/firebase/public/logout.html
index 365395b0ed9cb..b07f93b4fd69b 100644
--- a/src/firebase/public/logout.html
+++ b/src/firebase/public/logout.html
@@ -23,7 +23,7 @@
function redirect() {
// Re-clear the cookie, just in case.
- document.cookie = "__session=; expires=Thu, 01 Jan 1970 00:00:00 GMT";
+ document.cookie = "session=; expires=Thu, 01 Jan 1970 00:00:00 GMT";
window.location.href = window.location.href.replace(
"/logout.html", "/login.html");
};
@@ -34,7 +34,7 @@
});
window.onload = function() {
- document.cookie = "__session=; expires=Thu, 01 Jan 1970 00:00:00 GMT";
+ document.cookie = "session=; expires=Thu, 01 Jan 1970 00:00:00 GMT";
firebase.auth().signOut();
};
From dde55472d573dd4288c536d713c5d90274be6075 Mon Sep 17 00:00:00 2001
From: jonmeow <46229924+jonmeow@users.noreply.github.com>
Date: Fri, 29 May 2020 14:05:09 -0700
Subject: [PATCH 2/3] Rewrite authentication flow.
- Use session tokens for:
- The 14-day expiry vs previous 1-hour
- Validate GitHub identity once per-session
- Use caching for performance
- Improve logging.
---
src/firebase/functions/index.js | 267 ++++++++++++++++++----------
src/firebase/functions/package.json | 3 +-
src/firebase/public/login.html | 2 +-
src/firebase/public/logout.html | 4 +-
4 files changed, 181 insertions(+), 95 deletions(-)
diff --git a/src/firebase/functions/index.js b/src/firebase/functions/index.js
index 72c793e9fed7a..9ec98b3e358ed 100644
--- a/src/firebase/functions/index.js
+++ b/src/firebase/functions/index.js
@@ -8,120 +8,205 @@ const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp();
const cookieParser = require('cookie-parser')();
-const cors = require('cors')({origin: true});
+const cors = require('cors')({ origin: true });
const path = require('path');
const express = require('express');
const app = express();
-const {SecretManagerServiceClient} = require('@google-cloud/secret-manager');
-const secrets = new SecretManagerServiceClient();
+const cache = require('js-cache');
+const contentCache = new cache();
+const sessionCache = new cache();
-const { Octokit } = require('@octokit/rest');
-
-const {Storage} = require('@google-cloud/storage');
+const { Storage } = require('@google-cloud/storage');
const gcs = new Storage();
-app.post('/loginSession', (req, res) => {
- // Get the ID token passed and the CSRF token.
- const idToken = req.body.idToken.toString();
- // Create a 14-day session (the limit).
- const expiresIn = 14 * 24 * 60 * 60 * 1000;
- admin.auth().createSessionCookie(idToken, {expiresIn})
- .then((sessionCookie) => {
- console.log("loginSession: Created session cookie");
- const options = {maxAge: expiresIn, httpOnly: true, secure: true};
- res.cookie('session', sessionCookie, options);
- res.end(JSON.stringify({status: 'success'}));
- }, error => {
- console.log("loginSession: Rejecting due to: " + error);
- res.redirect(302, "/logout.html");
+// Validates that the GitHub user associated with the ID token is in the
+// 'carbon-language' organization.
+const validateIdToken = async (idToken) => {
+ const startTime = new Date();
+ var result = 'unknown';
+ var gitHubId = '';
+ var username = '';
+ try {
+ var user = null;
+ try {
+ user = await admin.auth().verifyIdToken(idToken);
+ } catch (error) {
+ return false;
+ }
+ gitHubId = user.firebase.identities['github.com'][0];
+
+ // The associated secret is attached to the CarbonLangInfra GitHub account.
+ const { SecretManagerServiceClient } =
+ require('@google-cloud/secret-manager');
+ const secrets = new SecretManagerServiceClient();
+ const [secret] = await secrets.accessSecretVersion({
+ name:
+ 'projects/985662022432/secrets/github-org-lookup-token-for-www/versions/latest',
+ });
+
+ const { Octokit } = require('@octokit/rest');
+ const octokit = new Octokit({
+ auth: secret.payload.data.toString('utf8'),
+ });
+
+ const { data: ghUser } = await octokit.users.list({
+ since: gitHubId - 1,
+ per_page: 1,
});
-});
-
-// Checks for a session cookie. If present, the content will be added as
-// req.user.
-const validateFirebaseIdToken = async (req, res, next) => {
- if (!req.cookies || !req.cookies.session) {
- // Not logged in.
- console.log("validateFirebaseIdToken: No cookie");
- res.redirect(302, "/login.html");
- return;
+ if (ghUser.length < 1 || ghUser[0].id != gitHubId) {
+ result = 'Failed to fetch matching GitHub ID';
+ return false;
+ }
+ username = ghUser[0].login;
+
+ try {
+ const { data: member } = await octokit.orgs.getMembership({
+ org: 'carbon-language',
+ username: username,
+ });
+ if (member && member.state == 'active' && member.user.id == gitHubId) {
+ result = 'Pass';
+ return true;
+ }
+ result = 'Not an active member';
+ return false;
+ } catch (err) {
+ result = 'Not a member';
+ return false;
+ }
+
+ return false; // Should be unreachable.
+ } finally {
+ const elapsed = new Date() - startTime;
+ console.log(
+ `validateIdToken: ${result} (${gitHubId}/${username}; ${elapsed}ms)`
+ );
}
+};
+// Handles a user logging into a session.
+const loginSession = async (req, res) => {
+ const startTime = new Date();
+ var result = 'unknown';
try {
- req.user =
- await admin.auth().verifySessionCookie(req.cookies.session, true);
- } catch (error) {
- // Invalid login, use logout to clear it.
- console.log("validateFirebaseIdToken: Invalid session: " + error);
- res.redirect(302, "/logout.html");
- return;
- }
+ // Get the ID token passed and the CSRF token.
+ const idToken = req.body.idToken.toString();
+ const isOk = await validateIdToken(idToken);
+ if (!isOk) {
+ result = 'validateIdToken failed';
+ res.redirect(302, '/logout.html');
+ return;
+ }
+
+ // Create a 14-day session (the limit).
+ const expiresIn = 14 * 24 * 60 * 60 * 1000;
+ var sessionCookie;
+ try {
+ sessionCookie = await admin
+ .auth()
+ .createSessionCookie(idToken, { expiresIn });
+ } catch (error) {
+ result = `createSessionCookie failed: ${error}`;
+ res.redirect(302, '/logout.html');
+ return;
+ }
- // Load the GitHub auth token. The associated secret is attached to the
- // CarbonLangInfra GitHub account.
- const [secret] = await secrets.accessSecretVersion({
- name: 'projects/985662022432/secrets/github-org-lookup-token-for-www/versions/latest',
- });
- const octokit = new Octokit({
- auth: secret.payload.data.toString('utf8'),
- });
-
- // Translate the GitHub ID to a username.
- const wantId = req.user.firebase.identities["github.com"][0];
- const { data: ghUser } = await octokit.users.list({
- since: wantId - 1,
- per_page: 1,
- });
- if (ghUser.length < 1 || ghUser[0].id != wantId) {
- // An issue with the GitHub ID.
- console.log("validateFirebaseIdToken: GitHub ID issue: " + wantId);
- res.redirect(302, "/logout.html");
+ result = 'Pass';
+ const options = { maxAge: expiresIn, httpOnly: true, secure: true };
+ res.cookie('__session', sessionCookie, options);
+ res.end(JSON.stringify({ status: 'success' }));
+ } finally {
+ const elapsed = new Date() - startTime;
+ console.log(`loginSessior: ${result} (${elapsed}ms)`);
}
- const username = ghUser[0].login;
+};
- // Validate that the GitHub user is in carbon-language.
+// Checks for a session cookie.
+const validateSessionToken = async (req, res, next) => {
+ const startTime = new Date();
+ var result = 'unknown';
try {
- const { data: member } = await octokit.orgs.getMembership({
- org: 'carbon-language',
- username: username,
- });
- if (member && member.state == 'active' && member.user.id == wantId) {
+ if (!req.cookies || !req.cookies.__session) {
+ // Not logged in.
+ result = 'No __session cookie';
+ res.redirect(302, '/login.html');
+ return;
+ }
+ const sessionCookie = req.cookies.__session;
+
+ if (sessionCache.get(sessionCookie)) {
+ result = 'Pass (cache hit)';
next();
- console.log("validateFirebaseIdToken: Accepted");
return;
}
- // No access, force logout.
- console.log("validateFirebaseIdToken: Not an active member: " + wantId);
- res.redirect(302, "/logout.html");
- } catch (err) {
- // Not a member, force logout.
- console.log("validateFirebaseIdToken: Not a member: " + wantId);
- res.redirect(302, "/logout.html");
+
+ try {
+ const user = await admin.auth().verifySessionCookie(sessionCookie, false);
+ } catch (error) {
+ // Invalid login, use logout to clear it.
+ result = `Invalid __session: ${error}`;
+ res.redirect(302, '/logout.html');
+ return;
+ }
+
+ result = 'Pass (cache miss)';
+ // Cache positive results for an hour.
+ sessionCache.set(sessionCookie, true, 60 * 60 * 1000);
+ next();
+ } finally {
+ const elapsed = new Date() - startTime;
+ console.log(
+ `validateSessionToken at ${req.path}: ${result} (${elapsed}ms)`
+ );
}
};
-app.use(cors);
-app.use(cookieParser);
-app.use(validateFirebaseIdToken);
+// Handles serving content from GCS.
+const serveContent = async (req, res) => {
+ const startTime = new Date();
+ var result = 'unknown';
+ try {
+ // Remove the prefix /, and default to index.html.
+ var file = req.path.replace(/^(\/)/, '');
+ if (file === '') {
+ file = 'index.html';
+ }
+ res.type(path.extname(file));
+
+ // Check cache.
+ var cacheHit = "hit";
+ var contents = contentCache.get(file);
+ if (!contents) {
+ cacheHit = "miss";
+ // Serve the requested data from the carbon-lang bucket.
+ const bucket = gcs.bucket('gs://www.carbon-lang.dev');
+ var contents;
+ try {
+ const data = await bucket.file(file).download();
+ contents = data[0];
+ // Cache content for 15 minutes.
+ contentCache.set(file, contents, 5 * 60 * 1000);
+ } catch (error) {
+ result = `Error: ${error}`;
+ res.status(404).send('Not found');
+ return;
+ }
+ }
-app.get('*', (req, res) => {
- // Remove the prefix /, and default to index.html.
- var file = req.path.replace(/^(\/)/, "");
- if (file === "") {
- file = "index.html";
+ result = `Pass (cache ${cacheHit}; ${contents.length} bytes)`;
+ res.send(contents);
+ } finally {
+ const elapsed = new Date() - startTime;
+ console.log(`serveContent at ${req.path}: ${result} (${elapsed}ms)`);
}
- res.type(path.extname(file));
- // Serve the requested data from the carbon-lang bucket.
- const bucket = gcs.bucket("gs://www.carbon-lang.dev");
- const stream = bucket.file(file).createReadStream();
- //stream.on('error', function(err) { res.status(404).send(err.message); });
- stream.on('error', function(err) {
- console.log(err.message);
- res.status(404).send("Not found");
- });
- stream.pipe(res);
-});
+};
+
+app.use(cors);
+app.use(cookieParser);
+app.post('/loginSession', loginSession);
+app.use(validateSessionToken);
+app.get('*', serveContent);
exports.site = functions.https.onRequest(app);
diff --git a/src/firebase/functions/package.json b/src/firebase/functions/package.json
index 7c4d5764d08fa..9457f0819b7bd 100644
--- a/src/firebase/functions/package.json
+++ b/src/firebase/functions/package.json
@@ -18,7 +18,8 @@
"cookie-parser": "^1.4.5",
"cors": "^2.8.5",
"firebase-admin": "^8.10.0",
- "firebase-functions": "^3.6.1"
+ "firebase-functions": "^3.6.2",
+ "js-cache": "^1.0.3"
},
"devDependencies": {
"firebase-functions-test": "^0.2.0"
diff --git a/src/firebase/public/login.html b/src/firebase/public/login.html
index c4dc1d52d79af..4bf124959fa6f 100644
--- a/src/firebase/public/login.html
+++ b/src/firebase/public/login.html
@@ -47,7 +47,7 @@
};
window.onload = function() {
- document.cookie = "session=; expires=Thu, 01 Jan 1970 00:00:00 GMT";
+ document.cookie = "__session=; expires=Thu, 01 Jan 1970 00:00:00 GMT";
firebase.auth().signOut();
};
diff --git a/src/firebase/public/logout.html b/src/firebase/public/logout.html
index b07f93b4fd69b..365395b0ed9cb 100644
--- a/src/firebase/public/logout.html
+++ b/src/firebase/public/logout.html
@@ -23,7 +23,7 @@
function redirect() {
// Re-clear the cookie, just in case.
- document.cookie = "session=; expires=Thu, 01 Jan 1970 00:00:00 GMT";
+ document.cookie = "__session=; expires=Thu, 01 Jan 1970 00:00:00 GMT";
window.location.href = window.location.href.replace(
"/logout.html", "/login.html");
};
@@ -34,7 +34,7 @@
});
window.onload = function() {
- document.cookie = "session=; expires=Thu, 01 Jan 1970 00:00:00 GMT";
+ document.cookie = "__session=; expires=Thu, 01 Jan 1970 00:00:00 GMT";
firebase.auth().signOut();
};
From 3b1da2c5bf77422a33457d183003af65b8b86e45 Mon Sep 17 00:00:00 2001
From: jonmeow <46229924+jonmeow@users.noreply.github.com>
Date: Mon, 1 Jun 2020 11:13:19 -0700
Subject: [PATCH 3/3] validateSessionToken -> validateSessionCookie
---
src/firebase/functions/index.js | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/src/firebase/functions/index.js b/src/firebase/functions/index.js
index 9ec98b3e358ed..9c2c9aa158921 100644
--- a/src/firebase/functions/index.js
+++ b/src/firebase/functions/index.js
@@ -124,7 +124,7 @@ const loginSession = async (req, res) => {
};
// Checks for a session cookie.
-const validateSessionToken = async (req, res, next) => {
+const validateSessionCookie = async (req, res, next) => {
const startTime = new Date();
var result = 'unknown';
try {
@@ -158,7 +158,7 @@ const validateSessionToken = async (req, res, next) => {
} finally {
const elapsed = new Date() - startTime;
console.log(
- `validateSessionToken at ${req.path}: ${result} (${elapsed}ms)`
+ `validateSessionCookie at ${req.path}: ${result} (${elapsed}ms)`
);
}
};
@@ -206,7 +206,7 @@ const serveContent = async (req, res) => {
app.use(cors);
app.use(cookieParser);
app.post('/loginSession', loginSession);
-app.use(validateSessionToken);
+app.use(validateSessionCookie);
app.get('*', serveContent);
exports.site = functions.https.onRequest(app);