-
Notifications
You must be signed in to change notification settings - Fork 565
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Allowing id token on oauth for appengine (#4314)
### Motivation In order to get healthchecks on appengine handlers, we need to use GCP Uptimes. They authenticate through an [oauth id token](https://cloud.google.com/monitoring/uptime-checks#create). As things currently stand, oauth on appengine only supportes access tokens, so this PR solves that. ### Alternatives considered It would be ideal if we only did one api call, either to validade an id or access token. However, the [specification](https://auth0.com/docs/secure/tokens/id-tokens/id-token-structure) for oauth2 does not present a standard way of, given a token, differentiating between the two. For that reason, two api calls to GCP are made. Part of #4271
- Loading branch information
1 parent
cb68312
commit 6ab160f
Showing
2 changed files
with
115 additions
and
49 deletions.
There are no files selected for viewing
This file contains 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 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 |
---|---|---|
|
@@ -494,13 +494,74 @@ def test_no_header(self): | |
self.assertEqual(0, self.mock.get_email_and_access_token.call_count) | ||
|
||
|
||
class TestValidateToken(unittest.TestCase): | ||
"""Test the ability to get an access token from either an id or acces JWT.""" | ||
|
||
def setUp(self): | ||
test_helpers.patch(self, ['requests.get']) | ||
|
||
def _assert_requests_get_call(self, token_type, token): | ||
assert token_type in ['access_token', 'id_token'] | ||
self.mock.get.assert_has_calls([ | ||
mock.call( | ||
'https://www.googleapis.com/oauth2/v3/tokeninfo', | ||
params={token_type: token}, | ||
timeout=30) | ||
]) | ||
|
||
def test_gets_response_when_id_token_is_valid(self): | ||
"""Tests the case when the id token is valid.""" | ||
mocked_response = mock.Mock( | ||
status_code=200, | ||
text=json.dumps({ | ||
'email': '[email protected]', | ||
'email_verified': True | ||
})) | ||
|
||
self.mock.get.return_value = mocked_response | ||
actual_response = handler.validate_token('Bearer Token') | ||
assert actual_response == mocked_response | ||
self._assert_requests_get_call('id_token', 'Token') | ||
|
||
def test_gets_response_when_id_token_is_invalid_and_access_token_is_valid( | ||
self): | ||
"""Tests the case when the access token is valid.""" | ||
test_helpers.patch(self, ['libs.handler.validate_id_token']) | ||
mock_response_access_token = mock.Mock( | ||
status_code=200, | ||
text=json.dumps({ | ||
'email': '[email protected]', | ||
'email_verified': True | ||
})) | ||
self.mock.validate_id_token.return_value = None | ||
self.mock.get.return_value = mock_response_access_token | ||
|
||
actual_response = handler.validate_token('Bearer Token') | ||
assert actual_response == mock_response_access_token | ||
self._assert_requests_get_call('access_token', 'Token') | ||
|
||
def test_bad_status(self): | ||
"""Test bad status.""" | ||
# Applies to both id_token and access_token | ||
self.mock.get.return_value = mock.Mock(status_code=403) | ||
|
||
with self.assertRaises(helpers.UnauthorizedError) as cm: | ||
handler.get_email_and_access_token('Bearer AccessToken') | ||
self.assertEqual(401, cm.exception.status) | ||
self.assertEqual( | ||
('Failed to authorize. The Authorization header (Bearer AccessToken)' | ||
' is neither a valid id or access token.'), str(cm.exception)) | ||
self._assert_requests_get_call('id_token', 'AccessToken') | ||
self._assert_requests_get_call('access_token', 'AccessToken') | ||
|
||
|
||
class TestGetEmailAndAccessToken(unittest.TestCase): | ||
"""Test get_email_and_access_token.""" | ||
|
||
def setUp(self): | ||
test_helpers.patch(self, [ | ||
'clusterfuzz._internal.config.local_config._load_yaml_file', | ||
'requests.get', | ||
'libs.handler.validate_token', | ||
]) | ||
|
||
self.mock._load_yaml_file.side_effect = mocked_load_yaml_file # pylint: disable=protected-access | ||
|
@@ -510,48 +571,35 @@ def setUp(self): | |
'whitelisted_oauth_client_ids') | ||
self.test_whitelisted_oauth_emails = config.get('whitelisted_oauth_emails') | ||
|
||
def _assert_requests_get_call(self): | ||
self.assertEqual(1, self.mock.get.call_count) | ||
self.mock.get.assert_has_calls([ | ||
mock.call( | ||
'https://www.googleapis.com/oauth2/v3/tokeninfo', | ||
params={'access_token': 'AccessToken'}, | ||
timeout=30) | ||
]) | ||
self.mock.get.reset_mock() | ||
|
||
def test_allowed_bearer(self): | ||
"""Test allowing Bearer.""" | ||
for aud in self.test_whitelisted_oauth_client_ids: | ||
self.mock.get.return_value = mock.Mock( | ||
mocked_response = mock.Mock( | ||
status_code=200, | ||
text=json.dumps({ | ||
'aud': aud, | ||
'email': '[email protected]', | ||
'email_verified': True | ||
})) | ||
|
||
self.mock.validate_token.return_value = mocked_response | ||
email, token = handler.get_email_and_access_token('Bearer AccessToken') | ||
self.assertEqual('[email protected]', email) | ||
self.assertEqual('Bearer AccessToken', token) | ||
self._assert_requests_get_call() | ||
|
||
def test_allow_whitelised_accounts(self): | ||
"""Test allow compute engine service account.""" | ||
for email in self.test_whitelisted_oauth_emails: | ||
self.mock.get.reset_mock() | ||
self.mock.get.return_value = mock.Mock( | ||
mocked_response = mock.Mock( | ||
status_code=200, | ||
text=json.dumps({ | ||
'email_verified': True, | ||
'email': email | ||
})) | ||
|
||
self.mock.validate_token.return_value = mocked_response | ||
returned_email, token = handler.get_email_and_access_token( | ||
'Bearer AccessToken') | ||
self.assertEqual(email, returned_email) | ||
self.assertEqual('Bearer AccessToken', token) | ||
self._assert_requests_get_call() | ||
|
||
def test_invalid_authorization_header(self): | ||
"""Test invalid authorization header.""" | ||
|
@@ -562,65 +610,50 @@ def test_invalid_authorization_header(self): | |
self.assertEqual( | ||
'The Authorization header is invalid. It should have been started with' | ||
" 'Bearer '.", str(cm.exception)) | ||
self.assertEqual(0, self.mock.get.call_count) | ||
|
||
def test_bad_status(self): | ||
"""Test bad status.""" | ||
self.mock.get.return_value = mock.Mock(status_code=403) | ||
|
||
with self.assertRaises(helpers.UnauthorizedError) as cm: | ||
handler.get_email_and_access_token('Bearer AccessToken') | ||
self.assertEqual(401, cm.exception.status) | ||
self.assertEqual( | ||
('Failed to authorize. The Authorization header (Bearer AccessToken)' | ||
' might be invalid.'), str(cm.exception)) | ||
self._assert_requests_get_call() | ||
|
||
def test_invalid_json(self): | ||
"""Test invalid json.""" | ||
self.mock.get.return_value = mock.Mock(status_code=200, text='test') | ||
self.mock.validate_token.return_value = mock.Mock( | ||
status_code=200, text='test') | ||
|
||
with self.assertRaises(helpers.EarlyExitError) as cm: | ||
handler.get_email_and_access_token('Bearer AccessToken') | ||
self.assertEqual(500, cm.exception.status) | ||
self.assertEqual('Parsing the JSON response body failed: test', | ||
str(cm.exception)) | ||
self._assert_requests_get_call() | ||
|
||
def test_invalid_client_id(self): | ||
"""Test the invalid client id.""" | ||
self.mock.get.return_value = mock.Mock( | ||
mock_response = mock.Mock( | ||
status_code=200, | ||
text=json.dumps({ | ||
'aud': 'InvalidClientId', | ||
'email': '[email protected]', | ||
'email_verified': False | ||
})) | ||
|
||
self.mock.validate_token.return_value = mock_response | ||
with self.assertRaises(helpers.EarlyExitError) as cm: | ||
handler.get_email_and_access_token('Bearer AccessToken') | ||
self.assertEqual(401, cm.exception.status) | ||
self.assertIn( | ||
"The access token doesn't belong to one of the allowed OAuth clients", | ||
str(cm.exception)) | ||
self._assert_requests_get_call() | ||
|
||
def test_unverified_email(self): | ||
"""Test unverified email.""" | ||
self.mock.get.return_value = mock.Mock( | ||
mocked_response = mock.Mock( | ||
status_code=200, | ||
text=json.dumps({ | ||
'aud': 'test-cf-tools.apps.googleusercontent.com', | ||
'email': '[email protected]', | ||
'email_verified': False | ||
})) | ||
|
||
self.mock.validate_token.return_value = mocked_response | ||
with self.assertRaises(helpers.EarlyExitError) as cm: | ||
handler.get_email_and_access_token('Bearer AccessToken') | ||
self.assertEqual(401, cm.exception.status) | ||
self.assertIn('The email ([email protected]) is not verified', | ||
str(cm.exception)) | ||
self._assert_requests_get_call() | ||
|
||
|
||
class AllowedCorsHandlerTest(unittest.TestCase): | ||
|