From c7f4070ff5462e8995f7ebb3a50978f883960919 Mon Sep 17 00:00:00 2001 From: Martin McKeaveney Date: Thu, 8 Apr 2021 11:20:37 +0100 Subject: [PATCH] encapsulate more auth functionality into shared module and use in worker --- packages/auth/src/db/utils.js | 2 + packages/auth/src/index.js | 15 +++- packages/auth/src/middleware/authenticated.js | 73 ----------------- packages/auth/src/utils.js | 79 ++++++++++++++++++ .../src/api/controllers/search/index.js | 2 +- .../worker/src/api/controllers/admin/auth.js | 2 +- packages/worker/src/api/routes/admin/index.js | 17 ++-- packages/worker/src/db/utils.js | 3 + packages/worker/src/index.js | 2 +- .../worker/src/middleware/authenticated.js | 44 ++++++++++ packages/worker/src/utils.js | 81 +++++++++++++++++-- 11 files changed, 222 insertions(+), 98 deletions(-) delete mode 100644 packages/auth/src/middleware/authenticated.js create mode 100644 packages/auth/src/utils.js create mode 100644 packages/worker/src/middleware/authenticated.js diff --git a/packages/auth/src/db/utils.js b/packages/auth/src/db/utils.js index cda7708825..17d09ceaeb 100644 --- a/packages/auth/src/db/utils.js +++ b/packages/auth/src/db/utils.js @@ -8,6 +8,8 @@ const DocumentTypes = { USER: "us", } +exports.DocumentTypes = DocumentTypes + const UNICODE_MAX = "\ufff0" const SEPARATOR = "_" diff --git a/packages/auth/src/index.js b/packages/auth/src/index.js index 3088008086..84e3fe6595 100644 --- a/packages/auth/src/index.js +++ b/packages/auth/src/index.js @@ -5,6 +5,9 @@ const JwtStrategy = require("passport-jwt").Strategy const CouchDB = require("./db") const { StaticDatabases } = require("./db/utils") const { jwt, local, google } = require("./middleware") +const { Cookies, UserStatus } = require("./constants") +const { hash, compare } = require("./hashing") +const { getAppId, setCookie } = require("./utils") // Strategies passport.use(new LocalStrategy(local.options, local.authenticate)) @@ -25,6 +28,12 @@ passport.deserializeUser(async (user, done) => { } }) -// exports.Cookies = Cookies - -module.exports = passport +module.exports = { + passport, + Cookies, + UserStatus, + hash, + compare, + getAppId, + setCookie, +} diff --git a/packages/auth/src/middleware/authenticated.js b/packages/auth/src/middleware/authenticated.js deleted file mode 100644 index 7797649b18..0000000000 --- a/packages/auth/src/middleware/authenticated.js +++ /dev/null @@ -1,73 +0,0 @@ -const jwt = require("jsonwebtoken") -const STATUS_CODES = require("../utilities/statusCodes") -const { getRole, getBuiltinRoles } = require("../utilities/security/roles") -const { AuthTypes, Cookies } = require("../constants") -const { - getAppId, - getCookieName, - clearCookie, - setCookie, - isClient, -} = require("../utilities") - -module.exports = async (ctx, next) => { - // if (ctx.path === "/_builder") { - // await next() - // return - // } - - // do everything we can to make sure the appId is held correctly - // we hold it in state as a - let appId = getAppId(ctx) - const cookieAppId = ctx.cookies.get(Cookies.CurrentApp) - // const builtinRoles = getBuiltinRoles() - if (appId && cookieAppId !== appId) { - setCookie(ctx, appId, Cookies.CurrentApp) - } else if (cookieAppId) { - appId = cookieAppId - } - let token, authType - // if (!isClient(ctx)) { - // token = ctx.cookies.get(getCookieName()) - // authType = AuthTypes.BUILDER - // } - - if (!token && appId) { - token = ctx.cookies.get(getCookieName(appId)) - // authType = AuthTypes.APP - } - - // if (!token) { - // ctx.auth.authenticated = false - // ctx.appId = appId - // ctx.user = { - // role: builtinRoles.PUBLIC, - // } - // await next() - // return - // } - - try { - // ctx.auth.authenticated = authType - const jwtPayload = jwt.verify(token, ctx.config.jwtSecret) - ctx.appId = appId - // ctx.auth.apiKey = jwtPayload.apiKey - ctx.user = { - ...jwtPayload, - role: await getRole(appId, jwtPayload.roleId), - } - // appId no longer carried in user, make sure - delete ctx.user.appId - } catch (err) { - console.log(err) - // if (authType === AuthTypes.BUILDER) { - // clearCookie(ctx) - // ctx.status = 200 - // return - // } else { - ctx.throw(err.status || STATUS_CODES.FORBIDDEN, err.text) - // } - } - - await next() -} diff --git a/packages/auth/src/utils.js b/packages/auth/src/utils.js new file mode 100644 index 0000000000..7eb39a3005 --- /dev/null +++ b/packages/auth/src/utils.js @@ -0,0 +1,79 @@ +const { DocumentTypes, SEPARATOR } = require("./db/utils") + +const APP_PREFIX = DocumentTypes.APP + SEPARATOR + +function confirmAppId(possibleAppId) { + return possibleAppId && possibleAppId.startsWith(APP_PREFIX) + ? possibleAppId + : undefined +} + +/** + * Given a request tries to find the appId, which can be located in various places + * @param {object} ctx The main request body to look through. + * @returns {string|undefined} If an appId was found it will be returned. + */ +exports.getAppId = ctx => { + const options = [ctx.headers["x-budibase-app-id"], ctx.params.appId] + if (ctx.subdomains) { + options.push(ctx.subdomains[1]) + } + let appId + for (let option of options) { + appId = confirmAppId(option) + if (appId) { + break + } + } + + // look in body if can't find it in subdomain + if (!appId && ctx.request.body && ctx.request.body.appId) { + appId = confirmAppId(ctx.request.body.appId) + } + let appPath = + ctx.request.headers.referrer || + ctx.path.split("/").filter(subPath => subPath.startsWith(APP_PREFIX)) + if (!appId && appPath.length !== 0) { + appId = confirmAppId(appPath[0]) + } + return appId +} + +/** + * Store a cookie for the request, has a hardcoded expiry. + * @param {object} ctx The request which is to be manipulated. + * @param {string} name The name of the cookie to set. + * @param {string|object} value The value of cookie which will be set. + */ +exports.setCookie = (ctx, value, name = "builder") => { + const expires = new Date() + expires.setDate(expires.getDate() + 1) + + if (!value) { + ctx.cookies.set(name) + } else { + ctx.cookies.set(name, value, { + expires, + path: "/", + httpOnly: false, + overwrite: true, + }) + } +} + +/** + * Utility function, simply calls setCookie with an empty string for value + */ +exports.clearCookie = (ctx, name) => { + exports.setCookie(ctx, "", name) +} + +/** + * Checks if the API call being made (based on the provided ctx object) is from the client. If + * the call is not from a client app then it is from the builder. + * @param {object} ctx The koa context object to be tested. + * @return {boolean} returns true if the call is from the client lib (a built app rather than the builder). + */ +exports.isClient = ctx => { + return ctx.headers["x-budibase-type"] === "client" +} diff --git a/packages/server/src/api/controllers/search/index.js b/packages/server/src/api/controllers/search/index.js index d3c588d079..234c7eb258 100644 --- a/packages/server/src/api/controllers/search/index.js +++ b/packages/server/src/api/controllers/search/index.js @@ -1,7 +1,7 @@ const { QueryBuilder, buildSearchUrl, search } = require("./utils") exports.rowSearch = async ctx => { - const appId = ctx.user.appId + const appId = ctx.appId const { tableId } = ctx.params const { bookmark, query, raw } = ctx.request.body let url diff --git a/packages/worker/src/api/controllers/admin/auth.js b/packages/worker/src/api/controllers/admin/auth.js index 2c41c1f47d..62128efedf 100644 --- a/packages/worker/src/api/controllers/admin/auth.js +++ b/packages/worker/src/api/controllers/admin/auth.js @@ -1,4 +1,4 @@ -const passport = require("@budibase/auth") +const { passport } = require("@budibase/auth") exports.authenticate = async (ctx, next) => { return passport.authenticate("local", async (err, user) => { diff --git a/packages/worker/src/api/routes/admin/index.js b/packages/worker/src/api/routes/admin/index.js index c87e395a22..1c64110d2a 100644 --- a/packages/worker/src/api/routes/admin/index.js +++ b/packages/worker/src/api/routes/admin/index.js @@ -2,22 +2,15 @@ const Router = require("@koa/router") const passport = require("@budibase/auth") const controller = require("../../controllers/admin") const authController = require("../../controllers/admin/auth") +const authenticated = require("../../../middleware/authenticated") const router = Router() router - .post("/api/admin/users", passport.authenticate("jwt"), controller.userSave) + .post("/api/admin/users", authenticated, controller.userSave) .post("/api/admin/authenticate", authController.authenticate) - .delete( - "/api/admin/users/:email", - passport.authenticate("jwt"), - controller.userDelete - ) - .get("/api/admin/users", passport.authenticate("jwt"), controller.userFetch) - .get( - "/api/admin/users/:email", - passport.authenticate("jwt"), - controller.userFind - ) + .delete("/api/admin/users/:email", authenticated, controller.userDelete) + .get("/api/admin/users", authenticated, controller.userFetch) + .get("/api/admin/users/:email", authenticated, controller.userFind) module.exports = router diff --git a/packages/worker/src/db/utils.js b/packages/worker/src/db/utils.js index cda7708825..b250b895bb 100644 --- a/packages/worker/src/db/utils.js +++ b/packages/worker/src/db/utils.js @@ -6,8 +6,11 @@ exports.StaticDatabases = { const DocumentTypes = { USER: "us", + APP: "app", } +exports.DocumentTypes = DocumentTypes + const UNICODE_MAX = "\ufff0" const SEPARATOR = "_" diff --git a/packages/worker/src/index.js b/packages/worker/src/index.js index aad8bf54f7..2e031b9f64 100644 --- a/packages/worker/src/index.js +++ b/packages/worker/src/index.js @@ -1,7 +1,7 @@ const Koa = require("koa") const destroyable = require("server-destroy") const koaBody = require("koa-body") -const passport = require("@budibase/auth") +const { passport } = require("@budibase/auth") const logger = require("koa-pino-logger") const http = require("http") const api = require("./api") diff --git a/packages/worker/src/middleware/authenticated.js b/packages/worker/src/middleware/authenticated.js new file mode 100644 index 0000000000..8701da7316 --- /dev/null +++ b/packages/worker/src/middleware/authenticated.js @@ -0,0 +1,44 @@ +const { passport, getAppId, setCookie, Cookies } = require("@budibase/auth") + +module.exports = async (ctx, next) => { + // do everything we can to make sure the appId is held correctly + let appId = getAppId(ctx) + const cookieAppId = ctx.cookies.get(Cookies.CurrentApp) + // const builtinRoles = getBuiltinRoles() + if (appId && cookieAppId !== appId) { + setCookie(ctx, appId, Cookies.CurrentApp) + } else if (cookieAppId) { + appId = cookieAppId + } + + let token + if (appId) { + token = ctx.cookies.get(Cookies.Auth) + } + + if (!token) { + ctx.auth = { + authenticated: true, + } + ctx.appId = appId + // ctx.user = { + // // TODO: introduce roles again + // // role: builtinRoles.PUBLIC, + // } + return await next() + } + + return passport.authenticate("jwt", async (err, user) => { + if (err) { + return ctx.throw(err) + } + + try { + ctx.user = user + await next() + } catch (err) { + console.log(err) + ctx.throw(err.status || 403, err.text) + } + })(ctx, next) +} diff --git a/packages/worker/src/utils.js b/packages/worker/src/utils.js index 0711ae67bf..260a64cda5 100644 --- a/packages/worker/src/utils.js +++ b/packages/worker/src/utils.js @@ -1,13 +1,80 @@ -const bcrypt = require("bcryptjs") const env = require("./environment") +const { DocumentTypes, SEPARATOR } = require("./db/utils") -const SALT_ROUNDS = env.SALT_ROUNDS || 10 +const APP_PREFIX = DocumentTypes.APP + SEPARATOR -exports.hash = async data => { - const salt = await bcrypt.genSalt(SALT_ROUNDS) - return bcrypt.hash(data, salt) +function confirmAppId(possibleAppId) { + return possibleAppId && possibleAppId.startsWith(APP_PREFIX) + ? possibleAppId + : undefined } -exports.compare = async (data, encrypted) => { - return bcrypt.compare(data, encrypted) +/** + * Given a request tries to find the appId, which can be located in various places + * @param {object} ctx The main request body to look through. + * @returns {string|undefined} If an appId was found it will be returned. + */ +exports.getAppId = ctx => { + const options = [ctx.headers["x-budibase-app-id"], ctx.params.appId] + if (ctx.subdomains) { + options.push(ctx.subdomains[1]) + } + let appId + for (let option of options) { + appId = confirmAppId(option) + if (appId) { + break + } + } + + // look in body if can't find it in subdomain + if (!appId && ctx.request.body && ctx.request.body.appId) { + appId = confirmAppId(ctx.request.body.appId) + } + let appPath = + ctx.request.headers.referrer || + ctx.path.split("/").filter(subPath => subPath.startsWith(APP_PREFIX)) + if (!appId && appPath.length !== 0) { + appId = confirmAppId(appPath[0]) + } + return appId +} + +/** + * Store a cookie for the request, has a hardcoded expiry. + * @param {object} ctx The request which is to be manipulated. + * @param {string} name The name of the cookie to set. + * @param {string|object} value The value of cookie which will be set. + */ +exports.setCookie = (ctx, value, name = "builder") => { + const expires = new Date() + expires.setDate(expires.getDate() + 1) + + if (!value) { + ctx.cookies.set(name) + } else { + ctx.cookies.set(name, value, { + expires, + path: "/", + httpOnly: false, + overwrite: true, + }) + } +} + +/** + * Utility function, simply calls setCookie with an empty string for value + */ +exports.clearCookie = (ctx, name) => { + exports.setCookie(ctx, "", name) +} + +/** + * Checks if the API call being made (based on the provided ctx object) is from the client. If + * the call is not from a client app then it is from the builder. + * @param {object} ctx The koa context object to be tested. + * @return {boolean} returns true if the call is from the client lib (a built app rather than the builder). + */ +exports.isClient = ctx => { + return ctx.headers["x-budibase-type"] === "client" }