@budibase/server
Version:
Budibase Web Server
156 lines (133 loc) • 4.11 kB
text/typescript
import fetch, { RequestInit } from "node-fetch"
import { HttpError } from "koa"
import { get } from "../oauth2"
import {
Document,
OAuth2CredentialsMethod,
OAuth2GrantType,
} from "@budibase/types"
import { cache, context, docIds } from "@budibase/backend-core"
import { processEnvironmentVariable } from "../../utils"
interface OAuth2LogDocument extends Document {
lastUsage: number
}
const { DocWritethrough } = cache.docWritethrough
async function fetchToken(config: {
url: string
clientId: string
clientSecret: string
method: OAuth2CredentialsMethod
grantType: OAuth2GrantType
scope?: string
}) {
config = await processEnvironmentVariable(config)
const fetchConfig: RequestInit = {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body: new URLSearchParams({
grant_type: "client_credentials",
}),
redirect: "follow",
}
const bodyParams: Record<string, string> = {
grant_type: config.grantType,
}
if (config.method === OAuth2CredentialsMethod.HEADER) {
fetchConfig.headers = {
"Content-Type": "application/x-www-form-urlencoded",
Authorization: `Basic ${Buffer.from(
`${config.clientId}:${config.clientSecret}`,
"utf-8"
).toString("base64")}`,
}
} else {
bodyParams["client_id"] = config.clientId
bodyParams["client_secret"] = config.clientSecret
}
if (config.scope) {
bodyParams.scope = config.scope
}
fetchConfig.body = new URLSearchParams(bodyParams)
const resp = await fetch(config.url, fetchConfig)
return resp
}
const trackUsage = async (id: string) => {
const writethrough = new DocWritethrough<OAuth2LogDocument>(
context.getAppDB(),
docIds.generateOAuth2LogID(id)
)
await writethrough.patch({
lastUsage: Date.now(),
})
}
export async function getToken(id: string) {
const token = await cache.withCacheWithDynamicTTL(
cache.CacheKey.OAUTH2_TOKEN(id),
async () => {
const config = await get(id)
if (!config) {
throw new HttpError(`oAuth config ${id} count not be found`)
}
const resp = await fetchToken(config)
const jsonResponse = await resp.json()
if (!resp.ok) {
const message = jsonResponse.error_description ?? resp.statusText
throw new Error(`Error fetching oauth2 token: ${message}`)
}
const token = `${jsonResponse.token_type} ${jsonResponse.access_token}`
const ttl = jsonResponse.expires_in ?? -1
return { value: token, ttl }
}
)
await trackUsage(id)
return token
}
export async function validateConfig(config: {
url: string
clientId: string
clientSecret: string
method: OAuth2CredentialsMethod
grantType: OAuth2GrantType
scope?: string
}): Promise<{ valid: boolean; message?: string }> {
try {
const resp = await fetchToken(config)
const jsonResponse = await resp.json()
if (!resp.ok) {
const message = jsonResponse.error_description ?? resp.statusText
return { valid: false, message }
}
return { valid: true }
} catch (e: any) {
return { valid: false, message: e.message }
}
}
export async function getLastUsages(ids: string[]) {
const devDocs = await context
.getAppDB()
.getMultiple<OAuth2LogDocument>(ids.map(docIds.generateOAuth2LogID), {
allowMissing: true,
})
const prodDocs = await context
.getProdAppDB()
.getMultiple<OAuth2LogDocument>(ids.map(docIds.generateOAuth2LogID), {
allowMissing: true,
})
const result = ids.reduce<Record<string, number>>((acc, id) => {
const devDoc = devDocs.find(d => d._id === docIds.generateOAuth2LogID(id))
if (devDoc) {
acc[id] = devDoc.lastUsage
}
const prodDoc = prodDocs.find(d => d._id === docIds.generateOAuth2LogID(id))
if (prodDoc && (!acc[id] || acc[id] < prodDoc.lastUsage)) {
acc[id] = prodDoc.lastUsage
}
return acc
}, {})
return result
}
export async function cleanStoredToken(id: string) {
await cache.destroy(cache.CacheKey.OAUTH2_TOKEN(id), { useTenancy: true })
}