mirror of
https://github.com/langgenius/dify-docs.git
synced 2026-03-27 13:28:32 +07:00
380 lines
14 KiB
Plaintext
380 lines
14 KiB
Plaintext
---
|
|
title: "Add OAuth Support to Your Tool Plugin"
|
|
---
|
|
|
|

|
|
|
|
This guide teaches you how to build [OAuth](https://oauth.net/2/) support into your tool plugin.
|
|
|
|
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 should 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 setup flow.
|
|
</Note>
|
|
|
|
Dify instance's admins or developers 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">
|
|
Enter the client_id and client_secret on the OAuth Client configuration popup to set up the tool provider as a client.
|
|
|
|
<img
|
|
src="/images/acd5f5057235c3a0c554abaedcf276fb48f80567f0231eae9158a795f8e1c45d.png"
|
|
alt="acd5f5057235c3a0c554abaedcf276fb48f80567f0231eae9158a795f8e1c45d.png"
|
|
title="acd5f5057235c3a0c554abaedcf276fb48f80567f0231eae9158a795f8e1c45d.png"
|
|
className="mx-auto"
|
|
style={{ width:"66%" }}
|
|
/>
|
|
</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:"77%" }}
|
|
/>
|
|
|
|
<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
|
|
```
|
|
|
|
For self-hosted Dify, the `your-dify-domain` should be consistent with the `CONSOLE_WEB_URL`.
|
|
</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/833c205f5441910763b27d3e3ff0c4449a730a690da91abc3ce032c70da04223.png"
|
|
alt="833c205f5441910763b27d3e3ff0c4449a730a690da91abc3ce032c70da04223.png"
|
|
title="833c205f5441910763b27d3e3ff0c4449a730a690da91abc3ce032c70da04223.png"
|
|
className="mx-auto"
|
|
style={{ width:"67%" }}
|
|
/>
|
|
|
|
## Implementation
|
|
|
|
### 1. Define OAuth schema in provider manifest
|
|
|
|
The `oauth_schema` section of the provider manifest definitions tells 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 three OAuth methods (taking `GmailProvider` as an example):
|
|
|
|
<Warning>
|
|
Under no circumstances should the `client_secret` be returned in the credentials of `ToolOAuthCredentials`, as this could lead to security issues.
|
|
</Warning>
|
|
|
|
<CodeGroup>
|
|
|
|
```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
|
|
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)
|
|
|
|
# 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:
|
|
if self.runtime.credential_type == CredentialType.OAUTH:
|
|
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.
|
|
|
|
For plugins that support both OAuth and API_KEY authentication, you can use `self.runtime.credential_type` to differentiate between the two authentication types.
|
|
|
|
### 4. Specifying the Correct Versions
|
|
|
|
Previous versions of the plugin SDK and Dify do not support OAuth authentication. Therefore, you need to set the plugin SDK version to:
|
|
|
|
```
|
|
dify_plugin>=0.4.2,<0.5.0.
|
|
```
|
|
|
|
In `manifest.yaml`, add the minimum Dify version:
|
|
|
|
```yaml
|
|
meta:
|
|
version: 0.0.1
|
|
arch:
|
|
- amd64
|
|
- arm64
|
|
runner:
|
|
language: python
|
|
version: "3.12"
|
|
entrypoint: main
|
|
minimum_dify_version: 1.7.1
|
|
```
|
|
|
|
{/*
|
|
Contributing Section
|
|
DO NOT edit this section!
|
|
It will be automatically generated by the script.
|
|
*/}
|
|
|
|
---
|
|
|
|
[Edit this page](https://github.com/langgenius/dify-docs/edit/main/en/plugins/quick-start/develop-plugins/tool-oauth.mdx) | [Report an issue](https://github.com/langgenius/dify-docs/issues/new?template=docs.yml) |