jpetrucciani / hubspot3

python3.6+ hubspot client based on hapipy, but modified to use the newer endpoints and non-legacy python
MIT License
147 stars 73 forks source link

Proposal to improve support for oauth2 authentication with multiple clients #72

Closed sangaline closed 4 years ago

sangaline commented 4 years ago

Introduction

Hi @jpetrucciani, I've submitted a small handful of PRs in the past and I'm a regular user/advocate of the project. The company I work at would like to use the library in a distributed environment with oauth2 authentication, but we've run into contention issues where the different containers essentially fight over access tokens because only one can be active at any given time. This leads to almost every request initially failing with 401 errors and then only working after a new access token has been requested from Hubspot. It would be ideal if we could share the tokens across workers through an in-memory store like Redis to avoid these issues. This is a significant enough change that I thought it would be good to open an issue to discuss the approach before implementing it (and I would be happy to implement it, this isn't a request for you to do so).

How oauth2 is currently handled

The easiest way to initialize the client is through the Hubspot3 class in __init__.py. This supports four options which are all required for Hubspot's oauth authentication scheme:

These are stored in a dictionary called auth on a Hubspot3 instance after initialization, and the arguments are passed into the different client classes when they're accessed as properties. For example, accessing the contacts property on the client will initialize a new ContactsClient instance via this code:

    @property
    def contacts(self):
        """returns a hubspot3 contacts client"""
        from hubspot3.contacts import ContactsClient

        return ContactsClient(**self.auth, **self.options)

All of the various clients inherit from BaseClient which stores the credentials on instance properties when initialized. The credentials are then used in BaseClient._call_raw() whenever a request is made, and a new access token is requested if a HubspotUnauthorized exception is raised. The access_token and refresh_token properties are then replaced on the instance by this code:

                        client = OAuth2Client(**self.options)
                        refresh_result = client.refresh_tokens(
                            client_id=self.client_id,
                            client_secret=self.client_secret,
                            refresh_token=self.refresh_token,
                        )
                        self.access_token = refresh_result["access_token"]
                        self.refresh_token = refresh_result["refresh_token"]

The OAuth2Client class handles refreshing the tokens, but it isn't responsible for storage or maintenance of the tokens.

Proposal for supporting dynamic storage of tokens

A relatively simple and backwards compatible change is adding two new initialization parameters to the Hubspot3 and BaseClient classes:

The logic in the __init__() methods of BaseClient and Hubspot3 as well as the BaseClient._call_raw() method would then use the setter and getter in place of direct attribute access. The access_token/refresh_token attributes would still be used by default, and this should be completely backward compatible while also supporting more sophisticated storage mechanisms.

Conclusion

Apologies for the verbose issue, but I wanted to clearly lay out what I'm proposing. If this sounds good to you, then I'll go ahead and implement it.

jpetrucciani commented 4 years ago

Hey @sangaline, I recognized your name! Thanks again for all of your contributions/advocacy, and for this very detailed issue (no need to apologize for verbosity!)

That makes sense if you're dealing with that sort of orchestrated and scaled out environment and using OAuth.

I like your proposal for how to deal with it - it sounds like a powerful way to implement this sort of functionality. Go right ahead! 😄