├── .gitignore ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.md ├── README.rst ├── aiohttp_oauth_client ├── __init__.py ├── assertion_client.py ├── base.py ├── oauth1_client.py └── oauth2_client.py ├── requirements.txt ├── setup.py └── tests ├── __init__.py ├── test_assertion_client.py ├── test_oauth1_client.py └── test_oauth2_client.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.pyo 3 | *.egg-info 4 | *.swp 5 | __pycache__ 6 | build 7 | develop-eggs 8 | dist 9 | eggs 10 | parts 11 | .DS_Store 12 | .installed.cfg 13 | docs/_build 14 | htmlcov/ 15 | venv/ 16 | .tox 17 | .coverage* 18 | .pytest_cache/ 19 | *.egg 20 | .idea/ 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2019, Hsiaoming Yang 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | * Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst 2 | include LICENSE 3 | prune tests* 4 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: clean clean-pyc clean-build docs 2 | 3 | clean: clean-build clean-pyc clean-docs clean-tox 4 | 5 | clean-build: 6 | @rm -fr build/ 7 | @rm -fr dist/ 8 | @rm -fr *.egg 9 | @rm -fr *.egg-info 10 | 11 | 12 | clean-pyc: 13 | @find . -name '*.pyc' -exec rm -f {} + 14 | @find . -name '*.pyo' -exec rm -f {} + 15 | @find . -name '*~' -exec rm -f {} + 16 | @find . -name '__pycache__' -exec rm -fr {} + 17 | 18 | clean-docs: 19 | @rm -fr docs/_build 20 | 21 | clean-tox: 22 | @rm -rf .tox/ 23 | 24 | docs: 25 | @$(MAKE) -C docs html 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # aiohttp OAuth Clients 2 | 3 | 4 | ## OAuth 1.0 Client 5 | 6 | Taking Twitter as an example on how to use `OAuth1Client`. 7 | 8 | ```python 9 | from aiohttp_oauth_client import OAuth1Client 10 | ``` 11 | 12 | ### 1. Fetch Temporary Credential 13 | 14 | For OAuth 1.0, the first step is requesting a temporary credential (aka 15 | request token). 16 | 17 | 18 | ```python 19 | client_id = '__YOUR_TWITTER_CLIENT_ID__' 20 | client_secret = '__YOUR_TWITTER_CLIENT_SECRET__' 21 | 22 | async def fetch_request_token(): 23 | url = 'https://api.twitter.com/oauth/request_token' 24 | 25 | async with OAuth1Client(client_id, client_secret) as session: 26 | request_token = await session.fetch_request_token(url) 27 | return request_token 28 | ``` 29 | 30 | This `fetch_request_token` method will return the temporary credential like: 31 | 32 | ``` 33 | {'oauth_token': 'Ih....Jw', 'oauth_token_secret': 'gr...GE', 'oauth_callback_confirmed': 'true'} 34 | ``` 35 | 36 | We can test this function with: 37 | 38 | ```py 39 | # twitter.py 40 | 41 | import asyncio 42 | from aiohttp_oauth_client import OAuth1Client 43 | 44 | client_id = '__YOUR_TWITTER_CLIENT_ID__' 45 | client_secret = '__YOUR_TWITTER_CLIENT_SECRET__' 46 | 47 | async def fetch_request_token(): 48 | url = 'https://api.twitter.com/oauth/request_token' 49 | 50 | async with OAuth1Client(client_id, client_secret) as session: 51 | request_token = await session.fetch_request_token(url) 52 | return request_token 53 | 54 | 55 | async def main(): 56 | request_token = await fetch_request_token() 57 | print(request_token) 58 | 59 | if __name__ == '__main__': 60 | loop = asyncio.get_event_loop() 61 | loop.run_until_complete(main()) 62 | ``` 63 | 64 | 65 | ### 2. Redirect to Authorization Endpoint 66 | 67 | The second step is to generate the authorization URL: 68 | 69 | ```py 70 | def create_authorization_url(request_token): 71 | authenticate_url = 'https://api.twitter.com/oauth/authenticate' 72 | client = OAuth1Client(client_id, client_secret) 73 | url = client.create_authorization_url(authenticate_url, request_token['oauth_token']) 74 | return url 75 | ``` 76 | 77 | This step is synchronized, no need for `async` and `await`. Passing the 78 | `request_token` we got from the first step, we would get a url like: 79 | 80 | ``` 81 | https://api.twitter.com/oauth/authenticate?oauth_token=Ih....Jw 82 | ``` 83 | 84 | Then visit this URL with your browser, and approve the request. Twitter 85 | will redirect back to your default `redirect_uri` you registered in Twitter. 86 | 87 | If you want to redirect to your specified URL, rewrite the code like: 88 | 89 | ```py 90 | def create_authorization_url(request_token): 91 | authenticate_url = 'https://api.twitter.com/oauth/authenticate' 92 | client = OAuth1Client(client_id, client_secret, redirect_uri='https://your-defined-url') 93 | url = client.create_authorization_url(authenticate_url, request_token['oauth_token']) 94 | return url 95 | ``` 96 | 97 | ### 3. Fetch Access Token 98 | 99 | The last step is to fetch the access token. In previous step, twitter 100 | will redirect back to a URL, for instance, something like: 101 | 102 | ``` 103 | https://your-domain.org/auth?oauth_token=Ih...Jw&oauth_verifier=fcg..1Dq 104 | ``` 105 | 106 | We will use the `oauth_verifier` in this URL to fetch access token. Here 107 | is our code: 108 | 109 | 110 | ```py 111 | async def fetch_access_token(request_token, oauth_verifier): 112 | url = 'https://api.twitter.com/oauth/access_token' 113 | async with OAuth1Client(client_id, client_secret) as session: 114 | session.token = request_token 115 | token = await session.fetch_access_token(url, oauth_verifier) 116 | return token 117 | 118 | 119 | # if you specified redirect_uri in previous step, create the session with 120 | # OAuth1Client(client_id, client_secret, redirect_uri='....') 121 | ``` 122 | 123 | We can test this function with: 124 | 125 | ```py 126 | # twitter.py 127 | 128 | import asyncio 129 | from aiohttp_oauth_client import OAuth1Client 130 | 131 | client_id = '__YOUR_TWITTER_CLIENT_ID__' 132 | client_secret = '__YOUR_TWITTER_CLIENT_SECRET__' 133 | 134 | async def fetch_access_token(request_token, oauth_verifier): 135 | url = 'https://api.twitter.com/oauth/access_token' 136 | async with OAuth1Client(client_id, client_secret) as session: 137 | session.token = request_token 138 | token = await session.fetch_access_token(url, oauth_verifier) 139 | return token 140 | 141 | async def main(): 142 | # from step 1 143 | request_token = { 144 | 'oauth_token': 'Ih....Jw', 145 | 'oauth_token_secret': 'gr...GE', 146 | 'oauth_callback_confirmed': 'true' 147 | } 148 | # from step 2 149 | verifier = 'fcg..1Dq' 150 | token = await fetch_access_token(request_token, verifier) 151 | print(request_token) 152 | 153 | if __name__ == '__main__': 154 | loop = asyncio.get_event_loop() 155 | loop.run_until_complete(main()) 156 | ``` 157 | 158 | 159 | ## OAuth 2.0 Client 160 | 161 | ```python 162 | from aiohttp_oauth_client import OAuth2Client 163 | 164 | ``` 165 | 166 | 167 | ## OAuth 2.0 Assertion 168 | 169 | ```python 170 | from aiohttp_oauth_client import AssertionClient 171 | ``` 172 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | OAuth Clients for aiohttp 2 | ========================= 3 | 4 | OAuth 1.0, OAuth 2.0 and Assertion OAuth Client for aiohttp, powered by Authlib_. 5 | 6 | 7 | Useful Links 8 | ------------ 9 | 10 | 1. Homepage: https://authlib.org/. 11 | 2. Documentation: https://docs.authlib.org/. 12 | 3. Blog: https://blog.authlib.org/. 13 | 4. Repository: https://github.com/authlib/aiohttp-oauth-client. 14 | 5. Twitter: https://twitter.com/authlib. 15 | 6. Donate: https://www.patreon.com/lepture. 16 | 17 | 18 | License 19 | ------- 20 | 21 | This library is licensed under BSD. Please see LICENSE for licensing details. 22 | 23 | .. _Authlib: https://authlib.org/ 24 | -------------------------------------------------------------------------------- /aiohttp_oauth_client/__init__.py: -------------------------------------------------------------------------------- 1 | from .oauth1_client import OAuth1Client 2 | from .oauth2_client import OAuth2Client 3 | from .assertion_client import AssertionClient 4 | 5 | __version__ = "0.1.1" 6 | __license__ = "BSD-3-Clause" 7 | __author__ = "Hsiaoming Yang " 8 | -------------------------------------------------------------------------------- /aiohttp_oauth_client/assertion_client.py: -------------------------------------------------------------------------------- 1 | from aiohttp import ClientSession 2 | from authlib.oauth2.rfc7521 import AssertionClient as _AssertionClient 3 | from authlib.oauth2.rfc7523 import JWTBearerGrant 4 | from .base import ClientMixin, OAuth2Request 5 | 6 | 7 | class AssertionClient(ClientMixin, _AssertionClient): 8 | """The OAuth 2.0 Assertion Client for ``aiohttp.ClientSession``. Here 9 | is how it works:: 10 | 11 | async with AssertionClient(token_url, issuer, ...) as client: 12 | await client.get(...) 13 | """ 14 | JWT_BEARER_GRANT_TYPE = JWTBearerGrant.GRANT_TYPE 15 | ASSERTION_METHODS = { 16 | JWT_BEARER_GRANT_TYPE: JWTBearerGrant.sign, 17 | } 18 | DEFAULT_GRANT_TYPE = JWT_BEARER_GRANT_TYPE 19 | 20 | def __init__(self, token_url, issuer, subject, audience, grant_type=None, 21 | claims=None, token_placement='header', scope=None, **kwargs): 22 | 23 | session = ClientSession(request_class=OAuth2Request) 24 | _AssertionClient.__init__( 25 | self, session=session, 26 | token_url=token_url, issuer=issuer, subject=subject, 27 | audience=audience, grant_type=grant_type, claims=claims, 28 | token_placement=token_placement, scope=scope, **kwargs 29 | ) 30 | 31 | async def _refresh_token(self, data): 32 | async with self.session.post(self.token_url, data=data) as resp: 33 | self.token = await resp.json() 34 | return self.token 35 | 36 | async def _request(self, method, url, **kwargs): 37 | if not self.token or self.token.is_expired(): 38 | await self.refresh_token() 39 | return await self.session.request( 40 | method, url, auth=self.token_auth, **kwargs) 41 | -------------------------------------------------------------------------------- /aiohttp_oauth_client/base.py: -------------------------------------------------------------------------------- 1 | from yarl import URL 2 | from aiohttp import ClientRequest 3 | from authlib.client.errors import OAuthError 4 | 5 | 6 | class OAuth2Request(ClientRequest): 7 | def __init__(self, *args, **kwargs): 8 | auth = kwargs.pop('auth', None) 9 | data = kwargs.get('data') 10 | super(OAuth2Request, self).__init__(*args, **kwargs) 11 | self.update_oauth_auth(auth, data) 12 | 13 | def update_oauth_auth(self, auth, data): 14 | if auth is None: 15 | return 16 | 17 | url, headers, body = auth.prepare(str(self.url), self.headers, data) 18 | self.url = URL(url) 19 | self.update_headers(headers) 20 | if body: 21 | self.update_body_from_data(body) 22 | 23 | 24 | class ClientMixin(object): 25 | async def __aenter__(self): 26 | return self 27 | 28 | async def __aexit__(self, exc_type, exc, tb): 29 | await self.session.close() 30 | 31 | @staticmethod 32 | def handle_error(error_type, error_description): 33 | raise OAuthError(error_type, error_description) 34 | 35 | def _request(self, method, url, **kwargs): 36 | raise NotImplementedError() 37 | 38 | def get(self, url, **kwargs): 39 | return self._request('GET', url, **kwargs) 40 | 41 | def options(self, url, **kwargs): 42 | return self._request('OPTIONS', url, **kwargs) 43 | 44 | def head(self, url, **kwargs): 45 | return self._request('HEAD', url, **kwargs) 46 | 47 | def post(self, url, **kwargs): 48 | return self._request('POST', url, **kwargs) 49 | 50 | def put(self, url, **kwargs): 51 | return self._request('PUT', url, **kwargs) 52 | 53 | def patch(self, url, **kwargs): 54 | return self._request('PATCH', url, **kwargs) 55 | 56 | def delete(self, url, **kwargs): 57 | return self._request('DELETE', url, **kwargs) 58 | -------------------------------------------------------------------------------- /aiohttp_oauth_client/oauth1_client.py: -------------------------------------------------------------------------------- 1 | from yarl import URL 2 | from aiohttp import ClientRequest, ClientSession 3 | from authlib.oauth1 import ( 4 | SIGNATURE_HMAC_SHA1, 5 | SIGNATURE_TYPE_HEADER, 6 | ) 7 | from authlib.oauth1.client import OAuth1Client as _OAuth1Client 8 | from .base import ClientMixin 9 | 10 | 11 | class OAuth1Request(ClientRequest): 12 | def __init__(self, *args, **kwargs): 13 | auth = kwargs.pop('auth', None) 14 | data = kwargs.get('data') 15 | super(OAuth1Request, self).__init__(*args, **kwargs) 16 | self.update_oauth_auth(auth, data) 17 | 18 | def update_oauth_auth(self, auth, data): 19 | if auth is None: 20 | return 21 | 22 | url, headers, body = auth.prepare( 23 | self.method, str(self.url), self.headers, data) 24 | self.url = URL(url) 25 | self.update_headers(headers) 26 | if body: 27 | self.update_body_from_data(body) 28 | 29 | 30 | class OAuth1Client(ClientMixin, _OAuth1Client): 31 | """The OAuth 1.0 Client for ``aiohttp.ClientSession``. Here 32 | is how it works:: 33 | 34 | async with OAuth1Client(client_id, client_secret, ...) as client: 35 | await client.fetch_access_token(...) 36 | """ 37 | 38 | def __init__(self, client_id, client_secret=None, 39 | token=None, token_secret=None, 40 | redirect_uri=None, rsa_key=None, verifier=None, 41 | signature_method=SIGNATURE_HMAC_SHA1, 42 | signature_type=SIGNATURE_TYPE_HEADER, 43 | force_include_body=False, **kwargs): 44 | session = ClientSession(request_class=OAuth1Request) 45 | _OAuth1Client.__init__( 46 | self, session=session, 47 | client_id=client_id, client_secret=client_secret, 48 | token=token, token_secret=token_secret, 49 | redirect_uri=redirect_uri, rsa_key=rsa_key, verifier=verifier, 50 | signature_method=signature_method, signature_type=signature_type, 51 | force_include_body=force_include_body, **kwargs) 52 | 53 | async def fetch_access_token(self, url, verifier=None, **kwargs): 54 | """Method for fetching an access token from the token endpoint. 55 | 56 | This is the final step in the OAuth 1 workflow. An access token is 57 | obtained using all previously obtained credentials, including the 58 | verifier from the authorization step. 59 | 60 | :param url: Access Token endpoint. 61 | :param verifier: A verifier string to prove authorization was granted. 62 | :param kwargs: Extra parameters to include for fetching access token. 63 | :return: A token dict. 64 | """ 65 | if verifier: 66 | self.auth.verifier = verifier 67 | if not self.auth.verifier: 68 | self.handle_error('missing_verifier', 'Missing "verifier" value') 69 | token = await self._fetch_token(url, **kwargs) 70 | self.auth.verifier = None 71 | return token 72 | 73 | async def _fetch_token(self, url, **kwargs): 74 | async with self.post(url, **kwargs) as resp: 75 | text = await resp.text() 76 | token = self.parse_response_token(resp.status, text) 77 | self.token = token 78 | return token 79 | 80 | def _request(self, method, url, **kwargs): 81 | return self.session.request(method, url, auth=self.auth, **kwargs) 82 | -------------------------------------------------------------------------------- /aiohttp_oauth_client/oauth2_client.py: -------------------------------------------------------------------------------- 1 | from aiohttp import ClientSession 2 | from authlib.oauth2.client import OAuth2Client as _OAuth2Client 3 | from .base import ClientMixin, OAuth2Request 4 | 5 | 6 | class OAuth2Client(ClientMixin, _OAuth2Client): 7 | """The OAuth 2.0 Client for ``aiohttp.ClientSession``. Here 8 | is how it works:: 9 | 10 | async with OAuth2Client.create(client_id, client_secret, ...) as client: 11 | await client.fetch_token(...) 12 | """ 13 | SESSION_REQUEST_PARAMS = ( 14 | 'timeout', 'allow_redirects', 'max_redirects', 15 | 'expect100', 'read_until_eof', 16 | 'json', 'cookies', 'skip_auto_headers', 'compress', 17 | 'chunked', 'raise_for_status', 'proxy', 'proxy_auth', 18 | 'verify_ssl', 'fingerprint', 'ssl_context', 'ssl', 19 | 'proxy_headers', 'trace_request_ctx', 20 | ) 21 | 22 | 23 | def __init__(self, client_id=None, client_secret=None, 24 | token_endpoint=None, token_endpoint_auth_method=None, 25 | scope=None, redirect_uri=None, 26 | token=None, token_placement='header', token_updater=None, **kwargs): 27 | session = ClientSession(request_class=OAuth2Request) 28 | _OAuth2Client.__init__( 29 | self, session=session, 30 | client_id=client_id, client_secret=client_secret, 31 | client_auth_method=token_endpoint_auth_method, 32 | refresh_token_url=token_endpoint, 33 | scope=scope, redirect_uri=redirect_uri, 34 | token=token, token_placement=token_placement, 35 | token_updater=token_updater, **kwargs 36 | ) 37 | 38 | async def _fetch_token(self, url, body='', headers=None, auth=None, 39 | method='POST', **kwargs): 40 | if method.upper() == 'POST': 41 | async with self.session.post( 42 | url, data=dict(url_decode(body)), headers=headers, 43 | auth=auth, **kwargs) as resp: 44 | token = await self._parse_token(resp, 'access_token_response') 45 | return self.parse_response_token(token) 46 | else: 47 | async with self.session.get( 48 | url, params=dict(url_decode(body)), headers=headers, 49 | auth=auth, **kwargs) as resp: 50 | token = await self._parse_token(resp, 'access_token_response') 51 | return self.parse_response_token(token) 52 | 53 | async def _refresh_token(self, url, refresh_token=None, body='', headers=None, 54 | auth=None, **kwargs): 55 | async with self.session.post( 56 | url, data=dict(url_decode(body)), headers=headers, 57 | auth=auth, **kwargs) as resp: 58 | token = await self._parse_token(resp, 'refresh_token_response') 59 | if 'refresh_token' not in token: 60 | self.token['refresh_token'] = refresh_token 61 | 62 | if callable(self.token_updater): 63 | await self.token_updater(self.token) 64 | 65 | return self.token 66 | 67 | async def _parse_token(self, resp, hook_type): 68 | for hook in self.compliance_hook[hook_type]: 69 | resp = await hook(resp) 70 | token = await resp.json() 71 | return token 72 | 73 | async def _request(self, method, url, **kwargs): 74 | if self.refresh_token_url and self.token.is_expired(): 75 | refresh_token = self.token.get('refresh_token') 76 | if refresh_token: 77 | await self.refresh_token(refresh_token=refresh_token) 78 | return await self.session.request( 79 | method, url, auth=self.token_auth, **kwargs) 80 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aiohttp 2 | Authlib==0.12 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import re 5 | from setuptools import setup 6 | 7 | with open('README.rst') as f: 8 | readme = f.read() 9 | 10 | 11 | with open('aiohttp_oauth_client/__init__.py') as f: 12 | version = re.search(r'__version__ = "(.*?)"', f.read()).group(1) 13 | 14 | 15 | setup( 16 | name='aiohttp-oauth-client', 17 | version=version, 18 | author='Hsiaoming Yang', 19 | author_email='me@lepture.com', 20 | url='https://github.com/authlib/aiohttp-oauth-client', 21 | packages=['aiohttp_oauth_client'], 22 | description=( 23 | 'OAuth 1.0, OAuth 2.0 and Assertion OAuth Client for aiohttp' 24 | ), 25 | zip_safe=False, 26 | include_package_data=True, 27 | platforms='any', 28 | long_description=readme, 29 | license='BSD-3-Clause', 30 | install_requires=['Authlib==0.12', 'aiohttp'], 31 | project_urls={ 32 | 'Website': 'https://authib.org/', 33 | 'Blog': 'https://blog.authlib.org/', 34 | }, 35 | classifiers=[ 36 | 'Development Status :: 3 - Alpha', 37 | 'Environment :: Console', 38 | 'Environment :: Web Environment', 39 | 'Intended Audience :: Developers', 40 | 'License :: OSI Approved :: BSD License', 41 | 'Operating System :: OS Independent', 42 | 'Programming Language :: Python', 43 | 'Programming Language :: Python :: 3', 44 | 'Programming Language :: Python :: 3.5', 45 | 'Programming Language :: Python :: 3.6', 46 | 'Programming Language :: Python :: 3.7', 47 | 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', 48 | 'Topic :: Internet :: WWW/HTTP :: WSGI :: Application', 49 | 'Topic :: Software Development :: Libraries :: Python Modules', 50 | ] 51 | ) 52 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/authlib/aiohttp-oauth-client/b48d1be993204974eb4e67abe086cc3a8f99f7d8/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_assertion_client.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/authlib/aiohttp-oauth-client/b48d1be993204974eb4e67abe086cc3a8f99f7d8/tests/test_assertion_client.py -------------------------------------------------------------------------------- /tests/test_oauth1_client.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase, mock 2 | from aiohttp_oauth_client import OAuth1Client 3 | 4 | 5 | class OAuth1ClientTest(TestCase): 6 | def test_no_client_id(self): 7 | self.assertRaises(ValueError, lambda: OAuth1Client(None)) 8 | -------------------------------------------------------------------------------- /tests/test_oauth2_client.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/authlib/aiohttp-oauth-client/b48d1be993204974eb4e67abe086cc3a8f99f7d8/tests/test_oauth2_client.py --------------------------------------------------------------------------------