---
title: "Adding OAuth to Your Tool Plugin"
---

![b0e673ba3e339b31ac36dc3cd004df04787bcaa64bb6d2cac6feb7152b7b515f.png](/images/b0e673ba3e339b31ac36dc3cd004df04787bcaa64bb6d2cac6feb7152b7b515f.png)

This guide teachs you how to build plugins that support [OAuth](https://oauth.net/2/). OAuth is a better way to authorize tool plugins that need to access user data from third-party services, like Gmail or GitHub. Instead of requiring the user to manually enter API keys, OAuth lets the tool act on behalf of the user with their explicit consent.

## Background

OAuth in Dify involves **two separate flows** that developers must understand and design for:

### Flow 1: OAuth Client Setup (Admin / Developer Flow)

<Note>
  On Dify Cloud, Dify team would create OAuth apps for popular tool plugins and set up OAuth clients, saving users the trouble to configure this themselves.

  \
  Admins of Self-Hosted Dify instances must go through this set up flow.
</Note>

Dify instance's admin / developer first need to register an OAuth app at the third-party service as a trusted application. From this, they'll be able to obtain the necessary credentials to configure the Dify tool provider as an OAuth client. As an example, here are the steps to setting up an OAuth client for Dify's Gmail tool provider:

<AccordionGroup>
  <Accordion title="Create a Google Cloud Project">
    1. Go to [Google Cloud Console](https://console.cloud.google.com) and create a new project, or select existing one
    2. Enable the required APIs (e.g., Gmail API)
  </Accordion>
  <Accordion title="Configure OAuth Consent Screen:">
    1. Navigate to "APIs & Services" → "OAuth consent screen"
    2. Choose "External" user type for public plugins
    3. Fill in application name, user support email, and developer contact
    4. Add authorized domains if needed
    5. For testing: Add test users in the "Test users" section
  </Accordion>
  <Accordion title="Create OAuth 2.0 Credentials">
    1. Go to "APIs & Services" → "Credentials"
    2. Click "Create Credentials" → "OAuth 2.0 Client IDs"
    3. Choose "Web application" type
    4. A`client_id` and a`client_secret` will be generated. Save these as the credentials.
  </Accordion>
  <Accordion title="Enter Credentials in Dify">
    <img
      src="/images/acd5f5057235c3a0c554abaedcf276fb48f80567f0231eae9158a795f8e1c45d.png"
      alt="acd5f5057235c3a0c554abaedcf276fb48f80567f0231eae9158a795f8e1c45d.png"
      title="acd5f5057235c3a0c554abaedcf276fb48f80567f0231eae9158a795f8e1c45d.png"
      className="mx-auto"
      style={{ width:"77%" }}
    />

    Enter the client_id and aclient_secret  on the OAuth Client configuration popup to set up the tool provider as a client.
  </Accordion>
  <Accordion title="Authorize Redirect URI">
    Register the redirect URI generated by Dify on the Google OAuth Client's page:

    <img
      src="/images/dfe60a714a275c5bf65f814673bd2f0a0db4fda27573a2f0b28a1c39e4c61da2.png"
      alt="dfe60a714a275c5bf65f814673bd2f0a0db4fda27573a2f0b28a1c39e4c61da2.png"
      title="dfe60a714a275c5bf65f814673bd2f0a0db4fda27573a2f0b28a1c39e4c61da2.png"
      className="mx-auto"
      style={{ width:"75%" }}
    />

    <Info>
      Dify displays the `redirect_uri`  in the OAuth Client configuration popup. It usually follows the format:

      ```bash
      https://{your-dify-domain}/console/api/oauth/plugin/{plugin-id}/{provider-name}/{tool-name}/callback
      ```
    </Info>
  </Accordion>
</AccordionGroup>

<Tip>
  Each service has unique requirements, so always consult the specific OAuth documentation for the services you're integrating with.
</Tip>

### Flow 2: User Authorization (Dify User Flow)

After configuring OAuth clients, individual Dify users can now authorize your plugin to access their personal accounts.

<img
  src="/images/CleanShot2025-07-25at00.50.19@2x.png"
  alt="CleanShot 2025-07-25 at 00.50.19@2x.png"
  title="CleanShot 2025-07-25 at 00.50.19@2x.png"
  className="mx-auto"
  style={{ width:"80%" }}
/>

## Implementation

### 1. Define OAuth schema in provider manifest

The `oauth_schema` section of the provider manifest definitions tell Dify what credentials your plugin OAuth needs and what the OAuth flow will produce. Two schemas are required for setting up OAuth:

#### client_schema

This defines the input for OAuth client setup:

```yaml gmail.yaml
oauth_schema:
  client_schema:
    - name: "client_id"
      type: "secret-input"
      required: true
      url: "https://developers.google.com/identity/protocols/oauth2"
    - name: "client_secret"
      type: "secret-input" 
      required: true
```

<Info>
  The `url` field links directly to help documentations for the third-party service. This helps confused admins / developers.
</Info>

#### credentials_schema

Specifies what the user authorization flow produces (Dify manages these automatically):

```yaml
# also under oauth_schema
  credentials_schema:
    - name: "access_token"
      type: "secret-input"
    - name: "refresh_token"
      type: "secret-input"
    - name: "expires_at"
      type: "secret-input"
```

<Info>
  Include both `oauth_schema` and `credentials_for_provider` to offer OAuth \+ API key auth options.
</Info>

### 2. Complete required OAuth methods in Tool Provider

Add these imports to where your `ToolProvider` is implemented:

```python
from dify_plugin.entities.oauth import ToolOAuthCredentials
from dify_plugin.errors.tool import ToolProviderCredentialValidationError, ToolProviderOAuthError
```

Your `ToolProvider` class must implement these four OAuth methods (taking `GmailProvider`  as an example):

<CodeGroup>

```python _validate_credentials expandable
def _validate_credentials(self, credentials: dict[str, Any]) -> None:
    """
    Validate OAuth credentials by making a test API call when users connect
    """
    try:
        access_token = credentials.get("access_token")
        if not access_token:
            raise ToolProviderCredentialValidationError("Access token is required")
        
        # Test with a lightweight API call
        headers = {
            "Authorization": f"Bearer {access_token}",
            "Accept": "application/json"
        }
        
        response = requests.get(
            f"{self._API_BASE_URL}/user",  # Using Google Platform OAuth API's user endpoint
            headers=headers,
            timeout=10
        )
        
        # Handle common OAuth error scenarios
        if response.status_code == 401:
            raise ToolProviderCredentialValidationError("Invalid or expired access token")
        elif response.status_code == 403:
            raise ToolProviderCredentialValidationError("Access forbidden - check scopes")
        elif response.status_code != 200:
            raise ToolProviderCredentialValidationError(f"API error: {response.status_code}")
            
    except requests.RequestException as e:
        raise ToolProviderCredentialValidationError(f"Network error: {str(e)}")
    except Exception as e:
        raise ToolProviderCredentialValidationError(f"Credential validation failed: {str(e)}")
```


```python _oauth_get_authorization_url expandable
def _oauth_get_authorization_url(self, redirect_uri: str, system_credentials: Mapping[str, Any]) -> str:
	"""
	Generate the authorization URL using credentials from OAuth Client Setup Flow. 
    This URL is where users grant permissions.
    """
    # Generate random state for CSRF protection (recommended for all OAuth flows)
    state = secrets.token_urlsafe(16)
    
    # Define Gmail-specific scopes - request minimal necessary permissions
    scope = "read:user read:data"  # Replace with your required scopes
    
    # Assemble Gmail-specific payload
    params = {
        "client_id": system_credentials["client_id"],    # From OAuth Client Setup
        "redirect_uri": redirect_uri,                    # Dify generates this - DON'T modify
        "scope": scope,                                  
        "response_type": "code",                         # Standard OAuth authorization code flow
        "access_type": "offline",                        # Critical: gets refresh token (if supported)
        "prompt": "consent",                             # Forces reauth when scopes change (if supported)
        "state": state,                                  # CSRF protection
    }
    
    return f"{self._AUTH_URL}?{urllib.parse.urlencode(params)}"
```


```python _oauth_get_credentials expandable
def _oauth_get_credentials(
    self, redirect_uri: str, system_credentials: Mapping[str, Any], request: Request
) -> ToolOAuthCredentials:
    """
    Exchange authorization code for access token and refresh token. This is called
	to creates ONE credential set for one account connection
    """
    # Extract authorization code from OAuth callback
    code = request.args.get("code")
    if not code:
        raise ToolProviderOAuthError("Authorization code not provided")
    
    # Check for authorization errors from OAuth provider
    error = request.args.get("error")
    if error:
        error_description = request.args.get("error_description", "")
        raise ToolProviderOAuthError(f"OAuth authorization failed: {error} - {error_description}")
    
    # Exchange authorization code for tokens using OAuth Client Setup credentials

	# Assemble Gmail-specific payload
    data = {
        "client_id": system_credentials["client_id"],        # From OAuth Client Setup
        "client_secret": system_credentials["client_secret"], # From OAuth Client Setup
        "code": code,                                        # From user's authorization
        "grant_type": "authorization_code",                  # Standard OAuth flow type
        "redirect_uri": redirect_uri,                        # Must exactly match authorization URL
    }
    
    headers = {"Content-Type": "application/x-www-form-urlencoded"}
    
    try:
        response = requests.post(
            self._TOKEN_URL,
            data=data,
            headers=headers,
            timeout=10
        )
        response.raise_for_status()
        
        token_data = response.json()
        
        # Handle OAuth provider errors in response
        if "error" in token_data:
            error_desc = token_data.get('error_description', token_data['error'])
            raise ToolProviderOAuthError(f"Token exchange failed: {error_desc}")
        
        access_token = token_data.get("access_token")
        if not access_token:
            raise ToolProviderOAuthError("No access token received from provider")
        
        # Build credentials dict matching your credentials_schema
        credentials = {
            "access_token": access_token,
            "token_type": token_data.get("token_type", "Bearer"),
        }
        
        # Include refresh token if provided (critical for long-term access)
        refresh_token = token_data.get("refresh_token")
        if refresh_token:
            credentials["refresh_token"] = refresh_token
        
        # Handle token expiration - some providers don't provide expires_in
        expires_in = token_data.get("expires_in", 3600)  # Default to 1 hour
        credentials["expires_in"] = str(expires_in)
        expires_at = int(time.time()) + expires_in
        
        return ToolOAuthCredentials(credentials=credentials, expires_at=expires_at)
        
    except requests.RequestException as e:
        raise ToolProviderOAuthError(f"Network error during token exchange: {str(e)}")
    except Exception as e:
        raise ToolProviderOAuthError(f"Failed to exchange authorization code: {str(e)}")
```


```python _oauth_refresh_credentials
def _oauth_refresh_credentials(
    self, redirect_uri: str, system_credentials: Mapping[str, Any], credentials: Mapping[str, Any]
) -> ToolOAuthCredentials:
    """
    Refresh the credentials using refresh token. 
	Dify calls this automatically when tokens expire
    """
    refresh_token = credentials.get("refresh_token")
    if not refresh_token:
        raise ToolProviderOAuthError("No refresh token available")

    # Standard OAuth refresh token flow
    data = {
        "client_id": system_credentials["client_id"],       # From OAuth Client Setup
        "client_secret": system_credentials["client_secret"], # From OAuth Client Setup
        "refresh_token": refresh_token,                     # From previous authorization
        "grant_type": "refresh_token",                      # OAuth refresh flow
    }

    headers = {"Content-Type": "application/x-www-form-urlencoded"}

    try:
        response = requests.post(
            self._TOKEN_URL,
            data=data,
            headers=headers,
            timeout=10
        )
        response.raise_for_status()

        token_data = response.json()

        # Handle refresh errors
        if "error" in token_data:
            error_desc = token_data.get('error_description', token_data['error'])
            raise ToolProviderOAuthError(f"Token refresh failed: {error_desc}")

        access_token = token_data.get("access_token")
        if not access_token:
            raise ToolProviderOAuthError("No access token received from provider")

        # Build new credentials, preserving existing refresh token
        new_credentials = {
            "access_token": access_token,
            "token_type": token_data.get("token_type", "Bearer"),
            "refresh_token": refresh_token,  # Keep existing refresh token
        }

        # Handle token expiration
        expires_in = token_data.get("expires_in", 3600)
        new_credentials["expires_in"] = str(expires_in)

        # update refresh token if new one provided
        new_refresh_token = token_data.get("refresh_token")
        if new_refresh_token:
            new_credentials["refresh_token"] = new_refresh_token

        # Calculate new expiration timestamp for Dify's token management
        expires_at = int(time.time()) + expires_in

        return ToolOAuthCredentials(credentials=new_credentials, expires_at=expires_at)

    except requests.RequestException as e:
        raise ToolProviderOAuthError(f"Network error during token refresh: {str(e)}")
    except Exception as e:
        raise ToolProviderOAuthError(f"Failed to refresh credentials: {str(e)}")
```

</CodeGroup>

### 3. Access Tokens in Your Tools

You may use OAuth credentials to make authenticated API calls in your `Tool` implementation like so:

```python
class YourTool(BuiltinTool):
    def _invoke(self, user_id: str, tool_parameters: dict[str, Any]) -> ToolInvokeMessage:
        access_token = self.runtime.credentials["access_token"]
        
        response = requests.get("https://api.service.com/data",
                              headers={"Authorization": f"Bearer {access_token}"})
        return self.create_text_message(response.text)
```

`self.runtime.credentials` automatically provides the current user's tokens. Dify handles refresh automatically.