Dev.to AI 🤖 Ai 👁 0 📖 4 min read

How do AI agents authenticate? (The problem nobody has solved yet)

If you've built an MCP server or an API that agents call, you've run into this problem. Your agent needs to call a tool. That tool needs to know who's calling. So you do what everyone does: generate an API key, paste it

If you've built an MCP server or an API that agents call, you've run into this problem.

Your agent needs to call a tool. That tool needs to know who's calling. So you do what everyone does: generate an API key, paste it in an env var, and move on.

It works. Until it doesn't.

The key gets committed to a repo. Or it gets shared across a whole team's agents, so every agent looks identical in your logs. Or someone leaves and you realize you have no idea which credentials they had. Or you're running 50 agents and managing 50 sets of credentials per tool.

This isn't a process problem. It's an architectural one. API keys were designed for humans. They were never designed for machines.

What agents actually need

Think about what authentication is supposed to do: prove that the entity making a request is who they say they are.

For humans, passwords work (barely) because you can reset them, remember them, and tie them to a person. For machines, a static shared secret is just a liability — it has to exist somewhere, which means it can leak from somewhere.

What agents need is proof of identity without a credential that can be stolen. That's a solved problem in cryptography. It's called a challenge-response. The server issues a one-time nonce. The client signs it with a private key. The server verifies the signature. No credential ever travels over the wire — only a signature that proves the client holds the key.

SSH works this way. TLS works this way. There's no reason agent authentication shouldn't.

What Arken does

Arken is an open-source implementation of this for HTTP APIs and MCP servers.

Each agent gets a key pair. The did:key identifier derived from that key pair is the agent's identity — a string that looks like did:key:zQ3sh.... It's deterministic from the private key, portable across every Arken-protected endpoint, and requires no registration with any central authority.

Server side — one line of Express middleware:

const { arkenAuth } = require('@arkenforge/arken-auth');

app.get('/tool', arkenAuth(), (req, res) => {
  res.json({ did: req.user.did });
});

An unsigned request gets a 401 with a one-time nonce:

{
  "challenge": {
    "nonce": "a3f9...",
    "statement": "Sign this message to prove you control this DID."
  }
}

The client signs the nonce and retries with an Authorization: Signature header. The server verifies the ECDSA signature in Rust, consumes the nonce, and sets req.user.did. No credentials stored. Nonce expires in 60 seconds.

Agent side — a drop-in fetch wrapper:

const { createClient } = require('arken-client');

const { fetch, did } = createClient(process.env.AGENT_PRIVATE_KEY);
console.log(did); // did:key:zQ3s… — same every time for this key

const res = await fetch('https://your-mcp-server.com/tool');
// 401 → sign → retry happens automatically
// your code never sees the auth flow

If no key is provided, one is generated. For agents that need a stable identity across runs, generate once and store the private key.

What this looks like in practice

Every agent in your fleet has a unique did:key. When it calls a tool, the server logs exactly which DID called — not "some agent using the shared service token."

You can do things that were previously impossible with API keys:

  • Per-agent audit trail — you know exactly which agent called which tool and when
  • No credential rotation — there are no credentials to rotate
  • No credential storage — the server holds nothing. There's no credential database to breach
  • Fleet identity management — each agent in a 100-agent fleet has its own cryptographic identity, issued at creation time, with no manual key management

FastAPI

from arken_auth import arken_auth

@app.get("/tool")
async def tool(user = Depends(arken_auth())):
    return {"did": user["did"]}

Production deployments

By default nonces are stored in memory. For multi-instance deployments, pass a Redis client:

const { arkenAuth, RedisNonceStore } = require('@arkenforge/arken-auth');
const { createClient } = require('redis');

const redis = createClient({ url: process.env.REDIS_URL });
await redis.connect();

app.use('/tool', arkenAuth({ store: new RedisNonceStore(redis) }));

Install

npm install @arkenforge/arken-auth  # server
npm install arken-client            # agent
pip install arken-auth              # FastAPI
pip install arken-client            # Python agents

Pre-1.0 — the API may change before the stable release. Apache 2.0.

Why this matters beyond MCP

Agents proliferating without per-agent identity is going to cause problems. Most enterprises don't know which agents exist inside their infrastructure, what they're authorized to do, or what they've accessed. A shared service token doesn't tell you any of that.

A did:key per agent is the primitive that makes visibility possible — not just for security, but for billing, auditing, and access control.

If you're building MCP servers, agent backends, or APIs that agents call — especially if you've hit the credentials problem in production — I'd like to hear about your setup.

Repo: github.com/arkenforge/arken-auth

📰 Read the original article on Dev.to AI

Originally published by Dev.to AI. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.