Complexity: 🟨 Intermediate
This example demonstrates an end-to-end OAuth2-protected A2A workflow with NVIDIA NeMo Agent Toolkit functioning as both A2A client and server. The workflow performs mathematical calculations integrated with time queries and logical reasoning, with added OAuth2 authentication for secure per-user agent-to-agent communication:
- Protected A2A Server: NeMo Agent Toolkit calculator service requiring OAuth2 authentication
- OAuth2 A2A Client: NeMo Agent Toolkit math assistant with per-user OAuth2 credentials that connects to the protected A2A server
- Authorization Server: Keycloak authorization server for testing OAuth2-protected A2A communication
- Type: A2A Server (Resource Server) hosted by NeMo Agent Toolkit
- Authentication: OAuth2 with JWT validation
- Skills: Basic arithmetic operations (add, subtract, multiply, divide, compare)
- Type: Per-user A2A client workflow hosted by NeMo Agent Toolkit
- Authentication: OAuth2 authorization code flow to obtain JWT tokens for authentication
- Functionality: Connects to calculator server, local time operations, logic evaluator to answer math questions
This example is designed for development and testing. See Production Considerations for deployment guidance.
This example consists of three main components:
graph TB
subgraph Client["A2A Client (Math Assistant)"]
direction TB
MathAssistant["Math Assistant Workflow"]
A2AClientPlugin["A2A Client Plugin<br/>• Discovers agent card<br/>• Handles OAuth2 flow<br/>• Manages JWT tokens"]
MathAssistant --> A2AClientPlugin
end
subgraph Server["A2A Server (Calculator)"]
direction TB
Calculator["Calculator Workflow"]
OAuthMiddleware["OAuth Middleware<br/>• Validates JWT tokens<br/>• Checks scopes & audience<br/>• Caches JWKS"]
Calculator --> OAuthMiddleware
end
subgraph Auth["Authorization Server (Keycloak)"]
direction LR
AuthCore["Keycloak OAuth2 Server<br/>• Authenticates users<br/>• Issues JWT tokens<br/>• Provides JWKS endpoint"]
end
A2AClientPlugin -->|"① GET /.well-known/agent-card.json<br/>(Public - No Auth)"| Calculator
A2AClientPlugin -->|"② OAuth2 Authorization Flow<br/>(Browser-based)"| AuthCore
A2AClientPlugin -->|"③ A2A JSON-RPC Requests<br/>(Authorization: Bearer JWT)"| Calculator
OAuthMiddleware -.->|"Verify JWT<br/>(Fetch JWKS)"| AuthCore
style Client fill:#e1f5ff
style Server fill:#ffe1e1
style Auth fill:#e1ffe1
Components:
-
Math Assistant (Client)
- Per-user workflow using
per_user_react_agent - Each user gets isolated A2A client instance with separate OAuth2 credentials
- Uses A2A client plugin to connect to calculator
- Handles user authentication flow through browser
- Per-user workflow using
-
Calculator A2A Server (Resource Server)
- Protected A2A server requiring authentication
- Publishes agent card with security requirements
- Validates JWT tokens before processing requests
-
Keycloak (Authorization Server)
- Example OAuth2 server for testing OAuth2-protected A2A servers in NeMo Agent Toolkit
- Provides OAuth2 endpoints for token exchange by the A2A client
- Provides JWKS endpoint for token verification by the A2A server
Per-User Architecture: Each user identified by nat-session cookie gets their own:
- A2A client connection with isolated state
- OAuth2 authentication flow and tokens
- Independent calculator session
This example demonstrates the A2A protocol with OAuth 2.1 Authorization Code Flow:
sequenceDiagram
participant Client as Math Assistant<br/>(A2A Client)
participant Browser as User Browser
participant AuthServer as Keycloak<br/>(Auth Server)
participant Resource as Calculator<br/>(A2A Server)
Note over Client,Resource: 1. Discovery (Public - No Auth)
Client->>Resource: GET /.well-known/agent-card.json
Resource-->>Client: Agent card with security schemes<br/>{security: [{oauth2: [...]}], securitySchemes: {...}}
Client->>Client: Parse security requirements<br/>Discover OAuth2 endpoints
Note over Client,AuthServer: 2. OAuth Authorization (if required)
Client->>Browser: Open authorization URL<br/>(from agent card)
Browser->>AuthServer: GET authorization_endpoint<br/>(client_id, redirect_uri, scope)
AuthServer->>Browser: Login page
Browser->>AuthServer: User credentials
AuthServer->>Browser: Consent page<br/>(Request calculator_a2a_execute scope)
Browser->>AuthServer: User approves
Note over Client,AuthServer: 3. Token Exchange
AuthServer->>Browser: Redirect with authorization code
Browser->>Client: Authorization code
Client->>AuthServer: POST token_endpoint<br/>(code, client_secret)
AuthServer-->>Client: Access token (JWT)<br/>{scope: calculator_a2a_execute, ...}
Note over Client,Resource: 4. Authenticated A2A Request
Client->>Resource: POST / (JSON-RPC)<br/>Authorization: Bearer <JWT>
Resource->>AuthServer: Fetch JWKS (cached)
AuthServer-->>Resource: Public keys
Resource->>Resource: Verify JWT signature<br/>Validate issuer, scopes, expiration
Resource-->>Client: Calculator result
Key Steps (Per User Session):
- Agent card discovery - Client fetches public metadata to discover authentication requirements
- Dynamic authentication - Client initiates OAuth flow based on agent card security schemes
- Token acquisition - User authenticates through browser, client obtains JWT token
- Authenticated communication - Client includes token in A2A requests, server validates JWT
- Docker installed and running
- NeMo Agent Toolkit development environment set up
- No services running on ports 8080 or 10000
- NVIDIA API key
From the root directory of the NeMo Agent Toolkit library, install this example:
uv pip install -e examples/A2A/math_assistant_a2a_protectedSet your NVIDIA API key:
export NVIDIA_API_KEY=<YOUR_API_KEY># Start Keycloak
docker run -d --name keycloak \
-p 127.0.0.1:8080:8080 \
-e KC_BOOTSTRAP_ADMIN_USERNAME=admin \
-e KC_BOOTSTRAP_ADMIN_PASSWORD=admin \
quay.io/keycloak/keycloak:latest start-devWait for Keycloak to start (about 30-60 seconds). Check logs:
docker logs -f keycloakLook for: Listening on: http://0.0.0.0:8080
Access Keycloak: Open http://localhost:8080 in your browser
-
Log in to Keycloak Admin Console:
- Username:
admin - Password:
admin
- Username:
-
Verify you're in the
masterrealm (top-left dropdown) -
Create the
calculator_a2a_executescope (for the calculator agent):- Go to Client scopes (left sidebar)
- Click Create client scope
- Fill in:
- Name:
calculator_a2a_execute - Description:
Permission to execute calculator operations - Type:
Optional - Protocol:
openid-connect - Include in token scope:
On✅
- Name:
- Click Save
-
Add audience mapper to the scope:
You need to add an audience mapper to ensure the calculator URL is included in tokens.
Audience Mapper (adds calculator URL to audience claim)
- Click Configure a new mapper
- Select Audience mapper type
- Configure the mapper:
- Name:
calculator-audience - Included Client Audience: Leave blank
- Included Custom Audience:
http://localhost:10000 - Add to ID token:
Off - Add to access token:
On✅ - Add to token introspection:
On✅ (if available in your Keycloak version)
- Name:
- Click Save
This mapper ensures
http://localhost:10000is included in the token'saudclaim (required for JWT validation). -
Verify OpenID Discovery endpoint:
curl http://localhost:8080/realms/master/.well-known/openid-configuration | python3 -m json.toolYou should see the OAuth2 and OpenID Connect endpoints:
authorization_endpoint:http://localhost:8080/realms/master/protocol/openid-connect/authtoken_endpoint:http://localhost:8080/realms/master/protocol/openid-connect/tokenjwks_uri:http://localhost:8080/realms/master/protocol/openid-connect/certsintrospection_endpoint:http://localhost:8080/realms/master/protocol/openid-connect/token/introspect
Note: These endpoints use Keycloak's standard paths (
/protocol/openid-connect/*), not generic/oauth/*paths. The NeMo Agent Toolkit A2A client discovers these URLs automatically from the discovery endpoint.
You can register the client manually or use the dynamic client registration (DCR) feature. For testing, manual registration is used.
-
In Keycloak Admin Console, go to Clients (left sidebar)
-
Click Create client
-
General Settings:
- Client ID:
math-assistant-client - Client type:
OpenID Connect - Click Next
- Client ID:
-
Capability config:
- Client authentication:
On(confidential client) - Authorization:
Off - Authentication flow:
- ✓ Standard flow (authorization code)
- ✓ Direct access grants
- Click Next
- Client authentication:
-
Login settings:
- Valid redirect URIs:
http://localhost:8000/auth/redirect - Web origins:
http://localhost:8000 - Click Save
- Valid redirect URIs:
-
Add client scope if not added by default:
- Go to Client scopes tab
- Click Add client scope
- Select
calculator_a2a_execute - Choose Optional
- Click Add
-
Set Consent required:
- Go to Settings tab
- Toggle Consent required to
On(scroll down to the bottom of the page to see the setting) - Click Save
-
Get client credentials:
- Go to Credentials tab
- Copy the Client secret
- Note the Client ID:
math-assistant-client
# Terminal 1
nat a2a serve --config_file examples/A2A/math_assistant_a2a_protected/configs/config-server.ymlYou should see:
[INFO] OAuth2 token validation enabled for A2A server
[INFO] Starting A2A server 'Protected Calculator' at http://localhost:10000
Set the client id and client secret from Step 3 in the environment variables:
# Terminal 2
# Make sure environment variables are set
export CALCULATOR_CLIENT_ID="math-assistant-client"
export CALCULATOR_CLIENT_SECRET="<your-client-secret>"
nat run --config_file examples/A2A/math_assistant_a2a_protected/configs/config-client.yml \
--input "Is the product of 2 and 4 greater than the current hour of the day?"What should happen:
- Browser opens with Keycloak login page
- Log in with any user (or create one)
- Consent page appears requesting
calculator_a2a_executescope - Browser redirects back to
localhost:8000/auth/redirect - Workflow continues and calls the calculator
- Response returned successfully
Sample output:
Workflow Result:
['No, the product of 2 and 4 is not greater than the current hour of the day.']
--------------------------------------------------
The per-user architecture allows each user to have their own OAuth2 authentication. Test this with nat serve:
- Start the math assistant as a server:
# Terminal 2: Start the math assistant as a server
# Make sure environment variables are set
export CALCULATOR_CLIENT_ID="math-assistant-client"
export CALCULATOR_CLIENT_SECRET="<your-client-secret>"
nat serve --config_file examples/A2A/math_assistant_a2a_protected/configs/config-client.yml-
Start the UI by following the instructions in the Launching the UI documentation.
-
Connect to the UI at
http://localhost:3000 -
Enable WebSocket mode in the UI by toggling the WebSocket button on the top right corner of the UI.
:::important Per-user workflows are not supported in HTTP mode. You must use WebSocket mode to test multi-user support. :::
- Send a message to the agent by typing in the chat input:
Is the sum of 5 and 3 greater than the current hour of the day?
- The workflow will be instantiated for the user on the first message. The user will be authenticated and the workflow will be executed.
Workflow Result:
['Yes, the sum of 5 and 3 is greater than the current hour of the day.']
--------------------------------------------------
Expected behavior:
- Each new user session triggers its own OAuth2 authorization flow
- Different users authenticate independently with their own Keycloak credentials
- Each user maintains separate JWT tokens and workflow instances
Use the following checks to confirm each service is reachable. If you are running the services elsewhere, replace localhost with the appropriate host name and use https instead of http for public endpoints.
curl -sS http://localhost:8080/realms/master/.well-known/openid-configuration | python3 -m json.toolcurl -sS http://localhost:10000/.well-known/agent-card.json | python3 -m json.toolIf you started the math assistant with nat serve, verify the server is reachable:
curl -sS http://localhost:8000/openapi.json | python3 -m json.toolIf you prefer a quick HTTP status check, follow redirects:
curl -iL http://localhost:8000/If you started the UI, confirm it is serving content:
curl -i http://localhost:3000/To stop and remove Keycloak:
docker stop keycloak
docker rm keycloakTo restart with clean state:
docker rm -f keycloak
# Then run the start command againThis setup is for development and testing only. For production:
- Use HTTPS everywhere: Keycloak, redirect URIs, and A2A servers must all use TLS
- Secure credentials: Store client secrets in a secrets manager, rotate regularly, never commit to version control
- Configure short-lived tokens: Set short access token lifetime with refresh tokens for long sessions
- Use dedicated realms: Don't use
masterrealm; create separate realms per environment (dev, staging, prod)
- Math Assistant A2A - Unprotected A2A client example
- Currency Agent A2A - External A2A service integration