Merge pull request #6535 from Budibase/sso-rest-requests

SSO rest request refresh
This commit is contained in:
deanhannigan 2022-07-05 10:06:26 +01:00 committed by GitHub
commit 93c79dbc08
18 changed files with 324 additions and 80 deletions

14
.vscode/settings.json vendored
View File

@ -3,5 +3,17 @@
"editor.codeActionsOnSave": { "editor.codeActionsOnSave": {
"source.fixAll": true "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/**",
"<node_internals>/**"
]
},
} }

View File

@ -36,6 +36,7 @@
"passport-google-oauth": "2.0.0", "passport-google-oauth": "2.0.0",
"passport-jwt": "4.0.0", "passport-jwt": "4.0.0",
"passport-local": "1.0.0", "passport-local": "1.0.0",
"passport-oauth2-refresh": "^2.1.0",
"posthog-node": "1.3.0", "posthog-node": "1.3.0",
"pouchdb": "7.3.0", "pouchdb": "7.3.0",
"pouchdb-find": "7.2.2", "pouchdb-find": "7.2.2",

View File

@ -2,6 +2,9 @@ const passport = require("koa-passport")
const LocalStrategy = require("passport-local").Strategy const LocalStrategy = require("passport-local").Strategy
const JwtStrategy = require("passport-jwt").Strategy const JwtStrategy = require("passport-jwt").Strategy
const { getGlobalDB } = require("./tenancy") const { getGlobalDB } = require("./tenancy")
const refresh = require("passport-oauth2-refresh")
const { Configs } = require("./constants")
const { getScopedConfig } = require("./db/utils")
const { const {
jwt, jwt,
local, local,
@ -12,6 +15,7 @@ const {
tenancy, tenancy,
appTenancy, appTenancy,
authError, authError,
ssoCallbackUrl,
csrf, csrf,
internalApi, internalApi,
} = require("./middleware") } = 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 = { module.exports = {
buildAuthMiddleware: authenticated, buildAuthMiddleware: authenticated,
passport, passport,
@ -46,4 +166,7 @@ module.exports = {
authError, authError,
buildCsrfMiddleware: csrf, buildCsrfMiddleware: csrf,
internalApi, internalApi,
refreshOAuthToken,
updateUserOAuth,
ssoCallbackUrl,
} }

View File

@ -387,7 +387,9 @@ export const getScopedFullConfig = async function (
if (type === Configs.SETTINGS) { if (type === Configs.SETTINGS) {
if (scopedConfig && scopedConfig.doc) { if (scopedConfig && scopedConfig.doc) {
// overrides affected by environment variables // overrides affected by environment variables
scopedConfig.doc.config.platformUrl = await getPlatformUrl() scopedConfig.doc.config.platformUrl = await getPlatformUrl({
tenantAware: true,
})
scopedConfig.doc.config.analyticsEnabled = scopedConfig.doc.config.analyticsEnabled =
await events.analytics.enabled() await events.analytics.enabled()
} else { } else {
@ -396,7 +398,7 @@ export const getScopedFullConfig = async function (
doc: { doc: {
_id: generateConfigID({ type, user, workspace }), _id: generateConfigID({ type, user, workspace }),
config: { config: {
platformUrl: await getPlatformUrl(), platformUrl: await getPlatformUrl({ tenantAware: true }),
analyticsEnabled: await events.analytics.enabled(), analyticsEnabled: await events.analytics.enabled(),
}, },
}, },

View File

@ -94,7 +94,6 @@ module.exports = (
user = await getUser(userId, session.tenantId) user = await getUser(userId, session.tenantId)
} }
user.csrfToken = session.csrfToken user.csrfToken = session.csrfToken
delete user.password
authenticated = true authenticated = true
} catch (err) { } catch (err) {
error = err error = err
@ -128,6 +127,8 @@ module.exports = (
} }
if (!user && tenantId) { if (!user && tenantId) {
user = { tenantId } user = { tenantId }
} else {
delete user.password
} }
// be explicit // be explicit
if (authenticated !== true) { if (authenticated !== true) {

View File

@ -2,7 +2,7 @@ const jwt = require("./passport/jwt")
const local = require("./passport/local") const local = require("./passport/local")
const google = require("./passport/google") const google = require("./passport/google")
const oidc = require("./passport/oidc") const oidc = require("./passport/oidc")
const { authError } = require("./passport/utils") const { authError, ssoCallbackUrl } = require("./passport/utils")
const authenticated = require("./authenticated") const authenticated = require("./authenticated")
const auditLog = require("./auditLog") const auditLog = require("./auditLog")
const tenancy = require("./tenancy") const tenancy = require("./tenancy")
@ -20,6 +20,7 @@ module.exports = {
tenancy, tenancy,
authError, authError,
internalApi, internalApi,
ssoCallbackUrl,
datasource: { datasource: {
google: datasourceGoogle, google: datasourceGoogle,
}, },

View File

@ -1,6 +1,7 @@
const GoogleStrategy = require("passport-google-oauth").OAuth2Strategy const GoogleStrategy = require("passport-google-oauth").OAuth2Strategy
const { ssoCallbackUrl } = require("./utils")
const { authenticateThirdParty } = require("./third-party-common") const { authenticateThirdParty } = require("./third-party-common")
const { Configs } = require("../../../constants")
const buildVerifyFn = saveUserFn => { const buildVerifyFn = saveUserFn => {
return (accessToken, refreshToken, profile, done) => { 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 // expose for testing
exports.buildVerifyFn = buildVerifyFn exports.buildVerifyFn = buildVerifyFn

View File

@ -55,6 +55,7 @@ exports.authenticate = async function (ctx, email, password, done) {
if (await compare(password, dbUser.password)) { if (await compare(password, dbUser.password)) {
const sessionId = newid() const sessionId = newid()
const tenantId = getTenantId() const tenantId = getTenantId()
await createASession(dbUser._id, { sessionId, tenantId }) await createASession(dbUser._id, { sessionId, tenantId })
dbUser.token = jwt.sign( dbUser.token = jwt.sign(

View File

@ -1,6 +1,8 @@
const fetch = require("node-fetch") const fetch = require("node-fetch")
const OIDCStrategy = require("@techpass/passport-openidconnect").Strategy const OIDCStrategy = require("@techpass/passport-openidconnect").Strategy
const { authenticateThirdParty } = require("./third-party-common") const { authenticateThirdParty } = require("./third-party-common")
const { ssoCallbackUrl } = require("./utils")
const { Configs } = require("../../../constants")
const buildVerifyFn = saveUserFn => { 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. * from couchDB rather than environment variables, using this factory is necessary for dynamically configuring passport.
* @returns Dynamically configured Passport OIDC Strategy * @returns Dynamically configured Passport OIDC Strategy
*/ */
exports.strategyFactory = async function (config, callbackUrl, saveUserFn) { exports.strategyFactory = async function (config, saveUserFn) {
try { 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) { if (!clientID || !clientSecret || !callbackUrl || !configUrl) {
//check for remote config and all required elements
throw new Error( throw new Error(
"Configuration invalid. Must contain clientID, clientSecret, callbackUrl and configUrl" "Configuration invalid. Must contain clientID, clientSecret, callbackUrl and configUrl"
) )
@ -109,9 +124,7 @@ exports.strategyFactory = async function (config, callbackUrl, saveUserFn) {
const body = await response.json() const body = await response.json()
const verify = buildVerifyFn(saveUserFn) return {
return new OIDCStrategy(
{
issuer: body.issuer, issuer: body.issuer,
authorizationURL: body.authorization_endpoint, authorizationURL: body.authorization_endpoint,
tokenURL: body.token_endpoint, tokenURL: body.token_endpoint,
@ -119,14 +132,16 @@ exports.strategyFactory = async function (config, callbackUrl, saveUserFn) {
clientID: clientID, clientID: clientID,
clientSecret: clientSecret, clientSecret: clientSecret,
callbackURL: callbackUrl, callbackURL: callbackUrl,
}, }
verify
)
} catch (err) { } catch (err) {
console.error(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 // expose for testing
exports.buildVerifyFn = buildVerifyFn exports.buildVerifyFn = buildVerifyFn

View File

@ -48,8 +48,8 @@ describe("oidc", () => {
it("should create successfully create an oidc strategy", async () => { it("should create successfully create an oidc strategy", async () => {
const oidc = require("../oidc") const oidc = require("../oidc")
const enrichedConfig = await oidc.fetchStrategyConfig(oidcConfig, callbackUrl)
await oidc.strategyFactory(oidcConfig, callbackUrl) await oidc.strategyFactory(enrichedConfig, callbackUrl)
expect(mockFetch).toHaveBeenCalledWith(oidcConfig.configUrl) expect(mockFetch).toHaveBeenCalledWith(oidcConfig.configUrl)

View File

@ -1,3 +1,7 @@
const { isMultiTenant, getTenantId } = require("../../tenancy")
const { getScopedConfig } = require("../../db/utils")
const { Configs } = require("../../constants")
/** /**
* Utility to handle authentication errors. * Utility to handle authentication errors.
* *
@ -5,6 +9,7 @@
* @param {*} message Message that will be returned in the response body * @param {*} message Message that will be returned in the response body
* @param {*} err (Optional) error that will be logged * @param {*} err (Optional) error that will be logged
*/ */
exports.authError = function (done, message, err = null) { exports.authError = function (done, message, err = null) {
return done( return done(
err, err,
@ -12,3 +17,21 @@ exports.authError = function (done, message, err = null) {
{ message: message } { 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}`
}

View File

@ -291,6 +291,11 @@
resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== 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": "@istanbuljs/load-nyc-config@^1.0.0":
version "1.1.0" version "1.1.0"
resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced" 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" passport-strategy "1.x.x"
utils-merge "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: passport-oauth2@1.x.x:
version "1.6.1" version "1.6.1"
resolved "https://registry.yarnpkg.com/passport-oauth2/-/passport-oauth2-1.6.1.tgz#c5aee8f849ce8bd436c7f81d904a3cd1666f181b" resolved "https://registry.yarnpkg.com/passport-oauth2/-/passport-oauth2-1.6.1.tgz#c5aee8f849ce8bd436c7f81d904a3cd1666f181b"

View File

@ -85,11 +85,11 @@ filterTests(['all'], () => {
cy.get(interact.APP_TABLE_APP_NAME).click({ force: true }) cy.get(interact.APP_TABLE_APP_NAME).click({ force: true })
}) })
cy.get(interact.DEPLOYMENT_TOP_NAV).click() cy.get(interact.DEPLOYMENT_TOP_GLOBE).should("exist").click({ force: true })
cy.get(interact.PUBLISH_POPOVER_ACTION).click({ force: true })
cy.get(interact.UNPUBLISH_MODAL) cy.get("[data-cy='publish-popover-menu']")
.within(() => { .within(() => {
cy.get(interact.CONFIRM_WRAP_BUTTON).click({ force: true }) cy.get(interact.PUBLISH_POPOVER_ACTION).click({ force: true })
}) })
cy.get(interact.UNPUBLISH_MODAL).should("be.visible") cy.get(interact.UNPUBLISH_MODAL).should("be.visible")

View File

@ -8,6 +8,8 @@ import { QUERY_THREAD_TIMEOUT } from "../../../environment"
import { getAppDB } from "@budibase/backend-core/context" import { getAppDB } from "@budibase/backend-core/context"
import { quotas } from "@budibase/pro" import { quotas } from "@budibase/pro"
import { events } from "@budibase/backend-core" 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, { const Runner = new Thread(ThreadType.QUERY, {
timeoutMs: QUERY_THREAD_TIMEOUT || 10000, timeoutMs: QUERY_THREAD_TIMEOUT || 10000,
@ -110,6 +112,21 @@ export async function find(ctx: any) {
ctx.body = query 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) { export async function preview(ctx: any) {
const db = getAppDB() const db = getAppDB()
@ -119,6 +136,8 @@ export async function preview(ctx: any) {
// this stops dynamic variables from calling the same query // this stops dynamic variables from calling the same query
const { fields, parameters, queryVerb, transformer, queryId } = query const { fields, parameters, queryVerb, transformer, queryId } = query
const authConfigCtx: any = getAuthConfig(ctx)
try { try {
const runFn = () => const runFn = () =>
Runner.run({ Runner.run({
@ -131,9 +150,9 @@ export async function preview(ctx: any) {
queryId, queryId,
ctx: { ctx: {
user: ctx.user, user: ctx.user,
auth: { ...authConfigCtx },
}, },
}) })
const { rows, keys, info, extra } = await quotas.addQuery(runFn) const { rows, keys, info, extra } = await quotas.addQuery(runFn)
await events.query.previewed(datasource, query) await events.query.previewed(datasource, query)
ctx.body = { ctx.body = {
@ -153,6 +172,8 @@ async function execute(ctx: any, opts = { rowsOnly: false }) {
const query = await db.get(ctx.params.queryId) const query = await db.get(ctx.params.queryId)
const datasource = await db.get(query.datasourceId) const datasource = await db.get(query.datasourceId)
const authConfigCtx: any = getAuthConfig(ctx)
const enrichedParameters = ctx.request.body.parameters || {} const enrichedParameters = ctx.request.body.parameters || {}
// make sure parameters are fully enriched with defaults // make sure parameters are fully enriched with defaults
if (query && query.parameters) { if (query && query.parameters) {
@ -177,6 +198,7 @@ async function execute(ctx: any, opts = { rowsOnly: false }) {
queryId: ctx.params.queryId, queryId: ctx.params.queryId,
ctx: { ctx: {
user: ctx.user, user: ctx.user,
auth: { ...authConfigCtx },
}, },
}) })

View File

@ -8,4 +8,5 @@ declare module "@budibase/backend-core/constants"
declare module "@budibase/backend-core/auth" declare module "@budibase/backend-core/auth"
declare module "@budibase/backend-core/sessions" declare module "@budibase/backend-core/sessions"
declare module "@budibase/backend-core/encryption" declare module "@budibase/backend-core/encryption"
declare module "@budibase/backend-core/utils"
declare module "@budibase/backend-core/redis" declare module "@budibase/backend-core/redis"

View File

@ -4,6 +4,12 @@ const ScriptRunner = require("../utilities/scriptRunner")
const { integrations } = require("../integrations") const { integrations } = require("../integrations")
const { processStringSync } = require("@budibase/string-templates") const { processStringSync } = require("@budibase/string-templates")
const { doInAppContext, getAppDB } = require("@budibase/backend-core/context") 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 { isSQL } = require("../integrations/utils")
const { const {
enrichQueryFields, enrichQueryFields,
@ -28,10 +34,12 @@ class QueryRunner {
// it can be // it can be
this.queryResponse = {} this.queryResponse = {}
this.hasRerun = false this.hasRerun = false
this.hasRefreshedOAuth = false
} }
async execute() { async execute() {
let { datasource, fields, queryVerb, transformer } = this let { datasource, fields, queryVerb, transformer } = this
const Integration = integrations[datasource.source] const Integration = integrations[datasource.source]
if (!Integration) { if (!Integration) {
throw "Integration type does not exist." throw "Integration type does not exist."
@ -98,14 +106,24 @@ class QueryRunner {
} }
// if the request fails we retry once, invalidating the cached value // if the request fails we retry once, invalidating the cached value
if (info && info.code >= 400 && !this.hasRerun) {
if ( if (
info && this.ctx.user?.provider &&
info.code >= 400 && info.code === 401 &&
this.cachedVariables.length > 0 && !this.hasRefreshedOAuth
!this.hasRerun
) { ) {
// 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 this.hasRerun = true
// invalidate the cache value
await threadUtils.invalidateDynamicVariables(this.cachedVariables) await threadUtils.invalidateDynamicVariables(this.cachedVariables)
return this.execute() return this.execute()
} }
@ -151,6 +169,31 @@ class QueryRunner {
).execute() ).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) { async getDynamicVariable(variable) {
let { parameters } = this let { parameters } = this
const queryId = variable.queryId, const queryId = variable.queryId,

View File

@ -1,49 +1,22 @@
const core = require("@budibase/backend-core") 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 { Configs, EmailTemplatePurpose } = require("../../../constants")
const { sendEmail, isEmailConfigured } = require("../../../utilities/email") const { sendEmail, isEmailConfigured } = require("../../../utilities/email")
const { setCookie, getCookie, clearCookie, hash, platformLogout } = core.utils const { setCookie, getCookie, clearCookie, hash, platformLogout } = core.utils
const { Cookies, Headers } = core.constants const { Cookies, Headers } = core.constants
const { passport } = core.auth const { passport, ssoCallbackUrl, google, oidc } = core.auth
const { checkResetPasswordCode } = require("../../../utilities/redis") const { checkResetPasswordCode } = require("../../../utilities/redis")
const { const { getGlobalDB } = require("@budibase/backend-core/tenancy")
getGlobalDB,
getTenantId,
isMultiTenant,
} = require("@budibase/backend-core/tenancy")
const env = require("../../../environment") const env = require("../../../environment")
import { events, users as usersCore, context } from "@budibase/backend-core" import { events, users as usersCore, context } from "@budibase/backend-core"
import { users } from "../../../sdk" import { users } from "../../../sdk"
import { User } from "@budibase/types" 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) => { export const googleCallbackUrl = async (config: any) => {
return ssoCallbackUrl(config, "google") return ssoCallbackUrl(getGlobalDB(), config, "google")
} }
export const oidcCallbackUrl = async (config: any) => { 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) { 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, { return passport.authenticate(strategy, {
scope: ["profile", "email"], scope: ["profile", "email"],
accessType: "offline",
prompt: "consent",
})(ctx, next) })(ctx, next)
} }
@ -224,7 +199,7 @@ export const googleAuth = async (ctx: any, next: any) => {
)(ctx, next) )(ctx, next)
} }
async function oidcStrategyFactory(ctx: any, configId: any) { export const oidcStrategyFactory = async (ctx: any, configId: any) => {
const db = getGlobalDB() const db = getGlobalDB()
const config = await core.db.getScopedConfig(db, { const config = await core.db.getScopedConfig(db, {
type: Configs.OIDC, 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] const chosenConfig = config.configs.filter((c: any) => c.uuid === configId)[0]
let callbackUrl = await exports.oidcCallbackUrl(chosenConfig) 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, { return passport.authenticate(strategy, {
// required 'openid' scope is added by oidc strategy factory // 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) })(ctx, next)
} }

View File

@ -77,27 +77,30 @@ describe("/api/global/auth", () => {
describe("oidc", () => { describe("oidc", () => {
const auth = require("@budibase/backend-core/auth") 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") const passportSpy = jest.spyOn(auth.passport, "authenticate")
let oidcConf let oidcConf
let chosenConfig let chosenConfig
let configId 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 () => { beforeEach(async () => {
oidcConf = await config.saveOIDCConfig() oidcConf = await config.saveOIDCConfig()
chosenConfig = oidcConf.config.configs[0] chosenConfig = oidcConf.config.configs[0]
configId = chosenConfig.uuid configId = chosenConfig.uuid
mockStrategyConfig.mockReturnValue(chosenConfig)
}) })
afterEach(() => { afterEach(() => {
expect(strategyFactory).toBeCalledWith( expect(strategyFactory).toBeCalledWith(
chosenConfig, chosenConfig,
`http://localhost:10000/api/global/auth/${TENANT_ID}/oidc/callback`,
expect.any(Function) expect.any(Function)
) )
}) })
@ -107,7 +110,7 @@ describe("/api/global/auth", () => {
await request.get(`/api/global/auth/${TENANT_ID}/oidc/configs/${configId}`) await request.get(`/api/global/auth/${TENANT_ID}/oidc/configs/${configId}`)
expect(passportSpy).toBeCalledWith(mockStrategyReturn, { expect(passportSpy).toBeCalledWith(mockStrategyReturn, {
scope: ["profile", "email"], scope: ["profile", "email", "offline_access"]
}) })
expect(passportSpy.mock.calls.length).toBe(1); expect(passportSpy.mock.calls.length).toBe(1);
}) })