Skip to content

Commit cc74e83

Browse files
committed
chore: auth0 sample (Auth for Headless Agents)
1 parent 8a44cff commit cc74e83

File tree

10 files changed

+2314
-0
lines changed

10 files changed

+2314
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# Gemini
2+
GOOGLE_API_KEY=
3+
4+
# Auth0
5+
HR_AUTH0_DOMAIN=
6+
7+
# A2A Client -> HR AGENT (auth method: Client Credentials)
8+
A2A_CLIENT_AUTH0_CLIENT_ID=
9+
A2A_CLIENT_AUTH0_CLIENT_SECRET=
10+
11+
# HR AGENT -> HR API (auth method: CIBA)
12+
HR_AGENT_AUTH0_CLIENT_ID=
13+
HR_AGENT_AUTH0_CLIENT_SECRET=
14+
15+
HR_AGENT_AUTH0_AUDIENCE=https://staff0/agent
16+
HR_API_AUTH0_AUDIENCE=https://staff0/api
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
# Auth for Headless Agents
2+
3+
This sample demonstrates how headless agent's tools can leverage [Auth0's Client-Initiated Backchannel Authentication (CIBA) flow](https://auth0.com/docs/get-started/authentication-and-authorization-flow/client-initiated-backchannel-authentication-flow) to request user authorization via push notification and obtain tokens for accessing separate APIs.
4+
5+
Additionally, it shows agent-level authorization via the [OAuth 2.0 Client Credentials flow](https://auth0.com/docs/get-started/authentication-and-authorization-flow/client-credentials-flow).
6+
7+
## How It Works
8+
9+
Allows an A2A client to securely interact with an external HR agent owned by the fictional company Staff0 to verify whether the provided user data corresponds to an active employee.
10+
11+
With the authorization of the employee involved (via push notification), the Staff0 HR agent can access the company's internal HR API to retrieve employment details.
12+
13+
```mermaid
14+
sequenceDiagram
15+
participant Employee as John Doe
16+
participant A2AClient as A2A Client
17+
participant Auth0 as Auth0 (staff0.auth0.com)
18+
participant HR_Agent as Staff0 HR Agent
19+
participant HR_API as Staff0 HR API
20+
21+
A2AClient->>HR_Agent: Get A2A Agent Card
22+
HR_Agent-->>A2AClient: Agent Card
23+
A2AClient->>Auth0: Request access token (Client Credentials)
24+
Auth0-->>A2AClient: Access Token
25+
A2AClient->>HR_Agent: Does John Doe with email [email protected] work at Staff0? (Access Token)
26+
HR_Agent->>Auth0: Request access token (CIBA)
27+
Auth0->>Employee: Push notification to approve access
28+
Employee-->>Auth0: Approves access
29+
Auth0-->>HR_Agent: Access Token
30+
HR_Agent->>HR_API: Retrieve employment details (Access Token)
31+
HR_API-->>HR_Agent: Employment details
32+
HR_Agent-->>A2AClient: Yes, John Doe is an active employee.
33+
```
34+
35+
## Prerequisites
36+
37+
- Python 3.12 or higher
38+
- [UV](https://docs.astral.sh/uv/)
39+
- [Gemini API key](https://ai.google.dev/gemini-api/docs/api-key)
40+
- An [Auth0](https://auth0.com/) tenant with the following configuration:
41+
- **APIs**
42+
- HR API
43+
- Audience: `https://staff0/api`
44+
- Permissions: `read:employee`
45+
- HR Agent
46+
- Audience: `https://staff0/agent`
47+
- Permissions: `read:employee_status`
48+
- **Applications**
49+
- A2A Client
50+
- Grant Types: `Client Credentials`
51+
- APIs: `HR Agent` (enabled permissions: `read:employee_status`)
52+
- HR Agent
53+
- Grant Types: `Client Initiated Backchannel Authentication (CIBA)`
54+
- APIs: `Auth0 Management API` (enabled permissions: `read:users`)
55+
- Push Notifications using [Auth0 Guardian](https://auth0.com/docs/secure/multi-factor-authentication/auth0-guardian) must be `enabled`.
56+
- A test user enrolled in Guardian MFA.
57+
58+
## Running the Sample
59+
60+
1. Create a `.env` file by copying [.env.example](.env.example), and provide the required environment variable values.
61+
62+
2. Start HR Agent and HR API:
63+
64+
```bash
65+
uv run --prerelease=allow .
66+
```
67+
68+
3. Run the test client:
69+
```bash
70+
uv run --prerelease=allow test_client.py
71+
```
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import asyncio
2+
import click
3+
import json
4+
import logging
5+
import os
6+
import sys
7+
import uvicorn
8+
9+
from dotenv import load_dotenv
10+
load_dotenv()
11+
12+
from agent import HRAgent
13+
from agent_executor import HRAgentExecutor
14+
from api import hr_api
15+
from oauth2_middleware import OAuth2Middleware
16+
17+
from a2a.server.apps import A2AStarletteApplication
18+
from a2a.server.request_handlers import DefaultRequestHandler
19+
from a2a.server.tasks import InMemoryTaskStore
20+
from a2a.types import (
21+
AgentAuthentication,
22+
AgentCapabilities,
23+
AgentCard,
24+
AgentSkill,
25+
# ClientCredentialsOAuthFlow,
26+
# OAuth2SecurityScheme,
27+
# OAuthFlows,
28+
)
29+
30+
31+
logging.basicConfig(level=logging.INFO)
32+
logger = logging.getLogger()
33+
34+
35+
@click.command()
36+
@click.option('--host', default='0.0.0.0')
37+
@click.option('--port_agent', default=10050)
38+
@click.option('--port_api', default=10051)
39+
def main(host: str, port_agent: int, port_api: int):
40+
async def run_all():
41+
await asyncio.gather(
42+
start_agent(host, port_agent),
43+
start_api(host, port_api),
44+
)
45+
46+
asyncio.run(run_all())
47+
48+
49+
async def start_agent(host: str, port):
50+
agent_card = AgentCard(
51+
name='Staff0 HR Agent',
52+
description='This agent handles external verification requests about Staff0 employees made by third parties.',
53+
url=f'http://{host}:{port}/',
54+
version='0.1.0',
55+
defaultInputModes=HRAgent.SUPPORTED_CONTENT_TYPES,
56+
defaultOutputModes=HRAgent.SUPPORTED_CONTENT_TYPES,
57+
capabilities=AgentCapabilities(streaming=True),
58+
skills=[
59+
AgentSkill(
60+
id='is_active_employee',
61+
name='Check Employment Status Tool',
62+
description='Confirm whether a person is an active employee of the company.',
63+
tags=['employment status'],
64+
examples=[
65+
'Does John Doe with email [email protected] work at Staff0?'
66+
],
67+
)
68+
],
69+
authentication=AgentAuthentication(
70+
schemes=['oauth2'],
71+
credentials=json.dumps({
72+
'tokenUrl': f'https://{os.getenv("HR_AUTH0_DOMAIN")}/oauth/token',
73+
'scopes': {
74+
'read:employee_status': 'Allows confirming whether a person is an active employee of the company.'
75+
}
76+
}),
77+
),
78+
# securitySchemes={
79+
# 'oauth2_m2m_client': OAuth2SecurityScheme(
80+
# description='',
81+
# flows=OAuthFlows(
82+
# authorizationCode=ClientCredentialsOAuthFlow(
83+
# tokenUrl=f'https://{os.getenv("HR_AUTH0_DOMAIN")}/oauth/token',
84+
# scopes={
85+
# 'read:employee_status': 'Allows confirming whether a person is an active employee of the company.',
86+
# },
87+
# ),
88+
# ),
89+
# ),
90+
# },
91+
# security=[{
92+
# 'oauth2_m2m_client': [
93+
# 'read:employee_status',
94+
# ],
95+
# }],
96+
)
97+
98+
request_handler = DefaultRequestHandler(
99+
agent_executor=HRAgentExecutor(),
100+
task_store=InMemoryTaskStore(),
101+
)
102+
103+
server = A2AStarletteApplication(
104+
agent_card=agent_card, http_handler=request_handler
105+
)
106+
107+
app = server.build()
108+
app.add_middleware(OAuth2Middleware, agent_card=agent_card, public_paths=['/.well-known/agent.json'])
109+
110+
logger.info(f'Starting HR Agent server on {host}:{port}')
111+
await uvicorn.Server(uvicorn.Config(app=app, host=host, port=port)).serve()
112+
113+
114+
async def start_api(host: str, port):
115+
logger.info(f'Starting HR API server on {host}:{port}')
116+
await uvicorn.Server(uvicorn.Config(app=hr_api, host=host, port=port)).serve()
117+
118+
119+
# this ensures that `main()` runs when using `uv run .`
120+
if not hasattr(sys, '_called_from_uvicorn'):
121+
main()

0 commit comments

Comments
 (0)