diff --git a/.vscode/settings.json b/.vscode/settings.json index d471924fe0..4838a4fd89 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -3,5 +3,17 @@ "editor.codeActionsOnSave": { "source.fixAll": true }, - "editor.defaultFormatter": "svelte.svelte-vscode" + "editor.defaultFormatter": "svelte.svelte-vscode", + "[json]": { + "editor.defaultFormatter": "vscode.json-language-features" + }, + "[javascript]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "debug.javascript.terminalOptions": { + "skipFiles": [ + "${workspaceFolder}/packages/backend-core/node_modules/**", + "/**" + ] + }, } diff --git a/packages/backend-core/package.json b/packages/backend-core/package.json index 244a19d9e0..d2b3d3e3a0 100644 --- a/packages/backend-core/package.json +++ b/packages/backend-core/package.json @@ -36,6 +36,7 @@ "passport-google-oauth": "2.0.0", "passport-jwt": "4.0.0", "passport-local": "1.0.0", + "passport-oauth2-refresh": "^2.1.0", "posthog-node": "1.3.0", "pouchdb": "7.3.0", "pouchdb-find": "7.2.2", diff --git a/packages/backend-core/src/auth.js b/packages/backend-core/src/auth.js index b13cd932c6..b6d6a2027f 100644 --- a/packages/backend-core/src/auth.js +++ b/packages/backend-core/src/auth.js @@ -2,6 +2,9 @@ const passport = require("koa-passport") const LocalStrategy = require("passport-local").Strategy const JwtStrategy = require("passport-jwt").Strategy const { getGlobalDB } = require("./tenancy") +const refresh = require("passport-oauth2-refresh") +const { Configs } = require("./constants") +const { getScopedConfig } = require("./db/utils") const { jwt, local, @@ -12,6 +15,7 @@ const { tenancy, appTenancy, authError, + ssoCallbackUrl, csrf, internalApi, } = require("./middleware") @@ -34,6 +38,122 @@ passport.deserializeUser(async (user, done) => { } }) +async function refreshOIDCAccessToken(db, chosenConfig, refreshToken) { + const callbackUrl = await oidc.getCallbackUrl(db, chosenConfig) + let enrichedConfig + let strategy + + try { + enrichedConfig = await oidc.fetchStrategyConfig(chosenConfig, callbackUrl) + if (!enrichedConfig) { + throw new Error("OIDC Config contents invalid") + } + strategy = await oidc.strategyFactory(enrichedConfig) + } catch (err) { + console.error(err) + throw new Error("Could not refresh OAuth Token") + } + + refresh.use(strategy, { + setRefreshOAuth2() { + return strategy._getOAuth2Client(enrichedConfig) + }, + }) + + return new Promise(resolve => { + refresh.requestNewAccessToken( + Configs.OIDC, + refreshToken, + (err, accessToken, refreshToken, params) => { + resolve({ err, accessToken, refreshToken, params }) + } + ) + }) +} + +async function refreshGoogleAccessToken(db, config, refreshToken) { + let callbackUrl = await google.getCallbackUrl(db, config) + + let strategy + try { + strategy = await google.strategyFactory(config, callbackUrl) + } catch (err) { + console.error(err) + throw new Error("Error constructing OIDC refresh strategy", err) + } + + refresh.use(strategy) + + return new Promise(resolve => { + refresh.requestNewAccessToken( + Configs.GOOGLE, + refreshToken, + (err, accessToken, refreshToken, params) => { + resolve({ err, accessToken, refreshToken, params }) + } + ) + }) +} + +async function refreshOAuthToken(refreshToken, configType, configId) { + const db = getGlobalDB() + + const config = await getScopedConfig(db, { + type: configType, + group: {}, + }) + + let chosenConfig = {} + let refreshResponse + if (configType === Configs.OIDC) { + // configId - retrieved from cookie. + chosenConfig = config.configs.filter(c => c.uuid === configId)[0] + if (!chosenConfig) { + throw new Error("Invalid OIDC configuration") + } + refreshResponse = await refreshOIDCAccessToken( + db, + chosenConfig, + refreshToken + ) + } else { + chosenConfig = config + refreshResponse = await refreshGoogleAccessToken( + db, + chosenConfig, + refreshToken + ) + } + + return refreshResponse +} + +async function updateUserOAuth(userId, oAuthConfig) { + const details = { + accessToken: oAuthConfig.accessToken, + refreshToken: oAuthConfig.refreshToken, + } + + try { + const db = getGlobalDB() + const dbUser = await db.get(userId) + + //Do not overwrite the refresh token if a valid one is not provided. + if (typeof details.refreshToken !== "string") { + delete details.refreshToken + } + + dbUser.oauth2 = { + ...dbUser.oauth2, + ...details, + } + + await db.put(dbUser) + } catch (e) { + console.error("Could not update OAuth details for current user", e) + } +} + module.exports = { buildAuthMiddleware: authenticated, passport, @@ -46,4 +166,7 @@ module.exports = { authError, buildCsrfMiddleware: csrf, internalApi, + refreshOAuthToken, + updateUserOAuth, + ssoCallbackUrl, } diff --git a/packages/backend-core/src/db/utils.ts b/packages/backend-core/src/db/utils.ts index 4910899565..ba3f1dd3e9 100644 --- a/packages/backend-core/src/db/utils.ts +++ b/packages/backend-core/src/db/utils.ts @@ -387,7 +387,9 @@ export const getScopedFullConfig = async function ( if (type === Configs.SETTINGS) { if (scopedConfig && scopedConfig.doc) { // overrides affected by environment variables - scopedConfig.doc.config.platformUrl = await getPlatformUrl() + scopedConfig.doc.config.platformUrl = await getPlatformUrl({ + tenantAware: true, + }) scopedConfig.doc.config.analyticsEnabled = await events.analytics.enabled() } else { @@ -396,7 +398,7 @@ export const getScopedFullConfig = async function ( doc: { _id: generateConfigID({ type, user, workspace }), config: { - platformUrl: await getPlatformUrl(), + platformUrl: await getPlatformUrl({ tenantAware: true }), analyticsEnabled: await events.analytics.enabled(), }, }, diff --git a/packages/backend-core/src/middleware/authenticated.js b/packages/backend-core/src/middleware/authenticated.js index 9c35336dda..4e6e0b7ba2 100644 --- a/packages/backend-core/src/middleware/authenticated.js +++ b/packages/backend-core/src/middleware/authenticated.js @@ -94,7 +94,6 @@ module.exports = ( user = await getUser(userId, session.tenantId) } user.csrfToken = session.csrfToken - delete user.password authenticated = true } catch (err) { error = err @@ -128,6 +127,8 @@ module.exports = ( } if (!user && tenantId) { user = { tenantId } + } else { + delete user.password } // be explicit if (authenticated !== true) { diff --git a/packages/backend-core/src/middleware/index.js b/packages/backend-core/src/middleware/index.js index 6c4c0d8883..1721d56a3c 100644 --- a/packages/backend-core/src/middleware/index.js +++ b/packages/backend-core/src/middleware/index.js @@ -2,7 +2,7 @@ const jwt = require("./passport/jwt") const local = require("./passport/local") const google = require("./passport/google") const oidc = require("./passport/oidc") -const { authError } = require("./passport/utils") +const { authError, ssoCallbackUrl } = require("./passport/utils") const authenticated = require("./authenticated") const auditLog = require("./auditLog") const tenancy = require("./tenancy") @@ -20,6 +20,7 @@ module.exports = { tenancy, authError, internalApi, + ssoCallbackUrl, datasource: { google: datasourceGoogle, }, diff --git a/packages/backend-core/src/middleware/passport/google.js b/packages/backend-core/src/middleware/passport/google.js index 858029ca80..7419974cd7 100644 --- a/packages/backend-core/src/middleware/passport/google.js +++ b/packages/backend-core/src/middleware/passport/google.js @@ -1,6 +1,7 @@ const GoogleStrategy = require("passport-google-oauth").OAuth2Strategy - +const { ssoCallbackUrl } = require("./utils") const { authenticateThirdParty } = require("./third-party-common") +const { Configs } = require("../../../constants") const buildVerifyFn = saveUserFn => { return (accessToken, refreshToken, profile, done) => { @@ -57,5 +58,10 @@ exports.strategyFactory = async function (config, callbackUrl, saveUserFn) { ) } } + +exports.getCallbackUrl = async function (db, config) { + return ssoCallbackUrl(db, config, Configs.GOOGLE) +} + // expose for testing exports.buildVerifyFn = buildVerifyFn diff --git a/packages/backend-core/src/middleware/passport/local.js b/packages/backend-core/src/middleware/passport/local.js index 445893b1df..b955d29102 100644 --- a/packages/backend-core/src/middleware/passport/local.js +++ b/packages/backend-core/src/middleware/passport/local.js @@ -55,6 +55,7 @@ exports.authenticate = async function (ctx, email, password, done) { if (await compare(password, dbUser.password)) { const sessionId = newid() const tenantId = getTenantId() + await createASession(dbUser._id, { sessionId, tenantId }) dbUser.token = jwt.sign( diff --git a/packages/backend-core/src/middleware/passport/oidc.js b/packages/backend-core/src/middleware/passport/oidc.js index 1e93e20b1c..20dbd4669b 100644 --- a/packages/backend-core/src/middleware/passport/oidc.js +++ b/packages/backend-core/src/middleware/passport/oidc.js @@ -1,6 +1,8 @@ const fetch = require("node-fetch") const OIDCStrategy = require("@techpass/passport-openidconnect").Strategy const { authenticateThirdParty } = require("./third-party-common") +const { ssoCallbackUrl } = require("./utils") +const { Configs } = require("../../../constants") const buildVerifyFn = saveUserFn => { /** @@ -89,11 +91,24 @@ function validEmail(value) { * from couchDB rather than environment variables, using this factory is necessary for dynamically configuring passport. * @returns Dynamically configured Passport OIDC Strategy */ -exports.strategyFactory = async function (config, callbackUrl, saveUserFn) { +exports.strategyFactory = async function (config, saveUserFn) { try { - const { clientID, clientSecret, configUrl } = config + const verify = buildVerifyFn(saveUserFn) + const strategy = new OIDCStrategy(config, verify) + strategy.name = "oidc" + return strategy + } catch (err) { + console.error(err) + throw new Error("Error constructing OIDC authentication strategy", err) + } +} + +exports.fetchStrategyConfig = async function (enrichedConfig, callbackUrl) { + try { + const { clientID, clientSecret, configUrl } = enrichedConfig if (!clientID || !clientSecret || !callbackUrl || !configUrl) { + //check for remote config and all required elements throw new Error( "Configuration invalid. Must contain clientID, clientSecret, callbackUrl and configUrl" ) @@ -109,24 +124,24 @@ exports.strategyFactory = async function (config, callbackUrl, saveUserFn) { const body = await response.json() - const verify = buildVerifyFn(saveUserFn) - return new OIDCStrategy( - { - issuer: body.issuer, - authorizationURL: body.authorization_endpoint, - tokenURL: body.token_endpoint, - userInfoURL: body.userinfo_endpoint, - clientID: clientID, - clientSecret: clientSecret, - callbackURL: callbackUrl, - }, - verify - ) + return { + issuer: body.issuer, + authorizationURL: body.authorization_endpoint, + tokenURL: body.token_endpoint, + userInfoURL: body.userinfo_endpoint, + clientID: clientID, + clientSecret: clientSecret, + callbackURL: callbackUrl, + } } catch (err) { console.error(err) - throw new Error("Error constructing OIDC authentication strategy", err) + throw new Error("Error constructing OIDC authentication configuration", err) } } +exports.getCallbackUrl = async function (db, config) { + return ssoCallbackUrl(db, config, Configs.OIDC) +} + // expose for testing exports.buildVerifyFn = buildVerifyFn diff --git a/packages/backend-core/src/middleware/passport/tests/oidc.spec.js b/packages/backend-core/src/middleware/passport/tests/oidc.spec.js index c5e9fe0034..c00ab2ea7d 100644 --- a/packages/backend-core/src/middleware/passport/tests/oidc.spec.js +++ b/packages/backend-core/src/middleware/passport/tests/oidc.spec.js @@ -48,8 +48,8 @@ describe("oidc", () => { it("should create successfully create an oidc strategy", async () => { const oidc = require("../oidc") - - await oidc.strategyFactory(oidcConfig, callbackUrl) + const enrichedConfig = await oidc.fetchStrategyConfig(oidcConfig, callbackUrl) + await oidc.strategyFactory(enrichedConfig, callbackUrl) expect(mockFetch).toHaveBeenCalledWith(oidcConfig.configUrl) diff --git a/packages/backend-core/src/middleware/passport/utils.js b/packages/backend-core/src/middleware/passport/utils.js index cbb93bfa3b..217130cd6d 100644 --- a/packages/backend-core/src/middleware/passport/utils.js +++ b/packages/backend-core/src/middleware/passport/utils.js @@ -1,3 +1,7 @@ +const { isMultiTenant, getTenantId } = require("../../tenancy") +const { getScopedConfig } = require("../../db/utils") +const { Configs } = require("../../constants") + /** * Utility to handle authentication errors. * @@ -5,6 +9,7 @@ * @param {*} message Message that will be returned in the response body * @param {*} err (Optional) error that will be logged */ + exports.authError = function (done, message, err = null) { return done( err, @@ -12,3 +17,21 @@ exports.authError = function (done, message, err = null) { { message: message } ) } + +exports.ssoCallbackUrl = async (db, config, type) => { + // incase there is a callback URL from before + if (config && config.callbackURL) { + return config.callbackURL + } + const publicConfig = await getScopedConfig(db, { + type: Configs.SETTINGS, + }) + + let callbackUrl = `/api/global/auth` + if (isMultiTenant()) { + callbackUrl += `/${getTenantId()}` + } + callbackUrl += `/${type}/callback` + + return `${publicConfig.platformUrl}${callbackUrl}` +} diff --git a/packages/backend-core/yarn.lock b/packages/backend-core/yarn.lock index e40cddc468..7d4d422631 100644 --- a/packages/backend-core/yarn.lock +++ b/packages/backend-core/yarn.lock @@ -291,6 +291,11 @@ resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== +"@budibase/types@^1.0.206": + version "1.0.208" + resolved "https://registry.yarnpkg.com/@budibase/types/-/types-1.0.208.tgz#c45cb494fb5b85229e15a34c6ac1805bae5be867" + integrity sha512-zKIHg6TGK+soVxMNZNrGypP3DCrd3jhlUQEFeQ+rZR6/tCue1G74bjzydY5FjnLEsXeLH1a0hkS5HulTmvQ2bA== + "@istanbuljs/load-nyc-config@^1.0.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced" @@ -4123,6 +4128,11 @@ passport-oauth1@1.x.x: passport-strategy "1.x.x" utils-merge "1.x.x" +passport-oauth2-refresh@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/passport-oauth2-refresh/-/passport-oauth2-refresh-2.1.0.tgz#c31cd133826383f5539d16ad8ab4f35ca73ce4a4" + integrity sha512-4ML7ooCESCqiTgdDBzNUFTBcPR8zQq9iM6eppEUGMMvLdsjqRL93jKwWm4Az3OJcI+Q2eIVyI8sVRcPFvxcF/A== + passport-oauth2@1.x.x: version "1.6.1" resolved "https://registry.yarnpkg.com/passport-oauth2/-/passport-oauth2-1.6.1.tgz#c5aee8f849ce8bd436c7f81d904a3cd1666f181b" diff --git a/packages/builder/cypress/integration/appPublishWorkflow.spec.js b/packages/builder/cypress/integration/appPublishWorkflow.spec.js index a431051075..e65c01c1b6 100644 --- a/packages/builder/cypress/integration/appPublishWorkflow.spec.js +++ b/packages/builder/cypress/integration/appPublishWorkflow.spec.js @@ -85,12 +85,12 @@ filterTests(['all'], () => { cy.get(interact.APP_TABLE_APP_NAME).click({ force: true }) }) - cy.get(interact.DEPLOYMENT_TOP_NAV).click() - cy.get(interact.PUBLISH_POPOVER_ACTION).click({ force: true }) - cy.get(interact.UNPUBLISH_MODAL) - .within(() => { - cy.get(interact.CONFIRM_WRAP_BUTTON).click({ force: true }) - }) + cy.get(interact.DEPLOYMENT_TOP_GLOBE).should("exist").click({ force: true }) + + cy.get("[data-cy='publish-popover-menu']") + .within(() => { + cy.get(interact.PUBLISH_POPOVER_ACTION).click({ force: true }) + }) cy.get(interact.UNPUBLISH_MODAL).should("be.visible") .within(() => { diff --git a/packages/server/src/api/controllers/query/index.ts b/packages/server/src/api/controllers/query/index.ts index 19a547b401..ce6eeda7c7 100644 --- a/packages/server/src/api/controllers/query/index.ts +++ b/packages/server/src/api/controllers/query/index.ts @@ -8,6 +8,8 @@ import { QUERY_THREAD_TIMEOUT } from "../../../environment" import { getAppDB } from "@budibase/backend-core/context" import { quotas } from "@budibase/pro" import { events } from "@budibase/backend-core" +import { getCookie } from "@budibase/backend-core/utils" +import { Cookies, Configs } from "@budibase/backend-core/constants" const Runner = new Thread(ThreadType.QUERY, { timeoutMs: QUERY_THREAD_TIMEOUT || 10000, @@ -110,6 +112,21 @@ export async function find(ctx: any) { ctx.body = query } +//Required to discern between OIDC OAuth config entries +function getOAuthConfigCookieId(ctx: any) { + if (ctx.user.providerType === Configs.OIDC) { + return getCookie(ctx, Cookies.OIDC_CONFIG) + } +} + +function getAuthConfig(ctx: any) { + const authCookie = getCookie(ctx, Cookies.Auth) + let authConfigCtx: any = {} + authConfigCtx["configId"] = getOAuthConfigCookieId(ctx) + authConfigCtx["sessionId"] = authCookie ? authCookie.sessionId : null + return authConfigCtx +} + export async function preview(ctx: any) { const db = getAppDB() @@ -119,6 +136,8 @@ export async function preview(ctx: any) { // this stops dynamic variables from calling the same query const { fields, parameters, queryVerb, transformer, queryId } = query + const authConfigCtx: any = getAuthConfig(ctx) + try { const runFn = () => Runner.run({ @@ -131,9 +150,9 @@ export async function preview(ctx: any) { queryId, ctx: { user: ctx.user, + auth: { ...authConfigCtx }, }, }) - const { rows, keys, info, extra } = await quotas.addQuery(runFn) await events.query.previewed(datasource, query) ctx.body = { @@ -153,6 +172,8 @@ async function execute(ctx: any, opts = { rowsOnly: false }) { const query = await db.get(ctx.params.queryId) const datasource = await db.get(query.datasourceId) + const authConfigCtx: any = getAuthConfig(ctx) + const enrichedParameters = ctx.request.body.parameters || {} // make sure parameters are fully enriched with defaults if (query && query.parameters) { @@ -177,6 +198,7 @@ async function execute(ctx: any, opts = { rowsOnly: false }) { queryId: ctx.params.queryId, ctx: { user: ctx.user, + auth: { ...authConfigCtx }, }, }) diff --git a/packages/server/src/module.d.ts b/packages/server/src/module.d.ts index 843267fba8..4c0e13586a 100644 --- a/packages/server/src/module.d.ts +++ b/packages/server/src/module.d.ts @@ -8,4 +8,5 @@ declare module "@budibase/backend-core/constants" declare module "@budibase/backend-core/auth" declare module "@budibase/backend-core/sessions" declare module "@budibase/backend-core/encryption" +declare module "@budibase/backend-core/utils" declare module "@budibase/backend-core/redis" diff --git a/packages/server/src/threads/query.js b/packages/server/src/threads/query.js index f228f22159..e85fde970e 100644 --- a/packages/server/src/threads/query.js +++ b/packages/server/src/threads/query.js @@ -4,6 +4,12 @@ const ScriptRunner = require("../utilities/scriptRunner") const { integrations } = require("../integrations") const { processStringSync } = require("@budibase/string-templates") const { doInAppContext, getAppDB } = require("@budibase/backend-core/context") +const { + refreshOAuthToken, + updateUserOAuth, +} = require("@budibase/backend-core/auth") +const { getGlobalIDFromUserMetadataID } = require("../db/utils") + const { isSQL } = require("../integrations/utils") const { enrichQueryFields, @@ -28,10 +34,12 @@ class QueryRunner { // it can be this.queryResponse = {} this.hasRerun = false + this.hasRefreshedOAuth = false } async execute() { let { datasource, fields, queryVerb, transformer } = this + const Integration = integrations[datasource.source] if (!Integration) { throw "Integration type does not exist." @@ -98,14 +106,24 @@ class QueryRunner { } // if the request fails we retry once, invalidating the cached value - if ( - info && - info.code >= 400 && - this.cachedVariables.length > 0 && - !this.hasRerun - ) { + if (info && info.code >= 400 && !this.hasRerun) { + if ( + this.ctx.user?.provider && + info.code === 401 && + !this.hasRefreshedOAuth + ) { + // Attempt to refresh the access token from the provider + this.hasRefreshedOAuth = true + const authResponse = await this.refreshOAuth2(this.ctx) + + if (!authResponse || authResponse.err) { + // In this event the user may have oAuth issues that + // could require re-authenticating with their provider. + throw new Error("OAuth2 access token could not be refreshed") + } + } + this.hasRerun = true - // invalidate the cache value await threadUtils.invalidateDynamicVariables(this.cachedVariables) return this.execute() } @@ -151,6 +169,31 @@ class QueryRunner { ).execute() } + async refreshOAuth2(ctx) { + const { oauth2, providerType, _id } = ctx.user + const { configId } = ctx.auth + + if (!providerType || !oauth2?.refreshToken) { + console.error("No refresh token found for authenticated user") + return + } + + const resp = await refreshOAuthToken( + oauth2.refreshToken, + providerType, + configId + ) + + // Refresh session flow. Should be in same location as refreshOAuthToken + // There are several other properties available in 'resp' + if (!resp.error) { + const globalUserId = getGlobalIDFromUserMetadataID(_id) + await updateUserOAuth(globalUserId, resp) + } + + return resp + } + async getDynamicVariable(variable) { let { parameters } = this const queryId = variable.queryId, diff --git a/packages/worker/src/api/controllers/global/auth.ts b/packages/worker/src/api/controllers/global/auth.ts index 896a7c48de..dc96554cb2 100644 --- a/packages/worker/src/api/controllers/global/auth.ts +++ b/packages/worker/src/api/controllers/global/auth.ts @@ -1,49 +1,22 @@ const core = require("@budibase/backend-core") -const { getScopedConfig } = require("@budibase/backend-core/db") -const { google } = require("@budibase/backend-core/middleware") -const { oidc } = require("@budibase/backend-core/middleware") const { Configs, EmailTemplatePurpose } = require("../../../constants") const { sendEmail, isEmailConfigured } = require("../../../utilities/email") const { setCookie, getCookie, clearCookie, hash, platformLogout } = core.utils const { Cookies, Headers } = core.constants -const { passport } = core.auth +const { passport, ssoCallbackUrl, google, oidc } = core.auth const { checkResetPasswordCode } = require("../../../utilities/redis") -const { - getGlobalDB, - getTenantId, - isMultiTenant, -} = require("@budibase/backend-core/tenancy") +const { getGlobalDB } = require("@budibase/backend-core/tenancy") const env = require("../../../environment") import { events, users as usersCore, context } from "@budibase/backend-core" import { users } from "../../../sdk" import { User } from "@budibase/types" -const ssoCallbackUrl = async (config: any, type: any) => { - // incase there is a callback URL from before - if (config && config.callbackURL) { - return config.callbackURL - } - - const db = getGlobalDB() - const publicConfig = await getScopedConfig(db, { - type: Configs.SETTINGS, - }) - - let callbackUrl = `/api/global/auth` - if (isMultiTenant()) { - callbackUrl += `/${getTenantId()}` - } - callbackUrl += `/${type}/callback` - - return `${publicConfig.platformUrl}${callbackUrl}` -} - export const googleCallbackUrl = async (config: any) => { - return ssoCallbackUrl(config, "google") + return ssoCallbackUrl(getGlobalDB(), config, "google") } export const oidcCallbackUrl = async (config: any) => { - return ssoCallbackUrl(config, "oidc") + return ssoCallbackUrl(getGlobalDB(), config, "oidc") } async function authInternal(ctx: any, user: any, err = null, info = null) { @@ -198,6 +171,8 @@ export const googlePreAuth = async (ctx: any, next: any) => { return passport.authenticate(strategy, { scope: ["profile", "email"], + accessType: "offline", + prompt: "consent", })(ctx, next) } @@ -224,7 +199,7 @@ export const googleAuth = async (ctx: any, next: any) => { )(ctx, next) } -async function oidcStrategyFactory(ctx: any, configId: any) { +export const oidcStrategyFactory = async (ctx: any, configId: any) => { const db = getGlobalDB() const config = await core.db.getScopedConfig(db, { type: Configs.OIDC, @@ -234,7 +209,12 @@ async function oidcStrategyFactory(ctx: any, configId: any) { const chosenConfig = config.configs.filter((c: any) => c.uuid === configId)[0] let callbackUrl = await exports.oidcCallbackUrl(chosenConfig) - return oidc.strategyFactory(chosenConfig, callbackUrl, users.save) + //Remote Config + const enrichedConfig = await oidc.fetchStrategyConfig( + chosenConfig, + callbackUrl + ) + return oidc.strategyFactory(enrichedConfig, users.save) } /** @@ -249,7 +229,7 @@ export const oidcPreAuth = async (ctx: any, next: any) => { return passport.authenticate(strategy, { // required 'openid' scope is added by oidc strategy factory - scope: ["profile", "email"], + scope: ["profile", "email", "offline_access"], //auth0 offline_access scope required for the refresh token behaviour. })(ctx, next) } diff --git a/packages/worker/src/api/routes/tests/auth.spec.js b/packages/worker/src/api/routes/tests/auth.spec.js index 03b00d8312..165ecd0f4a 100644 --- a/packages/worker/src/api/routes/tests/auth.spec.js +++ b/packages/worker/src/api/routes/tests/auth.spec.js @@ -77,27 +77,30 @@ describe("/api/global/auth", () => { describe("oidc", () => { const auth = require("@budibase/backend-core/auth") - // mock the oidc strategy implementation and return value - let strategyFactory = jest.fn() - let mockStrategyReturn = jest.fn() - strategyFactory.mockReturnValue(mockStrategyReturn) - auth.oidc.strategyFactory = strategyFactory - const passportSpy = jest.spyOn(auth.passport, "authenticate") let oidcConf let chosenConfig let configId + // mock the oidc strategy implementation and return value + let strategyFactory = jest.fn() + let mockStrategyReturn = jest.fn() + let mockStrategyConfig = jest.fn() + auth.oidc.fetchStrategyConfig = mockStrategyConfig + + strategyFactory.mockReturnValue(mockStrategyReturn) + auth.oidc.strategyFactory = strategyFactory + beforeEach(async () => { oidcConf = await config.saveOIDCConfig() chosenConfig = oidcConf.config.configs[0] configId = chosenConfig.uuid + mockStrategyConfig.mockReturnValue(chosenConfig) }) afterEach(() => { expect(strategyFactory).toBeCalledWith( chosenConfig, - `http://localhost:10000/api/global/auth/${TENANT_ID}/oidc/callback`, expect.any(Function) ) }) @@ -107,7 +110,7 @@ describe("/api/global/auth", () => { await request.get(`/api/global/auth/${TENANT_ID}/oidc/configs/${configId}`) expect(passportSpy).toBeCalledWith(mockStrategyReturn, { - scope: ["profile", "email"], + scope: ["profile", "email", "offline_access"] }) expect(passportSpy.mock.calls.length).toBe(1); })