feat(SW-3476): add support for using different curity clients when creating service tokens * fix(SW-3476): add support for using different curity clients when creating service tokens * remove log statement Approved-by: Hrishikesh Vaipurkar
124 lines
3.0 KiB
TypeScript
124 lines
3.0 KiB
TypeScript
import { trace, type Tracer } from "@opentelemetry/api"
|
|
|
|
import { getCacheClient } from "../dataCache"
|
|
import { env } from "../env/server"
|
|
import { createCounter } from "../telemetry"
|
|
|
|
interface ServiceTokenResponse {
|
|
access_token: string
|
|
scope?: string
|
|
token_type: string
|
|
expires_in: number
|
|
}
|
|
|
|
export async function getServiceToken() {
|
|
const tracer = trace.getTracer("getServiceToken")
|
|
|
|
return await tracer.startActiveSpan("getServiceToken", async () => {
|
|
const scopes = env.CURITY_CLIENT_SERVICE_SCOPES
|
|
|
|
const cacheKey = getServiceTokenCacheKey(scopes)
|
|
const cacheClient = await getCacheClient()
|
|
const token = await getOrSetServiceTokenFromCache(cacheKey, scopes, tracer)
|
|
|
|
if (token.expiresAt < Date.now()) {
|
|
await cacheClient.deleteKey(cacheKey)
|
|
|
|
const newToken = await getOrSetServiceTokenFromCache(
|
|
cacheKey,
|
|
scopes,
|
|
tracer
|
|
)
|
|
return newToken.jwt
|
|
}
|
|
|
|
return token.jwt
|
|
})
|
|
}
|
|
|
|
async function getOrSetServiceTokenFromCache(
|
|
cacheKey: string,
|
|
scopes: string[],
|
|
tracer: Tracer
|
|
) {
|
|
const cacheClient = await getCacheClient()
|
|
const token = await cacheClient.cacheOrGet(
|
|
cacheKey,
|
|
async () => {
|
|
return await tracer.startActiveSpan("fetch new token", async () => {
|
|
const newToken = await getJwt(scopes)
|
|
return newToken
|
|
})
|
|
},
|
|
"1h"
|
|
)
|
|
return token
|
|
}
|
|
|
|
async function getJwt(scopes: string[]) {
|
|
const getJwtCounter = createCounter("tokenManager", "getJwt")
|
|
const metricsGetJwt = getJwtCounter.init({
|
|
scopes,
|
|
})
|
|
|
|
metricsGetJwt.start()
|
|
|
|
const jwt = await fetchServiceToken(scopes)
|
|
|
|
const expiresAt = Date.now() + jwt.expires_in * 1000
|
|
|
|
metricsGetJwt.success()
|
|
|
|
return { expiresAt, jwt }
|
|
}
|
|
|
|
async function fetchServiceToken(scopes: string[]) {
|
|
const fetchServiceTokenCounter = createCounter(
|
|
"tokenManager",
|
|
"fetchServiceToken"
|
|
)
|
|
const metricsFetchServiceToken = fetchServiceTokenCounter.init({
|
|
scopes,
|
|
})
|
|
|
|
metricsFetchServiceToken.start()
|
|
|
|
const response = await fetch(`${env.CURITY_ISSUER_USER}/oauth/v2/token`, {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/x-www-form-urlencoded",
|
|
Accept: "application/json",
|
|
},
|
|
body: new URLSearchParams({
|
|
grant_type: "client_credentials",
|
|
client_id: env.CURITY_CLIENT_ID_SERVICE,
|
|
client_secret: env.CURITY_CLIENT_SECRET_SERVICE,
|
|
scope: scopes.join(" "),
|
|
}),
|
|
signal: AbortSignal.timeout(15_000),
|
|
})
|
|
|
|
if (!response.ok) {
|
|
await metricsFetchServiceToken.httpError(response)
|
|
|
|
const text = await response.text()
|
|
throw new Error(
|
|
`[fetchServiceToken] Failed to obtain service token: ${JSON.stringify({
|
|
status: response.status,
|
|
statusText: response.statusText,
|
|
text,
|
|
})}`
|
|
)
|
|
}
|
|
|
|
const result = response.json() as Promise<ServiceTokenResponse>
|
|
|
|
metricsFetchServiceToken.success()
|
|
|
|
return result
|
|
}
|
|
|
|
function getServiceTokenCacheKey(scopes: string[]): string {
|
|
return `serviceToken:${scopes.join(",")}`
|
|
}
|