import type { Context, HttpRequest } from '@yaakapp/api'; import { readFileSync } from 'node:fs'; import type { AccessToken, AccessTokenRawResponse, TokenStoreArgs } from './store'; import { deleteToken, getToken, storeToken } from './store'; import { isTokenExpired } from './util'; export async function getOrRefreshAccessToken( ctx: Context, tokenArgs: TokenStoreArgs, { scope, accessTokenUrl, credentialsInBody, clientId, clientSecret, forceRefresh, }: { scope: string | null; accessTokenUrl: string; credentialsInBody: boolean; clientId: string; clientSecret: string; forceRefresh?: boolean; }, ): Promise { const token = await getToken(ctx, tokenArgs); if (token == null) { return null; } const isExpired = isTokenExpired(token); // Return the current access token if it's still valid if (!isExpired && !forceRefresh) { return token; } // Token is expired, but there's no refresh token :( if (!token.response.refresh_token) { return null; } // Access token is expired, so get a new one const httpRequest: Partial = { method: 'POST', url: accessTokenUrl, bodyType: 'application/x-www-form-urlencoded', body: { form: [ { name: 'grant_type', value: 'refresh_token' }, { name: 'refresh_token', value: token.response.refresh_token }, ], }, headers: [ { name: 'User-Agent', value: 'yaak' }, { name: 'Accept', value: 'application/x-www-form-urlencoded, application/json' }, { name: 'Content-Type', value: 'application/x-www-form-urlencoded' }, ], }; if (scope) httpRequest.body!.form.push({ name: 'scope', value: scope }); if (credentialsInBody) { httpRequest.body!.form.push({ name: 'client_id', value: clientId }); httpRequest.body!.form.push({ name: 'client_secret', value: clientSecret }); } else { const value = 'Basic ' + Buffer.from(`${clientId}:${clientSecret}`).toString('base64'); httpRequest.headers!.push({ name: 'Authorization', value }); } httpRequest.authenticationType = 'none'; // Don't inherit workspace auth const resp = await ctx.httpRequest.send({ httpRequest }); if (resp.status === 401) { // Bad refresh token, so we'll force it to fetch a fresh access token by deleting // and returning null; console.log('[oauth2] Unauthorized refresh_token request'); await deleteToken(ctx, tokenArgs); return null; } const body = resp.bodyPath ? readFileSync(resp.bodyPath, 'utf8') : ''; console.log('[oauth2] Got refresh token response', resp.status); if (resp.status < 200 || resp.status >= 300) { throw new Error( 'Failed to refresh access token with status=' + resp.status + ' and body=' + body, ); } let response; try { response = JSON.parse(body); } catch { response = Object.fromEntries(new URLSearchParams(body)); } if (response.error) { throw new Error( `Failed to fetch access token with ${response.error} -> ${response.error_description}`, ); } const newResponse: AccessTokenRawResponse = { ...response, // Assign a new one or keep the old one, refresh_token: response.refresh_token ?? token.response.refresh_token, }; return storeToken(ctx, tokenArgs, newResponse); }