Co-authored-by: George Weale <gweale@google.com> PiperOrigin-RevId: 963703932
12 KiB
AuthConfig and authenticated tools
A tool that calls a third-party API on the user's behalf declares an
AuthConfig. ADK pauses the run to collect the credential, then resumes the
same tool call once it arrives.
Introduction
A tool that reads someone's calendar, mailbox, or documents needs a credential belonging to that person. Only the end user can grant it, and granting it means leaving the agent: opening a consent screen and coming back with a redirect. That round trip cannot happen inside a tool call, so ADK models it as an interruption. The tool declares what it needs and returns a placeholder, and the invocation ends carrying a request for credentials. The application runs the consent flow and starts a new run with the answer, and ADK re-executes the tool call that was waiting.
Two classes describe what is needed, and AuthConfig pairs them:
AuthSchemesays how the API expects to be authenticated. It is a union ofSecuritySchemefromfastapi.openapi.models(APIKey,HTTPBase,OAuth2, and the rest),OpenIdConnectWithConfig, andCustomAuthScheme.AuthCredentialis the secret.auth_typepicks the shape (API_KEY,HTTP,OAUTH2,OPEN_ID_CONNECT,SERVICE_ACCOUNT) and the matching field (api_key,http,oauth2,service_account) holds it.
AuthenticatedFunctionTool, BaseAuthenticatedTool, and McpTool all take an
AuthConfig and delegate to CredentialManager. The auth request processor in
the LLM flow pauses the invocation and later resumes the waiting call, and a
BaseCredentialService remembers the credential between turns.
Get started
This agent has one tool that needs an OAuth2 access token. Running it prints the authorization URL, waits for you to paste the redirect you land on, and then finishes the original request.
import asyncio
from fastapi.openapi.models import OAuth2
from fastapi.openapi.models import OAuthFlowAuthorizationCode
from fastapi.openapi.models import OAuthFlows
from google.adk.agents import LlmAgent
from google.adk.apps import App
from google.adk.auth import AuthConfig
from google.adk.auth import AuthCredential
from google.adk.auth import AuthCredentialTypes
from google.adk.auth import OAuth2Auth
from google.adk.auth.credential_service.in_memory_credential_service import InMemoryCredentialService
from google.adk.runners import Runner
from google.adk.sessions import InMemorySessionService
from google.adk.tools.authenticated_function_tool import AuthenticatedFunctionTool
from google.genai import types
auth_config = AuthConfig(
auth_scheme=OAuth2(
flows=OAuthFlows(
authorizationCode=OAuthFlowAuthorizationCode(
authorizationUrl="https://provider.example.com/authorize",
tokenUrl="https://provider.example.com/token",
scopes={"documents.read": "Read your documents"},
)
)
),
raw_auth_credential=AuthCredential(
auth_type=AuthCredentialTypes.OAUTH2,
oauth2=OAuth2Auth(
client_id="YOUR_CLIENT_ID",
client_secret="YOUR_CLIENT_SECRET",
redirect_uri="http://localhost:8080/callback",
),
),
credential_key="documents_api",
)
def list_documents(folder: str, credential: AuthCredential) -> list[str]:
"""Lists the documents in a folder."""
access_token = credential.oauth2.access_token
# Call the provider's API with access_token here.
return [f"{folder}/report.pdf"]
agent = LlmAgent(
name="documents_agent",
instruction="Use list_documents to answer questions about the user's files.",
tools=[
AuthenticatedFunctionTool(func=list_documents, auth_config=auth_config)
],
)
runner = Runner(
app=App(name="documents_app", root_agent=agent),
session_service=InMemorySessionService(),
credential_service=InMemoryCredentialService(),
)
async def main():
session = await runner.session_service.create_session(
app_name="documents_app", user_id="user"
)
message = types.Content(
role="user", parts=[types.Part(text="What is in my reports folder?")]
)
while True:
auth_call = None
async for event in runner.run_async(
user_id="user", session_id=session.id, new_message=message
):
for function_call in event.get_function_calls():
if function_call.name == "adk_request_credential":
auth_call = function_call
if event.content and event.content.parts:
for part in event.content.parts:
if part.text:
print(part.text)
if auth_call is None:
break
# The run paused. Send the user through consent and hand back the redirect.
requested = auth_call.args["authConfig"]
oauth2 = requested["exchangedAuthCredential"]["oauth2"]
print("Open this URL:", oauth2["authUri"])
oauth2["authResponseUri"] = input("Paste the URL you landed on: ")
response = types.Part.from_function_response(
name="adk_request_credential", response=requested
)
response.function_response.id = auth_call.id
message = types.Content(role="user", parts=[response])
asyncio.run(main())
The credential parameter is supplied by the framework and hidden from the
model, so the model only sees folder. The adk web UI runs the consent step
for you; the loop above is what a custom client does instead.
How it works
Declaring that a tool needs credentials
AuthenticatedFunctionTool wraps a plain function; BaseAuthenticatedTool is
the class-based equivalent, where you implement _run_async_impl and receive
the credential as a keyword argument. Both ask a CredentialManager for a
credential first, and when there is none they request one and return
response_for_auth_required (default "Pending User Authorization.") instead
of running your code. A tool can also do this by hand, with
tool_context.request_credential and tool_context.get_auth_response. The
first needs a function_call_id, so it only works inside a tool; from an agent
callback use save_credential and load_credential.
The pause and resume
sequenceDiagram
actor User
participant App as Your app
participant Flow
participant Tool
participant CM as CredentialManager
Tool->>CM: get_auth_credential
CM-->>Tool: None
Tool->>Flow: request_credential
Flow-->>App: adk_request_credential, then the invocation ends
App->>User: authorization URL
User-->>App: redirect with the code
App->>Flow: FunctionResponse with the filled config
Flow->>Tool: credential stored, the waiting call re-runs
- The tool asks
CredentialManager.get_auth_credential. A raw credential that is already usable, an API key or an HTTP credential, is returned as is and nothing pauses. Otherwise it checks the credential service, then the auth response in session state, then whether the scheme is a client-credentials flow needing no user at all. For an authorization-code flow with nothing stored, it returnsNone. - The tool calls
request_credential.AuthHandler.generate_auth_requestbuilds the authorization URL for OAuth2 and OIDC schemes and writes it toexchanged_auth_credential.oauth2.auth_uri, with thestateand, whencode_challenge_methodis"S256", a PKCEcode_verifier. The config is parked inevent_actions.requested_auth_configs, keyed by the id of the tool call that is waiting. - The flow emits a separate event holding one long-running function call named
adk_request_credentialper request. Its arguments arefunctionCallId, the waiting tool call, andauthConfig, the config from step 2. Keys are camelCase because the config is dumped by alias. The flow then ends the invocation, which is what "pauses" the run. - Your application reads
authConfig.exchangedAuthCredential.oauth2.authUri, sends the user there, and collects the redirect. - You resume with a new run whose message is a user
Contentcontaining aFunctionResponsenamedadk_request_credential. Its response is the same config with the answer filled intoexchangedAuthCredential: eitherauthResponseUri, the full redirect URL including the code, or a readyaccessToken. - Before the next model call, the auth request processor matches the response
to its request, stores the credential under
temp:<credential_key>in session state — exchanging the authorization code for a token first, for OAuth2 and OIDC — and re-executes the tool call that was waiting.
Two details decide whether the resume works. The FunctionResponse id must be
the id of the adk_request_credential call, not of the tool call waiting on it;
that id travels separately, in functionCallId. And the resume must be the most
recent event with content and be authored by user, because that is the only
event the processor looks at.
Where the credential is stored
Step 6 writes to a temp:-prefixed state key. Temp state is ephemeral by
design: session services keep it for the current invocation and do not persist
it. On its own it unblocks the waiting tool call and nothing more, so the next
turn asks the user to consent again.
A credential service is what makes consent stick. Pass one to the runner, as the
example above does. CredentialManager then saves the exchanged credential
under credential_key and reloads it on later calls, refreshing an expired
OAuth2 token rather than prompting again. SessionStateCredentialService is the
alternative, keeping the credential in session state under the same key.
Configuration options
| Option | Type | Default | Description |
|---|---|---|---|
auth_scheme |
AuthScheme |
required | How the API authenticates. For an authorization-code flow it carries the authorization and token URLs and the scopes, which the authorization URL is built from. |
raw_auth_credential |
AuthCredential | None |
None |
What you configured, such as an OAuth client id and secret. Required for OAuth2 and OIDC schemes; for an API key or HTTP credential it is the credential itself, and no consent is needed. |
exchanged_auth_credential |
AuthCredential | None |
None |
The working copy ADK and the client fill in: the authorization URL and state on the way out, the redirect or access token on the way back. Leave it unset when constructing the config. |
credential_key |
str | None |
derived | The key the credential is stored under, scoped to the app and user. Left unset it is derived from a digest of the scheme and the raw credential — stable, but opaque, and it changes whenever either does. Set it explicitly. |
Limitations
- Experimental.
AuthenticatedFunctionTool,BaseAuthenticatedTool,CredentialManager, the credential services, and the credential exchangers are all experimental. They are on by default and warn once on first use, but their APIs may change. - The OAuth2 helpers need
authlib. Without it no authorization URL is generated and no code is exchanged for a token; the credential passes through unchanged and the client must run the OAuth flow itself. - Session state is not a secret store.
SessionStateCredentialServiceputs tokens wherever session state lives. AuthConfig.get_credential_key()is deprecated. Setcredential_key.