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).
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?
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.