def testClientAttributeTypes(self): """ Ensure that all attributes of the client are of the expected type. """ client = PasswordClient('clientId', ['https://valid.nonexistent'], ['password'], 'secret') self.assertTrue(isAnyStr(client.id), msg='The client id must be a string.') self.assertIsInstance(client.secret, str, message='The client secret must be a string.') self.assertIsInstance(client.redirectUris, list, message='The redirect uris must be a list.') for uri in client.redirectUris: self.assertIsInstance(uri, str, message='All redirect uris must be strings.') self.assertIsInstance( client.authorizedGrantTypes, list, message='The authorized grant types must be a list.') for grantType in client.authorizedGrantTypes: self.assertIsInstance(grantType, str, message='All grant types must be strings.') client = PasswordClient(u'clientId', ['https://valid.nonexistent'], ['password'], 'secret') self.assertTrue(isAnyStr(client.id), msg='The client id must be a string.')
def testCustomResponseTypeNotAllowed(self): """ Test that a request with a custom response type is rejected if it is not enabled. """ responseType = 'myCustomResponseType' state = b'state\xFF\xFF' client = PasswordClient('customResponseTypeClientNotAllowed', ['https://redirect.noexistent'], [responseType], 'clientSecret') redirectUri = client.redirectUris[0] parameters = { 'response_type': responseType, 'client_id': client.id, 'redirect_uri': redirectUri, 'scope': 'All', 'state': state } request = self.createAuthRequest(arguments=parameters) self._CLIENT_STORAGE.addClient(client) result = self._AUTH_RESOURCE.render_GET(request) self.assertFailedRequest( request, result, UnsupportedResponseTypeError(responseType, state), redirectUri=redirectUri, msg='Expected the authorization token resource to reject a ' 'request with a custom response type that is not allowed.')
def testGrantAccessCustomResponseType(self): """ Test that grantAccess rejects a call for a request with a custom response type. """ responseType = 'myCustomResponseType' state = b'state\xFF\xFF' client = PasswordClient('customResponseTypeClientGrantAccess', ['https://redirect.noexistent'], [responseType], 'clientSecret') dataKey = 'customResponseTypeDataKey' self._PERSISTENT_STORAGE.put( dataKey, { 'response_type': responseType, 'client_id': client.id, 'redirect_uri': client.redirectUris[0], 'scope': 'All', 'state': state }) request = MockRequest('GET', 'some/path') self._CLIENT_STORAGE.addClient(client) self.assertRaises(ValueError, self._AUTH_RESOURCE.grantAccess, request, dataKey) try: self.assertEqual( self._AUTH_RESOURCE.requestDataLifetime, self._PERSISTENT_STORAGE.getExpireTime(dataKey), msg='Expected the data to be stored with the expected lifetime.' ) self._PERSISTENT_STORAGE.pop(dataKey) except KeyError: self.fail( 'Expected the data to still be in the persistent storage.')
def testCustomResponseTypeUnauthorizedClient(self): """ Test that a request with a custom response type is rejected if the client is not authorized to use that response type. """ responseType = 'myCustomResponseType' state = b'state\xFF\xFF' client = PasswordClient('customResponseTypeClientUnauthorized', ['https://redirect.noexistent'], [], 'clientSecret') redirectUri = client.redirectUris[0] parameters = { 'response_type': responseType, 'client_id': client.id, 'redirect_uri': redirectUri, 'scope': 'All', 'state': state } request = self.createAuthRequest(arguments=parameters) self._CLIENT_STORAGE.addClient(client) authResource = self.TestOAuth2Resource( self._TOKEN_FACTORY, self._PERSISTENT_STORAGE, self._CLIENT_STORAGE, authTokenStorage=self._TOKEN_STORAGE, grantTypes=[responseType]) result = authResource.render_GET(request) self.assertFailedRequest( request, result, UnauthorizedClientError(responseType, state), redirectUri=redirectUri, msg= 'Expected the authorization token resource to reject a request with a ' 'custom response type that the client is not allowed to use.')
def testCustomResponseType(self): """ Test that a request with a custom response type is accepted. """ responseType = 'myCustomResponseType' state = b'state\xFF\xFF' client = PasswordClient('customResponseTypeClient', ['https://redirect.noexistent'], [responseType], 'clientSecret') parameters = { 'response_type': responseType, 'client_id': client.id, 'redirect_uri': client.redirectUris[0], 'scope': 'All', 'state': state } request = self.createAuthRequest(arguments=parameters) self._CLIENT_STORAGE.addClient(client) authResource = self.TestOAuth2Resource( self._TOKEN_FACTORY, self._PERSISTENT_STORAGE, self._CLIENT_STORAGE, authTokenStorage=self._TOKEN_STORAGE, grantTypes=[responseType]) result = authResource.render_GET(request) self.assertValidAuthRequest( request, result, parameters, msg='Expected the authorization token resource to accept ' 'a valid request with a custom response type.')
def getTestClient(): """ :return: A client to use for this example. """ return PasswordClient(clientId='test', redirectUris=['https://clientServer.com/return'], secret='test_secret', authorizedGrantTypes=[ GrantTypes.REFRESH_TOKEN, GrantTypes.AUTHORIZATION_CODE ])
def getTestClient(): """ :return: A client to use for this example. """ return PasswordClient(clientId='test', redirectUris=['https://clientServer.com/return'], secret='test_secret', authorizedGrantTypes=[ GrantTypes.RefreshToken, GrantTypes.AuthorizationCode ])
def getTestPasswordClient(clientId=None, authorizedGrantTypes=None): """ :param clientId: The client id or None for a random client id. :param authorizedGrantTypes: The grant types the clients will be authorized to use, None for all. :return: A dummy password client that can be used in the tests. """ if clientId is None: clientId = str(uuid4()) if authorizedGrantTypes is None: # noinspection PyTypeChecker authorizedGrantTypes = list(GrantTypes) return PasswordClient( clientId, ['https://return.nonexistent'], authorizedGrantTypes, secret='ClientSecret')
def testAddClient(self): """ Test if a client can be added to the client storage. """ client = PublicClient( 'newPublicClientId', ['https://return.nonexistent', 'https://return2.nonexistent'], [GrantTypes.REFRESH_TOKEN]) self._CLIENT_STORAGE.addClient(client) self.assertListEqual( self._CLIENT_STORAGE.getClient(client.id).authorizedGrantTypes, client.authorizedGrantTypes, msg= 'Expected the client storage to contain a client after adding him.' ) client = PasswordClient( 'newPasswordClientId', ['https://return.nonexistent', 'https://return2.nonexistent'], ['client_credentials'], 'newClientSecret') self._CLIENT_STORAGE.addClient(client) self.assertEqual( client.secret, self._CLIENT_STORAGE.getClient(client.id).secret, msg= 'Expected the client storage to contain a client after adding him.' )
def testInsecureRedirectUriClient(self): """ Test that a request with a non https redirect uri is accepted. """ state = b'state\xFF\xFF' client = PasswordClient('customResponseTypeClientUnauthorized', ['custom://callback'], [GrantTypes.AUTHORIZATION_CODE], 'clientSecret') redirectUri = client.redirectUris[0] parameters = { 'response_type': 'code', 'client_id': client.id, 'redirect_uri': redirectUri, 'scope': 'All', 'state': state } request = self.createAuthRequest(arguments=parameters) self._CLIENT_STORAGE.addClient(client) result = self._AUTH_RESOURCE.render_GET(request) self.assertValidAuthRequest( request, result, parameters, msg='Expected the authorization token resource to accept ' 'a valid request with a non https redirect uri.')
class AuthResourceTest(TwistedTestCase): """ Abstract base class for test targeting the OAuth2 resource. """ # noinspection HttpUrlsUsage _VALID_CLIENT = PasswordClient('authResourceClientId', [ 'https://return.nonexistent?param=retain', 'http://return.nonexistent/notSecure?param=retain' ], list(GrantTypes), secret='ClientSecret') _RESPONSE_GRANT_TYPE_MAPPING = { 'code': GrantTypes.AUTHORIZATION_CODE.value, 'token': GrantTypes.IMPLICIT.value } class TestOAuth2Resource(OAuth2): """ A test OAuth2 resource that returns the parameters given to onAuthenticate. """ raiseErrorInOnAuthenticate = False UNKNOWN_SCOPE = 'unknown' UNKNOWN_SCOPE_RETURN = 'unknown_return' UNKNOWN_SCOPE_RAISING_OAUTH2_ERROR = 'unknown_raise_oauth2_error' TEMPORARY_UNAVAILABLE_SCOPE = 'temporary_unavailable' ERROR_MESSAGE = 'Expected the auth resource to catch this error' def onAuthenticate(self, request, client, responseType, scope, redirectUri, state, dataKey): if self.raiseErrorInOnAuthenticate: self.raiseErrorInOnAuthenticate = False raise RuntimeError(self.ERROR_MESSAGE) if self.UNKNOWN_SCOPE in scope: raise InvalidScopeError(scope, state=state) if self.UNKNOWN_SCOPE_RETURN in scope: return InvalidScopeError(scope, state=state) if self.UNKNOWN_SCOPE_RAISING_OAUTH2_ERROR in scope: raise InvalidTokenError(self.ERROR_MESSAGE) if self.TEMPORARY_UNAVAILABLE_SCOPE in scope: raise TemporarilyUnavailableError(state=state) return request, client, responseType, scope, redirectUri, state, dataKey @classmethod def setUpClass(cls): super(Abstract.AuthResourceTest, cls).setUpClass() cls._TOKEN_FACTORY = TestTokenFactory() cls._TOKEN_STORAGE = DictTokenStorage() cls._PERSISTENT_STORAGE = TestPersistentStorage() cls._CLIENT_STORAGE = TestClientStorage() cls._CLIENT_STORAGE.addClient(cls._VALID_CLIENT) cls._AUTH_RESOURCE = cls.TestOAuth2Resource( cls._TOKEN_FACTORY, cls._PERSISTENT_STORAGE, cls._CLIENT_STORAGE, authTokenStorage=cls._TOKEN_STORAGE) def setUp(self): super(Abstract.AuthResourceTest, self).setUp() self._TOKEN_FACTORY.reset(self) @staticmethod def createAuthRequest(**kwargs): """ :param kwargs: Arguments to the request. :return: A GET request to the OAuth2 resource with the given arguments. """ return MockRequest('GET', 'oauth2', **kwargs) @staticmethod def getParameterFromRedirectUrl(url, parameterInFragment): """ :param url: The url that the resource redirected to. :param parameterInFragment: Whether the parameter should be in the fragment or in the query. :return: The parameter transmitted via the redirect url. """ if not isinstance(url, str): url = url.decode('utf-8') parsedUrl = urlparse(url) parameter = parse_qs( parsedUrl.fragment if parameterInFragment else parsedUrl.query) for key, value in parameter.items(): if len(value) == 1: parameter[key] = value[0] return parameter def assertRedirectsTo(self, request, redirectUri, msg): """ Assert that the request redirects to the given uri and retains the query parameters. :param request: The request that should redirect. :param redirectUri: The uri where the request should redirect to. :param msg: The assertion message. :return: The actual url the request is redirecting to. """ self.assertEqual( 302, request.responseCode, msg=msg + ': Expected the auth resource to redirect the resource owner.') redirectUrl = request.getResponseHeader(b'location') self.assertIsNotNone( redirectUrl, msg=msg + ': Expected the auth resource to redirect the resource owner.') parsedUrl = urlparse(redirectUrl) parsedUri = urlparse(redirectUri.encode('utf-8')) self.assertTrue( parsedUrl.scheme == parsedUri.scheme and parsedUrl.netloc == parsedUri.netloc and parsedUrl.path == parsedUri.path and parsedUrl.params == parsedUri.params, msg='{msg}: The auth token endpoint did not redirect ' 'the resource owner to the expected url: ' '{expected} <> {actual}'.format(msg=msg, expected=redirectUri, actual=redirectUrl)) self.assertIn( parsedUri.query, parsedUrl.query, msg=msg + ': Expected the redirect uri to contain the query parameters ' 'of the original redirect uri of the client.') return redirectUrl def assertFailedRequest(self, request, result, expectedError, msg=None, redirectUri=None, parameterInFragment=False): """ Assert that the request did not succeed and that the auth resource returned an appropriate error response. :param request: The request. :param result: The return value of the render_POST function of the token resource. :param expectedError: The expected error. :param msg: The assertion error message. :param redirectUri: The redirect uri of the client. :param parameterInFragment: If the error parameters are in the fragment of the redirect uri. """ if result == NOT_DONE_YET: result = request.getResponse() if msg.endswith('.'): msg = msg[:-1] self.assertFalse( isinstance(result, tuple), msg=msg + ': Expected the auth resource not to call onAuthenticate.') if redirectUri is not None: redirectUrl = self.assertRedirectsTo(request, redirectUri, msg) errorResult = self.getParameterFromRedirectUrl( redirectUrl, parameterInFragment) else: self.assertEqual( 'application/json;charset=UTF-8', request.getResponseHeader('Content-Type'), msg= 'Expected the auth resource to return an error in the json format.' ) self.assertEqual( 'no-store', request.getResponseHeader('Cache-Control'), msg= 'Expected the auth resource to set Cache-Control to "no-store".' ) self.assertEqual( 'no-cache', request.getResponseHeader('Pragma'), msg= 'Expected the auth resource to set Pragma to "no-cache".') self.assertEqual( expectedError.code, request.responseCode, msg='Expected the auth resource to return a response ' 'with the HTTP code {code}.'.format( code=expectedError.code)) errorResult = json.loads(result.decode('utf-8')) self.assertIn('error', errorResult, msg=msg + ': Missing error parameter in response.') self.assertEqual( expectedError.name, errorResult['error'], msg=msg + ': Result contained a different error than expected.') self.assertIn('error_description', errorResult, msg=msg + ': Missing error_description parameter in response.') if not isinstance(expectedError.description, (bytes, str)): self.assertEqual( expectedError.description.encode('utf-8'), errorResult['error_description'], msg=msg + ': Result contained a different error description than expected.' ) else: self.assertEqual( expectedError.description, errorResult['error_description'], msg=msg + ': Result contained a different error description than expected.' ) if expectedError.errorUri is not None: self.assertIn('error_uri', errorResult, msg=msg + ': Missing error_uri parameter in response.') self.assertEqual(expectedError.errorUri, errorResult['error_uri'], msg=msg + ': Result contained an unexpected error_uri.') if hasattr(expectedError, 'state') and getattr( expectedError, 'state') is not None: self.assertIn('state', errorResult, msg=msg + ': Missing state parameter in response.') self.assertEqual( expectedError.state if isinstance(expectedError.state, str) else expectedError.state.decode('utf-8', errors='replace'), errorResult['state'], msg=msg + ': Result contained an unexpected state.') def assertValidAuthRequest(self, request, result, parameters, msg, expectedDataLifetime=None): """ Assert that a GET request is processed correctly and the expected data has been stored. :param request: The GET request. :param result: The result of render_GET and onAuthenticate. :param parameters: The parameters of the request. :param msg: The assertion error message. :param expectedDataLifetime: The expected lifetime of the stored data. """ if msg.endswith('.'): msg = msg[:-1] self.assertFalse( request.finished, msg=msg + ': Expected the auth resource not to close the request.') self.assertIsInstance( result, tuple, message=msg + ': Expected the auth resource to call onAuthenticate.') requestParam, client, responseType, scope, redirectUri, state, dataKey = result self.assertIs(request, requestParam, msg=msg + ': Expected the auth resource to pass the request ' 'to onAuthenticate as the first parameter.') self.assertEqual( parameters['client_id'], client.id, msg=msg + ': Expected the auth resource to pass the received ' 'client to onAuthenticate as the second parameter.') parameters[ 'response_type'] = self._RESPONSE_GRANT_TYPE_MAPPING.get( parameters['response_type'], parameters['response_type']) self.assertEqual( parameters['response_type'], responseType, msg=msg + ': Expected the auth resource to pass the response ' 'type to onAuthenticate as the third parameter.') parameters['scope'] = parameters['scope'].split(' ') self.assertListEqual( scope, parameters['scope'], msg=msg + ': Expected the auth resource to pass the scope ' 'to onAuthenticate as the fourth parameter.') expectedRedirectUri = parameters['redirect_uri'] \ if parameters['redirect_uri'] is not None else self._VALID_CLIENT.redirectUris[0] self.assertEqual( expectedRedirectUri, redirectUri, msg=msg + ': Expected the auth resource to pass the redirect ' 'uri to onAuthenticate as the fifth parameter.') expectedState = parameters.get('state', None) self.assertEqual(expectedState, state, msg=msg + ': Expected the auth resource to pass the state ' 'to onAuthenticate as the sixth parameter.') if expectedDataLifetime is None: expectedDataLifetime = self._AUTH_RESOURCE.requestDataLifetime try: self.assertEqual( expectedDataLifetime, self._PERSISTENT_STORAGE.getExpireTime(dataKey), msg=msg + ': Expected the auth resource to store ' 'the request data with the given lifetime.') data = self._PERSISTENT_STORAGE.pop(dataKey) except KeyError: self.fail(msg=msg + ': Expected the auth resource to pass a valid ' 'data key to onAuthenticate as the sixth parameter.') for key, value in parameters.items(): self.assertIn( key, data, msg=msg + ': Expected the data stored by auth token resource ' 'to contain the {name} parameter.'.format(name=key)) self.assertEqual( value, data[key], msg=msg + ': Expected the auth token resource to store the value ' 'of the {name} parameter.'.format(name=key))