Skip to content
API Reference
Guides
Use Keycard SDKs

Act on Behalf of Absent Users

Build a background agent that calls APIs as a specific user who isn't present, using short-lived per-request tokens and the Keycard SDK.

Build a background agent that acts as a specific user without that user being present. The user grants access once through a landing page. After that, the agent mints a short-lived token scoped to that user on every request, with no browser, no session, and nothing stored. This is impersonation: the agent authenticates as itself, names the user it acts for, and exchanges the two for a credential.

One-Time ConsentUser authorizes via a landing page, once
Background AgentRuns non-interactively, no user present
Per-Request TokensScoped, short-lived, never stored
Audit TrailActor and delegator both logged
  • Keycard. You need your Keycard Issuer URL (found under SettingsConnection, e.g. <keycard-issuer-url>).
  • A Provider and Resource. The Resource you want the agent to reach (e.g. a GitHub-backed Resource at https://api.github.com), linked to its OAuth Provider in Keycard Console. See Call External APIs from MCP for adding an API from the catalog.
  • Two Applications (created in the walkthrough): a public landing page for the one-time consent, and a confidential background agent that does the impersonation.
  • Python 3.10+, Node.js 18+, or Go 1.22+, depending on which SDK you use.
  1. Install the SDK

    Terminal window
    pip install keycardai-oauth
  2. Provision the two Applications in Keycard Console

    You set up two Applications. With the default policies, the agent can impersonate a user for any Resource it declares as a dependency once that user has delegated it — no policy is needed to enable impersonation. Add a policy only to restrict it.

    StepWhat to create
    Landing page appA public credential (identifier e.g. landing-page), redirect URI http://localhost:3000/callback. Add the Resource as a dependency.
    Background agent appA confidential credential (identifier e.g. background-agent) with a client secret. Add the same Resource as a dependency, and set its consent to implicit — no user is present to approve a consent screen during the exchange.
    Restriction policy (optional)A forbid policy if you want to limit impersonation — e.g. allow it only for specific users. Without one, the agent can impersonate any user who has delegated a Resource it depends on.

    To restrict background-agent so it may impersonate only one user (Cedar forbid overrides the default permit):

    forbid (
    principal is Keycard::Application,
    action,
    resource
    )
    when {
    principal.identifier == "background-agent" &&
    context has impersonate && context.impersonate == true
    }
    unless {
    context has subject &&
    context.subject.identifier == "troy.barnes@greendale.edu"
    };

    Put the agent’s client ID and secret in the environment as KEYCARD_CLIENT_ID and KEYCARD_CLIENT_SECRET. Never commit them.

  3. Build the landing page for one-time consent

    The landing page runs the standard authorization-code-with-PKCE flow. The user signs in once and authorizes the Resources configured as the app’s dependencies. That establishes the consent grant the agent relies on later — there is no token to keep here.

    # landing_page.py — the two calls that matter
    from keycardai.oauth import Client, build_authorize_url
    from keycardai.oauth.http.auth import NoneAuth
    from keycardai.oauth.types.models import ClientConfig
    from keycardai.oauth.utils.pkce import PKCEGenerator
    client = Client(
    base_url="<keycard-issuer-url>", # from Settings → Connection
    auth=NoneAuth(), # public client, PKCE only
    config=ClientConfig(enable_metadata_discovery=True, auto_register_client=False),
    )
    # On GET /authorize: redirect the user to Keycard
    pkce = PKCEGenerator().generate_pkce_pair()
    url = build_authorize_url(
    client.endpoints.authorize,
    client_id="<landing-page-client-id>",
    redirect_uri="http://localhost:3000/callback",
    pkce=pkce,
    resources=[], # dependencies determine what gets authorized
    scope="openid email",
    state=state, # store state -> pkce.code_verifier
    )
    # On GET /callback: trade the code for tokens (this records the grant)
    token_response = client.exchange_authorization_code(
    code=code,
    redirect_uri="http://localhost:3000/callback",
    code_verifier=code_verifier,
    client_id="<landing-page-client-id>",
    )
  4. Have the user authorize, once

    Open the landing page, sign in, and approve access. You may see a consent screen from the Provider that establishes trust between Keycard and the upstream API. After this, the user can close the tab — the agent never needs them again unless a new Resource has to be authorized.

  5. Get the user’s identifier from Keycard Console

    In Keycard Console, find the user under Users and copy their identifier (often an email or sub, depending on how the Provider maps claims). This is the value the agent names when it impersonates.

  6. Impersonate the user from the background agent

    The agent authenticates with its own confidential credential and calls impersonate() with the user identifier and target Resource. The SDK builds the substitute-user token and runs the RFC 8693 token exchange for you.

    import os
    from keycardai.oauth import Client
    from keycardai.oauth.http.auth import BasicAuth
    from keycardai.oauth.types.models import ClientConfig
    with Client(
    base_url="<keycard-issuer-url>", # from Settings → Connection
    auth=BasicAuth(
    os.environ["KEYCARD_CLIENT_ID"],
    os.environ["KEYCARD_CLIENT_SECRET"],
    ),
    config=ClientConfig(enable_metadata_discovery=True, auto_register_client=False),
    ) as client:
    response = client.impersonate(
    user_identifier="troy.barnes@greendale.edu",
    resource="https://api.github.com",
    )
    token = response.access_token # short-lived, scoped to the user
  7. Call the upstream API with the token

    The credential acts as the user against the Resource — for the brokered GitHub Resource here, that’s GitHub’s own token for that user. Use it like any bearer token:

    Terminal window
    curl -H "Authorization: Bearer $TOKEN" https://api.github.com/user

    Mint a fresh token per request — they are short-lived and meant to be thrown away, not cached across jobs.

  8. Verify in Keycard Console

    Open Keycard ConsoleAudit Log. The impersonated issuance is recorded with the Application as the actor and the impersonated user as its delegator, so access traces back to both:

    users:authorizeThe one-time grant from the landing page
    credentials:issueEach token the agent minted on the user’s behalf
ErrorCause
invalid_clientThe caller is a public client or failed client authentication.
invalid_requestThe substitute-user token is malformed — it is signed (it must be unsigned), has the wrong typ or alg, or is missing sub. The SDK builds it correctly, so this usually means a hand-rolled request.
invalid_grantThe sub identifier does not resolve to a known user.
invalid_targetThe resource is not a known, configured Resource.
interaction_requiredNo prior delegation exists for the Resource; the user must authorize through the landing page first.
access_deniedAccess policy denied the exchange — the Resource is not one of the agent’s dependencies, or a policy forbids impersonation.

Authenticate the agent without a static secret

Section titled “Authenticate the agent without a static secret”

This guide uses a client secret to keep the walkthrough short, but a shared secret is the weakest of the Application credentials — and impersonation only requires a confidential client, not a secret specifically. Swap it for a credential that nothing has to store; the impersonate() call stays the same.

  • Workload Identity (WI) — if the agent runs on AWS, Google Cloud, Vercel, or similar, it authenticates with the platform’s workload identity token instead of a secret. Run Apps Without Static Secrets walks through deploying an app this way.
  • URL credential (CIMD) — the agent proves control of a URL with a signed, key-based assertion, and Keycard fetches its public key from the agent’s OAuth Client ID Metadata Document. No shared secret lives anywhere. Grant Agent Access to APIs shows an agent bootstrapping this identity with the SDK’s WebIdentity helper.
The exchange returns access_denied

Access policy denied the exchange. With the default policies this happens when the Resource is not one of the agent Application’s dependencies, or when a policy explicitly forbids impersonation.

  • Confirm the Resource is listed as a dependency of the agent Application.
  • If you’ve added a restriction policy, confirm it isn’t forbidding this Application or subject — and that its principal.identifier matches the agent’s identifier exactly.
The exchange returns interaction_required

No prior grant exists for the user and Resource.

  • The user must complete the landing-page flow for that Resource at least once.
  • Adding a new Resource dependency requires the user to authorize again — the existing grant doesn’t cover it.
The exchange returns invalid_client

The agent failed client authentication, or you’re using a public client.

  • Impersonation requires a confidential client. Verify KEYCARD_CLIENT_ID / KEYCARD_CLIENT_SECRET are set and correct.
  • Confirm the agent Application has a client secret (or workload identity / URL credential), not a public credential.
The exchange returns invalid_grant

The user identifier doesn’t resolve to a known user.

  • Copy the exact identifier from Keycard Console → Users.
  • The identifier can be changed in Console; make sure your code uses the current value.