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

  1. Obtain the current client_secret from the OAuthProviderApp registration and use exactly that value.
  2. After any secret rotation, update all clients immediately and ensure no trailing newline is appended when copying.
  3. Confirm you are pairing the secret with the matching client_id (mismatched pairs fail this check).
  4. 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

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


AI-assisted analysis of langgenius/dify@ef8544b173 (2026-08-12). Data as JSON: /api/errors/d66e76230baa7aab. Report an issue: GitHub.