Grant Agent Access to APIs
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.
Prerequisites
Section titled “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 -- claudesession 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, solocalhostwon’t work.
Walkthrough
Section titled “Walkthrough”-
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.
-
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 9000Tailscale prints a public URL like
https://<host>.<tailnet>.ts.net. Copy it; Claude will ask for it asAGENT_BASE_URL. If you already have a funnel running, check withtailscale funnel status. -
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.jsonPublic key Keycard uses to verify the agent’s signed client assertions /.well-known/oauth-client-metadataOAuth Client ID Metadata Document. This URL is the agent’s client_id/.well-known/agent-card.jsonA2A 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:
// src/identity.ts: bootstrap the keypair and publish the CIMDconst 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 tokenconst 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 authenticatorconst 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 identityapp.use(identityRouter());app.use("/.well-known/agent-card.json",agentCardHandler({ agentCardProvider: requestHandler }),);app.use("/a2a/jsonrpc",jsonRpcHandler({ requestHandler, userBuilder }),); -
Configure your
.envClaude generates a
.envfile with placeholders. Fill in the values for your environment. You can find your Snowflake account identifier, warehouse, and database from Snowflake’s connection settings.AGENT_BASE_URLmust be the Tailscale Funnel URL from step 2, andPORTmust match the port you funneled.SNOWFLAKE_USERdefaults toautonomous_agent, which is the service user you’ll create in the next step. -
Create the Snowflake WIF service user
Claude prints a
CREATE USERstatement 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_agentWORKLOAD_IDENTITY = (TYPE = OIDCISSUER = '<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.
-
Start the agent
With the funnel running and the WIF user created, start the agent through
keycard runso the LLM key comes from your zone:Terminal window cd <project-name>keycard run -- npm startAt startup the agent:
- Loads or generates its RSA keypair
- Starts an Express server with the identity and A2A endpoints on
PORT - Trades a signed JWT at Keycard’s token endpoint for an access token scoped to
snowflakecomputing.com - Connects to Snowflake with that token using the
WORKLOAD_IDENTITYauthenticator
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.
-
Send a request via A2A
The agent exposes an A2A JSON-RPC endpoint at
/a2a/jsonrpcand 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.
-
Verify in Keycard Console
Open Keycard Console → Audit Log. You should see entries for:
credentials:issueAccess token issued for snowflakecomputing.comThat 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
Section titled “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
Section titled “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 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. 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 for the full identity and credential model.
Workload Identity Federation
Section titled “Workload Identity Federation”Instead of storing Snowflake credentials, the agent uses Keycard as a token broker for Workload Identity Federation. The flow:
- The agent signs a JWT with its private key and posts it to Keycard’s
/oauth/2/tokenendpoint, asking for a token scoped tosnowflakecomputing.com. - Keycard verifies the JWT against the agent’s published public key.
- Keycard returns an OIDC access token whose
issis your zone URL and whosesubis the agent’s application ID. Those are the same values you put into the SnowflakeCREATE USER ... WORKLOAD_IDENTITYstatement. - The agent connects to Snowflake with the
WORKLOAD_IDENTITYauthenticator, 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.
A2A Interface
Section titled “A2A Interface”The agent exposes the Agent-to-Agent (A2A) protocol 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
Section titled “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 statusand confirm it points at the same port the agent listens on. - Check
curl $AGENT_BASE_URL/.well-known/oauth-client-metadatareturns JSON. - Ask Claude to re-check the application credential’s
identifierandjwks_urimatch 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 → 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.
ISSUERmust equal yourKEYCARD_URL;SUBJECTmust 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
Section titled “What’s next”Your agent is running locally with autonomous access to Snowflake. From here you can:
- Run apps without static secrets: deploy the agent to Fly.io so it runs 24/7 without the Tailscale Funnel.
- How Keycard Works: the full authorization model and agentic access loop.
- Applications: how Keycard manages application identity and credentials.