From baab7141c0038c82eb70d06b173310b57585fdf5 Mon Sep 17 00:00:00 2001 From: Bernhard Hayden Date: Sun, 27 Jun 2021 16:46:04 +0200 Subject: [PATCH] Proof of concept OIDC implementation --- packages/auth/package.json | 1 + packages/auth/src/index.js | 3 +- packages/auth/src/middleware/index.js | 2 + packages/auth/src/middleware/passport/oidc.js | 111 ++++++++++++++++++ packages/auth/yarn.lock | 39 +++++- .../auth/_components/OIDCButton.svelte | 35 ++++++ .../src/pages/builder/auth/login.svelte | 2 + packages/worker/package.json | 1 + .../worker/src/api/controllers/admin/auth.js | 25 ++++ packages/worker/src/api/index.js | 8 ++ packages/worker/src/api/routes/admin/auth.js | 2 + packages/worker/src/index.js | 4 + packages/worker/yarn.lock | 39 +++++- 13 files changed, 263 insertions(+), 9 deletions(-) create mode 100644 packages/auth/src/middleware/passport/oidc.js create mode 100644 packages/builder/src/pages/builder/auth/_components/OIDCButton.svelte diff --git a/packages/auth/package.json b/packages/auth/package.json index 650d41b633..9155d37eeb 100644 --- a/packages/auth/package.json +++ b/packages/auth/package.json @@ -13,6 +13,7 @@ "koa-passport": "^4.1.4", "lodash": "^4.17.21", "node-fetch": "^2.6.1", + "@techpass/passport-openidconnect": "^0.3.0", "passport-google-auth": "^1.0.2", "passport-google-oauth": "^2.0.0", "passport-jwt": "^4.0.0", diff --git a/packages/auth/src/index.js b/packages/auth/src/index.js index 9582d6ffd6..cb4cb8d550 100644 --- a/packages/auth/src/index.js +++ b/packages/auth/src/index.js @@ -2,7 +2,7 @@ const passport = require("koa-passport") const LocalStrategy = require("passport-local").Strategy const JwtStrategy = require("passport-jwt").Strategy const { StaticDatabases } = require("./db/utils") -const { jwt, local, authenticated, google, auditLog } = require("./middleware") +const { jwt, local, authenticated, google, oidc, auditLog } = require("./middleware") const { setDB, getDB } = require("./db") // Strategies @@ -44,6 +44,7 @@ module.exports = { buildAuthMiddleware: authenticated, passport, google, + oidc, jwt: require("jsonwebtoken"), auditLog, }, diff --git a/packages/auth/src/middleware/index.js b/packages/auth/src/middleware/index.js index 2a249ce0f9..35c7d9c388 100644 --- a/packages/auth/src/middleware/index.js +++ b/packages/auth/src/middleware/index.js @@ -1,11 +1,13 @@ const jwt = require("./passport/jwt") const local = require("./passport/local") const google = require("./passport/google") +const oidc = require("./passport/oidc") const authenticated = require("./authenticated") const auditLog = require("./auditLog") module.exports = { google, + oidc, jwt, local, authenticated, diff --git a/packages/auth/src/middleware/passport/oidc.js b/packages/auth/src/middleware/passport/oidc.js new file mode 100644 index 0000000000..09c7e2a05e --- /dev/null +++ b/packages/auth/src/middleware/passport/oidc.js @@ -0,0 +1,111 @@ +const env = require("../../environment") +const jwt = require("jsonwebtoken") +const database = require("../../db") +const OIDCStrategy = require("@techpass/passport-openidconnect").Strategy +const { + StaticDatabases, + generateGlobalUserID, + ViewNames, +} = require("../../db/utils") + +// async function authenticate(token, tokenSecret, profile, done) { +async function authenticate(issuer, sub, profile, jwtClaims, accessToken, refreshToken, idToken, params, done) { + // Check the user exists in the instance DB by email + const db = database.getDB(StaticDatabases.GLOBAL.name) + + let dbUser + + const userId = generateGlobalUserID(profile.id) + + try { + // use the google profile id + dbUser = await db.get(userId) + } catch (err) { + const user = { + _id: userId, + provider: profile.provider, + roles: {}, + ...profile._json, + } + + // check if an account with the google email address exists locally + const users = await db.query(`database/${ViewNames.USER_BY_EMAIL}`, { + key: profile._json.email, + include_docs: true, + }) + + // Google user already exists by email + if (users.rows.length > 0) { + const existing = users.rows[0].doc + + // remove the local account to avoid conflicts + await db.remove(existing._id, existing._rev) + + // merge with existing account + user.roles = existing.roles + user.builder = existing.builder + user.admin = existing.admin + + const response = await db.post(user) + dbUser = user + dbUser._rev = response.rev + } else { + return done( + new Error( + "email does not yet exist. You must set up your local budibase account first." + ), + false + ) + } + } + + // authenticate + const payload = { + userId: dbUser._id, + builder: dbUser.builder, + email: dbUser.email, + } + + dbUser.token = jwt.sign(payload, env.JWT_SECRET, { + expiresIn: "1 day", + }) + + return done(null, dbUser) +} + +/** + * Create an instance of the google passport strategy. This wrapper fetches the configuration + * from couchDB rather than environment variables, using this factory is necessary for dynamically configuring passport. + * @returns Dynamically configured Passport Google Strategy + */ +exports.strategyFactory = async function () { + try { + + /* + const { clientID, clientSecret, callbackURL } = config + + if (!clientID || !clientSecret || !callbackURL) { + throw new Error( + "Configuration invalid. Must contain google clientID, clientSecret and callbackURL" + ) + } + */ + + return new OIDCStrategy( + { + issuer: "https://base.uri/auth/realms/realm_name", + authorizationURL: "https://base.uri/auth/realms/realm_name/protocol/openid-connect/auth", + tokenURL: "https://base.uri/auth/realms/realm_name/protocol/openid-connect/token", + userInfoURL: "https://base.uri/auth/realms/realm_name/protocol/openid-connect/userinfo", + clientID: "my_client_id", + clientSecret: "my_client_secret", + callbackURL: "http://localhost:10000/api/admin/auth/oidc/callback", + scope: "openid profile email", + }, + authenticate + ) + } catch (err) { + console.error(err) + throw new Error("Error constructing OIDC authentication strategy", err) + } +} diff --git a/packages/auth/yarn.lock b/packages/auth/yarn.lock index 80625a9345..d52ce0145c 100644 --- a/packages/auth/yarn.lock +++ b/packages/auth/yarn.lock @@ -2,6 +2,17 @@ # yarn lockfile v1 +"@techpass/passport-openidconnect@^0.3.0": + version "0.3.0" + resolved "https://registry.yarnpkg.com/@techpass/passport-openidconnect/-/passport-openidconnect-0.3.0.tgz#a60b2bbf3f262649a5a02d5d186219944acc3010" + integrity sha512-bVsPwl66s7J7GHxTPlW/RJYhZol9SshNznQsx83OOh9G+JWFGoeWxh+xbX+FTdJNoUvGIGbJnpWPY2wC6NOHPw== + dependencies: + base64url "^3.0.1" + oauth "^0.9.15" + passport-strategy "^1.0.0" + request "^2.88.0" + webfinger "^0.4.2" + ajv@^6.12.3: version "6.12.6" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" @@ -71,7 +82,7 @@ base64-js@^1.0.2, base64-js@^1.3.1: resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== -base64url@3.x.x: +base64url@3.x.x, base64url@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/base64url/-/base64url-3.0.1.tgz#6399d572e2bc3f90a9a8b22d5dbb0a32d33f788d" integrity sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A== @@ -574,7 +585,7 @@ oauth-sign@~0.9.0: resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ== -oauth@0.9.x: +oauth@0.9.x, oauth@^0.9.15: version "0.9.15" resolved "https://registry.yarnpkg.com/oauth/-/oauth-0.9.15.tgz#bd1fefaf686c96b75475aed5196412ff60cfb9c1" integrity sha1-vR/vr2hslrdUda7VGWQS/2DPucE= @@ -748,7 +759,7 @@ redis-parser@^3.0.0: dependencies: redis-errors "^1.0.0" -request@^2.72.0, request@^2.74.0: +request@^2.72.0, request@^2.74.0, request@^2.88.0: version "2.88.2" resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3" integrity sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw== @@ -794,7 +805,7 @@ sax@1.2.1: resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.1.tgz#7b8e656190b228e81a66aea748480d828cd2d37a" integrity sha1-e45lYZCyKOgaZq6nSEgNgozS03o= -sax@>=0.6.0: +sax@>=0.1.1, sax@>=0.6.0: version "1.2.4" resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== @@ -829,6 +840,11 @@ standard-as-callback@^2.1.0: resolved "https://registry.yarnpkg.com/standard-as-callback/-/standard-as-callback-2.1.0.tgz#8953fc05359868a77b5b9739a665c5977bb7df45" integrity sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A== +step@0.0.x: + version "0.0.6" + resolved "https://registry.yarnpkg.com/step/-/step-0.0.6.tgz#143e7849a5d7d3f4a088fe29af94915216eeede2" + integrity sha1-FD54SaXX0/SgiP4pr5SRUhbu7eI= + string-template@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/string-template/-/string-template-1.0.0.tgz#9e9f2233dc00f218718ec379a28a5673ecca8b96" @@ -943,11 +959,26 @@ verror@1.10.0: core-util-is "1.0.2" extsprintf "^1.2.0" +webfinger@^0.4.2: + version "0.4.2" + resolved "https://registry.yarnpkg.com/webfinger/-/webfinger-0.4.2.tgz#3477a6d97799461896039fcffc650b73468ee76d" + integrity sha1-NHem2XeZRhiWA5/P/GULc0aO520= + dependencies: + step "0.0.x" + xml2js "0.1.x" + wrappy@1: version "1.0.2" resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= +xml2js@0.1.x: + version "0.1.14" + resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.1.14.tgz#5274e67f5a64c5f92974cd85139e0332adc6b90c" + integrity sha1-UnTmf1pkxfkpdM2FE54DMq3GuQw= + dependencies: + sax ">=0.1.1" + xml2js@0.4.19: version "0.4.19" resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.19.tgz#686c20f213209e94abf0d1bcf1efaa291c7827a7" diff --git a/packages/builder/src/pages/builder/auth/_components/OIDCButton.svelte b/packages/builder/src/pages/builder/auth/_components/OIDCButton.svelte new file mode 100644 index 0000000000..98f5d3efb9 --- /dev/null +++ b/packages/builder/src/pages/builder/auth/_components/OIDCButton.svelte @@ -0,0 +1,35 @@ + + +{#if show} + window.open("/api/admin/auth/oidc", "_blank")} + > +
+

Sign in with OIDC

+
+
+{/if} + + diff --git a/packages/builder/src/pages/builder/auth/login.svelte b/packages/builder/src/pages/builder/auth/login.svelte index 9fb984c73e..3850431a0f 100644 --- a/packages/builder/src/pages/builder/auth/login.svelte +++ b/packages/builder/src/pages/builder/auth/login.svelte @@ -12,6 +12,7 @@ import { goto, params } from "@roxi/routify" import { auth, organisation } from "stores/portal" import GoogleButton from "./_components/GoogleButton.svelte" + import OIDCButton from "./_components/OIDCButton.svelte" import Logo from "assets/bb-emblem.svg" import { onMount } from "svelte" @@ -61,6 +62,7 @@ Sign in to {company} + Sign in with email diff --git a/packages/worker/package.json b/packages/worker/package.json index d6ce2edce1..833f7b3ee9 100644 --- a/packages/worker/package.json +++ b/packages/worker/package.json @@ -39,6 +39,7 @@ "koa-static": "^5.0.0", "node-fetch": "^2.6.1", "nodemailer": "^6.5.0", + "@techpass/passport-openidconnect": "^0.3.0", "passport-google-oauth": "^2.0.0", "passport-jwt": "^4.0.0", "passport-local": "^1.0.0", diff --git a/packages/worker/src/api/controllers/admin/auth.js b/packages/worker/src/api/controllers/admin/auth.js index 5304ac85d1..374aa5c47d 100644 --- a/packages/worker/src/api/controllers/admin/auth.js +++ b/packages/worker/src/api/controllers/admin/auth.js @@ -1,5 +1,6 @@ const authPkg = require("@budibase/auth") const { google } = require("@budibase/auth/src/middleware") +const { oidc } = require("@budibase/auth/src/middleware") const { Configs, EmailTemplatePurpose } = require("../../../constants") const CouchDB = require("../../../db") const { sendEmail, isEmailConfigured } = require("../../../utilities/email") @@ -129,3 +130,27 @@ exports.googleAuth = async (ctx, next) => { } )(ctx, next) } + +// Minimal OIDC attempt + +exports.oidcPreAuth = async (ctx, next) => { + const strategy = await oidc.strategyFactory() + + return passport.authenticate(strategy, { + scope: ["profile", "email"], + })(ctx, next) +} + +exports.oidcAuth = async (ctx, next) => { + const strategy = await oidc.strategyFactory() + + return passport.authenticate( + strategy, + { successRedirect: "/", failureRedirect: "/error" }, + async (err, user) => { + authInternal(ctx, user, err) + + ctx.redirect("/") + } + )(ctx, next) +} diff --git a/packages/worker/src/api/index.js b/packages/worker/src/api/index.js index bda57863f6..c77c70089e 100644 --- a/packages/worker/src/api/index.js +++ b/packages/worker/src/api/index.js @@ -25,6 +25,14 @@ const PUBLIC_ENDPOINTS = [ route: "/api/admin/auth/google/callback", method: "GET", }, + { + route: "/api/admin/auth/oidc", + method: "GET", + }, + { + route: "/api/admin/auth/oidc/callback", + method: "GET", + }, { route: "/api/admin/auth/reset", method: "POST", diff --git a/packages/worker/src/api/routes/admin/auth.js b/packages/worker/src/api/routes/admin/auth.js index 04e30fc006..27f09f74f9 100644 --- a/packages/worker/src/api/routes/admin/auth.js +++ b/packages/worker/src/api/routes/admin/auth.js @@ -39,5 +39,7 @@ router .post("/api/admin/auth/logout", authController.logout) .get("/api/admin/auth/google", authController.googlePreAuth) .get("/api/admin/auth/google/callback", authController.googleAuth) + .get("/api/admin/auth/oidc", authController.oidcPreAuth) + .get("/api/admin/auth/oidc/callback", authController.oidcAuth) module.exports = router diff --git a/packages/worker/src/index.js b/packages/worker/src/index.js index f59f8bab15..4e105a1435 100644 --- a/packages/worker/src/index.js +++ b/packages/worker/src/index.js @@ -5,6 +5,7 @@ require("@budibase/auth").init(CouchDB) const Koa = require("koa") const destroyable = require("server-destroy") const koaBody = require("koa-body") +const koaSession = require("koa-session") const { passport } = require("@budibase/auth").auth const logger = require("koa-pino-logger") const http = require("http") @@ -13,8 +14,11 @@ const redis = require("./utilities/redis") const app = new Koa() +app.keys = ['secret', 'key']; + // set up top level koa middleware app.use(koaBody({ multipart: true })) +app.use(koaSession(app)) app.use( logger({ diff --git a/packages/worker/yarn.lock b/packages/worker/yarn.lock index 53f10856e8..1d4227363f 100644 --- a/packages/worker/yarn.lock +++ b/packages/worker/yarn.lock @@ -566,6 +566,17 @@ dependencies: defer-to-connect "^2.0.0" +"@techpass/passport-openidconnect@^0.3.0": + version "0.3.0" + resolved "https://registry.yarnpkg.com/@techpass/passport-openidconnect/-/passport-openidconnect-0.3.0.tgz#a60b2bbf3f262649a5a02d5d186219944acc3010" + integrity sha512-bVsPwl66s7J7GHxTPlW/RJYhZol9SshNznQsx83OOh9G+JWFGoeWxh+xbX+FTdJNoUvGIGbJnpWPY2wC6NOHPw== + dependencies: + base64url "^3.0.1" + oauth "^0.9.15" + passport-strategy "^1.0.0" + request "^2.88.0" + webfinger "^0.4.2" + "@types/babel__core@^7.0.0", "@types/babel__core@^7.1.7": version "7.1.14" resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.14.tgz#faaeefc4185ec71c389f4501ee5ec84b170cc402" @@ -1058,7 +1069,7 @@ base64-js@^1.0.2, base64-js@^1.3.1: resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== -base64url@3.x.x: +base64url@3.x.x, base64url@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/base64url/-/base64url-3.0.1.tgz#6399d572e2bc3f90a9a8b22d5dbb0a32d33f788d" integrity sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A== @@ -4183,7 +4194,7 @@ oauth-sign@~0.9.0: resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ== -oauth@0.9.x: +oauth@0.9.x, oauth@^0.9.15: version "0.9.15" resolved "https://registry.yarnpkg.com/oauth/-/oauth-0.9.15.tgz#bd1fefaf686c96b75475aed5196412ff60cfb9c1" integrity sha1-vR/vr2hslrdUda7VGWQS/2DPucE= @@ -4933,7 +4944,7 @@ request-promise-native@^1.0.9: stealthy-require "^1.1.1" tough-cookie "^2.3.3" -request@^2.88.2: +request@^2.88.0, request@^2.88.2: version "2.88.2" resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3" integrity sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw== @@ -5080,7 +5091,7 @@ sax@1.2.1: resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.1.tgz#7b8e656190b228e81a66aea748480d828cd2d37a" integrity sha1-e45lYZCyKOgaZq6nSEgNgozS03o= -sax@>=0.6.0: +sax@>=0.1.1, sax@>=0.6.0: version "1.2.4" resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== @@ -5390,6 +5401,11 @@ stealthy-require@^1.1.1: resolved "https://registry.yarnpkg.com/stealthy-require/-/stealthy-require-1.1.1.tgz#35b09875b4ff49f26a777e509b3090a3226bf24b" integrity sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks= +step@0.0.x: + version "0.0.6" + resolved "https://registry.yarnpkg.com/step/-/step-0.0.6.tgz#143e7849a5d7d3f4a088fe29af94915216eeede2" + integrity sha1-FD54SaXX0/SgiP4pr5SRUhbu7eI= + string-length@^4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/string-length/-/string-length-4.0.2.tgz#a8a8dc7bd5c1a82b9b3c8b87e125f66871b6e57a" @@ -5923,6 +5939,14 @@ walker@^1.0.7, walker@~1.0.5: dependencies: makeerror "1.0.x" +webfinger@^0.4.2: + version "0.4.2" + resolved "https://registry.yarnpkg.com/webfinger/-/webfinger-0.4.2.tgz#3477a6d97799461896039fcffc650b73468ee76d" + integrity sha1-NHem2XeZRhiWA5/P/GULc0aO520= + dependencies: + step "0.0.x" + xml2js "0.1.x" + webidl-conversions@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-5.0.0.tgz#ae59c8a00b121543a2acc65c0434f57b0fc11aff" @@ -6031,6 +6055,13 @@ xml-name-validator@^3.0.0: resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a" integrity sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw== +xml2js@0.1.x: + version "0.1.14" + resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.1.14.tgz#5274e67f5a64c5f92974cd85139e0332adc6b90c" + integrity sha1-UnTmf1pkxfkpdM2FE54DMq3GuQw= + dependencies: + sax ">=0.1.1" + xml2js@0.4.19: version "0.4.19" resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.19.tgz#686c20f213209e94abf0d1bcf1efaa291c7827a7"