Use Intuit discovery document for OAuth endpoints
Fetch authorization, token, and revocation endpoints from Intuit's .well-known/openid_configuration at runtime instead of hardcoding. Cached per Lambda instance for performance.
This commit is contained in:
parent
066ff59d22
commit
9463a07e50
2 changed files with 42 additions and 9 deletions
|
|
@ -66,6 +66,20 @@ interface QBOVendor {
|
|||
|
||||
let cachedSecret: QBOSecret | undefined;
|
||||
|
||||
// Intuit discovery document — token endpoint resolved at runtime
|
||||
const DISCOVERY_URL = 'https://developer.api.intuit.com/.well-known/openid_configuration';
|
||||
let cachedTokenEndpoint: string | undefined;
|
||||
|
||||
async function getTokenEndpoint(): Promise<string> {
|
||||
if (!cachedTokenEndpoint) {
|
||||
const res = await fetch(DISCOVERY_URL);
|
||||
if (!res.ok) throw new Error(`Discovery fetch failed: ${res.status}`);
|
||||
const doc = (await res.json()) as { token_endpoint: string };
|
||||
cachedTokenEndpoint = doc.token_endpoint;
|
||||
}
|
||||
return cachedTokenEndpoint;
|
||||
}
|
||||
|
||||
async function getQBOSecret(): Promise<QBOSecret> {
|
||||
if (!cachedSecret) {
|
||||
const res = await secretsClient.send(
|
||||
|
|
@ -77,9 +91,10 @@ async function getQBOSecret(): Promise<QBOSecret> {
|
|||
}
|
||||
|
||||
async function refreshAccessToken(secret: QBOSecret): Promise<string> {
|
||||
const tokenEndpoint = await getTokenEndpoint();
|
||||
const credentials = Buffer.from(`${secret.clientId}:${secret.clientSecret}`).toString('base64');
|
||||
|
||||
const res = await fetch('https://oauth.platform.intuit.com/oauth2/v1/tokens/bearer', {
|
||||
const res = await fetch(tokenEndpoint, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Basic ${credentials}`,
|
||||
|
|
|
|||
|
|
@ -11,10 +11,25 @@ const QBO_CLIENT_ID = process.env.QBO_CLIENT_ID!;
|
|||
const QBO_CLIENT_SECRET = process.env.QBO_CLIENT_SECRET!;
|
||||
const REDIRECT_URI = process.env.REDIRECT_URI!; // https://bot.seahaven.com/qbo/callback
|
||||
|
||||
// Intuit OAuth endpoints
|
||||
const AUTHORIZE_URL = 'https://appcenter.intuit.com/connect/oauth2';
|
||||
const TOKEN_URL = 'https://oauth.platform.intuit.com/oauth2/v1/tokens/bearer';
|
||||
const REVOKE_URL = 'https://developer.api.intuit.com/v2/oauth2/tokens/revoke';
|
||||
// Intuit discovery document — endpoints resolved at runtime per Intuit requirements
|
||||
const DISCOVERY_URL = 'https://developer.api.intuit.com/.well-known/openid_configuration';
|
||||
|
||||
interface DiscoveryDocument {
|
||||
authorization_endpoint: string;
|
||||
token_endpoint: string;
|
||||
revocation_endpoint: string;
|
||||
}
|
||||
|
||||
let cachedDiscovery: DiscoveryDocument | undefined;
|
||||
|
||||
async function getDiscovery(): Promise<DiscoveryDocument> {
|
||||
if (!cachedDiscovery) {
|
||||
const res = await fetch(DISCOVERY_URL);
|
||||
if (!res.ok) throw new Error(`Discovery fetch failed: ${res.status}`);
|
||||
cachedDiscovery = (await res.json()) as DiscoveryDocument;
|
||||
}
|
||||
return cachedDiscovery;
|
||||
}
|
||||
|
||||
// Scopes needed for vendor queries
|
||||
const SCOPES = 'com.intuit.quickbooks.accounting';
|
||||
|
|
@ -88,7 +103,8 @@ function clearStateCookie(): string {
|
|||
|
||||
// ── /qbo/connect — redirect to Intuit OAuth ──────────────────────────────────
|
||||
|
||||
function handleConnect(): APIGatewayResponse {
|
||||
async function handleConnect(): Promise<APIGatewayResponse> {
|
||||
const discovery = await getDiscovery();
|
||||
const state = crypto.randomUUID();
|
||||
|
||||
const params = new URLSearchParams({
|
||||
|
|
@ -100,7 +116,7 @@ function handleConnect(): APIGatewayResponse {
|
|||
});
|
||||
|
||||
return redirect(
|
||||
`${AUTHORIZE_URL}?${params.toString()}`,
|
||||
`${discovery.authorization_endpoint}?${params.toString()}`,
|
||||
[makeStateCookie(state)],
|
||||
);
|
||||
}
|
||||
|
|
@ -129,9 +145,10 @@ async function handleCallback(
|
|||
return redirect('/qbo/launch?error=missing_params', clearCookie);
|
||||
}
|
||||
|
||||
const discovery = await getDiscovery();
|
||||
const credentials = Buffer.from(`${QBO_CLIENT_ID}:${QBO_CLIENT_SECRET}`).toString('base64');
|
||||
|
||||
const tokenRes = await fetch(TOKEN_URL, {
|
||||
const tokenRes = await fetch(discovery.token_endpoint, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Basic ${credentials}`,
|
||||
|
|
@ -191,10 +208,11 @@ async function handleDisconnect(): Promise<APIGatewayResponse> {
|
|||
|
||||
// Revoke the token at Intuit if we have one
|
||||
if (refreshToken) {
|
||||
const discovery = await getDiscovery();
|
||||
const credentials = Buffer.from(`${QBO_CLIENT_ID}:${QBO_CLIENT_SECRET}`).toString('base64');
|
||||
|
||||
try {
|
||||
await fetch(REVOKE_URL, {
|
||||
await fetch(discovery.revocation_endpoint, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Basic ${credentials}`,
|
||||
|
|
|
|||
Reference in a new issue