Skip to content

Commit cc7a478

Browse files
Merge branch 'main' into main
2 parents fceeff1 + 0b743db commit cc7a478

57 files changed

Lines changed: 3572 additions & 1406 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

e2e-tests/byo-custom-jwt.test.ts

Lines changed: 271 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,271 @@
1+
/**
2+
* E2E test: BYO agent with CUSTOM_JWT inbound auth (Cognito).
3+
*
4+
* Creates a Cognito user pool as the OIDC provider, deploys a BYO agent
5+
* configured with CUSTOM_JWT authorizer, and verifies that:
6+
* - Deploy succeeds with AuthorizerConfiguration in the CloudFormation template
7+
* - SigV4 invocation is rejected (auth method mismatch)
8+
* - Status reports the agent as deployed
9+
*
10+
* Unlike other E2E tests that use the globally installed CLI, this test uses
11+
* the local build (`runCLI`) because it exercises unreleased schema and CDK
12+
* changes. Set CDK_TARBALL to a path to the modified CDK package tarball.
13+
*
14+
* Requires: AWS credentials, npm, git, uv, CDK_TARBALL env var.
15+
*/
16+
import {
17+
type RunResult,
18+
hasAwsCredentials,
19+
parseJsonOutput,
20+
prereqs,
21+
runCLI,
22+
stripAnsi,
23+
} from '../src/test-utils/index.js';
24+
import { CloudFormationClient, GetTemplateCommand } from '@aws-sdk/client-cloudformation';
25+
import {
26+
CognitoIdentityProviderClient,
27+
CreateResourceServerCommand,
28+
CreateUserPoolClientCommand,
29+
CreateUserPoolCommand,
30+
CreateUserPoolDomainCommand,
31+
DeleteResourceServerCommand,
32+
DeleteUserPoolCommand,
33+
DeleteUserPoolDomainCommand,
34+
} from '@aws-sdk/client-cognito-identity-provider';
35+
import { execSync } from 'node:child_process';
36+
import { randomUUID } from 'node:crypto';
37+
import { mkdir, readFile, rm, writeFile } from 'node:fs/promises';
38+
import { tmpdir } from 'node:os';
39+
import { join } from 'node:path';
40+
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
41+
42+
const hasAws = hasAwsCredentials();
43+
const hasCdkTarball = !!process.env.CDK_TARBALL;
44+
const canRun = prereqs.npm && prereqs.git && prereqs.uv && hasAws && hasCdkTarball;
45+
const region = process.env.AWS_REGION ?? 'us-east-1';
46+
47+
/**
48+
* Run the local CLI build without skipping install (needed for deploy).
49+
*/
50+
function runLocalCLI(args: string[], cwd: string): Promise<RunResult> {
51+
return runCLI(args, cwd, /* skipInstall */ false);
52+
}
53+
54+
describe.sequential('e2e: BYO agent with CUSTOM_JWT auth', () => {
55+
let testDir: string;
56+
let projectPath: string;
57+
let agentName: string;
58+
59+
// Cognito resources
60+
let userPoolId: string;
61+
let clientId: string;
62+
let domainPrefix: string;
63+
let discoveryUrl: string;
64+
65+
const cognitoClient = new CognitoIdentityProviderClient({ region });
66+
const cfnClient = new CloudFormationClient({ region });
67+
68+
beforeAll(async () => {
69+
if (!canRun) return;
70+
71+
// ── Create Cognito user pool as OIDC provider ──
72+
const suffix = randomUUID().slice(0, 8);
73+
const poolName = `agentcore-e2e-jwt-${suffix}`;
74+
domainPrefix = `agentcore-e2e-jwt-${suffix}`;
75+
76+
const poolResult = await cognitoClient.send(new CreateUserPoolCommand({ PoolName: poolName }));
77+
userPoolId = poolResult.UserPool!.Id!;
78+
79+
await cognitoClient.send(new CreateUserPoolDomainCommand({ UserPoolId: userPoolId, Domain: domainPrefix }));
80+
81+
await cognitoClient.send(
82+
new CreateResourceServerCommand({
83+
UserPoolId: userPoolId,
84+
Identifier: 'agentcore',
85+
Name: 'AgentCore API',
86+
Scopes: [{ ScopeName: 'invoke', ScopeDescription: 'Invoke the runtime' }],
87+
})
88+
);
89+
90+
const clientResult = await cognitoClient.send(
91+
new CreateUserPoolClientCommand({
92+
UserPoolId: userPoolId,
93+
ClientName: 'e2e-test-client',
94+
GenerateSecret: true,
95+
AllowedOAuthFlows: ['client_credentials'],
96+
AllowedOAuthScopes: ['agentcore/invoke'],
97+
AllowedOAuthFlowsUserPoolClient: true,
98+
ExplicitAuthFlows: ['ALLOW_REFRESH_TOKEN_AUTH'],
99+
})
100+
);
101+
clientId = clientResult.UserPoolClient!.ClientId!;
102+
103+
discoveryUrl = `https://cognito-idp.${region}.amazonaws.com/${userPoolId}/.well-known/openid-configuration`;
104+
105+
// ── Create test project using local CLI build ──
106+
testDir = join(tmpdir(), `agentcore-e2e-jwt-${randomUUID()}`);
107+
await mkdir(testDir, { recursive: true });
108+
109+
agentName = `E2eJwt${String(Date.now()).slice(-8)}`;
110+
const createResult = await runLocalCLI(
111+
[
112+
'create',
113+
'--name',
114+
agentName,
115+
'--language',
116+
'Python',
117+
'--framework',
118+
'Strands',
119+
'--model-provider',
120+
'Bedrock',
121+
'--memory',
122+
'none',
123+
'--json',
124+
],
125+
testDir
126+
);
127+
expect(createResult.exitCode, `Create failed: ${createResult.stderr}`).toBe(0);
128+
const createJson = parseJsonOutput(createResult.stdout) as { projectPath: string };
129+
projectPath = createJson.projectPath;
130+
131+
// Write AWS targets
132+
const account =
133+
process.env.AWS_ACCOUNT_ID ??
134+
execSync('aws sts get-caller-identity --query Account --output text').toString().trim();
135+
await writeFile(
136+
join(projectPath, 'agentcore', 'aws-targets.json'),
137+
JSON.stringify([{ name: 'default', account, region }])
138+
);
139+
140+
// Install modified CDK tarball (required for auth fields support)
141+
execSync(`npm install -f ${process.env.CDK_TARBALL}`, {
142+
cwd: join(projectPath, 'agentcore', 'cdk'),
143+
stdio: 'pipe',
144+
});
145+
146+
// ── Patch agent with CUSTOM_JWT auth ──
147+
const specPath = join(projectPath, 'agentcore', 'agentcore.json');
148+
const spec = JSON.parse(await readFile(specPath, 'utf8'));
149+
const agent = spec.agents[0];
150+
agent.authorizerType = 'CUSTOM_JWT';
151+
agent.authorizerConfiguration = {
152+
customJwtAuthorizer: {
153+
discoveryUrl,
154+
allowedAudience: [clientId],
155+
},
156+
};
157+
await writeFile(specPath, JSON.stringify(spec, null, 2));
158+
}, 300000);
159+
160+
afterAll(async () => {
161+
if (!canRun) return;
162+
163+
// ── Tear down deployed stack ──
164+
if (projectPath) {
165+
try {
166+
await runLocalCLI(['remove', 'all', '--json'], projectPath);
167+
await runLocalCLI(['deploy', '--yes', '--json'], projectPath);
168+
} catch {
169+
// Best-effort cleanup
170+
}
171+
}
172+
173+
// ── Delete Cognito resources ──
174+
if (userPoolId) {
175+
try {
176+
await cognitoClient.send(new DeleteResourceServerCommand({ UserPoolId: userPoolId, Identifier: 'agentcore' }));
177+
} catch {
178+
/* best-effort */
179+
}
180+
try {
181+
await cognitoClient.send(new DeleteUserPoolDomainCommand({ UserPoolId: userPoolId, Domain: domainPrefix }));
182+
} catch {
183+
/* best-effort */
184+
}
185+
try {
186+
await cognitoClient.send(new DeleteUserPoolCommand({ UserPoolId: userPoolId }));
187+
} catch {
188+
/* best-effort */
189+
}
190+
}
191+
192+
// ── Clean up temp directory ──
193+
if (testDir) {
194+
await rm(testDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 1000 });
195+
}
196+
}, 600000);
197+
198+
it.skipIf(!canRun)(
199+
'deploys with CUSTOM_JWT authorizer configuration',
200+
async () => {
201+
expect(projectPath, 'Project should have been created').toBeTruthy();
202+
203+
const result = await runLocalCLI(['deploy', '--yes', '--json'], projectPath);
204+
205+
if (result.exitCode !== 0) {
206+
console.log('Deploy stdout:', result.stdout);
207+
console.log('Deploy stderr:', result.stderr);
208+
}
209+
210+
expect(result.exitCode, `Deploy failed: ${result.stderr}`).toBe(0);
211+
212+
const json = parseJsonOutput(result.stdout) as { success: boolean; stackName: string };
213+
expect(json.success, 'Deploy should report success').toBe(true);
214+
215+
// Verify CloudFormation template contains AuthorizerConfiguration
216+
const templateResult = await cfnClient.send(new GetTemplateCommand({ StackName: json.stackName }));
217+
const template = JSON.parse(templateResult.TemplateBody!) as {
218+
Resources: Record<string, { Type: string; Properties: Record<string, unknown> }>;
219+
};
220+
221+
const runtimeResource = Object.values(template.Resources).find(r => r.Type === 'AWS::BedrockAgentCore::Runtime');
222+
expect(runtimeResource, 'Template should contain a Runtime resource').toBeDefined();
223+
224+
const props = runtimeResource!.Properties;
225+
const authConfig = props.AuthorizerConfiguration as {
226+
CustomJWTAuthorizer: { DiscoveryUrl: string; AllowedAudience: string[] };
227+
};
228+
expect(authConfig, 'Runtime should have AuthorizerConfiguration').toBeDefined();
229+
expect(authConfig.CustomJWTAuthorizer.DiscoveryUrl).toBe(discoveryUrl);
230+
expect(authConfig.CustomJWTAuthorizer.AllowedAudience).toContain(clientId);
231+
},
232+
600000
233+
);
234+
235+
it.skipIf(!canRun)(
236+
'rejects SigV4 invocation (auth method mismatch)',
237+
async () => {
238+
expect(projectPath, 'Project should have been deployed').toBeTruthy();
239+
240+
// The CLI uses SigV4 by default — a CUSTOM_JWT runtime should reject it
241+
const result = await runLocalCLI(
242+
['invoke', '--prompt', 'Say hello', '--agent', agentName, '--json'],
243+
projectPath
244+
);
245+
246+
// Expect failure due to auth method mismatch
247+
const output = stripAnsi(result.stdout + result.stderr);
248+
expect(output).toMatch(/[Aa]uthoriz(ation|er).*mismatch|different.*authorization/i);
249+
},
250+
180000
251+
);
252+
253+
it.skipIf(!canRun)(
254+
'status shows the deployed agent',
255+
async () => {
256+
const result = await runLocalCLI(['status', '--json'], projectPath);
257+
expect(result.exitCode, `Status failed: ${result.stderr}`).toBe(0);
258+
259+
const json = parseJsonOutput(result.stdout) as {
260+
success: boolean;
261+
resources: { resourceType: string; name: string; deploymentState: string }[];
262+
};
263+
expect(json.success).toBe(true);
264+
265+
const agent = json.resources.find(r => r.resourceType === 'agent' && r.name === agentName);
266+
expect(agent, `Agent "${agentName}" should appear in status`).toBeDefined();
267+
expect(agent!.deploymentState).toBe('deployed');
268+
},
269+
120000
270+
);
271+
});

0 commit comments

Comments
 (0)