cd /news/ai-agents/learning-notes-golang-authorization-… · home topics ai-agents article
[ARTICLE · art-76451] src=dev.to ↗ pub= topic=ai-agents verified=true sentiment=· neutral

[Learning Notes][Golang] Authorization Challenges in the AI Agent Era: What is ID-JAG and Why I Re-implemented It in Go

A developer re-implemented the ID-JAG (Identity Assertion JWT Authorization Grant) mechanism in Go as an MCP server, addressing authorization challenges for AI agents accessing internal systems. ID-JAG allows agents to act on behalf of users with narrow, short-lived permissions, reducing risks from prompt injection or hallucinations. The implementation is based on the IETF Internet-Draft and is already cited by the Model Context Protocol specification.

read11 min views1 publishedJul 28, 2026

In the past six months or so, connecting AI Agents directly to internal systems to help get things done is no longer news. However, if you think one step further: in "whose identity" should the Agent call those APIs? If the permissions it receives are as broad as a human's, once it is tricked into performing an operation it shouldn't, the consequences could be more severe than a human's accidental slip.

This is exactly the problem that ID-JAG (Identity Assertion JWT Authorization Grant) aims to solve. I recently organized the principles of this mechanism and re-implemented the MCP Server from the tutorial repo athenz-community/id-jag-the-hard-way using Go:

kkdai/id-jag-mcp

This article will introduce the following in order:

ID-JAG is an authorization mechanism that allows AI Agents to access protected resources on behalf of users, with the keyword being "on behalf of." Currently, it is still an IETF Internet-Draft and has not yet become an official RFC, but organizations like LY Corporation (on the Athenz authorization system) and Okta have already begun implementation, and the MCP (Model Context Protocol) specification has already cited this draft.

Traditional service-to-service authorization usually falls into two extremes: either the entire service shares a universal master key (API Key, Service Account), or the user's session or long-lived token is directly lent to the program. The former has too much power, while the latter lacks an audit trail; if leaked, an attacker can almost completely impersonate the user, and it is very difficult to detect. When AI Agents start deciding which tools to call and which internal APIs to connect to on their own, the risks of both approaches are amplified: an Agent might perform operations the user never intended due to prompt injection or hallucinations. If the Agent holds a master key, the consequence is that all company data is at its mercy. In real-world scenarios, there is often a multi-layered architecture where an "Orchestrator Agent calls a Sub-Agent," and the risk is passed all the way down.

What ID-JAG wants to achieve is: for every action an Agent takes, it must be able to prove that "this is a specific user, at a specific moment, authorizing me to do this specific thing," and this authorization scope should be as narrow as possible, with a validity period as short as possible. It is built on top of OAuth2 token exchange (RFC 8693), adding a layer of proof that "this token is derived from a human's identity assertion." This is also why it is listed on OAuth.net's Cross-App Access (XAA) page—this is precisely a new problem emerging in the Agent era.

Incidentally, ID-JAG also solves a very practical user experience problem: if an Agent had to pop up a browser window for the user to manually click "Agree" every time it needed to access a new service, the experience would quickly become exhausting, leading users to just agree to everything. ID-JAG consolidates the authorization action to the moment the user logs in via SSO. After that, when the Agent needs new access permissions, it uses the already issued identity assertion to exchange for a token with the authorization server, without requiring the user to pop up and click again.

I previously wrote about How to develop OAuth2 PKCE via Golang, which covered the implementation experience of LINE Login adopting PKCE. The problem solved in that article and the one ID-JAG solves are actually on two different levels. Comparing them makes it clearer what is new about ID-JAG.

PKCE solves the problem of "whether the client identity is trustworthy": for public clients like mobile apps that cannot safely store a client secret, the authorization code might be intercepted by a malicious app on the same phone during transmission. PKCE uses a code_verifier

/ code_challenge

one-time pair to ensure that even if the code is stolen, it cannot be exchanged for a token without the correct verifier. The entire problem occurs in a "single hop" between the user and the app in their hand.

ID-JAG solves the problem of "whether this non-human service identity is qualified to act on behalf of this person for this task," and it often spans several hops: User logs into IdP → AI Client Gateway → MCP Server → Final Resource Server. The caller at each hop is not the user themselves, yet each must prove they are "acting under authority." The original design of OAuth 2.0 was for "human user ↔ application" scenarios and does not directly support this multi-layer Agent chain delegation scenario. PKCE protects the integrity of a single authorization exchange; ID-JAG protects the minimum necessary permissions for every link in an entire authorization chain. The two do not conflict; they are mechanisms solving problems at different stages under the same broad architecture.

ID-JAG did not invent a new protocol out of thin air; instead, it combines two existing IETF standards. Understanding these two cornerstones is necessary to understand what the subsequent complete exchange flow is doing.

The first is RFC 8693 — OAuth 2.0 Token Exchange, which defines a general protocol for "exchanging one type of token for another." Conceptually, it's like going to a currency exchange to swap Yen for Taiwan Dollars, except here you are swapping security tokens. A token exchange request looks roughly like this:

POST /token
grant_type=urn:ietf:params:oauth:grant-type:token-exchange
subject_token=<Identity Assertion JWT>
subject_token_type=urn:ietf:params:oauth:token-type:jwt
requested_token_type=urn:ietf:params:oauth:token-type:access_token
scope=read:orders

The subject_token

contains the token representing the delegated identity, requested_token_type

specifies what type of token you want to exchange it for, and scope

can narrow down the permission range at the moment of exchange.

The second is RFC 7523 — JWT Bearer Grant, which allows a JWT itself to be used directly as an OAuth 2.0 authorization credential without having to go through an authorization code exchange first:

POST /token
grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer
assertion=<Signed Identity Assertion JWT>

After receiving it, the authorization server verifies the signature of this JWT using the issuer's (IdP) public key. If fields like audience

, scope

, and exp

are reasonable, it can directly issue an Access Token without secondary user consent—because the IdP has already endorsed this identity assertion.

The "Identity Assertion JWT" to be exchanged usually contains the following fields:

{
  "iss": "https://enterprise.idp.example.com/v1",
  "sub": "alice@company.com",
  "aud": "https://api.service.example.com",
  "exp": 1773839486,
  "iat": 1773825086,
  "jti": "abc123-7dc6-42ab-b326-uniqueid",
  "scope": "read:orders write:tickets"
}

sub

is the user being proxied, aud

is which service this assertion is intended for, scope

is the allowed permission range, and jti

is a unique ID used to prevent the same assertion from being replayed. ID-JAG uses this short-lived identity assertion and, through the exchange mechanisms defined in the two RFCs above, gradually exchanges it for the Access Token that the Agent can actually use to call the API.

Using the architecture of the id-jag-the-hard-way

tutorial repo as an example (using Athenz as the authorization server), the complete chain connects the previous two RFCs:

User logs into IdP (Keycloak)
   │ Obtains OIDC ID Token
   ▼
AI Client Gateway
   │ Uses RFC 8693 Token Exchange to swap ID Token for ID-JAG
   │ (grant_type=token-exchange, subject_token_type=id_token,
   │ requested_token_type=id-jag)
   ▼
Then uses RFC 7523 JWT Bearer to swap ID-JAG for a usable Athenz Access Token
   │ (grant_type=jwt-bearer, assertion=<ID-JAG>)
   ▼
AI Client calls MCP Server with this Access Token
   │
   ▼
After receiving the request, the MCP Server performs an RFC 8693 Token Exchange "itself"
   │ Swaps the received Access Token for a new Access Token with the "minimum scope needed for this tool"
   │ (This step uses the MCP Server's own mTLS service identity, not the user's credentials)
   ▼
Calls the final Resource Server with the downscoped Access Token

Throughout the chain, the token is exchanged more than once; it is exchanged every time it crosses a trust boundary, and the scope becomes narrower with each exchange. This design intentionally ensures that the "credential" for each segment of the path is different—the token held by the AI Client Gateway cannot be used directly to trick the final Resource Server because the MCP Server stage forces re-verification and re-issuance. The issued Access Token usually records both sub

(the proxied user) and act

(the identity of the Agent actually performing the operation), so downstream services can clearly see that "Alice performed this operation through a certain Agent," and the audit trail is not broken midway.

This is what I find to be the most beautiful part of the entire architectural design: least privilege is not just a principle written in a document; it is physically enforced by the token exchange mechanism.

Taking my re-implemented id-jag-mcp

as an example, it provides three tools:

Tool Corresponding Athenz Scope
get_k8s_docs
api:role.docs-getter
delete_k8s_doc
api:role.docs-deleter
post_k8s_doc
api:role.docs-poster

When the MCP Server receives a request, it does not directly forward the Access Token sent by the AI Client to the upstream API—it will always use its own mTLS certificate to perform a token exchange with Athenz ZTS for the "scope actually needed by this tool." Only the newly exchanged token will be used to call the upstream. Even if the token scope at the AI Client Gateway is broader (e.g., possessing both read and delete permissions), the MCP Server will only request the specific small piece of permission actually needed for each tool before forwarding.

In other words, no part of the entire system "happens" to have more power than its current task requires—this is not checked via code review or internal regulations, but is architecturally impossible to bypass.

The original MCP Server in id-jag-the-hard-way

(api_server/mcp/

) was written in TypeScript + Express and used a hand-coded JSON-RPC 2.0 protocol (without the official SDK). I wanted to confirm two things: first, if this token exchange architecture is implemented in a different language with a different MCP SDK, can the logic truly be replicated; second, how the official modelcontextprotocol/go-sdk actually performs.

The final implementation maintains the original core logic (same scope mapping, same mTLS token exchange flow) but replaces the protocol layer entirely with the official Go SDK. The mTLS client was custom-built (without depending on Athenz's official Go client library).

The code is at kkdai/id-jag-mcp (Apache 2.0 license). The project structure and separation of responsibilities are roughly:

cmd/id-jag-mcp/ Entry point: reads settings, assembles all components, starts HTTP server
internal/config/ Environment variable configuration reading
internal/athenz/ mTLS client + Athenz ZTS RFC 8693 token exchange
internal/tools/ Tool input types + shared upstream forwarding logic
internal/server/ MCP tool registration (official SDK) + REST shortcut routes + logging

First, clone the project and build the binary:

git clone https://github.com/kkdai/id-jag-mcp.git
cd id-jag-mcp
go build -o id-jag-mcp ./cmd/id-jag-mcp

To actually run it, you need to prepare mTLS certificates, a reachable Athenz ZTS, and an upstream API server. Configuration is done entirely through environment variables:

mkdir -p certs
cp /path/to/api-mcp.crt /path/to/api-mcp.key /path/to/ca.crt certs/

export UPSTREAM_BASE_URL=http://localhost:14443
export AUTHORIZATION_SERVER_URL=https://athenz-zts-server.athenz:4443/zts/v1

go run ./cmd/id-jag-mcp

After startup, in addition to the /mcp

endpoint for MCP client connections, it also provides REST shortcut routes corresponding to the three tools, making it easy to test directly with curl

without an MCP client:

curl -H "Authorization: Bearer $AT" http://localhost:8101/api/docs

curl -X DELETE -H "Authorization: Bearer $AT" http://localhost:8101/api/docs/5

curl -X POST -H "Authorization: Bearer $AT" -H "Content-Type: application/json" \
  -d '{"name":"doc1","content":"hello"}' \
  http://localhost:8101/api/docs

If you just want to confirm if the logic itself is written correctly, you don't need to actually set up an Athenz/Keycloak environment—testing uses httptest

to simulate ZTS and the upstream API throughout:

go build ./...
go vet ./...
go test ./...

For example, the tests for internal/athenz

will start a fake ZTS server to verify if the outgoing grant_type

, subject_token

, scope

, and audience

parameters are correct; the tests for internal/tools

verify that when each tool forwards to the upstream, it carries the "downscoped token after exchange" rather than the original one received. This way, you can confirm the entire token exchange logic is correct without actually connecting to Athenz.

The README (available in both Chinese and English) contains a complete list of environment variables and more details.

ID-JAG does not solve "whether this client is who it says it is" (that is a PKCE problem), but rather "whether this non-human service identity is qualified to represent a specific person to do this specific thing right now." What supports this architecture is not documentation constraints, but the chaining of RFC 8693 and RFC 7523 standards, ensuring every hop is forced to re-verify and re-downscope permissions.

If your AI Agents have started interacting with internal systems, this is an architecture worth taking the time to understand—and you don't necessarily have to copy Athenz's implementation. The key is to understand the core principle that "every hop must re-issue, and the scope must get narrower," and apply it to your own authorization server.

── more in #ai-agents 4 stories · sorted by recency
── more on @id-jag 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/learning-notes-golan…] indexed:0 read:11min 2026-07-28 ·