From f456cffd3c17ce4501a992dffb3c60a1393fc012 Mon Sep 17 00:00:00 2001 From: Eirik Sletteberg Date: Sun, 19 May 2024 00:25:45 +0200 Subject: [PATCH] Setup auth0 --- .gitignore | 1 + backend/auth.js | 62 +++++++++++++++++++++---------- package-lock.json | 93 ++++++++++++++++++++++++++++++++++++++++------- package.json | 2 + server.js | 1 + 5 files changed, 126 insertions(+), 33 deletions(-) diff --git a/.gitignore b/.gitignore index 60a5673..32b9020 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ dist node_modules storybook-static +.env diff --git a/backend/auth.js b/backend/auth.js index 6b82db4..24ed3b1 100644 --- a/backend/auth.js +++ b/backend/auth.js @@ -3,18 +3,19 @@ import path from 'path'; import { fileURLToPath } from 'url'; import { Router } from 'express'; import * as jose from 'jose'; -import { OAuth2Client } from 'google-auth-library'; +import { Issuer, generators } from 'openid-client'; + import { isDevelopment } from './utils.js'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); -const CLIENT_ID = '273075937491-eb8o66k7dja2v1og8saorqhqnnc765ct.apps.googleusercontent.com'; +const AUTH0_CLIENT_ID = 'gq575MGFWuiYcXpPTXNoJNYfAuoq19KD'; let jwks; const secretsFile = '/secrets/unreed-production'; -if (isDevelopment() && fs.existsSync(secretsFile)) { +if (fs.existsSync(secretsFile)) { console.log(`Loading secrets from ${secretsFile}`); const input = fs.readFileSync(secretsFile, 'utf-8'); const data = JSON.parse(input); @@ -23,11 +24,25 @@ if (isDevelopment() && fs.existsSync(secretsFile)) { process.env.DB_PASS = data.DB_PASS; process.env.DB_NAME = data.DB_NAME; process.env.GOOGLE_CLIENT_SECRET = data.GOOGLE_CLIENT_SECRET; + process.env.AUTH0_CLIENT_SECRET = data.AUTH0_CLIENT_SECRET; jwks = data.JWKS; } else { jwks = JSON.parse(fs.readFileSync(path.resolve(__dirname, 'keys.localhost.json'))); } +const auth0Issuer = await Issuer.discover( + 'https://dev-fxrac16ih4t75q3f.us.auth0.com/.well-known/openid-configuration', +); + +function getAuth0Client(req) { + return new auth0Issuer.Client({ + client_id: AUTH0_CLIENT_ID, + client_secret: process.env.AUTH0_CLIENT_SECRET, + redirect_uris: [getRedirectUrl(req)], + response_types: ['code'], + }); +} + const privateKeys = jose.createLocalJWKSet(jwks.public); const privateKey = await jose.importJWK(jwks.private, 'ES256'); @@ -37,6 +52,7 @@ const audience = `urn:unreed-${isDevelopment() ? 'dev' : 'prod'}:audience`; const maxAge = 2147483647; const AUTH_COOKIE = 'UNREED_OIDC_TOKEN'; const REDIRECT_URI_COOKIE = 'UNREED_REDIRECT_URI'; +const AUTH_NONCE_COOKIE = 'AUTH0_NONCE'; export const authRouter = new Router(); @@ -45,12 +61,16 @@ async function localhostLoginPage(req, res) { } async function productionLoginPage(req, res) { - const client = new OAuth2Client(CLIENT_ID, process.env.GOOGLE_CLIENT_SECRET, getRedirectUrl(req)); - const authorizationUrl = client.generateAuthUrl({ - scope: ['openid email'], - redirect_uri: getRedirectUrl(req), + const client = getAuth0Client(req); + + const nonce = generators.nonce(); + + const authorizationUrl = client.authorizationUrl({ + scope: 'openid email profile', + nonce, }); + res.cookie(AUTH_NONCE_COOKIE, nonce, { maxAge: 300000, path: '/', httpOnly: true }); res.redirect(authorizationUrl); } @@ -79,18 +99,17 @@ async function localhostCallbackPage(req, res) { } async function productionCallbackPage(req, res) { + const nonce = req.cookies[AUTH_NONCE_COOKIE]; try { - const code = req.query.code; - const client = new OAuth2Client( - CLIENT_ID, - process.env.GOOGLE_CLIENT_SECRET, - getRedirectUrl(req), - ); - const googleToken = await client.getToken(code); - const googleClaims = jose.decodeJwt(googleToken.tokens.id_token); + const client = getAuth0Client(req); + const params = client.callbackParams(req); + const tokenSet = await client.callback(getRedirectUrl(req), params, { + nonce, + }); + const auth0Claims = tokenSet.claims(); const claims = { - email: googleClaims.email, + email: auth0Claims.email, }; const expirationTime = Date.now() + maxAge; @@ -110,6 +129,7 @@ async function productionCallbackPage(req, res) { secure: true, }); res.clearCookie(REDIRECT_URI_COOKIE); + res.clearCookie(AUTH_NONCE_COOKIE); res.redirect(req.cookies.UNREED_REDIRECT_URI ?? '/'); } catch (error) { console.error(error); @@ -119,9 +139,13 @@ async function productionCallbackPage(req, res) { } } -authRouter.get('/login', isDevelopment() ? localhostLoginPage : productionLoginPage); - -authRouter.get('/login/callback', isDevelopment() ? localhostCallbackPage : productionCallbackPage); +if (isDevelopment()) { + authRouter.get('/login', localhostLoginPage); + authRouter.get('/login/callback', localhostCallbackPage); +} else { + authRouter.get('/login', productionLoginPage); + authRouter.get('/login/callback', productionCallbackPage); +} export async function authMiddleware(req, res, next) { const token = req.cookies[AUTH_COOKIE]; diff --git a/package-lock.json b/package-lock.json index 4006c42..0dda877 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,10 +12,12 @@ "clsx": "^2.1.1", "cookie-parser": "^1.4.6", "date-fns": "^3.6.0", + "dotenv": "^16.4.5", "express": "^4.19.2", "google-auth-library": "^9.9.0", "jose": "^5.2.4", "mysql": "^2.18.1", + "openid-client": "^5.6.5", "react": "^18.3.1", "react-dom": "^18.3.1", "react-router-dom": "^6.22.3", @@ -5834,12 +5836,14 @@ } }, "node_modules/dotenv": { - "version": "16.0.3", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.3.tgz", - "integrity": "sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==", - "dev": true, + "version": "16.4.5", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", + "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", "engines": { "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" } }, "node_modules/dotenv-expand": { @@ -7785,7 +7789,6 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, "dependencies": { "yallist": "^4.0.0" }, @@ -8419,6 +8422,14 @@ "node": ">=0.10.0" } }, + "node_modules/object-hash": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.2.0.tgz", + "integrity": "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==", + "engines": { + "node": ">= 6" + } + }, "node_modules/object-inspect": { "version": "1.12.2", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz", @@ -8483,6 +8494,14 @@ "resolved": "https://registry.npmjs.org/ohash/-/ohash-1.1.3.tgz", "integrity": "sha512-zuHHiGTYTA1sYJ/wZN+t5HKZaH23i4yI1HMwbuXm24Nid7Dv0KcuRlKoNKS9UNfAVSBlnGLcuQrnOKWOZoEGaw==" }, + "node_modules/oidc-token-hash": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.0.3.tgz", + "integrity": "sha512-IF4PcGgzAr6XXSff26Sk/+P4KZFJVuHAJZj3wgO3vX2bMdNVp/QXTP3P7CEm9V1IdG8lDLY3HhiqpsE/nOwpPw==", + "engines": { + "node": "^10.13.0 || >=12.0.0" + } + }, "node_modules/on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", @@ -8544,6 +8563,28 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/openid-client": { + "version": "5.6.5", + "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-5.6.5.tgz", + "integrity": "sha512-5P4qO9nGJzB5PI0LFlhj4Dzg3m4odt0qsJTfyEtZyOlkgpILwEioOhVVJOrS1iVH494S4Ee5OCjjg6Bf5WOj3w==", + "dependencies": { + "jose": "^4.15.5", + "lru-cache": "^6.0.0", + "object-hash": "^2.2.0", + "oidc-token-hash": "^5.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/openid-client/node_modules/jose": { + "version": "4.15.5", + "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.5.tgz", + "integrity": "sha512-jc7BFxgKPKi94uOvEmzlSWFFe2+vASyXaKUpdQKatWAESU2MWjDfFf0fdfc83CDKcA5QecabZeNLyfhe3yKNkg==", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/ora": { "version": "5.4.1", "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", @@ -11038,8 +11079,7 @@ "node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, "node_modules/yargs": { "version": "17.7.2", @@ -15304,10 +15344,9 @@ } }, "dotenv": { - "version": "16.0.3", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.3.tgz", - "integrity": "sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==", - "dev": true + "version": "16.4.5", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", + "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==" }, "dotenv-expand": { "version": "10.0.0", @@ -16780,7 +16819,6 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, "requires": { "yallist": "^4.0.0" } @@ -17235,6 +17273,11 @@ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", "dev": true }, + "object-hash": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.2.0.tgz", + "integrity": "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==" + }, "object-inspect": { "version": "1.12.2", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz", @@ -17278,6 +17321,11 @@ "resolved": "https://registry.npmjs.org/ohash/-/ohash-1.1.3.tgz", "integrity": "sha512-zuHHiGTYTA1sYJ/wZN+t5HKZaH23i4yI1HMwbuXm24Nid7Dv0KcuRlKoNKS9UNfAVSBlnGLcuQrnOKWOZoEGaw==" }, + "oidc-token-hash": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.0.3.tgz", + "integrity": "sha512-IF4PcGgzAr6XXSff26Sk/+P4KZFJVuHAJZj3wgO3vX2bMdNVp/QXTP3P7CEm9V1IdG8lDLY3HhiqpsE/nOwpPw==" + }, "on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", @@ -17321,6 +17369,24 @@ "is-wsl": "^2.2.0" } }, + "openid-client": { + "version": "5.6.5", + "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-5.6.5.tgz", + "integrity": "sha512-5P4qO9nGJzB5PI0LFlhj4Dzg3m4odt0qsJTfyEtZyOlkgpILwEioOhVVJOrS1iVH494S4Ee5OCjjg6Bf5WOj3w==", + "requires": { + "jose": "^4.15.5", + "lru-cache": "^6.0.0", + "object-hash": "^2.2.0", + "oidc-token-hash": "^5.0.3" + }, + "dependencies": { + "jose": { + "version": "4.15.5", + "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.5.tgz", + "integrity": "sha512-jc7BFxgKPKi94uOvEmzlSWFFe2+vASyXaKUpdQKatWAESU2MWjDfFf0fdfc83CDKcA5QecabZeNLyfhe3yKNkg==" + } + } + }, "ora": { "version": "5.4.1", "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", @@ -19105,8 +19171,7 @@ "yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, "yargs": { "version": "17.7.2", diff --git a/package.json b/package.json index 9fdabc9..38d92df 100644 --- a/package.json +++ b/package.json @@ -32,10 +32,12 @@ "clsx": "^2.1.1", "cookie-parser": "^1.4.6", "date-fns": "^3.6.0", + "dotenv": "^16.4.5", "express": "^4.19.2", "google-auth-library": "^9.9.0", "jose": "^5.2.4", "mysql": "^2.18.1", + "openid-client": "^5.6.5", "react": "^18.3.1", "react-dom": "^18.3.1", "react-router-dom": "^6.22.3", diff --git a/server.js b/server.js index 634bc58..1f791ff 100644 --- a/server.js +++ b/server.js @@ -1,4 +1,5 @@ import process from 'process'; +import 'dotenv/config'; import { isDevelopment } from './backend/utils.js'; import { app } from './backend/app.js';