chore: import upstream snapshot with attribution
Continuous Integration / Pre-commit Linter (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.10) (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.11) (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.12) (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.13) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.10) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.11) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.12) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.13) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.14) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.10) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.11) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.12) (push) Has been cancelled
Copybara PR Handler / close-imported-pr (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.13) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.14) (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:25:13 +08:00
commit ec2b666284
2231 changed files with 491535 additions and 0 deletions
@@ -0,0 +1,149 @@
# OAuth2 Client Credentials Weather Agent
This sample demonstrates OAuth2 client credentials flow with ADK's `AuthenticatedFunctionTool` using a practical weather assistant agent.
## Overview
The OAuth2 client credentials grant type is used for server-to-server authentication where no user interaction is required. This demo shows:
- How to configure OAuth2 client credentials in ADK
- Using `AuthenticatedFunctionTool` for automatic token management
- Transparent authentication in a practical weather assistant
- Testing the OAuth2 client credentials implementation
## Architecture
```
[WeatherAssistant] -> [AuthenticatedFunctionTool] -> [OAuth2CredentialExchanger] -> [OAuth2 Server] -> [Weather API]
```
1. **WeatherAssistant** calls weather tool when user asks for weather data
1. **AuthenticatedFunctionTool** automatically handles OAuth2 flow
1. **OAuth2CredentialExchanger** exchanges client credentials for access token
1. **Authenticated requests** are made to weather API
## Files
### `agent.py` - WeatherAssistant Agent
Weather assistant agent that demonstrates OAuth2 client credentials flow transparently:
- **OAuth2 Configuration**: Client credentials setup with token URL and scopes
- **Weather Tool**: Single `get_weather_data` tool for fetching weather information
- **Agent Definition**: ADK LLM agent focused on providing weather information
**Key Features:**
- Automatic token exchange using client ID and secret
- Bearer token authentication
- Transparent OAuth2 handling (invisible to the model)
- Practical use case demonstrating machine-to-machine authentication
### `main.py` - CLI Interface
Command-line interface for running the WeatherAssistant agent:
```bash
# Ask for weather
python contributing/samples/oauth2_client_credentials/main.py "What's the weather in Tokyo?"
```
**Requirements:**
- LLM API key (Google AI or Vertex AI)
- OAuth2 test server running
### `oauth2_test_server.py` - Local OAuth2 Server
Mock OAuth2 server for testing the client credentials flow:
```bash
python contributing/samples/oauth2_client_credentials/oauth2_test_server.py
```
**Features:**
- OIDC discovery endpoint (`/.well-known/openid_configuration`)
- Client credentials token exchange (`/token`)
- Protected weather API (`/api/weather`)
- Supports both `authorization_code` and `client_credentials` grant types
- Test credentials: `client_id="test_client"`, `client_secret="test_secret"`
**Endpoints:**
- `GET /.well-known/openid_configuration` - OIDC discovery
- `POST /token` - Token exchange
- `GET /api/weather` - Weather API (requires Bearer token)
- `GET /` - Server info
## Quick Start
1. **Start the OAuth2 server:**
```bash
python contributing/samples/oauth2_client_credentials/oauth2_test_server.py &
```
1. Create a `.env` file in the project root with your API credentials:
```bash
# Choose Model Backend: 0 -> ML Dev, 1 -> Vertex
GOOGLE_GENAI_USE_ENTERPRISE=1
# ML Dev backend config
GOOGLE_API_KEY=your_google_api_key_here
# Vertex backend config
GOOGLE_CLOUD_PROJECT=your_project_id
GOOGLE_CLOUD_LOCATION=us-central1
```
3. **Run the agent:**
```bash
# Ask for weather
python contributing/samples/oauth2_client_credentials/main.py "What's the weather in Tokyo?"
```
1. **Interactive demo (use ADK commands):**
```bash
# Interactive CLI
adk run contributing/samples/oauth2_client_credentials
# Interactive web UI
adk web contributing/samples
```
## OAuth2 Configuration
The agent uses these OAuth2 settings (configured in `agent.py`):
```python
flows = OAuthFlows(
clientCredentials=OAuthFlowClientCredentials(
tokenUrl="http://localhost:8000/token",
scopes={
"read": "Read access to weather data",
"write": "Write access for data updates",
"admin": "Administrative access",
},
)
)
raw_credential = AuthCredential(
auth_type=AuthCredentialTypes.OAUTH2,
oauth2=OAuth2Auth(
client_id="test_client",
client_secret="test_secret",
),
)
```
## Authentication Flow
1. **Weather Request**: User asks WeatherAssistant for weather information
1. **Tool Invocation**: Agent calls `get_weather_data` authenticated function tool
1. **Credential Loading**: CredentialManager loads OAuth2 configuration
1. **Token Exchange**: OAuth2CredentialExchanger uses client credentials to get access token
1. **Request Enhancement**: AuthenticatedFunctionTool adds `Authorization: Bearer <token>` header
1. **API Call**: Weather API accessed with valid token
1. **Response**: Weather data returned to user
@@ -0,0 +1,15 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from . import agent
@@ -0,0 +1,134 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Weather Assistant Agent.
This agent provides weather information for cities worldwide.
It demonstrates OAuth2 client credentials flow transparently
through AuthenticatedFunctionTool usage.
"""
from fastapi.openapi.models import OAuth2
from fastapi.openapi.models import OAuthFlowClientCredentials
from fastapi.openapi.models import OAuthFlows
from google.adk.agents.llm_agent import Agent
from google.adk.auth.auth_credential import AuthCredential
from google.adk.auth.auth_credential import AuthCredentialTypes
from google.adk.auth.auth_credential import OAuth2Auth
from google.adk.auth.auth_tool import AuthConfig
from google.adk.tools.authenticated_function_tool import AuthenticatedFunctionTool
import requests
# OAuth2 configuration for weather API access
def create_auth_config() -> AuthConfig:
"""Create OAuth2 auth configuration for weather API."""
# Define OAuth2 scheme with client credentials flow
flows = OAuthFlows(
clientCredentials=OAuthFlowClientCredentials(
tokenUrl="http://localhost:8080/token",
scopes={
"read": "Read access to weather data",
"write": "Write access for data updates",
"admin": "Administrative access",
},
)
)
auth_scheme = OAuth2(flows=flows)
# Create credential with client ID and secret
raw_credential = AuthCredential(
auth_type=AuthCredentialTypes.OAUTH2,
oauth2=OAuth2Auth(
client_id="test_client",
client_secret="test_secret",
),
)
return AuthConfig(
auth_scheme=auth_scheme,
raw_auth_credential=raw_credential,
credential_key="weather_api_client",
)
def get_weather_data(city: str = "San Francisco", credential=None) -> str:
"""Get current weather data for a specified city.
Args:
city: City name to get weather for
credential: API credential (automatically injected by AuthenticatedFunctionTool)
Returns:
Current weather information for the city.
"""
try:
# Use the credential to make authenticated requests to weather API
headers = {}
if credential and credential.oauth2 and credential.oauth2.access_token:
headers["Authorization"] = f"Bearer {credential.oauth2.access_token}"
# Call weather API endpoint
params = {"city": city, "units": "metric"}
response = requests.get(
"http://localhost:8080/api/weather",
headers=headers,
params=params,
timeout=10,
)
if response.status_code == 200:
data = response.json()
result = f"🌤️ Weather for {city}:\n"
result += f"Temperature: {data.get('temperature', 'N/A')}°C\n"
result += f"Condition: {data.get('condition', 'N/A')}\n"
result += f"Humidity: {data.get('humidity', 'N/A')}%\n"
result += f"Wind Speed: {data.get('wind_speed', 'N/A')} km/h\n"
result += f"Last Updated: {data.get('timestamp', 'N/A')}\n"
return result
else:
return (
f"❌ Failed to get weather data: {response.status_code} -"
f" {response.text}"
)
except Exception as e:
return f"❌ Error getting weather data: {str(e)}"
# Create the weather assistant agent
root_agent = Agent(
name="WeatherAssistant",
description=(
"Weather assistant that provides current weather information for cities"
" worldwide."
),
model="gemini-2.5-pro",
instruction=(
"You are a helpful Weather Assistant that provides current weather"
" information for any city worldwide.\n\nWhen users ask for weather:\n"
" Ask for the city name if not provided\n• Provide temperature in"
" Celsius\n• Include helpful details like humidity, wind speed, and"
" conditions\n• Be friendly and conversational about the weather\n\nIf"
" there are any issues getting weather data, apologize and suggest"
" trying again or checking for a different city name."
),
tools=[
AuthenticatedFunctionTool(
func=get_weather_data, auth_config=create_auth_config()
),
],
)
@@ -0,0 +1,152 @@
"""WeatherAssistant Agent main script.
This script demonstrates OAuth2 client credentials flow using a practical
weather assistant agent with AuthenticatedFunctionTool.
"""
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import argparse
import asyncio
import logging
import sys
import time
import agent
from dotenv import load_dotenv
from google.adk.cli.utils import logs
from google.adk.runners import InMemoryRunner
APP_NAME = "weather_assistant_app"
USER_ID = "weather_user"
logs.setup_adk_logger(level=logging.INFO)
def process_arguments():
"""Parses command-line arguments."""
parser = argparse.ArgumentParser(
description=(
"WeatherAssistant Agent - demonstrates OAuth2 client credentials"
" authentication transparently through weather queries."
),
epilog=(
"Example usage:\n\tpython main.py"
' "What\'s the weather in Tokyo?"\n\n'
"For interactive usage, use ADK commands:\n"
"\tadk run .\n"
"\tadk web .\n"
),
formatter_class=argparse.RawTextHelpFormatter,
)
parser.add_argument(
"message",
type=str,
help=(
"Ask the weather assistant a question or request weather information."
),
)
return parser.parse_args()
async def process_message(runner, session_id, message):
"""Process a single message with the weather assistant."""
print(f"🌤️ Weather Assistant: ")
response = await call_agent_async(runner, USER_ID, session_id, message)
print(f"{response}\n")
async def call_agent_async(runner, user_id, session_id, prompt):
"""Helper function to call agent asynchronously."""
from google.adk.agents.run_config import RunConfig
from google.genai import types
content = types.Content(
role="user", parts=[types.Part.from_text(text=prompt)]
)
final_response_text = ""
async for event in runner.run_async(
user_id=user_id,
session_id=session_id,
new_message=content,
run_config=RunConfig(save_input_blobs_as_artifacts=False),
):
if event.content and event.content.parts:
if text := "".join(part.text or "" for part in event.content.parts):
if event.author != "user":
final_response_text += text
return final_response_text
async def main():
"""Main function."""
# Load environment variables from .env file
load_dotenv()
args = process_arguments()
print("🌤️ WeatherAssistant Agent")
print("=" * 40)
print("Ask me about weather in any city around the world!")
print("(OAuth2 client credentials authentication happens transparently)\n")
# Create runner and session
runner = InMemoryRunner(
agent=agent.root_agent,
app_name=APP_NAME,
)
session = await runner.session_service.create_session(
app_name=APP_NAME, user_id=USER_ID
)
try:
await process_message(runner, session.id, args.message)
except Exception as e:
print(f"❌ Error: {e}", file=sys.stderr)
return 1
return 0
if __name__ == "__main__":
start_time = time.time()
print(
"⏰ Started at"
f" {time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(start_time))}"
)
print("-" * 50)
try:
exit_code = asyncio.run(main())
except KeyboardInterrupt:
print("\n⏹️ Interrupted by user")
exit_code = 1
end_time = time.time()
print("-" * 50)
print(
"⏰ Finished at"
f" {time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(end_time))}"
)
print(f"⌛ Total execution time: {end_time - start_time:.2f} seconds")
sys.exit(exit_code)
@@ -0,0 +1,350 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
Weather API OAuth2 Test Server
A simple FastAPI server that implements OAuth2 flows for weather API testing:
- Client Credentials Flow
- Authorization Code Flow
Usage:
python oauth2_test_server.py
Endpoints:
GET /auth - Authorization endpoint (auth code flow)
POST /token - Token endpoint (both flows)
GET /.well-known/openid_configuration - OpenID Connect discovery
GET /api/weather - Weather API (requires Bearer token)
"""
import secrets
import time
from typing import Dict
from typing import Optional
from fastapi import FastAPI
from fastapi import Form
from fastapi import HTTPException
from fastapi import Query
from fastapi import Request
from fastapi import status
from fastapi.responses import HTMLResponse
from fastapi.responses import RedirectResponse
from pydantic import BaseModel
app = FastAPI(title="Weather API OAuth2 Server", version="1.0.0")
# In-memory storage (for testing only)
clients = {
"test_client": {
"client_secret": "test_secret",
"redirect_uris": [
"http://localhost:8080/callback",
"urn:ietf:wg:oauth:2.0:oob",
],
"scopes": ["read", "write", "admin"],
}
}
authorization_codes = {} # code -> {client_id, redirect_uri, scope, expires_at}
access_tokens = {} # token -> {client_id, scope, expires_at, token_type}
class TokenResponse(BaseModel):
access_token: str
token_type: str = "Bearer"
expires_in: int = 3600
refresh_token: Optional[str] = None
scope: Optional[str] = None
@app.get("/.well-known/openid_configuration")
async def openid_configuration():
"""OpenID Connect Discovery endpoint."""
return {
"issuer": "http://localhost:8080",
"authorization_endpoint": "http://localhost:8080/auth",
"token_endpoint": "http://localhost:8080/token",
"userinfo_endpoint": "http://localhost:8080/userinfo",
"revocation_endpoint": "http://localhost:8080/revoke",
"scopes_supported": ["openid", "read", "write", "admin"],
"response_types_supported": ["code"],
"grant_types_supported": ["authorization_code", "client_credentials"],
"token_endpoint_auth_methods_supported": [
"client_secret_basic",
"client_secret_post",
],
"subject_types_supported": ["public"],
}
@app.get("/auth")
async def authorize(
response_type: str = Query(...),
client_id: str = Query(...),
redirect_uri: str = Query(...),
scope: str = Query(default="read"),
state: str = Query(default=""),
):
"""Authorization endpoint for OAuth2 authorization code flow."""
# Validate client
if client_id not in clients:
raise HTTPException(status_code=400, detail="Invalid client_id")
client = clients[client_id]
if redirect_uri not in client["redirect_uris"]:
raise HTTPException(status_code=400, detail="Invalid redirect_uri")
if response_type != "code":
raise HTTPException(status_code=400, detail="Unsupported response_type")
# Generate authorization code
auth_code = secrets.token_urlsafe(32)
authorization_codes[auth_code] = {
"client_id": client_id,
"redirect_uri": redirect_uri,
"scope": scope,
"expires_at": time.time() + 600, # 10 minutes
}
# Simulate user consent - in real implementation, this would show a consent form
params = f"code={auth_code}"
if state:
params += f"&state={state}"
return RedirectResponse(url=f"{redirect_uri}?{params}")
@app.post("/token")
async def token_endpoint(
request: Request,
grant_type: str = Form(...),
client_id: str = Form(default=None),
client_secret: str = Form(default=None),
code: str = Form(default=None),
redirect_uri: str = Form(default=None),
scope: str = Form(default="read"),
):
"""Token endpoint for both client credentials and authorization code flows."""
# Support both HTTP Basic auth and form-based client authentication
auth_header = request.headers.get("Authorization")
if auth_header and auth_header.startswith("Basic "):
# HTTP Basic authentication
import base64
try:
encoded_credentials = auth_header[6:] # Remove "Basic " prefix
decoded = base64.b64decode(encoded_credentials).decode("utf-8")
basic_client_id, basic_client_secret = decoded.split(":", 1)
client_id = client_id or basic_client_id
client_secret = client_secret or basic_client_secret
except Exception:
raise HTTPException(
status_code=401, detail="Invalid authorization header"
)
if not client_id or not client_secret:
raise HTTPException(status_code=400, detail="Client credentials required")
# Validate client credentials
if client_id not in clients:
raise HTTPException(status_code=401, detail="Invalid client")
client = clients[client_id]
if client["client_secret"] != client_secret:
raise HTTPException(status_code=401, detail="Invalid client credentials")
if grant_type == "client_credentials":
return await handle_client_credentials(client_id, scope)
elif grant_type == "authorization_code":
return await handle_authorization_code(client_id, code, redirect_uri, scope)
else:
raise HTTPException(status_code=400, detail="Unsupported grant_type")
async def handle_client_credentials(
client_id: str, scope: str
) -> TokenResponse:
"""Handle client credentials flow."""
# Generate access token
access_token = secrets.token_urlsafe(32)
expires_at = time.time() + 3600 # 1 hour
# Store token
access_tokens[access_token] = {
"client_id": client_id,
"scope": scope,
"expires_at": expires_at,
"token_type": "Bearer",
}
return TokenResponse(
access_token=access_token,
token_type="Bearer",
expires_in=3600,
scope=scope,
)
async def handle_authorization_code(
client_id: str, code: str, redirect_uri: str, scope: str
) -> TokenResponse:
"""Handle authorization code flow."""
if not code:
raise HTTPException(status_code=400, detail="Missing authorization code")
if code not in authorization_codes:
raise HTTPException(status_code=400, detail="Invalid authorization code")
auth_data = authorization_codes[code]
# Validate authorization code
if time.time() > auth_data["expires_at"]:
del authorization_codes[code]
raise HTTPException(status_code=400, detail="Authorization code expired")
if auth_data["client_id"] != client_id:
raise HTTPException(status_code=400, detail="Client mismatch")
if redirect_uri and auth_data["redirect_uri"] != redirect_uri:
raise HTTPException(status_code=400, detail="Redirect URI mismatch")
# Generate tokens
access_token = secrets.token_urlsafe(32)
refresh_token = secrets.token_urlsafe(32)
expires_at = time.time() + 3600 # 1 hour
# Store token
access_tokens[access_token] = {
"client_id": client_id,
"scope": auth_data["scope"],
"expires_at": expires_at,
"token_type": "Bearer",
}
# Clean up authorization code (one-time use)
del authorization_codes[code]
return TokenResponse(
access_token=access_token,
token_type="Bearer",
expires_in=3600,
refresh_token=refresh_token,
scope=auth_data["scope"],
)
@app.get("/api/weather")
async def get_weather(
request: Request, city: str = "San Francisco", units: str = "metric"
):
"""Weather API endpoint that returns weather data for a city."""
# Check authentication
auth_header = request.headers.get("Authorization")
if not auth_header or not auth_header.startswith("Bearer "):
raise HTTPException(
status_code=401, detail="Missing or invalid authorization header"
)
token = auth_header[7:] # Remove "Bearer " prefix
if token not in access_tokens:
raise HTTPException(status_code=401, detail="Invalid access token")
token_data = access_tokens[token]
if time.time() > token_data["expires_at"]:
del access_tokens[token]
raise HTTPException(status_code=401, detail="Access token expired")
# Return weather data (simulated)
from datetime import datetime
import random
conditions = ["Sunny", "Partly Cloudy", "Cloudy", "Light Rain", "Clear"]
weather_data = {
"city": city,
"temperature": random.randint(15, 30),
"condition": random.choice(conditions),
"humidity": random.randint(40, 80),
"wind_speed": random.randint(5, 25),
"timestamp": datetime.now().isoformat(),
"units": units,
"api_client": token_data["client_id"],
}
return weather_data
@app.get("/")
async def root():
"""Root endpoint with server information."""
return HTMLResponse("""
<html>
<head><title>Weather API OAuth2 Server</title></head>
<body>
<h1>Weather API OAuth2 Server</h1>
<h2>Available Endpoints:</h2>
<ul>
<li><strong>GET /auth</strong> - Authorization endpoint</li>
<li><strong>POST /token</strong> - Token endpoint</li>
<li><strong>GET /.well-known/openid_configuration</strong> - Discovery</li>
<li><strong>GET /api/weather</strong> - Weather API (requires Bearer token)</li>
</ul>
<h2>Test Client Credentials:</h2>
<ul>
<li><strong>Client ID:</strong> test_client</li>
<li><strong>Client Secret:</strong> test_secret</li>
<li><strong>Scopes:</strong> read, write, admin</li>
</ul>
<h2>Example cURL Commands:</h2>
<h3>Client Credentials Flow:</h3>
<pre>
curl -X POST http://localhost:8080/token \\
-d "grant_type=client_credentials" \\
-d "client_id=test_client" \\
-d "client_secret=test_secret" \\
-d "scope=read write"
</pre>
<h3>Test Weather API:</h3>
<pre>
curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \\
"http://localhost:8080/api/weather?city=Tokyo"
</pre>
</body>
</html>
""")
if __name__ == "__main__":
import uvicorn
print("🌤️ Starting Weather API OAuth2 Server...")
print("📖 Documentation: http://localhost:8080/docs")
print("🏠 Server Info: http://localhost:8080")
print(
'🔧 Test with: curl -H "Authorization: Bearer TOKEN"'
' "http://localhost:8080/api/weather?city=Tokyo"'
)
uvicorn.run(app, host="0.0.0.0", port=8080, log_level="info")