Skip to content

Commit 5d184ae

Browse files
committed
feat: add OAuth credential provider creation during deploy
1 parent 37252b3 commit 5d184ae

File tree

7 files changed

+377
-14
lines changed

7 files changed

+377
-14
lines changed

src/cli/cloudformation/outputs.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,8 @@ export function buildDeployedState(
175175
agents: Record<string, AgentCoreDeployedState>,
176176
gateways: Record<string, { gatewayId: string; gatewayArn: string }>,
177177
existingState?: DeployedState,
178-
identityKmsKeyArn?: string
178+
identityKmsKeyArn?: string,
179+
credentials?: Record<string, { credentialProviderArn: string; clientSecretArn?: string; callbackUrl?: string }>
179180
): DeployedState {
180181
const targetState: TargetDeployedState = {
181182
resources: {
@@ -192,6 +193,11 @@ export function buildDeployedState(
192193
};
193194
}
194195

196+
// Add credential state if credentials exist
197+
if (credentials && Object.keys(credentials).length > 0) {
198+
targetState.resources!.credentials = credentials;
199+
}
200+
195201
return {
196202
targets: {
197203
...existingState?.targets,

src/cli/commands/deploy/actions.ts

Lines changed: 52 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,10 @@ import {
1111
checkStackDeployability,
1212
getAllCredentials,
1313
hasOwnedIdentityApiProviders,
14+
hasOwnedIdentityOAuthProviders,
1415
performStackTeardown,
1516
setupApiKeyProviders,
17+
setupOAuth2Providers,
1618
synthesizeCdk,
1719
validateProject,
1820
} from '../../operations/deploy';
@@ -166,20 +168,21 @@ export async function handleDeploy(options: ValidatedDeployOptions): Promise<Dep
166168

167169
// Set up identity providers if needed
168170
let identityKmsKeyArn: string | undefined;
169-
if (hasOwnedIdentityApiProviders(context.projectSpec)) {
170-
startStep('Creating credentials...');
171171

172-
// In CLI mode, also check process.env for credentials (enables non-interactive deploy with -y)
173-
const neededCredentials = getAllCredentials(context.projectSpec);
174-
const envCredentials: Record<string, string> = {};
175-
for (const cred of neededCredentials) {
176-
const value = process.env[cred.envVarName];
177-
if (value) {
178-
envCredentials[cred.envVarName] = value;
179-
}
172+
// Read runtime credentials from process.env (enables non-interactive deploy with -y)
173+
const neededCredentials = getAllCredentials(context.projectSpec);
174+
const envCredentials: Record<string, string> = {};
175+
for (const cred of neededCredentials) {
176+
const value = process.env[cred.envVarName];
177+
if (value) {
178+
envCredentials[cred.envVarName] = value;
180179
}
181-
const runtimeCredentials =
182-
Object.keys(envCredentials).length > 0 ? new SecureCredentials(envCredentials) : undefined;
180+
}
181+
const runtimeCredentials =
182+
Object.keys(envCredentials).length > 0 ? new SecureCredentials(envCredentials) : undefined;
183+
184+
if (hasOwnedIdentityApiProviders(context.projectSpec)) {
185+
startStep('Creating credentials...');
183186

184187
const identityResult = await setupApiKeyProviders({
185188
projectSpec: context.projectSpec,
@@ -200,6 +203,41 @@ export async function handleDeploy(options: ValidatedDeployOptions): Promise<Dep
200203
endStep('success');
201204
}
202205

206+
// Set up OAuth credential providers if needed
207+
const oauthCredentials: Record<
208+
string,
209+
{ credentialProviderArn: string; clientSecretArn?: string; callbackUrl?: string }
210+
> = {};
211+
if (hasOwnedIdentityOAuthProviders(context.projectSpec)) {
212+
startStep('Creating OAuth credentials...');
213+
214+
const oauthResult = await setupOAuth2Providers({
215+
projectSpec: context.projectSpec,
216+
configBaseDir: configIO.getConfigRoot(),
217+
region: target.region,
218+
runtimeCredentials,
219+
});
220+
if (oauthResult.hasErrors) {
221+
const errorResult = oauthResult.results.find(r => r.status === 'error');
222+
const errorMsg = errorResult?.error ?? 'OAuth credential setup failed';
223+
endStep('error', errorMsg);
224+
logger.finalize(false);
225+
return { success: false, error: errorMsg, logPath: logger.getRelativeLogPath() };
226+
}
227+
228+
// Collect credential ARNs for deployed state
229+
for (const result of oauthResult.results) {
230+
if (result.credentialProviderArn) {
231+
oauthCredentials[result.providerName] = {
232+
credentialProviderArn: result.credentialProviderArn,
233+
clientSecretArn: result.clientSecretArn,
234+
callbackUrl: result.callbackUrl,
235+
};
236+
}
237+
}
238+
endStep('success');
239+
}
240+
203241
// Deploy
204242
const hasGateways = mcpSpec?.agentCoreGateways && mcpSpec.agentCoreGateways.length > 0;
205243
const deployStepName = hasGateways ? 'Deploying gateways...' : 'Deploy to AWS';
@@ -273,7 +311,8 @@ export async function handleDeploy(options: ValidatedDeployOptions): Promise<Dep
273311
agents,
274312
gateways,
275313
existingState,
276-
identityKmsKeyArn
314+
identityKmsKeyArn,
315+
oauthCredentials
277316
);
278317
await configIO.writeDeployedState(deployedState);
279318

src/cli/operations/deploy/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,17 @@ export {
1616
// Pre-deploy identity setup for non-Bedrock model providers
1717
export {
1818
setupApiKeyProviders,
19+
setupOAuth2Providers,
1920
hasOwnedIdentityApiProviders,
21+
hasOwnedIdentityOAuthProviders,
2022
getMissingCredentials,
2123
getAllCredentials,
2224
type SetupApiKeyProvidersOptions,
25+
type SetupOAuth2ProvidersOptions,
2326
type PreDeployIdentityResult,
27+
type PreDeployOAuth2Result,
2428
type ApiKeyProviderSetupResult,
29+
type OAuth2ProviderSetupResult,
2530
type MissingCredential,
2631
} from './pre-deploy-identity';
2732

src/cli/operations/deploy/pre-deploy-identity.ts

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@ import { getCredentialProvider } from '../../aws';
44
import { isNoCredentialsError } from '../../errors';
55
import { apiKeyProviderExists, createApiKeyProvider, setTokenVaultKmsKey, updateApiKeyProvider } from '../identity';
66
import { computeDefaultCredentialEnvVarName } from '../identity/create-identity';
7+
import {
8+
createOAuth2Provider,
9+
oAuth2ProviderExists,
10+
updateOAuth2Provider,
11+
} from '../identity/oauth2-credential-provider';
712
import { BedrockAgentCoreControlClient, GetTokenVaultCommand } from '@aws-sdk/client-bedrock-agentcore-control';
813
import { CreateKeyCommand, KMSClient } from '@aws-sdk/client-kms';
914

@@ -238,3 +243,130 @@ export function getAllCredentials(projectSpec: AgentCoreProjectSpec): MissingCre
238243

239244
return credentials;
240245
}
246+
247+
// ─────────────────────────────────────────────────────────────────────────────
248+
// OAuth2 Credential Provider Setup
249+
// ─────────────────────────────────────────────────────────────────────────────
250+
251+
export interface OAuth2ProviderSetupResult {
252+
providerName: string;
253+
status: 'created' | 'updated' | 'skipped' | 'error';
254+
error?: string;
255+
credentialProviderArn?: string;
256+
clientSecretArn?: string;
257+
callbackUrl?: string;
258+
}
259+
260+
export interface SetupOAuth2ProvidersOptions {
261+
projectSpec: AgentCoreProjectSpec;
262+
configBaseDir: string;
263+
region: string;
264+
runtimeCredentials?: SecureCredentials;
265+
}
266+
267+
export interface PreDeployOAuth2Result {
268+
results: OAuth2ProviderSetupResult[];
269+
hasErrors: boolean;
270+
}
271+
272+
/**
273+
* Set up OAuth2 credential providers for all OAuth credentials in the project.
274+
* Reads client credentials from agentcore/.env.local and creates providers in AgentCore Identity.
275+
*/
276+
export async function setupOAuth2Providers(options: SetupOAuth2ProvidersOptions): Promise<PreDeployOAuth2Result> {
277+
const { projectSpec, configBaseDir, region, runtimeCredentials } = options;
278+
const results: OAuth2ProviderSetupResult[] = [];
279+
const credentials = getCredentialProvider();
280+
281+
const envVars = await readEnvFile(configBaseDir);
282+
const envCredentials = SecureCredentials.fromEnvVars(envVars);
283+
const allCredentials = runtimeCredentials ? envCredentials.merge(runtimeCredentials) : envCredentials;
284+
285+
const client = new BedrockAgentCoreControlClient({ region, credentials });
286+
287+
for (const credential of projectSpec.credentials) {
288+
if (credential.type === 'OAuthCredentialProvider') {
289+
const result = await setupSingleOAuth2Provider(client, credential, allCredentials);
290+
results.push(result);
291+
}
292+
}
293+
294+
return {
295+
results,
296+
hasErrors: results.some(r => r.status === 'error'),
297+
};
298+
}
299+
300+
/**
301+
* Check if the project has any OAuth credentials that need setup.
302+
*/
303+
export function hasOwnedIdentityOAuthProviders(projectSpec: AgentCoreProjectSpec): boolean {
304+
return projectSpec.credentials.some(c => c.type === 'OAuthCredentialProvider');
305+
}
306+
307+
async function setupSingleOAuth2Provider(
308+
client: BedrockAgentCoreControlClient,
309+
credential: Credential,
310+
credentials: SecureCredentials
311+
): Promise<OAuth2ProviderSetupResult> {
312+
if (credential.type !== 'OAuthCredentialProvider') {
313+
return { providerName: credential.name, status: 'error', error: 'Invalid credential type' };
314+
}
315+
316+
const nameKey = credential.name.toUpperCase().replace(/-/g, '_');
317+
const clientIdEnvVar = `AGENTCORE_CREDENTIAL_${nameKey}_CLIENT_ID`;
318+
const clientSecretEnvVar = `AGENTCORE_CREDENTIAL_${nameKey}_CLIENT_SECRET`;
319+
320+
const clientId = credentials.get(clientIdEnvVar);
321+
const clientSecret = credentials.get(clientSecretEnvVar);
322+
323+
if (!clientId || !clientSecret) {
324+
return {
325+
providerName: credential.name,
326+
status: 'skipped',
327+
error: `Missing ${clientIdEnvVar} or ${clientSecretEnvVar} in agentcore/.env.local`,
328+
};
329+
}
330+
331+
const params = {
332+
name: credential.name,
333+
vendor: credential.vendor,
334+
discoveryUrl: credential.discoveryUrl,
335+
clientId,
336+
clientSecret,
337+
};
338+
339+
try {
340+
const exists = await oAuth2ProviderExists(client, credential.name);
341+
342+
if (exists) {
343+
const updateResult = await updateOAuth2Provider(client, params);
344+
return {
345+
providerName: credential.name,
346+
status: updateResult.success ? 'updated' : 'error',
347+
error: updateResult.error,
348+
credentialProviderArn: updateResult.result?.credentialProviderArn,
349+
clientSecretArn: updateResult.result?.clientSecretArn,
350+
callbackUrl: updateResult.result?.callbackUrl,
351+
};
352+
}
353+
354+
const createResult = await createOAuth2Provider(client, params);
355+
return {
356+
providerName: credential.name,
357+
status: createResult.success ? 'created' : 'error',
358+
error: createResult.error,
359+
credentialProviderArn: createResult.result?.credentialProviderArn,
360+
clientSecretArn: createResult.result?.clientSecretArn,
361+
callbackUrl: createResult.result?.callbackUrl,
362+
};
363+
} catch (error) {
364+
let errorMessage: string;
365+
if (isNoCredentialsError(error)) {
366+
errorMessage = 'AWS credentials not found. Run `aws sso login` or set AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY.';
367+
} else {
368+
errorMessage = error instanceof Error ? error.message : String(error);
369+
}
370+
return { providerName: credential.name, status: 'error', error: errorMessage };
371+
}
372+
}

src/cli/operations/identity/index.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,14 @@ export {
44
setTokenVaultKmsKey,
55
updateApiKeyProvider,
66
} from './api-key-credential-provider';
7+
export {
8+
createOAuth2Provider,
9+
getOAuth2Provider,
10+
oAuth2ProviderExists,
11+
updateOAuth2Provider,
12+
type OAuth2ProviderParams,
13+
type OAuth2ProviderResult,
14+
} from './oauth2-credential-provider';
715
export {
816
computeDefaultCredentialEnvVarName,
917
resolveCredentialStrategy,

0 commit comments

Comments
 (0)