Read OAuth client credentials from Secrets Manager at runtime

Removes CloudFormation dynamic references for clientId/clientSecret
env vars. Credentials are now fetched from Secrets Manager at runtime
so secret updates don't require a redeploy.
This commit is contained in:
Adam Moussa 2026-04-13 20:22:21 -04:00
parent 9463a07e50
commit d1b91ae661
2 changed files with 26 additions and 14 deletions

View file

@ -7,13 +7,27 @@ import {
const secretsClient = new SecretsManagerClient({});
const QBO_SECRET_ARN = process.env.QBO_SECRET_ARN!;
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 discovery document — endpoints resolved at runtime per Intuit requirements
const DISCOVERY_URL = 'https://developer.api.intuit.com/.well-known/openid_configuration';
// Client credentials — read from Secrets Manager at runtime so updates
// don't require a redeploy
interface QBOClientCreds { clientId: string; clientSecret: string }
let cachedCreds: QBOClientCreds | undefined;
async function getClientCreds(): Promise<QBOClientCreds> {
if (!cachedCreds) {
const res = await secretsClient.send(
new GetSecretValueCommand({ SecretId: QBO_SECRET_ARN }),
);
const secret = JSON.parse(res.SecretString!);
cachedCreds = { clientId: secret.clientId, clientSecret: secret.clientSecret };
}
return cachedCreds;
}
interface DiscoveryDocument {
authorization_endpoint: string;
token_endpoint: string;
@ -104,11 +118,11 @@ function clearStateCookie(): string {
// ── /qbo/connect — redirect to Intuit OAuth ──────────────────────────────────
async function handleConnect(): Promise<APIGatewayResponse> {
const discovery = await getDiscovery();
const [discovery, creds] = await Promise.all([getDiscovery(), getClientCreds()]);
const state = crypto.randomUUID();
const params = new URLSearchParams({
client_id: QBO_CLIENT_ID,
client_id: creds.clientId,
response_type: 'code',
scope: SCOPES,
redirect_uri: REDIRECT_URI,
@ -145,8 +159,8 @@ 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 [discovery, creds] = await Promise.all([getDiscovery(), getClientCreds()]);
const credentials = Buffer.from(`${creds.clientId}:${creds.clientSecret}`).toString('base64');
const tokenRes = await fetch(discovery.token_endpoint, {
method: 'POST',
@ -179,8 +193,8 @@ async function handleCallback(
new PutSecretValueCommand({
SecretId: QBO_SECRET_ARN,
SecretString: JSON.stringify({
clientId: QBO_CLIENT_ID,
clientSecret: QBO_CLIENT_SECRET,
clientId: creds.clientId,
clientSecret: creds.clientSecret,
refreshToken: tokens.refresh_token,
realmId,
}),
@ -208,8 +222,8 @@ 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');
const [discovery, creds] = await Promise.all([getDiscovery(), getClientCreds()]);
const credentials = Buffer.from(`${creds.clientId}:${creds.clientSecret}`).toString('base64');
try {
await fetch(discovery.revocation_endpoint, {
@ -230,8 +244,8 @@ async function handleDisconnect(): Promise<APIGatewayResponse> {
new PutSecretValueCommand({
SecretId: QBO_SECRET_ARN,
SecretString: JSON.stringify({
clientId: QBO_CLIENT_ID,
clientSecret: QBO_CLIENT_SECRET,
clientId: creds.clientId,
clientSecret: creds.clientSecret,
refreshToken: '',
realmId: '',
}),

View file

@ -123,8 +123,6 @@ export class SlackHandlerConstruct extends Construct {
memorySize: 256,
environment: {
QBO_SECRET_ARN: qboSecret.secretArn,
QBO_CLIENT_ID: '{{resolve:secretsmanager:seahaven/qbo/oauth:SecretString:clientId}}',
QBO_CLIENT_SECRET: '{{resolve:secretsmanager:seahaven/qbo/oauth:SecretString:clientSecret}}',
REDIRECT_URI: 'https://bot.seahaven.com/qbo/callback',
},
vpc: props.vpc,