Skip to content
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

Merged
merged 16 commits into from
Jun 20, 2018
Merged

feat(integrations): Vsts OAuth refresh #8725

merged 16 commits into from
Jun 20, 2018

Conversation

lauryndbrown
Copy link
Contributor

@lauryndbrown lauryndbrown commented Jun 13, 2018

No description provided.

@@ -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):
Copy link
Contributor Author

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.

Copy link
Member

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?

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)

raise IdentityNotValid(formatted_error)

if req.status_code != 200:
raise Exception(formatted_error)
Copy link
Contributor Author

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?

Copy link
Member

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

super(OAuth2ApiClient, self).__init__(*args, **kwargs)
self.identity = identity

def check_auth(self):
Copy link
Contributor Author

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.

Copy link
Member

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()
Copy link
Contributor Author

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.

Copy link
Member

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 :)

@@ -102,6 +119,48 @@ def get_oauth_data(self, payload):

return data

def refresh_identity(self, auth_identity):
Copy link
Member

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)

if not refresh_token:
raise IdentityNotValid('Missing refresh token')

data = self.get_refresh_token_params(
Copy link
Member

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?

@@ -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')
Copy link
Member

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?

Copy link
Contributor Author

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)
Copy link
Member

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

@@ -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/'
Copy link
Member

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

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.

'client_assertion': self.get_oauth_client_secret(),
'grant_type': 'refresh_token',
'assertion': refresh_token,
'redirect_uri': absolute_uri(self.oauth_redirect_url),
Copy link
Member

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?

Copy link
Contributor Author

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

Copy link
Contributor Author

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

class OAuth2ApiClient(ApiClient):
def __init__(self, identity, *args, **kwargs):
super(OAuth2ApiClient, self).__init__(*args, **kwargs)
self.identity = identity
Copy link
Member

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.

super(OAuth2ApiClient, self).__init__(*args, **kwargs)
self.identity = identity

def check_auth(self):
Copy link
Member

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()
Copy link
Member

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),
Copy link
Member

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.

@lauryndbrown lauryndbrown merged commit e82d127 into master Jun 20, 2018
@lauryndbrown lauryndbrown deleted the vsts-auth-refresh branch June 20, 2018 17:48
@github-actions GitHub Actions bot locked and limited conversation to collaborators Dec 21, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants