-
Notifications
You must be signed in to change notification settings - Fork 20
[ENG-5769] Oauth 1.0a integration #78
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
adlius
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
First round. Some questions.
| case CredentialsFormats.OAUTH1A: | ||
| return credentials.OAuth1TokenCredentials |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For Oauth 1a, it should still be AccessTokenCredentials, right? I believe this affects how the header is constructed for the http requests.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There are oauth_token and oauth_token_secret, which in their nature and usage are drastically different from OAuth2 access and refresh tokens
| from addon_service.common.base_model import AddonsServiceBaseModel | ||
|
|
||
|
|
||
| class OAuth1ClientConfig(AddonsServiceBaseModel): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there not an auth_callback_url?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
auth_url is auth_callback_url, I'll change it if it's confusing
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For the record: we're storing the auth_callback_url explicitly in case there are services that won't allow us to update the list of callbacks without invalidating our client and/or old credentials. That way, if necessary, we can encode this as https://[environment.]osf.io/oauth/callback/[provider] -- where we already have things set up to forward requests to GV if appropriate.
addon_toolkit/credentials.py
Outdated
|
|
||
|
|
||
| @dataclasses.dataclass(frozen=True, slots=True) | ||
| class OAuth1TokenCredentials: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think for OAuth1 we should still use AccessTokenCredentials. The idea of the AccessTokenCredentials dataclass is to implement the iter_headers method so that GravyvaletHttpRequestors can automatically append the appropriate auth headers for the oauth requests.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Depends on if OAuth1 requests require the secret to be sent along with the token. If so, it's a different credentials shape. Otherwise, yes, we should keep using AccessTokenCredentials and create an OAuth1 equivalent to OAuth2TokenMetadata for tracking other details
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well, auth headers are different for OAuth1.0a and OAuth2, and even more: they are different for Zotero
| is_oauth1_ready = models.BooleanField( | ||
| "addon_service.OAuth2TokenMetadata", | ||
| null=True, | ||
| blank=True, | ||
| ) | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am not quite sure whether this boolean field is necessary.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is necessary as it allows to discern temporary Request token credentials from final credentials
| self.context["request"].session[ | ||
| "oauth1a_account_id" | ||
| ] = authorized_account.pk |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we have to store the authorized_account.pk in the session? Is there anything in the payload of the request to the callback url that can be used to identify which authorized account this is?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There isn't anything in payload which'd allow for is to identify it, I don't like this approach too, but I don't have any better ideas
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the callback request includes the same temporary token received when initiating oauth1 -- could add a model to hold oauth1 temporary state (parallel OAuth2TokenMetadata) with indexed temporary_token field
(edit: nevermind -- i think that's true in specs but not for zotero, oh well)
jwalz
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks! This is a great start. I'll probably dig a bit more into the testing stuff to see if there's an easy way to consolidate strategies.
addon_service/tests/_factories.py
Outdated
| class ExternalStorageOAuth2ServiceFactory(ExternalStorageServiceFactory): | ||
| oauth2_client_config = factory.SubFactory(OAuth2ClientConfigFactory) | ||
|
|
||
|
|
||
| class ExternalStorageOAuth1ServiceFactory(ExternalStorageServiceFactory): | ||
| oauth1_client_config = factory.SubFactory(OAuth1ClientConfigFactory) | ||
|
|
||
| @classmethod | ||
| def _create( | ||
| cls, | ||
| model_class, | ||
| credentials_format=CredentialsFormats.OAUTH1A, | ||
| service_type=ServiceTypes.PUBLIC, | ||
| *args, | ||
| **kwargs, | ||
| ): | ||
| return super()._create( | ||
| model_class, credentials_format, service_type, *args, **kwargs | ||
| ) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We never shoul had oauth_client_config as a sub-factory, considering it isnt' relevant and shouldn't be set for non-oauth(2) services.
Would recommend instead just updating the existing ExternalStorageServiceFactory._create function to
- Accept
oauth1_client_configandoauth2_client_configas parameters - Use the
credentials_formatto construct the appropriate form of client config using the appropriate factory if one wasn't passed - Ignore or error any irrelevant
client_configthat was passed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note, this is obviously simpler if we decide to collapse down to one OAuthConfig.
aaxelb
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
looks good so far! had some suggestions -- most breakingly, adding a new model for the "temporary" oauth1 credentials
fixed verbose comprehensions Co-authored-by: abram axel booth <aaxelb@users.noreply.github.com>
(avoid any logic in addon_service that explicitly branches by imp)
| This is Zotero specific as other OAuth1.0a clients require request signing, | ||
| as per current architecture, we cannot it here. | ||
| """ | ||
|
|
||
| yield "Authorization", f"Bearer {self.oauth_token_secret}" | ||
| # TODO: implement request signing for OAuth1.0a services that require it |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
gah, i missed that zotero uses the secret as bearer token instead of signing the request... feels wrong, but maybe it's true that https makes it safe, as argued in their example client code:
/** OAuth support for all api requests may be added in the future
* but for now secure https provides similar benefits anyway
*/
(this speaks to the need for modular/swappable request-auth logic (instead of iter_headers) but sounds like that'll be part of s3 work -- this TODO seems fine for now)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should be safe because https encypts entire http payload (including headers, query params, path, body, etc.)
| access_token_url = models.URLField(null=False) | ||
|
|
||
| client_key = models.CharField(null=True) | ||
| client_secret = models.CharField(null=True) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
hmm, the client secret should be stored encrypted -- but since OAuth2ClientConfig is the same, maybe worth splitting that into another ticket? (...and i'm starting to reconsider the EncryptedDataclassModel abstract base, now that we have three potential uses for it...)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree, storing any unencrypted credentials in the db, poses security risks
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, separate ticket. Need input from cloud eng/devops as to whether they want this in the database at all or if they'd prefer an approach like this one
Implementation of Oauth1.0a for GravyValet
ENG-5769