Builtin Tool Provider Credentials#
Overview#
Builtin tool provider credentials are per-tenant API keys or OAuth tokens that Dify stores so providers like Google Search or DALL-E can authenticate against external APIs. Each workspace can hold up to 100 credential sets per provider . Credentials are persisted in the BuiltinToolProvider table with the following key fields :
| Field | Purpose |
|---|---|
encrypted_credentials | AES-encrypted JSON blob of credential values |
credential_type | api-key or oauth2 |
visibility | all_team_members or only_me (immutable after creation) |
is_default | Whether this credential set is the workspace default |
expires_at | Expiry timestamp (used by OAuth tokens; -1 = no expiry) |
The main components involved:
| Layer | File |
|---|---|
| API controller | tool_providers.py |
| Service | builtin_tools_manage_service.py |
| DB model | models/tools.py |
| Encryption | provider_encryption.py |
| Redis cache | provider_cache.py |
API Endpoints#
All builtin credential endpoints live under /workspaces/current/tool-provider/builtin/<provider>/.
| Method | Path suffix | Handler | Auth required |
|---|---|---|---|
POST | /add | add_builtin_tool_provider | Admin/owner + CREDENTIAL_CREATE |
POST | /update | update_builtin_tool_provider | Admin/owner + CREDENTIAL_MANAGE |
POST | /delete | delete_builtin_tool_provider | Admin/owner + CREDENTIAL_MANAGE |
GET | /credentials | get_builtin_tool_provider_credentials | Login only |
GET | /credential/info | get_builtin_tool_provider_credential_info | Login only |
POST | /default-credential | set_default_provider | Admin/owner + CREDENTIAL_USE |
Request payload models are BuiltinToolAddPayload and BuiltinToolUpdatePayload . Add carries a type: CredentialType field that is immutable once saved; update takes a credential_id to identify which saved set to modify.
The /credentials GET accepts an include_credential_ids list so that workflow/agent nodes can request credential sets owned by other members that would otherwise be hidden by visibility filtering β those rows are returned with from_other_member=True .
Add and Update Flow#
Adding a credential (add_builtin_tool_provider)#
add_builtin_tool_provider runs inside a single DB transaction and follows this sequence:
- Redis lock β acquires
builtin_tool_provider_create_lock:{tenant_id}_{provider}with a 20-second timeout to prevent duplicate concurrent creates . - Provider count check β rejects the request if the tenant already has 100 credentials for this provider .
- Credential validation β if
CredentialType.is_validate_allowed()is true, callsprovider_controller.validate_credentials(user_id, credentials), which makes a live request to the external API . - Name generation β auto-generates an incremental name if none is supplied; rejects duplicate names .
- Encryption β creates a
ProviderConfigEncrypterviacreate_provider_encrypter()with aNoOpProviderCredentialCache(no caching for brand-new rows) and storesjson.dumps(encrypter.encrypt(credentials)). - Persist β inserts a new
BuiltinToolProviderrow with the chosenvisibilityenum .PARTIAL_TEAMvisibility is rejected at this layer .
Updating a credential (update_builtin_tool_provider)#
update_builtin_tool_provider is similar but targets an existing row by credential_id:
- Fetches the
BuiltinToolProviderrow; raises if not found . - Skips credential changes unless the type
is_editable()and new credentials were submitted. - HIDDEN_VALUE handling β decrypts existing credentials first, then replaces any
HIDDEN_VALUEplaceholders in the incoming payload with the original values, preventing accidental overwrite of masked secrets . - Validates the merged credential set if allowed .
- Re-encrypts and saves; calls
cache.delete()to evict the Redis-cached decrypted value . - Visibility is immutable after creation β update has no path to change it .
Encryption#
All credential values are encrypted at rest via create_provider_encrypter(tenant_id, config, cache), which returns a (ProviderConfigEncrypter, ProviderConfigCache) tuple. The config argument is built from the provider's credential schema (get_credentials_schema_by_type()), so only declared sensitive fields are encrypted.
The service has two helper patterns:
create_tool_encrypter()β for existingBuiltinToolProviderrows; attaches a liveToolProviderCredentialsCachekeyed on the row'sid.- Inline
create_provider_encrypter()withNoOpProviderCredentialCacheβ used during creation before a rowidexists; skips caching.
When serving credentials back to the API, the service decrypts and then calls encrypter.mask_plugin_credentials() to replace sensitive values with HIDDEN_VALUE before serialization .
Redis Cache#
Decrypted credentials are cached in Redis by ToolProviderCredentialsCache:
- Key pattern:
tool_credentials:tenant_id:{tenant_id}:provider:{provider}:credential_id:{credential_id} - TTL: 86 400 seconds (24 hours)
Every mutating operation (update, delete) explicitly calls cache.delete() to evict the stale entry. NoOpProviderCredentialCache is used when no caching is desired β during initial creation (before the row has an id) and for OAuth client operations .
Redis Lock Timeout on Add#
The creation path acquires redis_client.lock(lock_key, timeout=20) β a plain redis-py lock with a 20-second TTL . This guards against duplicate concurrent inserts for the same tenant_id + provider.
Timeout risk: If provider_controller.validate_credentials() calls an external API that takes longer than 20 seconds, the lock expires before the transaction completes. A second concurrent request can then acquire the lock and insert a duplicate row. The provider count cap (100) is the only backstop.
The auto-renewing DbMigrationAutoRenewLock pattern (which heartbeats to extend the TTL) is not used here β its module docstring explicitly warns it is "migration-specific" and that callers should "prefer explicit lock lifecycle management" for normal application code . Engineers who need to extend the lock window for slow validations should either shorten the validation timeout or switch to redis_client.lock(...).extend() from within the same thread.