The Agentic Profile is a thin layer over A2A, MCP, and other HTTP protocols, and that provides:
- Globally unique - user and business scoped - agent identity
- Decentralized authentication
This week I modified Google's A2A demo code (result here) to add identity and authentication. This post will detail the changes I made.
Quick Background on the Agentic Profile
The Agentic Profile leverages W3C DIDs and DID documents to provide decentralized identity and information about agents, including authentication methods. (Although DID is a blockchain motivated protocol, the specification allows HTTPS resolution of documents which is leveraged by the Agentic Profile)
An Agentic Profile is a DID document, that instead of representing an agent as a first class citizen, presents a person or business as the first class citizen and their agents as verified delegates.
Below is an example Agentic Profile for the DID web:did:localhost%3A:users:2:coder which is available at http://localhost:3003/users/2/coder
{
"@context": [
"https://www.w3.org/ns/did/v1",
"https://w3id.org/security/suites/jws-2020/v1",
"https://iamagentic.org/ns/agentic-profile/v1"
],
"id": "did:web:localhost%3A3003:users:2:coder",
"name": "Coder House, Inc.",
"verificationMethod": [
{
"id": "#identity-key",
"type": "JsonWebKey2020",
"publicKeyJwk": {
"kty": "OKP",
"alg": "EdDSA",
"crv": "Ed25519",
"x": "z6QvmN-GVCWpKUF-q_jFHuMGxWk7WH_CeRdjDl9A4OA"
}
}
],
"service": [
{
"name": "A2A coder with authentication",
"id": "#a2a-coder",
"type": "A2A",
"serviceEndpoint": "http://localhost:3003/users/2/coder/",
"capabilityInvocation": [
{
"id": "#agent-a2a-coder-key-0",
"type": "JsonWebKey2020",
"publicKeyJwk": {
"kty": "OKP",
"alg": "EdDSA",
"crv": "Ed25519",
"x": "fXjpVGo2mIEKxV6k071ihe2awaS5F3JUevycH9wwTr8"
}
}
]
}
]
}
DID documents list services under the service property, and with the Agentic Profile I chose to keep the same name for listing agent services.
To support A2A communications, I extended the Agentic Profile to support a service.type of "A2A" and use the serviceEndpoint to reference the agent card.
If the A2A agent only authenticates inbound requests, then no capabilityInvocation entries are necessary. However, if the agent wants to do outbound communications and authenticate itself, then a capabilityInvocation should be supplied, such as the one above that uses EdDSA.
Authentication Flow with the Agentic Profile
The following diagram uses "User Agent" to represent a client agent making a request, and "Peer Agent" to represent the remote/server agent that is responding to the request. The Agentic Profile Host for did:web DIDs can be any web server that hosts JSON documents.
The Agentic Profile protocol builds on JWT and a server generated challenge. The publicly available Agentic Profile DID document provides the public keys.
The above flow shows:
- The user, or client, agent has discovered the peer, or server, agent service URL and communication protocol (e.g. JSON-RPC) and makes an HTTPS request.
- The peer agent requires authentication and the Authorization HTTPS header has not been provided. The peer agent issues a 401 response with a challenge.
- The user agent uses its private key to create a JWT, which includes the server challenge, along with the agents DID.
- The peer agent uses the DID in the JWT to find the DID document with the user agents public key. The peer agent verifies the JWT to complete authentication.
- With authentication complete, the peer agent completes the request and provides the HTTPS response.
- Subsequent user agent HTTPS requests can reuse the JWT.
- Subsequent peer agent HTTPS responses can use a cache to quickly verify the JWT.
Modifications to the A2A client to support the Agentic Profile
I refactored the A2AClient and added a lower level JsonRpcClient for better separation of concerns.
type HttpHeaders = { [key: string]: string };
export interface AuthenticationHandler {
headers: () => HttpHeaders;
process401: (fetchResponse:Response) => Promise<boolean>,
onSuccess: () => Promise<void>
}
export interface JsonRpcClientOptions {
fetchImpl: typeof fetch,
authHandler?: AuthenticationHandler
}
export class JsonRpcClient {
private baseUrl: string;
private fetchImpl: typeof fetch;
private authHandler: AuthenticationHandler | undefined
/**
* Creates an instance of a JSON RPC client
* @param baseUrl The base URL of the A2A server endpoint.
* @param options Including custom fetch implementation (e.g., for Node.js environments without global fetch). Defaults to global fetch.
*/
constructor(baseUrl: string, { fetchImpl = fetch, authHandler }: JsonRpcClientOptions) {
// Ensure baseUrl doesn't end with a slash for consistency
this.baseUrl = baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl;
this.fetchImpl = fetchImpl;
this.authHandler = authHandler;
}
I added a generic authentication handler to the JsonRpcClient. The authentication handler can provide HTTP headers for the request, and provides a "process401" function to handle 401 responses. An authentication handler is passed to the modified A2AClient and then passed down to the JsonRpcClient.
Updates to the Command Line Interface
The main() of the CLI was modified to resolve an "agent" which could be a basic Agent Card (agent.json) or the full context with an Agentic Profile and an Agent Card.
const agentContext = await resolveAgent( peerAgentUrl as string );
displayAgentCard( agentContext );
const { agentCard } = agentContext;
const authHandler = await createAuthHandler( iam as string, userAgentDid as string );
const client = new A2AClient( agentCard.url, { authHandler } );
The authHandler is created from the did.json and keyring.json files in the users ~/.agentic/iam/<profile> directory. Those files were created from running "node scripts/create-global-agentic-profile" script.
async function createAuthHandler( iamProfile: string = "global-me", userAgentDid: string ) {
const myProfileAndKeyring = await loadProfileAndKeyring( join( os.homedir(), ".agentic", "iam", iamProfile ) );
const headers = {} as any;
const { documentId, fragmentId } = pruneFragmentId( userAgentDid );
const agentDid = documentId ? userAgentDid : myProfileAndKeyring.profile.id + fragmentId;
const authHandler = {
headers: () => headers,
process401: async (fetchResponse:Response) => {
const agenticChallenge = await fetchResponse.json();
if( agenticChallenge.type !== AGENTIC_CHALLENGE_TYPE )
throw new Error(`Unexpected 401 response ${agenticChallenge}`);
const authToken = await generateAuthToken({
agentDid,
agenticChallenge,
profileResolver: async (did:string) => {
const { documentId } = pruneFragmentId( did );
if( documentId !== myProfileAndKeyring.profile.id )
throw new Error(`Failed to resolve agentic profile for ${did}`);
return myProfileAndKeyring;
}
});
headers.Authorization = `Agentic ${authToken}`;
return true;
},
onSuccess: async () => {}
};
return authHandler;
}
Modifications to the A2A server to support the Agentic Profile
I revised the A2AServer to be an A2AService to more accurately represent what it does, and removed the Express setup code and added a routes() function in its place. The index.local.js uses this routes() function to add an A2A service to a complex Express app.
I added an AgentSessionResolver to the A2AService constructor:
export type AgentSessionResolver = ( req: Request, res: Response ) => Promise<ClientAgentSession | null>
This AgentSessionResolver is wired into the start of the endpoint() function:
endpoint(): RequestHandler {
return async (req: Request, res: Response, next: NextFunction) => {
const requestBody = req.body;
let taskId: string | undefined; // For error context
try {
// 0. Authenticate client agent
let agentSession: ClientAgentSession | null;
if( this.agentSessionResolver ) {
agentSession = await this.agentSessionResolver( req, res );
if( !agentSession )
return; // 401 response with challenge already issued
}
This code has the following results:
- If there is no AgentSessionResolver provided, then A2A requests are handled as normal with no authentication
- If the AgentSessionResolver is called and...
- there is no HTTP Authorization header, then a 401 response is issued with an agentic challenge, and no agent session is provided. The endpoint() handler returns immediately.
- The HTTP authorization header is present, and it is valid, then the AgentSession is provided and execution continues.
- The HTTP authorization header is present, and it is malformed or invalid in any way, then an Error is thrown by the agentSessionResolver() and handled by the Express middleware.
An implementation of the AgentSessionResolver is provided by the NPM package @agentic-profile/express-common and can be imported as follows:
import {
resolveAgentSession as agentSessionResolver
} from "@agentic-profile/express-common";
Summary
Adding agentic profile support to A2A was very straightforward, and opens the door to using A2A for consumer-to-business and business-to-business transactions which will massively increase the economic impact of AI agents. The working code is available on Github at https://github.com/agentic-profile/agentic-profile-express-a2a
If you have any questions or suggestions, or would like to contribute then please connect with me on LinkedIn
Enhancing A2A with the Agentic Profile
The Agentic Profile is a thin layer over A2A, MCP, and other HTTP protocols, and provides: - Globally unique - user and business scoped - agent identity - Decentralized authentication