Skip to main content
Some LLM providers use OAuth for authentication instead of API keys. Goose supports OAuth device code flow for secure, user-friendly authentication.

OAuth in Goose

Goose’s OAuth implementation (crates/goose/src/providers/oauth.rs) supports:
  • Device Code Flow - User authenticates via browser
  • Token Caching - Tokens stored securely, reused across sessions
  • Automatic Refresh - Expired tokens refreshed automatically
  • PKCE - Secure authorization without client secrets

When to Use OAuth

Use OAuth authentication when:
  • Provider requires OAuth (e.g., Databricks)
  • You want SSO integration
  • API keys aren’t available or practical
  • Enterprise authentication is needed

OAuth Flow Overview

1. User initiates configuration

2. Goose requests authorization

3. Browser opens to provider's auth page

4. User logs in and approves

5. Provider redirects with auth code

6. Goose exchanges code for tokens

7. Tokens cached for future use

Implementing OAuth Providers

1. Define Provider with OAuth ConfigKey

In your ProviderDef implementation:
use goose::providers::base::{ProviderDef, ProviderMetadata, ConfigKey};

impl ProviderDef for MyOAuthProvider {
    type Provider = Self;
    
    fn metadata() -> ProviderMetadata {
        ProviderMetadata::new(
            "myoauthprovider",
            "My OAuth Provider",
            "Provider using OAuth authentication",
            "default-model",
            vec!["model-1", "model-2"],
            "https://provider.com/models",
            vec![
                // Mark this key as using OAuth
                ConfigKey::new_oauth(
                    "ACCESS_TOKEN",
                    true,   // required
                    true,   // secret
                    None,   // no default
                    true,   // primary
                ),
                // Other config keys
                ConfigKey::new(
                    "HOST",
                    true,
                    false,
                    Some("https://provider.com"),
                    false,
                ),
            ],
        )
    }
    
    // ... other methods
}

2. Implement configure_oauth Method

use goose::providers::base::Provider;
use goose::providers::oauth::get_oauth_token_async;

#[async_trait]
impl Provider for MyOAuthProvider {
    async fn configure_oauth(&self) -> Result<(), ProviderError> {
        // Get OAuth token
        let token = get_oauth_token_async(
            &self.host,                    // Authorization server
            &self.client_id,               // OAuth client ID
            "http://localhost:8080",       // Redirect URI
            &[                             // Requested scopes
                "api.read".to_string(),
                "api.write".to_string(),
            ],
        )
        .await
        .map_err(|e| ProviderError::ConfigError(
            format!("OAuth authentication failed: {}", e)
        ))?;
        
        // Store token securely (in keychain)
        self.store_token(&token)?;
        
        println!("OAuth authentication successful!");
        Ok(())
    }
    
    // ... other Provider methods
}

3. Use Token in Requests

impl MyOAuthProvider {
    async fn make_api_request(&self, request: Request) -> Result<Response> {
        // Get token (from cache or refresh if needed)
        let token = self.get_access_token().await?;
        
        let response = self.client
            .post(&self.api_endpoint)
            .header("Authorization", format!("Bearer {}", token))
            .json(&request)
            .send()
            .await?;
        
        Ok(response)
    }
    
    async fn get_access_token(&self) -> Result<String> {
        // Load from cache or refresh
        // The oauth module handles this automatically
        let token = get_oauth_token_async(
            &self.host,
            &self.client_id,
            "http://localhost:8080",
            &self.scopes,
        ).await?;
        
        Ok(token)
    }
}

OAuth Token Management

Token Storage

Tokens are stored in:
~/.config/goose/provider/oauth/
  └── {hash}.json
The hash is based on:
  • Host URL
  • Client ID
  • Requested scopes
This ensures tokens are isolated per configuration.

Token Structure

struct TokenData {
    /// Access token for API requests
    access_token: String,
    
    /// Refresh token for obtaining new access tokens
    refresh_token: Option<String>,
    
    /// When the access token expires
    expires_at: Option<DateTime<Utc>>,
}

Automatic Refresh

The OAuth module automatically:
  1. Checks if cached token exists
  2. Validates expiration time
  3. Refreshes if expired using refresh token
  4. Falls back to full OAuth flow if refresh fails
// This automatically handles refresh
let token = get_oauth_token_async(
    host,
    client_id,
    redirect_url,
    scopes,
).await?;

Complete Example: Databricks Provider

Databricks uses OAuth for authentication:
use goose::providers::base::*;
use goose::providers::oauth::get_oauth_token_async;
use async_trait::async_trait;

pub struct DatabricksProvider {
    host: String,
    client_id: String,
    scopes: Vec<String>,
    model_config: ModelConfig,
    client: reqwest::Client,
}

impl DatabricksProvider {
    const DEFAULT_CLIENT_ID: &'static str = "databricks-cli";
    const DEFAULT_SCOPES: &[&'static str] = &[
        "all-apis",
        "offline_access",
    ];
    
    pub fn new(
        host: String,
        model_config: ModelConfig,
    ) -> Self {
        Self {
            host,
            client_id: Self::DEFAULT_CLIENT_ID.to_string(),
            scopes: Self::DEFAULT_SCOPES
                .iter()
                .map(|s| s.to_string())
                .collect(),
            model_config,
            client: reqwest::Client::new(),
        }
    }
    
    async fn get_token(&self) -> Result<String, ProviderError> {
        get_oauth_token_async(
            &self.host,
            &self.client_id,
            "http://localhost:8020",
            &self.scopes,
        )
        .await
        .map_err(|e| ProviderError::ConfigError(
            format!("Failed to get OAuth token: {}", e)
        ))
    }
}

#[async_trait]
impl Provider for DatabricksProvider {
    fn get_name(&self) -> &str {
        "databricks"
    }
    
    fn get_model_config(&self) -> ModelConfig {
        self.model_config.clone()
    }
    
    async fn configure_oauth(&self) -> Result<(), ProviderError> {
        println!("Starting Databricks OAuth authentication...");
        println!("A browser window will open for authentication.");
        
        let token = self.get_token().await?;
        
        println!("\nAuthentication successful!");
        println!("Token cached for future use.");
        
        Ok(())
    }
    
    async fn stream(
        &self,
        model_config: &ModelConfig,
        session_id: &str,
        system: &str,
        messages: &[Message],
        tools: &[Tool],
    ) -> Result<MessageStream, ProviderError> {
        let token = self.get_token().await?;
        
        let request = self.build_request(
            model_config,
            system,
            messages,
            tools,
        )?;
        
        let response = self.client
            .post(&format!(
                "{}/serving-endpoints/{}/invocations",
                self.host,
                model_config.model_name
            ))
            .header("Authorization", format!("Bearer {}", token))
            .json(&request)
            .send()
            .await
            .map_err(|e| ProviderError::RequestFailed(
                format!("Request failed: {}", e)
            ))?;
        
        self.process_stream(response).await
    }
}

impl ProviderDef for DatabricksProvider {
    type Provider = Self;
    
    fn metadata() -> ProviderMetadata {
        ProviderMetadata::new(
            "databricks",
            "Databricks",
            "Databricks Model Serving with OAuth",
            "databricks-meta-llama-3-1-70b-instruct",
            vec![
                "databricks-meta-llama-3-1-70b-instruct",
                "databricks-meta-llama-3-1-405b-instruct",
            ],
            "https://docs.databricks.com/en/machine-learning/foundation-models/",
            vec![
                ConfigKey::new(
                    "HOST",
                    true,
                    false,
                    None,
                    true,
                ),
                ConfigKey::new_oauth(
                    "ACCESS_TOKEN",
                    true,
                    true,
                    None,
                    true,
                ),
            ],
        )
    }
    
    fn from_env(
        model_config: ModelConfig,
        _extensions: Vec<ExtensionConfig>,
    ) -> BoxFuture<'static, anyhow::Result<Self::Provider>> {
        Box::pin(async move {
            let host = std::env::var("DATABRICKS_HOST")
                .map_err(|_| anyhow::anyhow!(
                    "DATABRICKS_HOST environment variable not set"
                ))?;
            
            Ok(Self::new(host, model_config))
        })
    }
}

OAuth Configuration UI

When a user configures an OAuth provider:
goose configure --provider databricks
Goose:
  1. Detects OAuth requirement from ConfigKey::new_oauth()
  2. Calls provider.configure_oauth()
  3. Opens browser for authentication
  4. Displays success message
  5. Caches tokens for future use

Testing OAuth Providers

Manual Testing

# Set provider host
export DATABRICKS_HOST=https://your-workspace.cloud.databricks.com

# Configure (triggers OAuth flow)
goose configure --provider databricks

# Test API call
goose session
> Hello, test the connection

Integration Tests

Skip OAuth in tests by mocking:
#[tokio::test]
async fn test_databricks_provider() {
    // Mock token retrieval
    let provider = DatabricksProvider::new_with_token(
        "https://test.databricks.com".to_string(),
        "test-token".to_string(),
        ModelConfig::new_or_fail("test-model"),
    );
    
    // Test API calls with mocked token
    let result = provider.complete(
        &provider.get_model_config(),
        "test-session",
        "You are helpful",
        &[Message::user().with_text("Hello")],
        &[],
    ).await;
    
    assert!(result.is_ok());
}

Security Considerations

PKCE (Proof Key for Code Exchange)

Goose uses PKCE for secure OAuth:
// Generate code verifier
let verifier = nanoid::nanoid!(64);

// Create code challenge
let challenge = {
    let digest = sha2::Sha256::digest(verifier.as_bytes());
    base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(digest)
};

// Include in authorization request
params.push(("code_challenge", &challenge));
params.push(("code_challenge_method", "S256"));

Token Storage

Tokens are stored:
  • In user’s config directory
  • With restrictive file permissions
  • Hashed filename for privacy
  • Never logged or printed

State Parameter

CSRF protection using state parameter:
let state = nanoid::nanoid!(16);

// Sent in auth request
params.push(("state", &state));

// Validated in callback
if received_state != state {
    return Err("State mismatch - possible CSRF attack");
}

Advanced OAuth Features

Custom Scopes

impl MyProvider {
    fn get_required_scopes(&self) -> Vec<String> {
        vec![
            "read:models".to_string(),
            "write:completions".to_string(),
            "offline_access".to_string(),  // For refresh tokens
        ]
    }
}

Custom Client ID

impl ProviderDef for MyProvider {
    fn from_env(
        model_config: ModelConfig,
        _extensions: Vec<ExtensionConfig>,
    ) -> BoxFuture<'static, anyhow::Result<Self::Provider>> {
        Box::pin(async move {
            let client_id = std::env::var("MYPROVIDER_CLIENT_ID")
                .unwrap_or_else(|_| "default-client-id".to_string());
            
            Ok(Self::new(client_id, model_config))
        })
    }
}

Custom Redirect Port

let token = get_oauth_token_async(
    host,
    client_id,
    "http://localhost:9999",  // Custom port
    scopes,
).await?;
Note: Port 0 allows OS to assign an available port.

Troubleshooting

Browser Doesn’t Open

If browser fails to open automatically:
Please open this URL in your browser:
https://provider.com/oauth/authorize?client_id=...
User can copy/paste URL manually.

Token Refresh Fails

If refresh fails:
  • Falls back to full OAuth flow
  • User re-authenticates in browser
  • New tokens cached

Port Already in Use

If redirect port is occupied:
  • Use port 0 for auto-assignment
  • Or specify different port

Token Expiration

Tokens automatically refresh before expiration:
// Check expiration
if let Some(expires_at) = token.expires_at {
    if expires_at > Utc::now() {
        // Token still valid
        return Ok(token.access_token);
    }
}

// Token expired, refresh it
let new_token = refresh_token(&token.refresh_token).await?;

OAuth vs API Keys

Use OAuth when:
  • Provider requires it
  • Enterprise SSO needed
  • Temporary access desired
  • Fine-grained permissions required
Use API Keys when:
  • Simpler for users
  • Provider supports them
  • No enterprise requirements
  • Faster setup needed

Next Steps

Build docs developers (and LLMs) love