-
-
Notifications
You must be signed in to change notification settings - Fork 4.1k
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
feat(integrations): Vsts OAuth refresh #8725
Conversation
@@ -64,6 +65,9 @@ def _get_oauth_parameter(self, parameter_name): | |||
def get_oauth_access_token_url(self): | |||
return self._get_oauth_parameter('access_token_url') | |||
|
|||
def get_oauth_refresh_token_url(self): |
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.
Had anticipated moving things in here, but couldn't figure out a clean way of doing so. Open to suggestions, otherwise I'll just remove this code.
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.
Did you see what we did in the auth version of the Oauth2
provider?
sentry/src/sentry/auth/providers/oauth2.py
Lines 181 to 216 in f683f22
def refresh_identity(self, auth_identity): | |
refresh_token = auth_identity.data.get('refresh_token') | |
if not refresh_token: | |
raise IdentityNotValid('Missing refresh token') | |
data = self.get_refresh_token_params( | |
refresh_token=refresh_token, | |
) | |
req = safe_urlopen(self.get_refresh_token_url(), data=data) | |
try: | |
body = safe_urlread(req) | |
payload = json.loads(body) | |
except Exception: | |
payload = {} | |
error = payload.get('error', 'unknown_error') | |
error_description = payload.get('error_description', 'no description available') | |
formatted_error = 'HTTP {} ({}): {}'.format(req.status_code, error, error_description) | |
if req.status_code == 401: | |
raise IdentityNotValid(formatted_error) | |
if req.status_code == 400: | |
# this may not be common, but at the very least Google will return | |
# an invalid grant when a user is suspended | |
if error == 'invalid_grant': | |
raise IdentityNotValid(formatted_error) | |
if req.status_code != 200: | |
raise Exception(formatted_error) | |
auth_identity.data.update(self.get_oauth_data(payload)) | |
auth_identity.update(data=auth_identity.data) |
We can probably do something really similar here and whatever needs to call refresh can do `identity.get_provider().refresh_identity(identity)
0d2844b
to
dfce3d0
Compare
raise IdentityNotValid(formatted_error) | ||
|
||
if req.status_code != 200: | ||
raise Exception(formatted_error) |
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 error handling is the same code from the old oauth code. I was wondering if that was still the desired behavior here?
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.
Yeah this is fine I think, we probably want to know what the error is
src/sentry/integrations/client.py
Outdated
super(OAuth2ApiClient, self).__init__(*args, **kwargs) | ||
self.identity = identity | ||
|
||
def check_auth(self): |
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.
This is small, but I sort of wish I had a different name for this. Open to suggestions.
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 name sounds good to me actually
|
||
def request(self, method, path, data=None, params=None): | ||
self.check_auth() |
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 really wanted to put this in the OAuth2ApiClient
class, but I didn't also want to have to go back and alter the headers/url/whatever the subclass using the old identity did.So, I felt this was the best option. Open to suggestions.
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.
This looks good to me :)
src/sentry/identity/oauth2.py
Outdated
@@ -102,6 +119,48 @@ def get_oauth_data(self, payload): | |||
|
|||
return data | |||
|
|||
def refresh_identity(self, auth_identity): |
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 I'd change the parameter name from auth_identity
to just identity
, since where this code was previously was specific to auth identities (which are still a separate thing)
src/sentry/identity/oauth2.py
Outdated
if not refresh_token: | ||
raise IdentityNotValid('Missing refresh token') | ||
|
||
data = self.get_refresh_token_params( |
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.
nit: Does this need to be on multiple lines?
src/sentry/identity/oauth2.py
Outdated
@@ -76,6 +80,12 @@ def get_oauth_client_secret(self): | |||
def get_oauth_scopes(self): | |||
return self.config.get('oauth_scopes', self.oauth_scopes) | |||
|
|||
def get_oauth_refresh_token(self): | |||
return self.config.get('refresh_token') |
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.
This seems a little strange, why would the refresh token be something that comes from the configuration object that's passed to the provider?
I'm not sure if this is used anywhere, maybe it's just some code that didn't get removed?
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.
Good catch. I'll remove it.
raise IdentityNotValid(formatted_error) | ||
|
||
if req.status_code != 200: | ||
raise Exception(formatted_error) |
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.
Yeah this is fine I think, we probably want to know what the error is
src/sentry/identity/vsts/provider.py
Outdated
@@ -16,7 +17,7 @@ class VSTSIdentityProvider(OAuth2Provider): | |||
|
|||
oauth_access_token_url = 'https://app.vssps.visualstudio.com/oauth2/token' | |||
oauth_authorize_url = 'https://app.vssps.visualstudio.com/oauth2/authorize' | |||
|
|||
oauth_redirect_url = '/extensions/vsts/setup/' |
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 identity provider has redirect URLs abstracted from it, I think this is breaking that abstraction. For example you can see her how it works
sentry/src/sentry/identity/oauth2.py
Line 144 in ea1b647
redirect_uri=absolute_uri(pipeline.redirect_url()), |
However in this case we have access to the pipeline from the dispatched method.
Reading further though I'd like to understand more why we actually need this, checkout some of my following comments.
src/sentry/identity/vsts/provider.py
Outdated
'client_assertion': self.get_oauth_client_secret(), | ||
'grant_type': 'refresh_token', | ||
'assertion': refresh_token, | ||
'redirect_uri': absolute_uri(self.oauth_redirect_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.
I'm not sure I understand why there's this redirect URL? Doesn't this call happen from our server?
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.
So @evanpurkhiser it's a VSTS thing. If you check out the docs the redirect URL is used as a check. The redirect URL is required to match the redirect url that was registered within the app, so passing it is required. This is true even in the case you're refreshing the token.
More here: https://docs.microsoft.com/en-us/vsts/integrate/get-started/authentication/oauth?view=vsts
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.
Let me know if there's a better way of doing it. This was the best I could think to make the minimal changes possible
src/sentry/integrations/client.py
Outdated
class OAuth2ApiClient(ApiClient): | ||
def __init__(self, identity, *args, **kwargs): | ||
super(OAuth2ApiClient, self).__init__(*args, **kwargs) | ||
self.identity = identity |
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 wonder if maybe we could actually make this more of a mixin OAuth2RefreshingMixin
, so instead of this extending ApiClient, it's just a mixin that has this functionality. Since this also ties the client specifically to using identities.
src/sentry/integrations/client.py
Outdated
super(OAuth2ApiClient, self).__init__(*args, **kwargs) | ||
self.identity = identity | ||
|
||
def check_auth(self): |
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 name sounds good to me actually
|
||
def request(self, method, path, data=None, params=None): | ||
self.check_auth() |
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.
This looks good to me :)
@@ -100,7 +97,7 @@ class VstsIntegrationProvider(IntegrationProvider): | |||
|
|||
def get_pipeline_views(self): | |||
identity_pipeline_config = { | |||
'redirect_url': absolute_uri('/extensions/vsts/setup/'), | |||
'redirect_url': absolute_uri(VSTSIdentityProvider.oauth_redirect_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.
Actually we want to keep this hardcoded here I think, since the extensions/vsts/setup
is integration specific, where as identity related things not specific to integrations.
…dentity field would be updated as well.
2dea6eb
to
c66c039
Compare
No description provided.