Files
web/packages/common/tokenManager/tokenManager.ts
Joakim Jäderberg 8b94540d19 Merged in chore/redirect-counter (pull request #3302)
Counter name is now searchable and add counter for redirects

* refactor: createCounter() only takes one argument, the name of the counter. Makes it easier to search for

* feat: add counter when we do a redirect from redirect-service


Approved-by: Linus Flood
2025-12-08 10:24:05 +00:00

106 lines
2.7 KiB
TypeScript

import * as Sentry from "@sentry/nextjs"
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(): Promise<ServiceTokenResponse> {
return Sentry.startSpan({ name: "getServiceToken" }, async () => {
const scopes = env.CURITY_CLIENT_SERVICE_SCOPES
const cacheKey = getServiceTokenCacheKey(scopes)
const cacheClient = await getCacheClient()
const token = await getOrSetServiceTokenFromCache(cacheKey, scopes)
if (token.expiresAt < Date.now()) {
await cacheClient.deleteKey(cacheKey)
const newToken = await getOrSetServiceTokenFromCache(cacheKey, scopes)
return newToken.jwt
}
return token.jwt
})
}
async function getOrSetServiceTokenFromCache(
cacheKey: string,
scopes: string[]
) {
const cacheClient = await getCacheClient()
const token = await cacheClient.cacheOrGet(
cacheKey,
async () => {
return Sentry.startSpan({ name: "fetch new serviceToken" }, async () => {
return await getJwt(scopes)
})
},
"1h"
)
return token
}
async function getJwt(scopes: string[]) {
const jwt = await fetchServiceToken(scopes)
const expiresAt = Date.now() + jwt.expires_in * 1000
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(",")}`
}