Authenticated tools

Declare auth requirements on tools and let the runner resolve API keys, OAuth2 tokens, and service accounts before dispatch.

A tool declares what the server expects (AuthScheme) plus what it starts with (a raw AuthCredential) in an AuthConfig. Before dispatching the tool, the runner resolves that config into a ready credential — from cache, by exchange, by refresh, or by pausing for interactive OAuth2 consent — and injects it into ToolContext::auth_credential. The data types are always available; the OAuth2 machinery lives behind feature = "auth".

The type layer

AuthScheme mirrors the OpenAPI 3 securityScheme subset adk-rs supports, plus a Custom escape hatch wired to the AuthProviderRegistry:

AuthScheme::ApiKey { location: ApiKeyLocation, name: String, description: Option<String> }
Static API key. ApiKeyLocation is Header, Query, or Cookie; name is the header/query/cookie key.
AuthScheme::Http { scheme: String, bearer_format: Option<String>, description: Option<String> }
HTTP auth: scheme is "basic" or "bearer".
AuthScheme::OAuth2 { flows: OAuthFlows, description: Option<String> }
OAuthFlows holds optional authorization_code, client_credentials, implicit, and password flow descriptors (each an OAuthFlow with authorization_url, token_url, refresh_url, scopes).
AuthScheme::OpenIdConnect { open_id_connect_url: String, scopes: Vec<String>, description: Option<String> }
OIDC via a discovery-document URL.
AuthScheme::Custom { tag: String, properties: serde_json::Value }
Vendor-specific scheme resolved by a custom BaseAuthProvider.

AuthCredential is the value side — a single envelope whose auth_type discriminator says which inner payload (api_key, http, oauth2, service_account) is populated. Constructors cover the common cases:

AuthCredential::api_key(value) / ::bearer(token) / ::basic(username, password)
Ready-to-use credentials — no exchange needed.
AuthCredential::oauth2(OAuth2Auth) / ::service_account(ServiceAccountAuth)
Raw credentials that need an exchange (or consent) before they carry an access_token.
AuthCredential::is_ready(&self) -> bool
True when a usable access value is present without further exchange.
AuthCredential::is_expired(&self, now_unix: i64) -> bool
True when an expiry is set and has passed, with a 60-second leeway.
AuthConfig::new(scheme).with_raw(cred).with_key(key)
Pair scheme + raw credential. with_key pins the cache key; otherwise resolve_credential_key() derives a stable SHA-256 digest from scheme + raw credential.

Credential storage

Resolved credentials persist through the CredentialService trait, keyed by (app_name, user_id, key) with load / save / delete. Two implementations ship in the crate: InMemoryCredentialService (volatile, process-local) and SessionStateCredentialService (stores under temp:-prefixed state keys, so credentials live no longer than the session). Register one on the runner with Runner::builder().credential_service(...).

The resolution pipeline

A tool opts in by returning Some(&AuthConfig) from DynTool::auth_config(). FunctionTool does not expose this hook — implement DynTool directly for bespoke authenticated tools, or use the OpenAPI toolset, whose RestApiTool carries an AuthConfig built from the spec's security schemes. During dispatch (after the confirmation gate, before run), the agent builds a CredentialManager and resolves:

  1. Validate the config (a raw_auth_credential is required).
  2. If the raw credential is_ready and not expired, hand it straight to the tool.
  3. Try the cache: credential_service.load(app, user, key); an expired cached credential is refreshed via the RefresherRegistry and saved back.
  4. OAuth2/OIDC authorization-code flow with no token yet → return NeedsUserConsent (the interactive pause below).
  5. Exchange: OAuth2Exchanger (auth-code + PKCE verifier, or client-credentials) or ServiceAccountExchanger (signed RS256 JWT traded for an access/ID token), then save.
  6. Custom-provider escape hatch via the AuthProviderRegistry.
  7. Otherwise Misconfigured(msg) — surfaced to the model as an {"error": ...} tool response.
CredentialManager::resolve(&self, app, user, credentials: Option<&dyn CredentialService>) -> Result<ResolveOutcome>
Runs the workflow. ResolveOutcome is Ready(AuthCredential), NeedsUserConsent(AuthConfig), or Misconfigured(String).
ToolContext::auth_credential: Option<AuthCredential>
Where a Ready credential lands before run is called. Read your token/key from here, never from the model-supplied args.

When resolution returns NeedsUserConsent, the tool is not run. The agent emits a FunctionResponse named adk_request_credential (REQUEST_CREDENTIAL_FUNCTION_NAME) carrying the pending AuthConfig, marks the call long-running, and pauses the invocation — the same suspend mechanics as tool confirmation. Your application walks the user through consent, then resubmits a FunctionResponse with the same call id, name adk_request_credential, and the AuthConfig with exchanged_auth_credential filled in. On the next invocation, AuthPreprocessor::process_event absorbs it: the credential is saved to the credential service and the original call id is returned in resumed_tool_call_ids, so the owning agent replays the deferred call with a ready credential. Ids prefixed _adk_toolset_auth_ (TOOLSET_AUTH_CREDENTIAL_ID_PREFIX) authorise an entire toolset before tool discovery.

For driving the browser leg yourself, CredentialManager exposes a hardened two-step API: begin_consent(&credentials) generates the authorization URL with a fresh PKCE S256 challenge and CSRF state (persisting both, keyed by an opaque flow_id), and complete_consent(app, user, flow_id, callback_state, callback_code, &credentials) validates the state in constant time, exchanges the code with the stored verifier, caches the result under the regular key, and deletes the single-use pending entry. AuthHandler underneath also rejects token endpoints that are neither https:// nor loopback — validation delegates to the crate-wide transport-security policy, and loopback http:// stays available for local testing.

Worked example: API key

A tool that reads its key from ToolContextrust
use adk_rs::auth::{ApiKeyLocation, AuthConfig, AuthCredential, AuthScheme};
use adk_rs::core::ToolContext;
use adk_rs::genai_types::FunctionDeclaration;
use adk_rs::tools::Tool; // re-export of core::DynTool
use serde_json::Value;

#[derive(Debug)]
struct WeatherApi {
    auth: AuthConfig,
}

impl WeatherApi {
    fn new(key: String) -> Self {
        Self {
            auth: AuthConfig::new(AuthScheme::ApiKey {
                location: ApiKeyLocation::Header,
                name: "X-API-Key".into(),
                description: None,
            })
            .with_raw(AuthCredential::api_key(key)),
        }
    }
}

#[async_trait::async_trait]
impl Tool for WeatherApi {
    fn name(&self) -> &str { "get_weather" }
    fn description(&self) -> &str { "Fetch the current weather" }
    fn auth_config(&self) -> Option<&AuthConfig> { Some(&self.auth) }
    fn declaration(&self) -> Option<FunctionDeclaration> {
        Some(FunctionDeclaration::new(self.name(), self.description()))
    }
    async fn run(&self, _args: Value, ctx: &mut ToolContext) -> adk_rs::Result<Value> {
        let key = ctx.auth_credential.as_ref()
            .and_then(|c| c.api_key.as_deref())
            .ok_or_else(|| adk_rs::Error::other("credential not resolved"))?;
        // ... call the API with `key` in the X-API-Key header ...
        Ok(serde_json::json!({ "ok": true }))
    }
}

OAuth2 sketch

Authorization-code config (consent happens via adk_request_credential)rust
use adk_rs::auth::{
    AuthConfig, AuthCredential, AuthScheme, InMemoryCredentialService,
    OAuth2Auth, OAuthFlow, OAuthFlows,
};

let cfg = AuthConfig::new(AuthScheme::OAuth2 {
    flows: OAuthFlows {
        authorization_code: Some(OAuthFlow {
            authorization_url: Some("https://provider/authorize".into()),
            token_url: "https://provider/token".into(),
            refresh_url: None,
            scopes: Default::default(),
        }),
        ..OAuthFlows::default()
    },
    description: None,
})
.with_raw(AuthCredential::oauth2(OAuth2Auth {
    client_id: "client".into(),
    client_secret: Some("secret".into()),
    ..OAuth2Auth::default()
}))
.with_key("provider-oauth");

let runner = Runner::builder()
    .app_name("app")
    .agent(agent) // a tool returning Some(&cfg) from auth_config()
    .session_service(svc)
    .credential_service(Arc::new(InMemoryCredentialService::new()))
    .build()?;
// First call to the tool pauses with adk_request_credential; resubmit
// the AuthConfig with exchanged_auth_credential set to resume. A
// client_credentials flow instead exchanges server-to-server with no
// pause, and refresh tokens are used automatically on expiry.