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:
parent
9463a07e50
commit
d1b91ae661
2 changed files with 26 additions and 14 deletions
|
|
@ -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: '',
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Reference in a new issue