Skip to content

fix: include chain ID audience claim in JWT created by create-api-key#513

Merged
chrisli30 merged 2 commits into
stagingfrom
fix/create-api-key-audience-claim
Apr 9, 2026
Merged

fix: include chain ID audience claim in JWT created by create-api-key#513
chrisli30 merged 2 commits into
stagingfrom
fix/create-api-key-audience-claim

Conversation

@chrisli30
Copy link
Copy Markdown
Member

Summary

  • The verifier (aggregator/auth.go::verifyAuth, tightened in release: staging → main (event trigger fixes, fee classification, JWT API key, sentry logging) #509) requires the JWT to include an aud claim containing the chain ID string.
  • aggregator/key.go::CreateAdminKey was still building JWTs with only ExpiresAt, Issuer, Subject, Roles — no aud — so every key from create-api-key was rejected with "API key is invalid".
  • Dial the eth RPC inside CreateAdminKey to fetch the chain ID (NewAggregator does not run the lifecycle init that would populate agg.chainID) and add Audience: jwt.ClaimStrings{chainID.String()} to the claims.

Discovered while debugging AvaProtocol/ava-sdk-js#209 CI failures: test:core generates a fresh key per run via docker compose exec aggregator /ava create-api-key, and every authenticated test in tests/core/auth.test.ts was returning 16 UNAUTHENTICATED: User authentication error: API key is invalid.

Test plan

  • go build ./aggregator/... ./cmd/... clean
  • docker compose exec aggregator /ava create-api-key --role=admin --subject=0x... and use the resulting key against the dev aggregator
  • Re-run ava-sdk-js test:core against a :latest image built from this branch

🤖 Generated with Claude Code

The verifier (aggregator/auth.go::verifyAuth, tightened in #509) requires
the JWT to have an `aud` claim containing the chain ID string. But
`CreateAdminKey` was building the JWT with only ExpiresAt, Issuer,
Subject, and Roles. As a result, every key generated by the
`create-api-key` CLI was rejected with "API key is invalid" — breaking
ava-sdk-js's test:core CI which generates a fresh key per run.

Dial the eth RPC inside CreateAdminKey to fetch the chain ID (NewAggregator
does not run the lifecycle init that would populate agg.chainID), then
add `Audience: jwt.ClaimStrings{chainID.String()}` to the registered claims.

Discovered while debugging AvaProtocol/ava-sdk-js#209 CI failures.
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes create-api-key generated JWTs being rejected by the aggregator auth verifier by adding the required aud (audience) claim containing the chain ID string.

Changes:

  • Dial an Ethereum RPC inside CreateAdminKey to fetch the chain ID.
  • Populate jwt.RegisteredClaims.Audience with the chain ID string when generating admin API keys.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread aggregator/key.go Outdated
Comment on lines +54 to +60
rpcClient, err := ethclient.Dial(nodeConfig.EthHttpRpcUrl)
if err != nil {
return fmt.Errorf("failed to dial eth rpc to determine chainId for audience claim: %w", err)
}
defer rpcClient.Close()
chainID, err := rpcClient.ChainID(context.Background())
if err != nil {
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The audience chain ID should match what the verifier expects (r.chainID), which is derived from the smart wallet RPC (see aggregator/rpc_server.go where smartWalletChainID comes from agg.config.SmartWallet.EthRpcUrl). Fetching chain ID from nodeConfig.EthHttpRpcUrl can produce a different value in cross-chain configs (e.g. EigenLayer on Ethereum but SmartWallet on Base), causing freshly generated keys to still fail auth. Prefer using nodeConfig.SmartWallet.ChainID (already derived in config.NewConfig) or dial nodeConfig.SmartWallet.EthRpcUrl instead of EthHttpRpcUrl.

Copilot uses AI. Check for mistakes.
Comment thread aggregator/key.go
Comment on lines +49 to +62
// The verifier (aggregator/auth.go::verifyAuth) requires the JWT to have an
// `aud` claim containing the chain ID string. NewAggregator does not run
// the lifecycle init that populates agg.chainID, so dial the RPC directly
// and read it here. Without this claim, every key generated by
// `create-api-key` is rejected with "API key is invalid".
rpcClient, err := ethclient.Dial(nodeConfig.EthHttpRpcUrl)
if err != nil {
return fmt.Errorf("failed to dial eth rpc to determine chainId for audience claim: %w", err)
}
defer rpcClient.Close()
chainID, err := rpcClient.ChainID(context.Background())
if err != nil {
return fmt.Errorf("failed to fetch chainId for audience claim: %w", err)
}
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This introduces an extra RPC dial + ChainID call during create-api-key, but config.NewConfig already dials RPCs and stores the chain ID on nodeConfig.SmartWallet.ChainID. Avoiding the additional network round-trip (and dependency on EthHttpRpcUrl reachability) will make key generation faster and less failure-prone.

Copilot uses AI. Check for mistakes.
Comment thread aggregator/key.go Outdated
return fmt.Errorf("failed to dial eth rpc to determine chainId for audience claim: %w", err)
}
defer rpcClient.Close()
chainID, err := rpcClient.ChainID(context.Background())
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using context.Background() for the ChainID call can hang indefinitely if the RPC endpoint is slow/unreachable (common in CI). Consider using a context with timeout/cancellation (e.g., context.WithTimeout) for the chain ID fetch so create-api-key fails fast with a clear error.

Suggested change
chainID, err := rpcClient.ChainID(context.Background())
chainIDCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
chainID, err := rpcClient.ChainID(chainIDCtx)

Copilot uses AI. Check for mistakes.
Comment thread aggregator/key.go Outdated
claims := &auth.APIClaim{
RegisteredClaims: &jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour * 24 * 365 * 10)),
Issuer: "AvaProtocol",
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Issuer is hard-coded as "AvaProtocol" here. Since the project already defines auth.Issuer, using the constant avoids accidental drift if the issuer changes in the future.

Suggested change
Issuer: "AvaProtocol",
Issuer: auth.Issuer,

Copilot uses AI. Check for mistakes.
- Use nodeConfig.SmartWallet.ChainID directly instead of dialing the EigenLayer RPC. The verifier's r.chainID is sourced from the smart wallet RPC, so cross-chain configs (e.g. EigenLayer on Ethereum + SmartWallet on Base) would have failed with the previous approach.
- Drops the extra RPC round-trip and the context-timeout concern entirely.
- Use the auth.Issuer constant instead of the hard-coded "AvaProtocol" string.
@chrisli30 chrisli30 merged commit c0b1c80 into staging Apr 9, 2026
18 checks passed
@chrisli30 chrisli30 deleted the fix/create-api-key-audience-claim branch April 9, 2026 04:40
chrisli30 added a commit that referenced this pull request Apr 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants