├── CONTRIBUTING.adoc ├── LICENSE.txt ├── README.adoc ├── oauthclient ├── __init__.py ├── credentialutil.py ├── model │ ├── __init__.py │ ├── model.py │ └── util.py └── oauth2api.py ├── requirements.txt └── test ├── GetApplicationToken.py ├── GetUserAccessToken.py ├── TestUtil.py └── config ├── ebay-config-sample.json ├── ebay-config-sample.yaml ├── test-config-sample.json └── test-config-sample.yaml /CONTRIBUTING.adoc: -------------------------------------------------------------------------------- 1 | = eBay OAuth Client Contribution Guidelines 2 | ifdef::env-github[] 3 | :outfilesuffix: .adoc 4 | :note-caption: :bulb: 5 | endif::[] 6 | :toc: 7 | :toclevels: 4 8 | 9 | Thank you so much for wanting to contribute to ebay-oauth-python-client ! Here are a few important things you should know about contributing: 10 | 11 | 1. API changes require discussion, use cases, etc. Code comes later. 12 | 2. Pull requests are great for small fixes for bugs, documentation, etc. 13 | 3. Code contributions require updating relevant documentation. 14 | 15 | This project takes all contributions through https://help.github.com/articles/using-pull-requests[pull requests]. 16 | Code should *not* be pushed directly to `master`. 17 | 18 | The following guidelines apply to all contributors. 19 | 20 | == Making Changes 21 | * Fork the `ebay-oauth-java-client` repository 22 | * Make your changes and push them to a topic branch in your fork 23 | * See our commit message guidelines further down in this document 24 | * Submit a pull request to the repository 25 | * Update `ebay-oauth-java-client` GITHUB issue with the generated pull request link 26 | 27 | == General Guidelines 28 | * Only one logical change per commit 29 | * Do not mix whitespace changes with functional code changes 30 | * Do not mix unrelated functional changes 31 | * When writing a commit message: 32 | ** Describe _why_ a change is being made 33 | ** Do not assume the reviewer understands what the original problem was 34 | ** Do not assume the code is self-evident/self-documenting 35 | ** Describe any limitations of the current code 36 | * Any significant changes should be accompanied by tests. 37 | * The project already has good test coverage, so look at some of the existing tests if you're unsure how to go about it. 38 | * Please squash all commits for a change into a single commit (this can be done using `git rebase -i`). 39 | 40 | == Commit Message Guidelines 41 | * Provide a brief description of the change in the first line. 42 | * Insert a single blank line after the first line 43 | * Provide a detailed description of the change in the following lines, breaking 44 | paragraphs where needed. 45 | * The first line should be limited to 50 characters and should not end in a 46 | period. 47 | * Subsequent lines should be wrapped at 72 characters. 48 | * Put `Closes #XXX` line at the very end (where `XXX` is the actual issue number) if the proposed change is relevant to a tracked issue. 49 | 50 | Note: In Git commits the first line of the commit message has special significance. It is used as the email subject line, in git annotate messages, in gitk viewer annotations, in merge commit messages and many more places where space is at a premium. Please make the effort to write a good first line! 51 | 52 | == API Change Guidelines 53 | We need to make public API changes, including adding new APIs, very carefully to maintain backward compatibility for contributions. Because of this, if you're interested in seeing a new feature, the best approach is to create an Github issue (or comment on an existing issue if there is one) requesting the feature and describing specific use cases for it. 54 | 55 | If the feature has merit, it will go through a thorough process of API design and review. Any code should come after this. 56 | 57 | 58 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Apache License, Version 2.0 2 | 3 | SPDX short identifier: Apache-2.0 4 | 5 | Further resources on the Apache License 2.0 6 | Apache License 7 | Version 2.0, January 2004 8 | http://www.apache.org/licenses/ 9 | 10 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 11 | 12 | 1. Definitions. 13 | 14 | "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. 15 | 16 | "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. 17 | 18 | "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. 19 | 20 | "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. 21 | 22 | "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. 23 | 24 | "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. 25 | 26 | "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). 27 | 28 | "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. 29 | 30 | "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." 31 | 32 | "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 33 | 34 | 2. Grant of Copyright License. 35 | 36 | Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 37 | 38 | 3. Grant of Patent License. 39 | 40 | Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 41 | 42 | 4. Redistribution. 43 | 44 | You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: 45 | 46 | You must give any other recipients of the Work or Derivative Works a copy of this License; and 47 | You must cause any modified files to carry prominent notices stating that You changed the files; and 48 | You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and 49 | If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. 50 | You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 51 | 52 | 5. Submission of Contributions. 53 | 54 | Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 55 | 56 | 6. Trademarks. 57 | 58 | This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 59 | 60 | 7. Disclaimer of Warranty. 61 | 62 | Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 63 | 64 | 8. Limitation of Liability. 65 | 66 | In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 67 | 68 | 9. Accepting Warranty or Additional Liability. 69 | 70 | While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. 71 | 72 | END OF TERMS AND CONDITIONS 73 | 74 | APPENDIX: How to apply the Apache License to your work 75 | 76 | To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. 77 | 78 | Copyright [yyyy] [name of copyright owner] 79 | 80 | Licensed under the Apache License, Version 2.0 (the "License"); 81 | you may not use this file except in compliance with the License. 82 | You may obtain a copy of the License at 83 | 84 | http://www.apache.org/licenses/LICENSE-2.0 85 | 86 | Unless required by applicable law or agreed to in writing, software 87 | distributed under the License is distributed on an "AS IS" BASIS, 88 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 89 | See the License for the specific language governing permissions and 90 | limitations under the License. -------------------------------------------------------------------------------- /README.adoc: -------------------------------------------------------------------------------- 1 | = eBay OAuth Client Library 2 | ifdef::env-github[] 3 | :outfilesuffix: .adoc 4 | :note-caption: :bulb: 5 | endif::[] 6 | :toc: 7 | :toclevels: 4 8 | 9 | image:https://travis-ci.org/eBay/ebay-oauth-java-client.svg?branch=master["Build Status", link="https://travis-ci.org/eBay/ebay-oauth-java-client"] 10 | 11 | image:https://codecov.io/gh/eBay/ebay-oauth-java-client/branch/master/graph/badge.svg["Code Coverage, link="https://codecov.io/gh/eBay/ebay-oauth-java-client"] 12 | 13 | image:https://img.shields.io/github/license/eBay/ebay-oauth-java-client.svg["GitHub license",link="https://github.com/eBay/ebay-oauth-java-client/blob/master/LICENSE"] 14 | 15 | 16 | eBay OAuth client library is a simple and easy-to-use library for integrating with eBay OAuth and designed to be used for OAuth 2.0 specification supported by eBay. There are multiple standard clients that can be used with eBay OAuth, such as Spring OAuth client. However, this library in addition to functioning as a simple eBay OAuth client, helps with additional features such as cached App tokens. There are also future enhancements planned to add id_token support, 'login with eBay' support etc., 17 | 18 | == What is OAuth 2.0 19 | https://tools.ietf.org/html/rfc6749[OAuth 2.0] is the most widely used standard for authentication and authorization for API based access. The complete end to end documentation on how eBay OAuth functions is available at https://developer.ebay.com/api-docs/static/oauth-tokens.html[developer.ebay.com]. 20 | 21 | == Supported Languages 22 | This is created as a Python 2.7 project and can be used as a dependency in Python based application 23 | 24 | == Setting up the local environment 25 | 1. Clone or download the repository 26 | 2. To set up your environment, please see the requirements listed in https://github.com/eBay/ebay-oauth-python-client/blob/master/requirements.txt[requirements.txt] You can run $ pip install -r requirements.txt command to install all the requirements. 27 | 28 | 29 | == Getting Started 30 | All interactions with this library can be performed using `oauth2api` 31 | 32 | == Library Setup 33 | 1. Ensure you have a config file in your source code of type YAML or JSON. 34 | 2. Edit ebay-config-sample.json or ebay-config-sample.yaml file (depending on configuration file type in your client code) with your application credentials (keyset). This file would hold all your application credentials such as AppId, DevId, and CertId. Refer to https://developer.ebay.com/api-docs/static/creating-edp-account.html[Creating eBay Developer Account] for details on how to get these credentials. 35 | 3. Import oauth2api, credentialutil, model 36 | 4. Define https://developer.ebay.com/api-docs/static/oauth-scopes.html[scopes] to use in your application. Ensure your keyset have permission to those scope(s) 37 | 5. Once the application config file is created, call `credentialutil.load());` to load the credentials. 38 | 6. Once the credentials are loaded, call any operation on `oauth2api`. 39 | 40 | If you are running the GetUserAccessToken.py test script in this package: 41 | * Edit test-config-sample.yaml with your eBay user crednetials 42 | * Place chrome driver into test directory 43 | 44 | 45 | == Types of Tokens 46 | There are mainly two types of tokens in usage. 47 | 48 | === Application Token 49 | An application token contains an application identity which is generated using `client_credentials` grant type. These application tokens are useful for interaction with application specific APIs such as usage statistics etc., 50 | 51 | === User Token 52 | A user token (_access token or refresh token_) contains a user identity and the application's identity. This is usually generated using the `authorization_code` grant type or the `refresh_token` grant type. 53 | 54 | == Supported Grant Types for OAuth 55 | All of the regular OAuth 2.0 specifications such as `client_credentials`, `authorization_code`, and `refresh_token` are all supported. Refer to https://developer.ebay.com/api-docs/static/oauth-tokens.html[eBay Developer Portal] 56 | 57 | === Grant Type: Client Credentials 58 | This grant type can be performed by simply using `oauth2api.get_application_token`. Read more about this grant type at https://developer.ebay.com/api-docs/static/oauth-client-credentials-grant.html[oauth-client-credentials-grant] 59 | 60 | === Grant Type: Authorization Code 61 | This grant type can be performed by a two step process. Call `oauth2api.generate_user_authorization_url()` to get the Authorization URL to redirect the user to. Once the user authenticates and approves the consent, the callback need to be captured by the redirect URL setup by the app and then call `oauth2api.exchange_code_for_access_token` to get the refresh and access tokens. 62 | 63 | Read more about this grant type at https://developer.ebay.com/api-docs/static/oauth-authorization-code-grant.html[oauth-authorization-code-grant] and https://developer.ebay.com/api-docs/static/oauth-qref-auth-code-grant.html[Quick Reference] 64 | 65 | === Grant Type: Refresh Token 66 | This grant type can be performed by simply using `oauth2api.exchange_code_for_access_token`. Usually access tokens are short lived and if the access token is expired, the caller can use the refresh token to generate a new access token. Read more about it at https://developer.ebay.com/api-docs/static/oauth-qref-auth-code-grant.html[Using a refresh token to update a user access token] 67 | 68 | == Contribution 69 | Contributions in terms of patches, features, or comments are always welcome. Refer to link:CONTRIBUTING.adoc[CONTRIBUTING] for guidelines. Submit Github issues for any feature enhancements, bugs, or documentation problems as well as questions and comments. 70 | 71 | == License 72 | Copyright (c) 2019 eBay Inc. -------------------------------------------------------------------------------- /oauthclient/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /oauthclient/credentialutil.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Copyright 2019 eBay Inc. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | You may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | """ 18 | import yaml, json 19 | import logging 20 | from model.model import environment, credentials 21 | 22 | user_config_ids = ["sandbox-user", "production-user"] 23 | 24 | class credentialutil(object): 25 | """ 26 | credential_list: dictionary key=string, value=credentials 27 | """ 28 | _credential_list = {} 29 | 30 | 31 | @classmethod 32 | def load(cls, app_config_path): 33 | logging.info("Loading credential configuration file at: %s", app_config_path) 34 | with open(app_config_path, 'r') as f: 35 | if app_config_path.endswith('.yaml') or app_config_path.endswith('.yml'): 36 | content = yaml.load(f) 37 | elif app_config_path.endswith('.json'): 38 | content = json.loads(f.read()) 39 | else: 40 | raise ValueError('Configuration file need to be in JSON or YAML') 41 | credentialutil._iterate(content) 42 | 43 | @classmethod 44 | def _iterate(cls, content): 45 | for key in content: 46 | logging.debug("Environment attempted: %s", key) 47 | 48 | if key in [environment.PRODUCTION.config_id, environment.SANDBOX.config_id]: 49 | client_id = content[key]['appid'] 50 | dev_id = content[key]['devid'] 51 | client_secret = content[key]['certid'] 52 | ru_name = content[key]['redirecturi'] 53 | 54 | app_info = credentials(client_id, client_secret, dev_id, ru_name) 55 | cls._credential_list.update({key: app_info}) 56 | 57 | 58 | 59 | @classmethod 60 | def get_credentials(cls, env_type): 61 | """ 62 | env_config_id: environment.PRODUCTION.config_id or environment.SANDBOX.config_id 63 | """ 64 | if len(cls._credential_list) == 0: 65 | msg = "No environment loaded from configuration file" 66 | logging.error(msg) 67 | raise CredentialNotLoadedError(msg) 68 | return cls._credential_list[env_type.config_id] 69 | 70 | class CredentialNotLoadedError(Exception): 71 | pass -------------------------------------------------------------------------------- /oauthclient/model/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /oauthclient/model/model.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Copyright 2019 eBay Inc. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | You may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | 18 | """ 19 | 20 | class env_type(object): 21 | def __init__(self, config_id, web_endpoint, api_endpoint): 22 | self.config_id = config_id 23 | self.web_endpoint = web_endpoint 24 | self.api_endpoint = api_endpoint 25 | 26 | class environment(object): 27 | PRODUCTION = env_type("api.ebay.com", "https://auth.ebay.com/oauth2/authorize", "https://api.ebay.com/identity/v1/oauth2/token") 28 | SANDBOX = env_type("api.sandbox.ebay.com", "https://auth.sandbox.ebay.com/oauth2/authorize", "https://api.sandbox.ebay.com/identity/v1/oauth2/token") 29 | 30 | 31 | class credentials(object): 32 | def __init__(self, client_id, client_secret, dev_id, ru_name): 33 | self.client_id = client_id 34 | self.dev_id = dev_id 35 | self.client_secret = client_secret 36 | self.ru_name = ru_name 37 | 38 | 39 | class oAuth_token(object): 40 | 41 | def __init__(self, error=None, access_token=None, refresh_token=None, refresh_token_expiry=None, token_expiry=None): 42 | ''' 43 | token_expiry: datetime in UTC 44 | refresh_token_expiry: datetime in UTC 45 | ''' 46 | self.access_token = access_token 47 | self.token_expiry = token_expiry 48 | self.refresh_token = refresh_token 49 | self.refresh_token_expiry = refresh_token_expiry 50 | self.error = error 51 | 52 | 53 | def __str__(self): 54 | token_str = '{' 55 | if self.error != None: 56 | token_str += '"error": "' + self.error + '"' 57 | elif self.access_token != None: 58 | token_str += '"access_token": "' + self.access_token + '", "expires_in": "' + self.token_expiry.strftime('%Y-%m-%dT%H:%M:%S:%f') + '"' 59 | if self.refresh_token != None: 60 | token_str += ', "refresh_token": "' + self.refresh_token + '", "refresh_token_expire_in": "' + self.refresh_token_expiry.strftime('%Y-%m-%dT%H:%M:%S:%f')+ '"' 61 | token_str += '}' 62 | return token_str -------------------------------------------------------------------------------- /oauthclient/model/util.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Copyright 2019 eBay Inc. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | You may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | 18 | """ 19 | import base64 20 | 21 | def _generate_request_headers(credential): 22 | 23 | b64_encoded_credential = base64.b64encode(credential.client_id + ':' + credential.client_secret) 24 | headers = { 25 | 'Content-Type': 'application/x-www-form-urlencoded', 26 | 'Authorization': 'Basic ' + b64_encoded_credential 27 | } 28 | 29 | return headers 30 | 31 | 32 | def _generate_application_request_body(credential, scopes): 33 | 34 | 35 | body = { 36 | 'grant_type': 'client_credentials', 37 | 'redirect_uri': credential.ru_name, 38 | 'scope': scopes 39 | } 40 | 41 | 42 | return body 43 | 44 | def _generate_refresh_request_body(scopes, refresh_token): 45 | if refresh_token == None: 46 | raise StandardError("credential object does not contain refresh_token and/or scopes") 47 | 48 | body = { 49 | 'grant_type': 'refresh_token', 50 | 'refresh_token': refresh_token, 51 | 'scope':scopes 52 | } 53 | return body 54 | 55 | def _generate_oauth_request_body(credential, code): 56 | body = { 57 | 'grant_type': 'authorization_code', 58 | 'redirect_uri': credential.ru_name, 59 | 'code':code 60 | } 61 | return body -------------------------------------------------------------------------------- /oauthclient/oauth2api.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Copyright 2019 eBay Inc. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | You may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | """ 18 | 19 | import json 20 | import urllib 21 | import requests 22 | import logging 23 | import model.util 24 | from datetime import datetime, timedelta 25 | from credentialutil import credentialutil 26 | from model.model import oAuth_token 27 | 28 | LOGFILE = 'eBay_Oauth_log.txt' 29 | logging.basicConfig(level=logging.DEBUG, filename=LOGFILE, format="%(asctime)s: %(levelname)s - %(funcName)s: %(message)s", filemode='w') 30 | 31 | 32 | class oauth2api(object): 33 | 34 | 35 | def generate_user_authorization_url(self, env_type, scopes, state=None): 36 | ''' 37 | env_type = environment.SANDBOX or environment.PRODUCTION 38 | scopes = list of strings 39 | ''' 40 | 41 | credential = credentialutil.get_credentials(env_type) 42 | 43 | scopes = ' '.join(scopes) 44 | param = { 45 | 'client_id':credential.client_id, 46 | 'redirect_uri':credential.ru_name, 47 | 'response_type':'code', 48 | 'prompt':'login', 49 | 'scope':scopes 50 | } 51 | 52 | if state != None: 53 | param.update({'state':state}) 54 | 55 | 56 | query = urllib.urlencode(param) 57 | return env_type.web_endpoint + '?' + query 58 | 59 | 60 | def get_application_token(self, env_type, scopes): 61 | """ 62 | makes call for application token and stores result in credential object 63 | returns credential object 64 | """ 65 | 66 | logging.info("Trying to get a new application access token ... ") 67 | credential = credentialutil.get_credentials(env_type) 68 | headers = model.util._generate_request_headers(credential) 69 | body = model.util._generate_application_request_body(credential, ' '.join(scopes)) 70 | 71 | resp = requests.post(env_type.api_endpoint, data=body, headers=headers) 72 | content = json.loads(resp.content) 73 | token = oAuth_token() 74 | 75 | if resp.status_code == requests.codes.ok: 76 | token.access_token = content['access_token'] 77 | # set token expiration time 5 minutes before actual expire time 78 | token.token_expiry = datetime.utcnow()+timedelta(seconds=int(content['expires_in']))-timedelta(minutes=5) 79 | 80 | else: 81 | token.error = str(resp.status_code) + ': ' + content['error_description'] 82 | logging.error("Unable to retrieve token. Status code: %s - %s", resp.status_code, requests.status_codes._codes[resp.status_code]) 83 | logging.error("Error: %s - %s", content['error'], content['error_description']) 84 | return token 85 | 86 | 87 | def exchange_code_for_access_token(self, env_type, code): 88 | logging.info("Trying to get a new user access token ... ") 89 | credential = credentialutil.get_credentials(env_type) 90 | 91 | headers = model.util._generate_request_headers(credential) 92 | body = model.util._generate_oauth_request_body(credential, code) 93 | resp = requests.post(env_type.api_endpoint, data=body, headers=headers) 94 | 95 | content = json.loads(resp.content) 96 | token = oAuth_token() 97 | 98 | if resp.status_code == requests.codes.ok: 99 | token.access_token = content['access_token'] 100 | token.token_expiry = datetime.utcnow()+timedelta(seconds=int(content['expires_in']))-timedelta(minutes=5) 101 | token.refresh_token = content['refresh_token'] 102 | token.refresh_token_expiry = datetime.utcnow()+timedelta(seconds=int(content['refresh_token_expires_in']))-timedelta(minutes=5) 103 | else: 104 | token.error = str(resp.status_code) + ': ' + content['error_description'] 105 | logging.error("Unable to retrieve token. Status code: %s - %s", resp.status_code, requests.status_codes._codes[resp.status_code]) 106 | logging.error("Error: %s - %s", content['error'], content['error_description']) 107 | return token 108 | 109 | 110 | def get_access_token(self, env_type, refresh_token, scopes): 111 | """ 112 | refresh token call 113 | """ 114 | 115 | logging.info("Trying to get a new user access token ... ") 116 | 117 | credential = credentialutil.get_credentials(env_type) 118 | 119 | headers = model.util._generate_request_headers(credential) 120 | body = model.util._generate_refresh_request_body(' '.join(scopes), refresh_token) 121 | resp = requests.post(env_type.api_endpoint, data=body, headers=headers) 122 | content = json.loads(resp.content) 123 | token = oAuth_token() 124 | token.token_response = content 125 | 126 | if resp.status_code == requests.codes.ok: 127 | token.access_token = content['access_token'] 128 | token.token_expiry = datetime.utcnow()+timedelta(seconds=int(content['expires_in']))-timedelta(minutes=5) 129 | else: 130 | token.error = str(resp.status_code) + ': ' + content['error_description'] 131 | logging.error("Unable to retrieve token. Status code: %s - %s", resp.status_code, requests.status_codes._codes[resp.status_code]) 132 | logging.error("Error: %s - %s", content['error'], content['error_description']) 133 | return token -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | selenium==3.141.0 2 | requests==2.21.0 3 | PyYAML==5.4 -------------------------------------------------------------------------------- /test/GetApplicationToken.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Copyright 2019 eBay Inc. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | You may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | 18 | """ 19 | import os, sys 20 | sys.path.insert(0, os.path.join(os.path.split(__file__)[0], '..')) 21 | from oauthclient.oauth2api import oauth2api 22 | from oauthclient.credentialutil import credentialutil 23 | from oauthclient.model.model import environment 24 | import unittest 25 | 26 | app_scopes = ["https://api.ebay.com/oauth/api_scope", "https://api.ebay.com/oauth/api_scope/buy.item.feed"] 27 | invalid_app_scopes = ["https://api.ebay.com/oauth/api_scope", "https://api.ebay.com/oauth/api_scope/sell.inventory"] 28 | 29 | class TestGetApplicationCredential(unittest.TestCase): 30 | 31 | 32 | def test_invalid_oauth_scope(self): 33 | config_path = os.path.join(os.path.split(__file__)[0], 'config' ,'ebay-config-sample.yaml') 34 | credentialutil.load(config_path) 35 | oauth2api_inst = oauth2api() 36 | app_token = oauth2api_inst.get_application_token(environment.SANDBOX, invalid_app_scopes) 37 | self.assertIsNone(app_token.access_token) 38 | self.assertIsNotNone(app_token.error) 39 | print '\n *** test_invalid_oauth_scope ***\n', app_token 40 | 41 | 42 | def test_client_credentials_grant_sandbox(self): 43 | config_path = os.path.join(os.path.split(__file__)[0], 'config' ,'ebay-config-sample.yaml') 44 | credentialutil.load(config_path) 45 | oauth2api_inst = oauth2api() 46 | app_token = oauth2api_inst.get_application_token(environment.SANDBOX, app_scopes) 47 | self.assertIsNone(app_token.error) 48 | self.assertIsNotNone(app_token.access_token) 49 | self.assertTrue(len(app_token.access_token) > 0) 50 | print '\n *** test_client_credentials_grant_sandbox ***:\n', app_token 51 | 52 | 53 | def test_client_credentials_grant_production(self): 54 | config_path = os.path.join(os.path.split(__file__)[0], 'config' ,'ebay-config-sample.yaml') 55 | credentialutil.load(config_path) 56 | oauth2api_inst = oauth2api() 57 | app_token = oauth2api_inst.get_application_token(environment.PRODUCTION, app_scopes) 58 | self.assertIsNone(app_token.error) 59 | self.assertIsNotNone(app_token.access_token) 60 | self.assertTrue(len(app_token.access_token) > 0) 61 | print '\n *** test_client_credentials_grant_production ***:\n', app_token 62 | 63 | 64 | if __name__ == '__main__': 65 | unittest.main() -------------------------------------------------------------------------------- /test/GetUserAccessToken.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Copyright 2019 eBay Inc. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | You may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | 18 | """ 19 | 20 | import os, sys 21 | sys.path.insert(0, os.path.join(os.path.split(__file__)[0], '..')) 22 | from oauthclient.oauth2api import oauth2api 23 | import TestUtil 24 | from oauthclient.credentialutil import credentialutil 25 | from oauthclient.model.model import environment 26 | import unittest 27 | 28 | app_scopes = ["https://api.ebay.com/oauth/api_scope", "https://api.ebay.com/oauth/api_scope/sell.inventory", "https://api.ebay.com/oauth/api_scope/sell.marketing", "https://api.ebay.com/oauth/api_scope/sell.account", "https://api.ebay.com/oauth/api_scope/sell.fulfillment"] 29 | 30 | class TestGetApplicationCredential(unittest.TestCase): 31 | def test_generate_authorization_url(self): 32 | app_config_path = os.path.join(os.path.split(__file__)[0], 'config', 'ebay-config-sample-user.yaml') 33 | credentialutil.load(app_config_path) 34 | oauth2api_inst = oauth2api() 35 | signin_url = oauth2api_inst.generate_user_authorization_url(environment.SANDBOX, app_scopes) 36 | self.assertIsNotNone(signin_url) 37 | print '\n *** test_get_signin_url ***: \n', signin_url 38 | 39 | def test_exchange_authorization_code(self): 40 | app_config_path = os.path.join(os.path.split(__file__)[0], 'config', 'ebay-config-sample-user.yaml') 41 | credentialutil.load(app_config_path) 42 | oauth2api_inst = oauth2api() 43 | signin_url = oauth2api_inst.generate_user_authorization_url(environment.SANDBOX, app_scopes) 44 | code = TestUtil.get_authorization_code(signin_url) 45 | user_token = oauth2api_inst.exchange_code_for_access_token(environment.SANDBOX, code) 46 | self.assertIsNotNone(user_token.access_token) 47 | self.assertTrue(len(user_token.access_token) > 0) 48 | print '\n *** test_get_user_access_token ***:\n', user_token 49 | 50 | def test_exchange_refresh_for_access_token(self): 51 | app_config_path = os.path.join(os.path.split(__file__)[0], 'config', 'ebay-config-sample-user.yaml') 52 | credentialutil.load(app_config_path) 53 | oauth2api_inst = oauth2api() 54 | signin_url = oauth2api_inst.generate_user_authorization_url(environment.SANDBOX, app_scopes) 55 | code = TestUtil.get_authorization_code(signin_url) 56 | user_token = oauth2api_inst.exchange_code_for_access_token(environment.SANDBOX, code) 57 | self.assertIsNotNone(user_token.refresh_token) 58 | self.assertTrue(len(user_token.refresh_token) > 0) 59 | 60 | user_token = oauth2api_inst.get_access_token(environment.SANDBOX, user_token.refresh_token, app_scopes) 61 | self.assertIsNotNone(user_token.access_token) 62 | self.assertTrue(len(user_token.access_token) > 0) 63 | 64 | print '\n *** test_refresh_user_access_token ***:\n', user_token 65 | 66 | if __name__ == '__main__': 67 | unittest.main() -------------------------------------------------------------------------------- /test/TestUtil.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Copyright 2019 eBay Inc. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | You may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | 18 | """ 19 | import os, logging, json, time, urllib, re, yaml 20 | from selenium import webdriver 21 | 22 | sandbox_key = "sandbox-user" 23 | production_key = "production-user" 24 | _user_credential_list = {} 25 | 26 | def read_user_info(conf = None): 27 | 28 | logging.info("Loading user credential configuration file at: %s", conf) 29 | with open(conf, 'r') as f: 30 | if conf.endswith('.yaml') or conf.endswith('.yml'): 31 | content = yaml.load(f) 32 | elif conf.endswith('.json'): 33 | content = json.loads(f.read()) 34 | else: 35 | raise ValueError('Configuration file need to be in JSON or YAML') 36 | 37 | for key in content: 38 | logging.debug("Environment attempted: %s", key) 39 | 40 | if key in [sandbox_key, production_key]: 41 | userid = content[key]['username'] 42 | password = content[key]['password'] 43 | _user_credential_list.update({key:[userid, password]}) 44 | 45 | 46 | def get_authorization_code(signin_url): 47 | 48 | user_config_path = os.path.join(os.path.split(__file__)[0], 'config\\test-config-sample.yaml') 49 | read_user_info(user_config_path) 50 | 51 | env_key = production_key 52 | if "sandbox" in signin_url: 53 | env_key = sandbox_key 54 | 55 | userid = _user_credential_list[env_key][0] 56 | password = _user_credential_list[env_key][1] 57 | 58 | browser = webdriver.Chrome() 59 | browser.get(signin_url) 60 | time.sleep(5) 61 | 62 | form_userid = browser.find_element_by_name('userid') 63 | form_pw = browser.find_element_by_name('pass') 64 | 65 | form_userid.send_keys(userid) 66 | form_pw.send_keys(password) 67 | 68 | browser.find_element_by_id('sgnBt').submit() 69 | 70 | time.sleep(5) 71 | 72 | url = browser.current_url 73 | browser.quit() 74 | 75 | if 'code=' in url: 76 | code = re.findall('code=(.*?)&', url)[0] 77 | logging.info("Code Obtained: %s", code) 78 | else: 79 | logging.error("Unable to obtain code via sign in URL") 80 | 81 | decoded_code = urllib.unquote(code).decode('utf8') 82 | return decoded_code 83 | 84 | -------------------------------------------------------------------------------- /test/config/ebay-config-sample.json: -------------------------------------------------------------------------------- 1 | { 2 | "api.ebay.com": { 3 | "devid": "", 4 | "certid": "", 5 | "redirecturi": "", 6 | "appid": "" 7 | }, 8 | "name": "ebay-config", 9 | "api.sandbox.ebay.com": { 10 | "devid": "", 11 | "certid": "", 12 | "redirecturi": "", 13 | "appid": "" 14 | } 15 | } -------------------------------------------------------------------------------- /test/config/ebay-config-sample.yaml: -------------------------------------------------------------------------------- 1 | name: ebay-config 2 | 3 | # Trading API OAuth - https://developer.ebay.com/api-docs/static/oauth-tokens.html 4 | 5 | # UPDATE the values in this file, before running the tests 6 | api.sandbox.ebay.com: 7 | appid: 8 | certid: 9 | devid: 10 | redirecturi: 11 | 12 | api.ebay.com: 13 | appid: 14 | certid: 15 | devid: 16 | redirecturi: -------------------------------------------------------------------------------- /test/config/test-config-sample.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test-config", 3 | "production-user": { 4 | "username": "", 5 | "password": "" 6 | }, 7 | "sandbox-user": { 8 | "username": "", 9 | "password": "" 10 | } 11 | } -------------------------------------------------------------------------------- /test/config/test-config-sample.yaml: -------------------------------------------------------------------------------- 1 | name: test-config 2 | 3 | # https://developer.ebay.com/DevZone/SandboxUser/ 4 | # Register a sandbox test user and use that credentials here 5 | sandbox-user: 6 | username: 7 | password: 8 | 9 | # Register a production test user and use that credentials here - https://reg.ebay.com/reg/PartialReg 10 | production-user: 11 | username: 12 | password: --------------------------------------------------------------------------------