snapshot of https://github.com/mountain-loop/yaak.git (main, 2026-09-23; blobs > 131072B omitted)
This commit is contained in:
128
plugins/auth-oauth2/src/getOrRefreshAccessToken.ts
Normal file
128
plugins/auth-oauth2/src/getOrRefreshAccessToken.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
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);
|
||||
}
|
||||
Reference in New Issue
Block a user