HubSpot OAuth Token Endpoint does not validate client_id on refresh_token Grants?

According to HubSpot’s official documentation, passing a valid, matching ‘client_id’ is mandatory.
If an incorrect client ID is sent, the gateway is expected to throw a MISMATCH_REFRESH_TOKEN_CLIENT or BAD_CLIENT_ID error.

However, during testing, we observed that HubSpot’s gateway returns an HTTP/1.1 200 OK with a fresh, fully active access token even when the request body contains explicitly invalid parameters (e.g., client_id=invalid).

Steps to Reproduce:

  1. Initiate a POST request to https://api.hubapi.com/oauth/v1/token
  2. Set Content-Type: application/x-www-form-urlencoded
  3. Provide a valid grant_type (refresh_token), a valid refresh_token, and a valid client_secret.
  4. Pass an intentionally broken string for the client identity: client_id=invalid
  5. Observe the response: The gateway bypasses the payload constraint and issues a valid token anyway.

Hey @KeerthanaJ,

Thanks for all of the details and reproduction steps. While we wait for some peer input, could you test if the client_secret you used is scoped to a different app than the client_id you sent?

You can reference the OAuth 2.0 Token Refresh documentation as the baseline for expected parameter validation in the meantime.

Tagging in a few folks from the community who may have encountered this.

Hey @Anton, @Alex_Boissonneault, @SteveHTM – do you have any additional suggestions for @KeerthanaJ on troubleshooting this?

Thanks!

Sam, Community Manager

Hey @KeerthanaJ, thanks for looping me in @stassey!

I dug into this and i don’t think it’s a gateway bug, it’s expected behaviour, just not obvious from the docs. On a refresh_token grant, the thing that actually identifies your app is the refresh token itself. it’s issued bound to one app, and the client_secret is what authenticates that app. So when the gateway sees a valid refresh_token + the matching client_secret, it already knows exactly which app is asking and that it’s legitimately that app. The client_id in the body is effectively redundant at that point, so a garbage value sails through.

Quick way to prove it to yourself: keep client_id=invalid but now send a client_secret from a different app (or just scramble one character). That should fail, because you’ve broken the part that actually authenticates. That’s the test Sam was pointing at, and it’s the real boundary here.

So the security model isn’t weaker than it looks: an attacker still needs both your live refresh token and your client secret, which are the two things that matter. Knowing the client_id buys them nothing.

Where i’d agree with you: it’s a validation-consistency gap. The docs say client_id is required and imply it’s checked, so silently ignoring a wrong value is misleading even if it’s harmless. That’s worth raising. I’d file it as a docs/validation-consistency issue (either enforce it, or document that refresh grants authenticate on secret + token) with your repro steps and a correlationId, rather than as a security vuln, since it’ll get triaged faster framed that way.