Merge pull request #6535 from Budibase/sso-rest-requests
SSO rest request refresh
This commit is contained in:
commit
93c79dbc08
|
@ -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/**",
|
||||
"<node_internals>/**"
|
||||
]
|
||||
},
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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(),
|
||||
},
|
||||
},
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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}`
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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(() => {
|
||||
|
|
|
@ -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 },
|
||||
},
|
||||
})
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
})
|
||||
|
|
Loading…
Reference in New Issue