Skip to content

feat(auth/consent-portal): add consent-dashboard sample for auth-code-flow gateway targets - #2070

Merged
satveerkhurpa merged 3 commits into
mainfrom
feat/consent-portal-auth-code-flow-targets
Sep 11, 2026
Merged

feat(auth/consent-portal): add consent-dashboard sample for auth-code-flow gateway targets#2070
satveerkhurpa merged 3 commits into
mainfrom
feat/consent-portal-auth-code-flow-targets

Conversation

@satveerkhurpa

@satveerkhurpa satveerkhurpa commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

Amazon Bedrock AgentCore Samples Pull Request

Issue number: #2069

Concise description of the PR

Adds 01-features/05-authenticate-and-authorize/07-consent-portal-auth-code-flow-targets:
a complete, deployable solution where an agent reaches a gateway target that requires
the OAuth 2.0 authorization code flow (3LO), and each end user grants that access
themselves on the AWS-managed consent dashboard.
user -> FastAPI BFF -> Strands agent on AgentCore Runtime -> AgentCore Gateway
     -> GitHub MCP server (AUTHORIZATION_CODE target)

A deployed agent cannot complete an authorization code flow on its own: that flow needs a browser, a consent screen, and somewhere to receive the redirect. The AgentCore consent portal supplies all three as a hosted, AWS-managed page. Users connect their own GitHub account there, out of band, before or independently of any conversation with the agent — so the agent never handles a GitHub credential, and there is no consent UI or callback server to write.

What the sample provides:

  • Both Entra ID and Okta from one deploy path, selected by a required --entra / --okta flag. IdP shape lives in deploy/idps/<name>.json; tenant values stay in .env, so adding a provider is a new profile plus one bootstrap script.
  • Passthrough on the agent→gateway hop — the runtime and gateway share a discovery URL and audience, so the caller JWT is forwarded unchanged and one token satisfies both. Deliberately not an OBO sample; obo-training/ covers token exchange.
  • Schema-upfront MCP target with 44 GitHub tools, so tools/list works before anyone consents and only tools/call needs the user token.
  • Graceful unconsented path. The gateway answers a tools/call with no stored token by returning a -32042 URL-mode elicitation rather than an error; the agent turns that into a plain instruction naming the portal, and never echoes the raw authorize URL.
  • Optional Lambda RESPONSE interceptor that rewrites the elicitation URL to the portal, for adopters who want every MCP client steered there rather than relying on agent-side handling. Off by default, with a configuration table.
  • Idempotent numbered deploy scripts and a teardown that verifies what it deleted and reports survivors.

User experience

Before: reaching a target that requires the authorization code flow from a deployed agent means building the browser consent leg yourself — a callback endpoint, session binding, and somewhere to store per-user tokens.

After: run deploy/0007, sign in at localhost:8000, and ask a question. A user who has not connected GitHub gets a portal link instead of an error. Once they connect, the same question returns their own data.

The demo that lands the idea is Who am I on GitHub? — two people asking it get two different answers from one agent, one gateway and one target, because the authorization is per user.

Most of the documentation effort went into invariants that fail late and quietly, each written up with the symptom it produces:

Invariant Symptom when wrong
Three callback URLs, none interchangeable generic login failure, or GitHub rejecting redirect_uri at Connect
Gateway authorizer and portal IdP provider need byte-identical discovery URLs login_unavailable, which names no cause
Entra sub is pairwise per resource, so both sign-ins need one shared audience consent stored under one identity, looked up under another — re-prompts forever, with no error anywhere
Okta needs a custom authorization server CreateConsentPortal rejects the provider, because the org server issues opaque tokens
supportedVersions must include 2025-11-25 no URL-mode elicitation, so there is no -32042 to catch
boto3/botocore ≥ 1.43.88 object has no attribute 'create_consent_portal', which reads like a missing feature

Validation

Deployed and run end to end in a real AWS account against real tenants, for both IdPs: portal sign-in, GitHub Connect, and a live GitHub MCP call returning the signed-in user’s own data. The optional interceptor was verified live (22 invocations, 2 rewrites — it fires only on -32042). Teardown was then run for both variants and the account verified empty.

The one hop not exercised is the Bedrock model call: the test account’s daily token quota is zero pending a Support increase. Everything the model depends on — runtime IAM, the inference profile, tool discovery — is verified. The README states this in a Validation status callout rather than implying full coverage.

Checklist

  • I have reviewed the contributing guidelines
  • Add your name to CONTRIBUTORS.md — already listed
  • Have you checked to ensure there aren't other open Pull Requests for the same update/change?
  • Are you uploading a dataset? — no
  • Have you documented Introduction, Architecture Diagram, Prerequisites, Usage, Sample Prompts, and Clean Up steps in your example README?
  • I agree to resolve any issues created for this example in the future
  • I have performed a self-review of this change
  • Changes have been tested
  • Changes are documented

Notes for reviewers

  • IAM: neither role uses Resource: "*". The AgentCore Identity actions do support resource-level permissions, so both roles are scoped to token-vault/* and workload-identity-directory/* in this account and region. The ids stay wildcarded because AgentCore owns those names and mints a workload identity per runtime/gateway. Both rendered policies validate with no IAM Access Analyzer findings. The README also records the InboundJwtClaim/* and userid condition keys as the next tightening step available to adopters.
  • The BFF session cookie is not Secure, because the sample runs on http://localhost. Documented, along with the one-line change needed to serve it anywhere else.
  • GitHub scopes (repo user workflow) are broad for a sample; the README points at the two places to trim them.
  • Terminology: "authorization code flow" is used throughout, with "(3LO)" noted once on first use for readers who know it by that name.
  • ruff check and ruff format --check are clean under the repo’s pyproject.toml.

Acknowledgment

By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of the project license.

…-flow gateway targets

Adds 01-features/05-authenticate-and-authorize/07-consent-portal-auth-code-flow-targets:
an end-to-end solution where end users authorize a third-party target themselves on
the AWS-managed consent dashboard, out of band, and the agent never handles their
credential.

  user -> FastAPI BFF -> Strands agent on AgentCore Runtime -> AgentCore Gateway
       -> GitHub MCP server (AUTHORIZATION_CODE / 3LO target)

- Entra ID and Okta both supported from one deploy path, selected by a required
  --entra / --okta profile flag; IdP shape lives in deploy/idps/<name>.json while
  tenant values stay in .env.
- Passthrough on the agent->gateway hop: runtime and gateway share a discovery URL
  and audience, so the caller's JWT is forwarded unchanged. Not an OBO sample.
- Schema-upfront MCP target (44 GitHub tools), so tools/list works before consent
  and only tools/call needs the user's token.
- Optional Lambda RESPONSE interceptor that rewrites the -32042 elicitation URL to
  the portal, for steering every MCP client rather than relying on agent-side
  handling. Off by default.
- Idempotent numbered deploy scripts and a teardown that verifies what it deleted.

Documents the invariants that otherwise fail late and quietly: three
non-interchangeable callback URLs, the gateway authorizer and portal IdP provider
needing byte-identical discovery URLs, Entra's pairwise sub requiring one shared
audience, and Okta requiring a custom authorization server.

Verified end to end against a real AWS account with both IdPs: portal sign-in,
GitHub Connect, and a live GitHub MCP call using the user's own authorization.
The Bedrock model hop is not exercised (test account daily token quota is zero,
pending a Support increase); README states this in a Validation status callout.

Refs #2069
…ments, drop "3LO" as primary term

Two follow-ups from review.

IAM: neither role uses "Resource": "*" any more. The AgentCore Identity actions
(GetWorkloadAccessToken*, GetResourceOauth2Token, CompleteResourceTokenAuth) do
support resource-level permissions, per the machine-readable AWS service
reference, so both the gateway service role and the portal execution role are now
scoped to

    arn:aws:bedrock-agentcore:<region>:<account>:token-vault/*
    arn:aws:bedrock-agentcore:<region>:<account>:workload-identity-directory/*

Both families are listed because no supported resource type is marked required.
The vault and directory ids stay wildcarded deliberately — AgentCore owns those
names and mints a workload identity per runtime/gateway, so pinning today's
"default" would break when the service picks another. The child ARNs
(.../oauth2credentialprovider/*, .../workload-identity/*) are intentionally
omitted: an IAM wildcard spans "/", so they are redundant, and IAM Access
Analyzer flags them as such. Both rendered policies now validate with no
Access Analyzer findings.

README also records the condition keys these actions accept
(InboundJwtClaim/{iss,aud,sub,client_id,scope} and userid) as the next step
available to adopters who want to pin an issuer or audience.

Terminology: "3LO" is not an OAuth term, so the primary wording is now
"authorization code flow" throughout, with "(3LO)" kept only once on first use in
the README intro as a signpost for readers who know it by that name.

Refs #2069
@github-actions

github-actions Bot commented Sep 10, 2026

Copy link
Copy Markdown

Latest scan for commit: 12154b9 | Updated: 2026-09-11 11:08:49 UTC

Security Scan Results

…env permissions

Addresses CodeQL py/clear-text-logging-sensitive-data on
04_create_github_target.py, which reported a callback URL as a leaked password.

Root cause was one shared accessor: must_env() served both ordinary config and
GITHUB_CLIENT_SECRET, so every value it returned inherited "may be a credential"
and taint analysis then flagged the non-secret defaultReturnUrl diagnostic. Adds
must_secret_env() for secrets and routes the one secret read through it, so the
distinction is visible at each call site and the false positive disappears at the
source rather than being suppressed.

Also chmod 0600 on .env after every write. It is gitignored, but the default
umask would otherwise leave client secrets and the Okta admin token
group/world-readable.

Refs #2069
@satveerkhurpa

Copy link
Copy Markdown
Contributor Author

CodeQL: 3 alerts reviewed and dismissed

All other checks pass (python-lint, Analyze (python), Analyze (javascript-typescript), js-lint, scan). Recording the reasoning here because dismissal comments are capped at 280 characters.

py/clear-text-logging-sensitive-datadeploy/04_create_github_target.py:99 → dismissed, false positive

The flagged expression logs two OAuth redirect URLs: the target's live defaultReturnUrl and the <portalUrl>/connect/callback this portal expects. Neither is a credential.

It is reported as a password because the value is read out of the GetGatewayTarget response through keys named credentialProviderConfigurationscredentialProvideroauthCredentialProvider, and the heuristic treats anything behind a key containing "credential" as sensitive. Those key names come from the AWS API and cannot be renamed.

The diagnostic is deliberate and load-bearing. A target created before the consent portal existed carries a stale defaultReturnUrl; consent then completes at GitHub but never binds to the portal session, and every tool call keeps asking for consent with nothing reporting an error. Printing both URLs side by side is how an operator spots that. No secret is logged anywhere in this sample — verified by grep across all deploy scripts, the BFF and the agent.

Note: I first tried to fix this by splitting must_env() into a separate must_secret_env() for secrets, on the theory that one shared accessor was tainting all its return values. That theory was wrong — the alert persisted — but the split is kept in 12154b97 because it makes sensitive env vars explicit at each call site, and that commit also adds chmod 0600 on .env.

py/clear-text-storage-sensitive-datadeploy/_common.py (×2) → dismissed, won't fix

A true positive, accepted by design. save_env() persists IdP client secrets — and for Okta a setup-only admin API token — to a local .env, so the numbered deploy steps and the local BFF can read them between runs. This is the same cp config.example.env .env model used throughout this repo.

Mitigations in place:

Not committable .env and .env.* are gitignored; verified never committed
Not world-readable chmod 0600 on every write (12154b97)
Not in process listings secrets are never passed on argv; prompted via getpass when absent
Never printed no code path logs a secret — only counts, e.g. "2 freshly minted"
Time-limited the Okta admin token is setup-only and can be blanked once deployed

The README's Security notes state plainly that this is a sandbox and not a template for production secret handling.

The alternative — writing each secret to AWS Secrets Manager and keeping only the ARN in .env — would remove the finding outright and is the right answer for a real deployment. I have not done it here because it is a design change I cannot validate end to end at the moment (both deployments are torn down, and the consent leg needs a human browser step). Happy to take it on if maintainers would prefer that over a dismissal.

@satveerkhurpa
satveerkhurpa merged commit eae25d8 into main Sep 11, 2026
7 checks passed
@satveerkhurpa
satveerkhurpa deleted the feat/consent-portal-auth-code-flow-targets branch September 11, 2026 13:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants