langgenius/dify · error · BadRequest
client_secret is invalid
Error message
client_secret is invalid
What it means
Flask BadRequest (HTTP 400) at oauth_server.py:211 in the AUTHORIZATION_CODE branch of the token endpoint. The code was present and grant_type valid, but payload.client_secret does not equal oauth_provider_app.client_secret. This is a constant-time-unsafe direct string comparison of the confidential client secret.
Source
Thrown at api/controllers/console/auth/oauth_server.py:211
@console_ns.route("/oauth/provider/token")
class OAuthServerUserTokenApi(Resource):
@setup_required
@console_ns.expect(console_ns.models[OAuthTokenRequest.__name__])
@console_ns.response(200, "Success", console_ns.models[OAuthProviderTokenResponse.__name__])
@oauth_server_client_id_required
@model_validate(OAuthTokenRequest)
def post(self, payload: OAuthTokenRequest, oauth_provider_app: OAuthProviderApp):
try:
grant_type = OAuthGrantType(payload.grant_type)
except ValueError:
raise BadRequest("invalid grant_type")
match grant_type:
case OAuthGrantType.AUTHORIZATION_CODE:
if not payload.code:
raise BadRequest("code is required")
if payload.client_secret != oauth_provider_app.client_secret:
raise BadRequest("client_secret is invalid")
if payload.redirect_uri not in oauth_provider_app.redirect_uris:
raise BadRequest("redirect_uri is invalid")
access_token, refresh_token = OAuthServerService.sign_oauth_access_token(
grant_type, code=payload.code, client_id=oauth_provider_app.client_id
)
return jsonable_encoder(
{
"access_token": access_token,
"token_type": "Bearer",
"expires_in": OAUTH_ACCESS_TOKEN_EXPIRES_IN,
"refresh_token": refresh_token,
}
)
case OAuthGrantType.REFRESH_TOKEN:
if not payload.refresh_token:
raise BadRequest("refresh_token is required")View on GitHub (pinned to ef8544b173)
Solutions
- Obtain the current client_secret from the OAuthProviderApp registration and use exactly that value.
- After any secret rotation, update all clients immediately and ensure no trailing newline is appended when copying.
- Confirm you are pairing the secret with the matching client_id (mismatched pairs fail this check).
- Treat the comparison as case- and whitespace-sensitive; strip nothing server-side.
Defensive patterns
Strategy: validation
Validate before calling
if (clientSecret !== registeredSecret) {
throw new Error('client_secret mismatch — fetch the current secret');
} Type guard
function hasClientSecret(s: unknown): s is string { return typeof s === 'string' && s.length > 0; } Try / catch
try {
await exchangeCodeForToken(code, clientId, clientSecret, redirectUri);
} catch (e) {
if (/client_secret is invalid/i.test(e.message)) { refreshSecret(); } else throw e;
} Prevention
- Store the secret securely and update all clients immediately after rotation.
- Pair the secret with its own client_id only.
- Avoid copying secrets with trailing newlines.
When it happens
Trigger: POST /oauth/provider/token with grant_type=authorization_code, a non-empty code, and a client_secret that does not match the registered OAuthProviderApp. Common after secret rotation or a copy/paste error.
Common situations: Client secret was rotated server-side but the client still holds the old value; secret copied with whitespace/newline; or a different app's secret used by mistake.
Related errors
- invalid grant_type
- code is required
- refresh_token is required
- KNOWLEDGE_FS_BASE_URL must not include credentials, query, o
- ELASTICSEARCH_API_KEY is required when using Elastic Cloud
AI-assisted analysis of langgenius/dify@ef8544b173 (2026-08-12).
Data as JSON: /api/errors/d66e76230baa7aab.
Report an issue: GitHub.