-
-
Notifications
You must be signed in to change notification settings - Fork 4.6k
feat(auth): Pull in sentry-google-auth #11769
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
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| from __future__ import absolute_import | ||
|
|
||
| from sentry import auth, options | ||
|
|
||
| from .provider import GoogleOAuth2Provider | ||
|
|
||
| auth.register('google', GoogleOAuth2Provider) | ||
|
|
||
| options.register( | ||
| 'auth-google.client-id', | ||
| flags=options.FLAG_ALLOW_EMPTY | options.FLAG_PRIORITIZE_DISK, | ||
| ) | ||
| options.register( | ||
| 'auth-google.client-secret', | ||
| flags=options.FLAG_ALLOW_EMPTY | options.FLAG_PRIORITIZE_DISK, | ||
| ) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| from __future__ import absolute_import, print_function | ||
|
|
||
| from django.conf import settings | ||
|
|
||
|
|
||
| AUTHORIZE_URL = 'https://accounts.google.com/o/oauth2/auth' | ||
|
|
||
| ACCESS_TOKEN_URL = 'https://www.googleapis.com/oauth2/v4/token' | ||
|
|
||
| ERR_INVALID_DOMAIN = 'The domain for your Google account (%s) is not allowed to authenticate with this provider.' | ||
|
|
||
| ERR_INVALID_RESPONSE = 'Unable to fetch user information from Google. Please check the log.' | ||
|
|
||
| SCOPE = 'email' | ||
|
|
||
| DOMAIN_BLOCKLIST = frozenset(getattr(settings, 'GOOGLE_DOMAIN_BLOCKLIST', ['gmail.com']) or []) | ||
|
|
||
| DATA_VERSION = '1' |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,116 @@ | ||
| from __future__ import absolute_import, print_function | ||
|
|
||
| from sentry import options | ||
| from sentry.auth.provider import MigratingIdentityId | ||
| from sentry.auth.providers.oauth2 import ( | ||
| OAuth2Callback, OAuth2Provider, OAuth2Login | ||
| ) | ||
|
|
||
| from .constants import ( | ||
| AUTHORIZE_URL, ACCESS_TOKEN_URL, DATA_VERSION, SCOPE | ||
| ) | ||
| from .views import FetchUser, GoogleConfigureView | ||
|
|
||
|
|
||
| class GoogleOAuth2Login(OAuth2Login): | ||
| authorize_url = AUTHORIZE_URL | ||
| scope = SCOPE | ||
|
|
||
| def __init__(self, client_id, domains=None): | ||
| self.domains = domains | ||
| super(GoogleOAuth2Login, self).__init__(client_id=client_id) | ||
|
|
||
| def get_authorize_params(self, state, redirect_uri): | ||
| params = super(GoogleOAuth2Login, self).get_authorize_params( | ||
| state, redirect_uri | ||
| ) | ||
| # TODO(dcramer): ideally we could look at the current resulting state | ||
| # when an existing auth happens, and if they're missing a refresh_token | ||
| # we should re-prompt them a second time with ``approval_prompt=force`` | ||
| params['approval_prompt'] = 'force' | ||
| params['access_type'] = 'offline' | ||
| return params | ||
|
|
||
|
|
||
| class GoogleOAuth2Provider(OAuth2Provider): | ||
| name = 'Google' | ||
|
|
||
| def __init__(self, domain=None, domains=None, version=None, **config): | ||
| if domain: | ||
| if domains: | ||
| domains.append(domain) | ||
| else: | ||
| domains = [domain] | ||
| self.domains = domains | ||
| # if a domain is not configured this is part of the setup pipeline | ||
| # this is a bit complex in Sentry's SSO implementation as we don't | ||
| # provide a great way to get initial state for new setup pipelines | ||
| # vs missing state in case of migrations. | ||
| if domains is None: | ||
| version = DATA_VERSION | ||
| else: | ||
| version = None | ||
| self.version = version | ||
| super(GoogleOAuth2Provider, self).__init__(**config) | ||
|
|
||
| def get_client_id(self): | ||
| return options.get('auth-google.client-id') | ||
|
|
||
| def get_client_secret(self): | ||
| return options.get('auth-google.client-secret') | ||
|
|
||
| def get_configure_view(self): | ||
| return GoogleConfigureView.as_view() | ||
|
|
||
| def get_auth_pipeline(self): | ||
| return [ | ||
| GoogleOAuth2Login(domains=self.domains, client_id=self.get_client_id()), | ||
| OAuth2Callback( | ||
| access_token_url=ACCESS_TOKEN_URL, | ||
| client_id=self.get_client_id(), | ||
| client_secret=self.get_client_secret(), | ||
| ), | ||
| FetchUser( | ||
| domains=self.domains, | ||
| version=self.version, | ||
| ), | ||
| ] | ||
|
|
||
| def get_refresh_token_url(self): | ||
| return ACCESS_TOKEN_URL | ||
|
|
||
| def build_config(self, state): | ||
| return { | ||
| 'domains': [state['domain']], | ||
| 'version': DATA_VERSION, | ||
| } | ||
|
|
||
| def build_identity(self, state): | ||
| # https://developers.google.com/identity/protocols/OpenIDConnect#server-flow | ||
| # data.user => { | ||
| # "iss":"accounts.google.com", | ||
| # "at_hash":"HK6E_P6Dh8Y93mRNtsDB1Q", | ||
| # "email_verified":"true", | ||
| # "sub":"10769150350006150715113082367", | ||
| # "azp":"1234987819200.apps.googleusercontent.com", | ||
| # "email":"jsmith@example.com", | ||
| # "aud":"1234987819200.apps.googleusercontent.com", | ||
| # "iat":1353601026, | ||
| # "exp":1353604926, | ||
| # "hd":"example.com" | ||
| # } | ||
| data = state['data'] | ||
| user_data = state['user'] | ||
|
|
||
| # XXX(epurkhiser): We initially were using the email as the id key. | ||
| # This caused account dupes on domain changes. Migrate to the | ||
| # account-unique sub key. | ||
| user_id = MigratingIdentityId(id=user_data['sub'], legacy_id=user_data['email']) | ||
|
|
||
| return { | ||
| 'id': user_id, | ||
| 'email': user_data['email'], | ||
| 'name': user_data['email'], | ||
| 'data': self.get_oauth_data(data), | ||
| 'email_verified': user_data['email_verified'], | ||
| } |
3 changes: 3 additions & 0 deletions
3
src/sentry/auth/providers/google/templates/sentry_auth_google/configure.html
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| <h3>Domain</h3> | ||
|
|
||
| <p>Users will be allowed to authenticate if they have an account under the Google Apps domain <strong>{{ domains|join:" or " }}</strong>.</p> | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| from __future__ import absolute_import, print_function | ||
|
|
||
| import base64 | ||
|
|
||
|
|
||
| def urlsafe_b64decode(b64string): | ||
| padded = b64string + b'=' * (4 - len(b64string) % 4) | ||
| return base64.urlsafe_b64decode(padded) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,81 @@ | ||
| from __future__ import absolute_import, print_function | ||
|
|
||
| import logging | ||
|
|
||
| from sentry.auth.view import AuthView, ConfigureView | ||
| from sentry.utils import json | ||
|
|
||
| from .constants import ( | ||
| DOMAIN_BLOCKLIST, ERR_INVALID_DOMAIN, ERR_INVALID_RESPONSE, | ||
| ) | ||
| from .utils import urlsafe_b64decode | ||
|
|
||
| logger = logging.getLogger('sentry.auth.google') | ||
|
|
||
|
|
||
| class FetchUser(AuthView): | ||
| def __init__(self, domains, version, *args, **kwargs): | ||
| self.domains = domains | ||
| self.version = version | ||
| super(FetchUser, self).__init__(*args, **kwargs) | ||
|
|
||
| def dispatch(self, request, helper): | ||
| data = helper.fetch_state('data') | ||
|
|
||
| try: | ||
| id_token = data['id_token'] | ||
| except KeyError: | ||
| logger.error('Missing id_token in OAuth response: %s' % data) | ||
| return helper.error(ERR_INVALID_RESPONSE) | ||
|
|
||
| try: | ||
| _, payload, _ = map(urlsafe_b64decode, id_token.split('.', 2)) | ||
| except Exception as exc: | ||
| logger.error(u'Unable to decode id_token: %s' % exc, exc_info=True) | ||
| return helper.error(ERR_INVALID_RESPONSE) | ||
|
|
||
| try: | ||
| payload = json.loads(payload) | ||
| except Exception as exc: | ||
| logger.error(u'Unable to decode id_token payload: %s' % exc, exc_info=True) | ||
| return helper.error(ERR_INVALID_RESPONSE) | ||
|
|
||
| if not payload.get('email'): | ||
| logger.error('Missing email in id_token payload: %s' % id_token) | ||
| return helper.error(ERR_INVALID_RESPONSE) | ||
|
|
||
| # support legacy style domains with pure domain regexp | ||
| if self.version is None: | ||
| domain = extract_domain(payload['email']) | ||
| else: | ||
| domain = payload.get('hd') | ||
|
|
||
| if domain is None: | ||
| return helper.error(ERR_INVALID_DOMAIN % (domain,)) | ||
|
|
||
| if domain in DOMAIN_BLOCKLIST: | ||
| return helper.error(ERR_INVALID_DOMAIN % (domain,)) | ||
|
|
||
| if self.domains and domain not in self.domains: | ||
| return helper.error(ERR_INVALID_DOMAIN % (domain,)) | ||
|
|
||
| helper.bind_state('domain', domain) | ||
| helper.bind_state('user', payload) | ||
|
|
||
| return helper.next_step() | ||
|
|
||
|
|
||
| class GoogleConfigureView(ConfigureView): | ||
| def dispatch(self, request, organization, auth_provider): | ||
| config = auth_provider.config | ||
| if config.get('domain'): | ||
| domains = [config['domain']] | ||
| else: | ||
| domains = config.get('domains') | ||
| return self.render('sentry_auth_google/configure.html', { | ||
dcramer marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| 'domains': domains or [], | ||
| }) | ||
|
|
||
|
|
||
| def extract_domain(email): | ||
| return email.rsplit('@', 1)[-1] | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,73 @@ | ||
| from __future__ import absolute_import | ||
|
|
||
| import pytest | ||
|
|
||
| from sentry.auth.exceptions import IdentityNotValid | ||
| from sentry.models import AuthIdentity, AuthProvider | ||
| from sentry.testutils import TestCase | ||
|
|
||
| from sentry.auth.providers.google.constants import DATA_VERSION | ||
|
|
||
|
|
||
| class GoogleOAuth2ProviderTest(TestCase): | ||
| def setUp(self): | ||
| self.org = self.create_organization(owner=self.user) | ||
| self.user = self.create_user('foo@example.com') | ||
| self.auth_provider = AuthProvider.objects.create( | ||
| provider='google', | ||
| organization=self.org, | ||
| ) | ||
| super(GoogleOAuth2ProviderTest, self).setUp() | ||
|
|
||
| def test_refresh_identity_without_refresh_token(self): | ||
| auth_identity = AuthIdentity.objects.create( | ||
| auth_provider=self.auth_provider, | ||
| user=self.user, | ||
| data={ | ||
| 'access_token': 'access_token', | ||
| } | ||
| ) | ||
|
|
||
| provider = self.auth_provider.get_provider() | ||
|
|
||
| with pytest.raises(IdentityNotValid): | ||
| provider.refresh_identity(auth_identity) | ||
|
|
||
| def test_handles_multiple_domains(self): | ||
| self.auth_provider.update( | ||
| config={'domains': ['example.com']}, | ||
| ) | ||
|
|
||
| provider = self.auth_provider.get_provider() | ||
| assert provider.domains == ['example.com'] | ||
|
|
||
| def test_handles_legacy_single_domain(self): | ||
| self.auth_provider.update( | ||
| config={'domain': 'example.com'}, | ||
| ) | ||
|
|
||
| provider = self.auth_provider.get_provider() | ||
| assert provider.domains == ['example.com'] | ||
|
|
||
| def test_build_config(self): | ||
| provider = self.auth_provider.get_provider() | ||
| state = { | ||
| 'domain': 'example.com', | ||
| 'user': { | ||
| 'iss': 'accounts.google.com', | ||
| 'at_hash': 'HK6E_P6Dh8Y93mRNtsDB1Q', | ||
| 'email_verified': 'true', | ||
| 'sub': '10769150350006150715113082367', | ||
| 'azp': '1234987819200.apps.googleusercontent.com', | ||
| 'email': 'jsmith@example.com', | ||
| 'aud': '1234987819200.apps.googleusercontent.com', | ||
| 'iat': 1353601026, | ||
| 'exp': 1353604926, | ||
| 'hd': 'example.com' | ||
| }, | ||
| } | ||
| result = provider.build_config(state) | ||
| assert result == { | ||
| 'domains': ['example.com'], | ||
| 'version': DATA_VERSION, | ||
| } |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
Would be nice to see this template live with other templates, but this is fine 🤷♂️
This comment was marked as resolved.
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
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.
imo something we can follow up on - theres improvements we need to make to SSO for onpremise at some point anyways