Files
upstream-yaak/plugins/auth-oauth2/src/getOrRefreshAccessToken.ts
upstream-mirror 906fb231cc
Some checks failed
Lint and Test / Lint/Test (push) Has been cancelled
Generate Sponsors README / deploy (push) Has been skipped
snapshot of https://github.com/mountain-loop/yaak.git (main, 2026-09-23; blobs > 131072B omitted)
2026-09-23 17:39:41 +05:00

129 lines
4.1 KiB
TypeScript

import type { Context, HttpRequest } from "@yaakapp/api";
import type { CustomRequestParams } from "./customParams";
import { mergeFormParams, mergeHeaders } from "./customParams";
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,
tokenName,
forceRefresh,
custom,
}: {
scope: string | null;
accessTokenUrl: string;
credentialsInBody: boolean;
clientId: string;
clientSecret: string;
tokenName?: "access_token" | "id_token";
forceRefresh?: boolean;
custom?: CustomRequestParams;
},
): Promise<AccessToken | null> {
const token = await getToken(ctx, tokenArgs);
if (token == null) {
return null;
}
const isExpired = isTokenExpired(token, tokenName);
// 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<HttpRequest> = {
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 });
}
// Merged last so custom entries override the credential headers and params above
if (custom) {
httpRequest.headers = mergeHeaders(httpRequest.headers ?? [], custom.headers);
httpRequest.body = { form: mergeFormParams(httpRequest.body?.form ?? [], custom.body) };
}
httpRequest.authenticationType = "none"; // Don't inherit workspace auth
const { httpResponse: resp, body: responseBody } = await ctx.httpRequest.send({ httpRequest });
if (resp.error) {
throw new Error(`Failed to refresh access token: ${resp.error}`);
}
if (resp.status >= 400 && resp.status < 500) {
// Client errors (4xx) indicate the refresh token is invalid, expired, or revoked
// Delete the token and return null to trigger a fresh authorization flow
console.log("[oauth2] Refresh token request failed with client error, deleting token");
await deleteToken(ctx, tokenArgs);
return null;
}
// Sent ad-hoc, so this body came back with the response rather than being
// saved anywhere to read later.
const body = await responseBody.text();
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}`);
}
// oxlint-disable-next-line no-explicit-any
let response: any;
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, tokenName);
}