---
title: Grant Agent Access to APIs | Keycard
description: Build an autonomous agent that authenticates to Snowflake using Workload Identity Federation. No human approvals, no stored secrets.
---

Ask Claude to build an autonomous agent for you. By the end of this guide you’ll have a standalone agent that authenticates to Snowflake using Workload Identity Federation (WIF). No human approves each request, and no API keys live on disk. Other agents and clients talk to it over the [A2A protocol](https://google.github.io/A2A/).

Keycard

Agent

Snowflake

## Prerequisites

- **Keycard**. You need a Keycard URL (e.g. `https://<zone-id>.keycard.cloud`).

- **Snowflake account** with permission to run `CREATE USER ... TYPE = SERVICE`. The agent will walk you through creating a Workload Identity Federation service user that trusts tokens from your Keycard zone.

- **Keycard session active**. Your `keycard run -- claude` session should still be running.

- **Tailscale with Funnel enabled**. The agent serves its identity (public key and OAuth client metadata) and A2A agent card at `/.well-known/` endpoints. Keycard pulls these from the public internet to check the agent’s signed assertions, so `localhost` won’t work.

  Note

  Install [Tailscale](https://tailscale.com/download) and enable [Funnel](https://tailscale.com/kb/1223/funnel) on your tailnet (Admin Console → DNS → enable HTTPS, then DNS → Funnel). Funnel exposes a single local port on a stable `https://<host>.<tailnet>.ts.net` URL, with no reverse proxy or tunnels to set up.

  If you’d rather deploy the agent than run it locally, skip Funnel and follow [Run apps without static secrets](/guides/run-apps-without-static-secrets/index.md) after this guide to host the agent on Fly.io.

## Walkthrough

1. **Ask Claude to create your agent**

   Inside your Keycard session, send a prompt like:

   > Create a Snowflake autonomous AI agent with its own identity so I can grant it access to tools

   The Keycard skill drafts an action plan and shows it to you before doing anything.

   Tip

   While Claude scaffolds the agent and sets up Keycard resources, skim the [Patterns](#patterns) section below so you know what to expect once the agent boots.

2. **Expose the agent with Tailscale Funnel**

   Before scaffolding, start a Tailscale Funnel on the port the agent will use (default `9000`). Claude needs the public URL when it registers the agent with Keycard:

   Terminal window

   ```
   tailscale funnel --bg 9000
   ```

   Tailscale prints a public URL like `https://<host>.<tailnet>.ts.net`. Copy it; Claude will ask for it as `AGENT_BASE_URL`. If you already have a funnel running, check with `tailscale funnel status`.

   Caution

   Funnel requires HTTPS and Funnel to be enabled in your tailnet’s Admin Console. If `tailscale funnel` errors, follow Tailscale’s [Funnel setup guide](https://tailscale.com/kb/1223/funnel) to enable it.

3. **Claude scaffolds the project and bootstraps the agent’s identity**

   Claude generates the code, configures it for your Keycard zone, installs dependencies, and creates the agent’s keypair (an RSA key stored in `agent_keys/`).

   The agent serves three documents at well-known URLs that Keycard and A2A clients consume:

   | Endpoint                             | Purpose                                                                   |
   | ------------------------------------ | ------------------------------------------------------------------------- |
   | `/.well-known/jwks.json`             | Public key Keycard uses to verify the agent’s signed client assertions    |
   | `/.well-known/oauth-client-metadata` | OAuth Client ID Metadata Document. This URL is the agent’s `client_id`    |
   | `/.well-known/agent-card.json`       | A2A agent card advertising the agent’s capabilities and auth requirements |

   These documents are fetched from the public internet, so the agent has to be reachable through your Tailscale Funnel URL. Claude uses the URL you copied in step 2 when it registers the agent with Keycard.

   Here’s how the generated code handles identity, token exchange, and A2A:

   - [Identity](#tab-panel-60)
   - [Token exchange](#tab-panel-61)
   - [Snowflake WIF](#tab-panel-62)
   - [A2A](#tab-panel-63)

   ```
   // src/identity.ts: bootstrap the keypair and publish the CIMD
   const identity = new WebIdentity({
     storageDir: "./agent_keys",
     keyId: process.env.AGENT_NAME,
   });
   await identity.bootstrap();


   router.get("/.well-known/oauth-client-metadata", (_req, res) => {
     res.json({
       client_id: getClientId(),
       token_endpoint_auth_method: "private_key_jwt",
       grant_types: ["urn:ietf:params:oauth:grant-type:token-exchange"],
       jwks: identity.getPublicJwks(),
     });
   });
   ```

   ```
   // src/tokenProvider.ts: sign an assertion, exchange for a Snowflake-scoped token
   const request = await identity.prepareTokenExchangeRequest("", resource, {
     tokenEndpoint: `${keycardUrl}/oauth/2/token`,
     authInfo: { resource_client_id: clientId },
   });


   const body = new URLSearchParams({
     grant_type: "client_credentials",
     client_assertion_type:
       "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
     client_assertion: request.clientAssertion!,
     resource, // "snowflakecomputing.com"
   });
   ```

   ```
   // src/snowflake.ts: hand the OIDC token to Snowflake's WORKLOAD_IDENTITY authenticator
   const connection = snowflakeSdk.createConnection({
     account: config.account,
     authenticator: "WORKLOAD_IDENTITY",
     workloadIdentityProvider: "OIDC",
     token: accessToken,
   });
   ```

   ```
   // src/server.ts: agent card and JSON-RPC, gated by Keycard-verified caller identity
   app.use(identityRouter());
   app.use(
     "/.well-known/agent-card.json",
     agentCardHandler({ agentCardProvider: requestHandler }),
   );
   app.use(
     "/a2a/jsonrpc",
     jsonRpcHandler({ requestHandler, userBuilder }),
   );
   ```

   Tip

   You can see the Keycard resources Claude created in [Keycard Console](https://console.keycard.ai). The [Patterns](#patterns) section below explains what got created and why.

4. **Configure your `.env`**

   Claude generates a `.env` file with placeholders. Fill in the values for your environment. You can find your Snowflake account identifier, warehouse, and database from [Snowflake’s connection settings](https://docs.snowflake.com/en/user-guide/gen-conn-config).

   `AGENT_BASE_URL` must be the Tailscale Funnel URL from step 2, and `PORT` must match the port you funneled. `SNOWFLAKE_USER` defaults to `autonomous_agent`, which is the service user you’ll create in the next step.

5. **Create the Snowflake WIF service user**

   Claude prints a `CREATE USER` statement with your Keycard zone URL and the agent’s application ID filled in. Run it in a Snowflake worksheet (or via SnowSQL) using whichever role your Snowflake admin tells you to use:

   ```
   CREATE USER autonomous_agent
     WORKLOAD_IDENTITY = (
       TYPE = OIDC
       ISSUER = '<keycard-url>'
       SUBJECT = '<agent-application-id>'
     )
     TYPE = SERVICE;
   ```

   That creates a Snowflake service user that trusts OIDC tokens from your Keycard zone for this specific agent. Grant the user a role and warehouse access based on what your Snowflake admin tells you.

6. **Start the agent**

   With the funnel running and the WIF user created, start the agent through `keycard run` so the LLM key comes from your zone:

   Terminal window

   ```
   cd <project-name>
   keycard run -- npm start
   ```

   At startup the agent:

   1. Loads or generates its RSA keypair
   2. Starts an Express server with the identity and A2A endpoints on `PORT`
   3. Trades a signed JWT at Keycard’s token endpoint for an access token scoped to `snowflakecomputing.com`
   4. Connects to Snowflake with that token using the `WORKLOAD_IDENTITY` authenticator

   No secrets get passed on the command line or stored in env vars. The agent proves who it is with a signed JWT and gets a short-lived token back.

   Tip

   In a second terminal, sanity-check that the funnel is forwarding to the agent:

   Terminal window

   ```
   curl -sf "$AGENT_BASE_URL/.well-known/jwks.json" | jq '.keys | length'
   curl -sf "$AGENT_BASE_URL/.well-known/oauth-client-metadata" | jq '.client_id'
   ```

   Both should return data. If they hang or 404, run `tailscale funnel status` and confirm it points at the same `PORT` the agent is listening on.

7. **Send a request via A2A**

   The agent exposes an A2A JSON-RPC endpoint at `/a2a/jsonrpc` and an agent card at `/.well-known/agent-card.json`. Any A2A-compatible client can send tasks to it:

   Terminal window

   ```
   curl -X POST http://localhost:9000/a2a/jsonrpc \
     -H "Content-Type: application/json" \
     -d '{"jsonrpc":"2.0","method":"tasks/send","id":"1","params":{"id":"task-1","message":{"role":"user","parts":[{"kind":"text","text":"Query Snowflake for the available sample datasets"}]}}}'
   ```

   The agent checks the inbound request with Keycard, runs the query against Snowflake using its short-lived OIDC token, and streams the result back over A2A.

8. **Verify in Keycard Console**

   Open [Keycard Console](https://console.keycard.ai) → **Audit Log**. You should see entries for:

   |                     |                                                  |
   | ------------------- | ------------------------------------------------ |
   | `credentials:issue` | Access token issued for `snowflakecomputing.com` |

   That entry is the agent’s token exchange. The agent presented a signed JWT, and Keycard returned a short-lived access token scoped to Snowflake.

## Patterns

Your agent is running. Here’s what Claude set up, and why it matters when you build your own agents or come back to debug this one.

### Agent Identity

The agent owns its own keypair instead of using an injected secret. On first startup it generates an RSA keypair and saves it under `agent_keys/`. The private key never leaves the machine. The public key gets published at `/.well-known/jwks.json`, and an [OAuth Client ID Metadata Document](https://datatracker.ietf.org/doc/draft-ietf-oauth-client-id-metadata-document/) at `/.well-known/oauth-client-metadata` tells Keycard how to authenticate the agent. That metadata URL is the agent’s `client_id`.

When the agent needs an access token, it signs a JWT with its private key and sends it to Keycard’s token endpoint. Keycard fetches the metadata document, finds the public key, checks the signature, and issues a scoped access token. That’s the `private_key_jwt` method from [RFC 7523](https://datatracker.ietf.org/doc/html/rfc7523). No shared secret ever lives on disk or in env vars.

To rotate the identity, delete `agent_keys/` and restart. The agent makes a new keypair, and Keycard picks up the new public key the next time it fetches the metadata.

See [Applications](/platform/concepts/applications/index.md) for the full identity and credential model.

### Workload Identity Federation

Instead of storing Snowflake credentials, the agent uses Keycard as a token broker for [Workload Identity Federation](https://docs.snowflake.com/en/user-guide/workload-identity-federation). The flow:

1. The agent signs a JWT with its private key and posts it to Keycard’s `/oauth/2/token` endpoint, asking for a token scoped to `snowflakecomputing.com`.
2. Keycard verifies the JWT against the agent’s published public key.
3. Keycard returns an OIDC access token whose `iss` is your zone URL and whose `sub` is the agent’s application ID. Those are the same values you put into the Snowflake `CREATE USER ... WORKLOAD_IDENTITY` statement.
4. The agent connects to Snowflake with the `WORKLOAD_IDENTITY` authenticator, presenting the OIDC token.

Tokens are cached per resource and refreshed before they expire. If a Snowflake connection drops mid-query, the client throws away its cached token, gets a fresh one, and retries with exponential backoff. You don’t rotate anything by hand, and nothing gets persisted.

For the broader authorization model, see [How Keycard Works](/guides/how-keycard-works/index.md).

### A2A Interface

The agent exposes the [Agent-to-Agent (A2A) protocol](https://google.github.io/A2A/) so other agents or clients can send it tasks. The `@keycardai/a2a` package provides the agent card handler, the JSON-RPC handler, and a `keycardUserBuilder` that checks inbound caller tokens against your Keycard zone before any task runs.

The agent card at `/.well-known/agent-card.json` lists the agent’s capabilities and how to authenticate to it, so A2A clients can discover and connect on their own.

## Troubleshooting

Keycard returns `invalid_client` when the agent requests a token

Keycard couldn’t fetch or verify the agent’s metadata document.

- Run `tailscale funnel status` and confirm it points at the same port the agent listens on.
- Check `curl $AGENT_BASE_URL/.well-known/oauth-client-metadata` returns JSON.
- Ask Claude to re-check the application credential’s `identifier` and `jwks_uri` match the current Funnel URL.

Token exchange fails with `invalid_target` or “unknown resource”

The `snowflakecomputing.com` resource isn’t wired as a dependency of the agent application.

- Open [Keycard Console](https://console.keycard.ai) → **Applications** → your agent → **Dependencies** and confirm Snowflake is listed.
- If it’s missing, ask Claude to re-run the dependency wiring step.

Snowflake rejects the OIDC token with an authentication error

The Snowflake WIF user’s `ISSUER` or `SUBJECT` don’t match the agent’s token.

- `ISSUER` must equal your `KEYCARD_URL`; `SUBJECT` must equal the agent’s application ID from Keycard Console.
- Fix with `ALTER USER autonomous_agent SET WORKLOAD_IDENTITY = (TYPE = OIDC ISSUER = '<keycard-url>' SUBJECT = '<agent-application-id>');`.

## What’s next

Your agent is running locally with autonomous access to Snowflake. From here you can:

- **[Run apps without static secrets](/guides/run-apps-without-static-secrets/index.md)**: deploy the agent to Fly.io so it runs 24/7 without the Tailscale Funnel.
- **[How Keycard Works](/guides/how-keycard-works/index.md)**: the full authorization model and agentic access loop.
- **[Applications](/platform/concepts/applications/index.md)**: how Keycard manages application identity and credentials.
