diff --git a/etc/storyboard.conf.sample b/etc/storyboard.conf.sample index 1b65dbb0..ba751738 100644 --- a/etc/storyboard.conf.sample +++ b/etc/storyboard.conf.sample @@ -60,6 +60,9 @@ lock_path = $state_path/lock # Time in seconds before an refresh_token expires # refresh_token_ttl = 604800 +# A list of valid client id's that may connect to StoryBoard. +# valid_oauth_clients = storyboard.openstack.org, localhost + [cron] # Storyboard's cron management configuration diff --git a/storyboard/api/auth/__init__.py b/storyboard/api/auth/__init__.py index e5e42151..e5d49343 100644 --- a/storyboard/api/auth/__init__.py +++ b/storyboard/api/auth/__init__.py @@ -33,7 +33,12 @@ OAUTH_OPTS = [ cfg.IntOpt("refresh_token_ttl", default=60 * 60 * 24 * 7, # One week - help="Time in seconds before an refresh_token expires") + help="Time in seconds before an refresh_token expires"), + + cfg.ListOpt("valid_oauth_clients", + default=['storyboard.openstack.org', 'localhost'], + help="A list of valid client id's that may connect to " + "StoryBoard.") ] CONF.register_opts(OAUTH_OPTS, "oauth") @@ -45,6 +50,7 @@ class ErrorMessages(object): NO_RESPONSE_TYPE = _('You did not provide a response_type.') INVALID_RESPONSE_TYPE = _('response_type must be \'code\'') NO_CLIENT_ID = _('You did not provide a client_id.') + INVALID_CLIENT_ID = _('You did not provide a valid client_id.') NO_REDIRECT_URI = _('You did not provide a redirect_uri.') INVALID_REDIRECT_URI = _('You did not provide a valid redirect_uri.') diff --git a/storyboard/api/auth/openid_client.py b/storyboard/api/auth/openid_client.py index 45983807..e1b215b5 100644 --- a/storyboard/api/auth/openid_client.py +++ b/storyboard/api/auth/openid_client.py @@ -25,6 +25,7 @@ from storyboard.common.exception import AccessDenied from storyboard.common.exception import InvalidClient from storyboard.common.exception import InvalidRequest from storyboard.common.exception import InvalidScope +from storyboard.common.exception import UnauthorizedClient from storyboard.common.exception import UnsupportedResponseType @@ -61,6 +62,9 @@ class OpenIdClient(object): if not client_id: raise InvalidClient(redirect_uri=redirect_uri, message=e_msg.NO_CLIENT_ID) + if client_id not in CONF.oauth.valid_oauth_clients: + raise UnauthorizedClient(redirect_uri=redirect_uri, + message=e_msg.INVALID_CLIENT_ID) # Sanity Check: scope if not scope: diff --git a/storyboard/tests/api/auth/test_oauth.py b/storyboard/tests/api/auth/test_oauth.py index 95d87084..9795af1a 100644 --- a/storyboard/tests/api/auth/test_oauth.py +++ b/storyboard/tests/api/auth/test_oauth.py @@ -195,6 +195,27 @@ class TestOAuthAuthorize(BaseOAuthTest): error='invalid_client', error_description=e_msg.NO_CLIENT_ID) + def test_authorize_invalid_client(self): + """Assert that an invalid client redirects back to the + redirect_uri and provides the expected error response. + """ + invalid_params = self.valid_params.copy() + invalid_params['client_id'] = 'invalid_client' + + # Simple GET with invalid code parameters + random_state = six.text_type(uuid.uuid4()) + response = self.get_json(path='/openid/authorize', + expect_errors=True, + state=random_state, + **invalid_params) + + # Validate the error response + self.assertValidRedirect(response=response, + expected_status_code=302, + redirect_uri=invalid_params['redirect_uri'], + error='unauthorized_client', + error_description=e_msg.INVALID_CLIENT_ID) + def test_authorize_invalid_scope(self): """Assert that an invalid scope redirects back to the redirect_uri and provides the expected error response.