[ENG-5769] Oauth 1.0a integration#78
[ENG-5769] Oauth 1.0a integration#78opaduchak merged 15 commits intoCenterForOpenScience:developfrom
Conversation
adlius
left a comment
There was a problem hiding this comment.
First round. Some questions.
| case CredentialsFormats.OAUTH1A: | ||
| return credentials.OAuth1TokenCredentials |
There was a problem hiding this comment.
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.
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.
Is there not an auth_callback_url?
There was a problem hiding this comment.
auth_url is auth_callback_url, I'll change it if it's confusing
There was a problem hiding this comment.
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.
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.
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.
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.
I am not quite sure whether this boolean field is necessary.
There was a problem hiding this comment.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
I agree, storing any unencrypted credentials in the db, poses security risks
There was a problem hiding this comment.
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