├── .gitignore ├── .travis.yml ├── AUTHORS ├── CONTRIBUTORS ├── LICENSE ├── README.md ├── identitytoolkit ├── __init__.py ├── errors.py ├── gitkitclient.py └── rpchelper.py ├── setup.cfg ├── setup.py └── tests ├── __init__.py ├── test_gitkitclient.py └── test_rpchelper.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Build artifacts 2 | *.py[cod] 3 | dist/ 4 | identity_toolkit_python_client.egg-info/ 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: python 3 | python: 4 | - 2.7 5 | - 3.3 6 | - 3.4 7 | 8 | install: python setup.py install 9 | 10 | script: python setup.py test 11 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | # This is the official list of Google Identity Toolkit client library authors for copyright purposes. 2 | # This file is distinct from the CONTRIBUTORS files. 3 | # See the latter for an explanation. 4 | 5 | # Names should be added to this file as: 6 | # Name or Organization 7 | # The email address is not required for organizations. 8 | 9 | Google Inc. -------------------------------------------------------------------------------- /CONTRIBUTORS: -------------------------------------------------------------------------------- 1 | # People who have agreed to one of the CLAs and can contribute patches. 2 | # The AUTHORS file lists the copyright holders; this file 3 | # lists people. For example, Google employees are listed here 4 | # but not in AUTHORS, because Google holds the copyright. 5 | # 6 | # https://developers.google.com/open-source/cla/individual 7 | # https://developers.google.com/open-source/cla/corporate 8 | # 9 | # Names should be added to this file as: 10 | # Name 11 | Jin Liu 12 | Cordelia Link 13 | Victor Menezes 14 | Lucas Connors 15 | Yanna Wu 16 | Yanhao Wang 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright 2014 Google Inc. 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This is the python client library for Google Identity Toolkit services. 2 | 3 | [![Build Status](https://travis-ci.org/google/identity-toolkit-python-client.svg)](https://travis-ci.org/google/identity-toolkit-python-client) 4 | 5 | Installation 6 | ===================== 7 | 8 | ``` 9 | pip install identity-toolkit-python-client 10 | ``` 11 | 12 | If you run into issues installing, you may be missing some dependencies such as `libffi` or `python-dev`. On Debian, you can install these with: 13 | ``` 14 | sudo apt-get install python-dev libffi-dev 15 | ``` 16 | 17 | *(replace python-dev with python3-dev if using Python 3)* 18 | 19 | Usage 20 | ===================== 21 | 22 | Initialize Gitkit client instance 23 | -------------- 24 | 25 | ```python 26 | p12_file = 'YOUR_SERVICE_ACCOUNT_PRIVATE_KEY_FILE.p12' 27 | f = file(p12_file, 'rb') 28 | key = f.read() 29 | f.close() 30 | gitkit_instance = gitkitclient.GitkitClient( 31 | client_id='YOUR_WEB_APPLICATION_CLIENT_ID_AT_GOOGLE_DEVELOPER_CONSOLE', 32 | service_account_email='YOUR_SERVICE_ACCOUNT_EMAIL@developer.gserviceaccount.com', 33 | service_account_key=key, 34 | widget_url='URL_ON_YOUR_SERVER_TO_HOST_GITKIT_WIDGET') 35 | ``` 36 | 37 | Verify Gitkit Token in HTTP request cookie 38 | -------------- 39 | ```python 40 | user = gitkit_instance.VerifyGitkitToken(request.COOKIES['gtoken']) 41 | ``` 42 | 43 | Upload Multiple Accounts 44 | -------------- 45 | 46 | ```python 47 | hashKey = 'hash-key' 48 | user1 = gitkitclient.GitkitUser() 49 | user1.email = '1234@example.com' 50 | user1.user_id = '1234' 51 | user1.salt = 'salt-1' 52 | user1.passwordHash = calcHmac(hashKey, '1111', 'salt-1') 53 | 54 | user2 = gitkitclient.GitkitUser() 55 | user2.email = '5678@example.com' 56 | user2.user_id = '5678' 57 | user2.salt = 'salt-2' 58 | user2.passwordHash = calcHmac(hashKey, '5555', 'salt-2') 59 | 60 | gitkit_instance.UploadUsers('HMAC_SHA1', hashKey, [user1, user2]) 61 | ``` 62 | 63 | Download Accounts 64 | -------------- 65 | 66 | ```python 67 | for account in gitkit_instance.GetAllUsers(2): 68 | pprint(vars(account)) 69 | ``` 70 | 71 | Get Account Info 72 | -------------- 73 | 74 | ```python 75 | pprint(vars(gitkit_instance.GetUserById('1234'))) 76 | pprint(vars(gitkit_instance.GetUserByEmail('5678@example.com'))) 77 | ``` 78 | 79 | Delete Account 80 | -------------- 81 | ```python 82 | gitkit_instance.DeleteUser('1234') 83 | ``` 84 | -------------------------------------------------------------------------------- /identitytoolkit/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/identity-toolkit-python-client/4cfe3013569c21576daa5d22ad21f9f4f8b30c4d/identitytoolkit/__init__.py -------------------------------------------------------------------------------- /identitytoolkit/errors.py: -------------------------------------------------------------------------------- 1 | # Copyright 2014 Google Inc. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | """Gitkit errors which could be raised by the Gitkit client library.""" 16 | 17 | 18 | class Error(Exception): 19 | """Base Gitkit error type.""" 20 | 21 | def __init__(self, value): 22 | Exception.__init__(self, value) 23 | self.value = value 24 | 25 | 26 | class GitkitClientError(Error): 27 | """Error due to invalid input from user or caller.""" 28 | 29 | def __init__(self, value): 30 | Error.__init__(self, value) 31 | self.value = value 32 | 33 | def __str__(self): 34 | return 'Gitkit client error: ' + repr(self.value) 35 | 36 | 37 | class GitkitServerError(Error): 38 | """Error due to Gitkit server.""" 39 | 40 | def __init__(self, value): 41 | Error.__init__(self, value) 42 | self.value = value 43 | 44 | def __str__(self): 45 | return 'Gitkit server error: ' + repr(self.value) 46 | -------------------------------------------------------------------------------- /identitytoolkit/gitkitclient.py: -------------------------------------------------------------------------------- 1 | # Copyright 2014 Google Inc. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | """Google Identity Toolkit Python client library. 16 | 17 | Client library for third party web sites to integrate with Gitkit service. 18 | 19 | Usage example: 20 | 21 | # Initialize with Google Developer Console information 22 | gitkit = gitkitclient.GitkitClient( 23 | client_id=GOOGLE_OAUTH2_WEB_CLIENT_ID, 24 | service_account_email=SERVICE_ACCOUNT_EMAIL, 25 | service_account_key=SERVICE_ACCOUNT_PRIVATE_KEY_P12, 26 | widget_url=FULL_URL_OF_GITKIT_WIDGET, 27 | cookie_name='gtoken', 28 | http=None, 29 | project_id=GOOGLE_DEVELOPER_CONSOLE_PROJECT_ID) 30 | 31 | # Verify Gitkit token locally 32 | user = gitkit.VerifyGitkitToken(token_string) 33 | 34 | # Get user account by email 35 | user = gitkit.GetUserByEmail('user@example.com') 36 | 37 | # Delete an user 38 | gitkit.DeleteUser(user.user_id) 39 | 40 | # Download all user accounts from Gitkit server 41 | for account in gitkit.GetAllUsers(): 42 | pprint(vars(account)) 43 | """ 44 | 45 | import base64 46 | import urllib 47 | try: 48 | from urllib import parse 49 | except ImportError: 50 | import urlparse as parse 51 | 52 | from oauth2client import crypt 53 | import simplejson 54 | 55 | from identitytoolkit import errors 56 | from identitytoolkit import rpchelper 57 | 58 | 59 | # Symbolic constants for hash algorithms supported by Gitkit service. 60 | ALGORITHM_HMAC_SHA256 = 'HMAC_SHA256' 61 | ALGORITHM_HMAC_SHA1 = 'HMAC_SHA1' 62 | ALGORITHM_HMAC_MD5 = 'HMAC_MD5' 63 | ALGORITHM_PBKDF_SHA1 = 'PBKDF_SHA1' 64 | ALGORITHM_MD5 = 'MD5' 65 | ALGORITHM_SCRYPT = 'SCRYPT' 66 | 67 | 68 | class GitkitUser(object): 69 | """Map between gitkit api request/response dict and object attribute.""" 70 | 71 | def __init__(self, decode=True, **kwargs): 72 | self.email = kwargs['email'] 73 | self.user_id = kwargs.get('user_id', kwargs.get('localId')) 74 | self.name = kwargs.get('displayName', kwargs.get('display_name', None)) 75 | self.photo_url = kwargs.get('photoUrl', kwargs.get('photo_url', None)) 76 | self.provider_id = kwargs.get('provider_id', None) 77 | self.email_verified = kwargs.get( 78 | 'emailVerified', kwargs.get('verified', None)) 79 | if 'passwordHash' in kwargs: 80 | if decode: 81 | self.password_hash = base64.urlsafe_b64decode(kwargs['passwordHash']) 82 | else: 83 | self.password_hash = kwargs['passwordHash'] 84 | else: 85 | self.password_hash = None 86 | if 'salt' in kwargs: 87 | if decode: 88 | self.salt = base64.urlsafe_b64decode(kwargs['salt']) 89 | else: 90 | self.salt = kwargs['salt'] 91 | else: 92 | self.salt = None 93 | self.provider_info = kwargs.get('providerUserInfo', {}) 94 | 95 | @classmethod 96 | def FromApiResponse(cls, response): 97 | """Initializes from gitkit api response. 98 | 99 | Args: 100 | response: dict, the Gitkit API response. 101 | Returns: 102 | GitkitUser object 103 | """ 104 | return cls(**response) 105 | 106 | @classmethod 107 | def FromToken(cls, token): 108 | """Initializes from token (Gitkit API request). 109 | 110 | Args: 111 | token: dict, the Gitkit API request. 112 | Returns: 113 | GitkitUser object 114 | """ 115 | return cls(decode=False, **token) 116 | 117 | @classmethod 118 | def FromDictionary(cls, dictionary): 119 | """Initializes from user specified dictionary. 120 | 121 | Args: 122 | dictionary: dict of user specified attributes 123 | Returns: 124 | GitkitUser object 125 | """ 126 | if 'user_id' in dictionary: 127 | raise errors.GitkitClientError('use localId instead') 128 | if 'localId' not in dictionary: 129 | raise errors.GitkitClientError('must specify localId') 130 | if 'email' not in dictionary: 131 | raise errors.GitkitClientError('must specify email') 132 | 133 | return cls(decode=False, **dictionary) 134 | 135 | def ToRequest(self): 136 | """Converts to gitkit api request parameter dict. 137 | 138 | Returns: 139 | Dict, containing non-empty user attributes. 140 | """ 141 | param = {} 142 | if self.email: 143 | param['email'] = self.email 144 | if self.user_id: 145 | param['localId'] = self.user_id 146 | if self.name: 147 | param['displayName'] = self.name 148 | if self.photo_url: 149 | param['photoUrl'] = self.photo_url 150 | if self.email_verified is not None: 151 | param['emailVerified'] = self.email_verified 152 | if self.password_hash: 153 | param['passwordHash'] = base64.urlsafe_b64encode(self.password_hash) 154 | if self.salt: 155 | param['salt'] = base64.urlsafe_b64encode(self.salt) 156 | if self.provider_info: 157 | param['providerUserInfo'] = self.provider_info 158 | return param 159 | 160 | 161 | class GitkitClient(object): 162 | """Public interface of Gitkit client library. 163 | 164 | This class is the only interface that third party developers needs to know to 165 | integrate Gitkit with their backend server. Main features are Gitkit token 166 | verification and Gitkit remote API wrapper. 167 | """ 168 | 169 | GOOGLE_API_BASE = 'https://www.googleapis.com/' 170 | RESET_PASSWORD_ACTION = 'resetPassword' 171 | CHANGE_EMAIL_ACTION = 'changeEmail' 172 | 173 | def __init__(self, client_id='', service_account_email='', service_account_key='', 174 | widget_url='', cookie_name='gtoken', http=None, project_id=''): 175 | """Inits the Gitkit client library. 176 | 177 | Args: 178 | client_id: string, developer's Google oauth2 web client id. 179 | service_account_email: string, Google service account email. 180 | service_account_key: string, Google service account private key. 181 | widget_url: string, Gitkit widget URL. 182 | cookie_name: string, Gitkit cookie name. 183 | http: Http, http client which support cache. 184 | project_id: string, developer console's project id. 185 | """ 186 | self.client_id = client_id 187 | self.widget_url = widget_url 188 | self.cookie_name = cookie_name 189 | self.project_id = project_id 190 | self.rpc_helper = rpchelper.RpcHelper(service_account_email, 191 | service_account_key, 192 | GitkitClient.GOOGLE_API_BASE, 193 | http) 194 | self.config_data_cached = None 195 | if not self.client_id: 196 | self.client_id = self.GetClientId() 197 | 198 | def _RetrieveProjectConfig(self): 199 | if not self.config_data_cached: 200 | self.config_data_cached = self.rpc_helper.GetProjectConfig() 201 | return self.config_data_cached 202 | 203 | def GetClientId(self): 204 | config_data = self._RetrieveProjectConfig() 205 | client_id = None 206 | for idp in config_data['idpConfig']: 207 | if idp['provider'] == 'GOOGLE': 208 | client_id = idp['clientId'] 209 | if not client_id: 210 | raise errors.GitkitServerError('Google client ID not configured yet.') 211 | return client_id 212 | 213 | def GetBrowserApiKey(self): 214 | config_data = self._RetrieveProjectConfig() 215 | return config_data['apiKey'] 216 | 217 | def GetSignInOptions(self): 218 | config_data = self._RetrieveProjectConfig() 219 | sign_in_options = [] 220 | for idp in config_data['idpConfig']: 221 | if idp['enabled']: 222 | sign_in_options.append(str(idp['provider']).lower()) 223 | if config_data['allowPasswordUser']: 224 | sign_in_options.append('password') 225 | if not sign_in_options: 226 | raise errors.GitkitServerError('no sign in option configured') 227 | return sign_in_options 228 | 229 | 230 | @classmethod 231 | def FromConfigFile(cls, config): 232 | json_data = simplejson.load(open(config)) 233 | 234 | key_file = open(json_data['serviceAccountPrivateKeyFile'], 'rb') 235 | key = key_file.read() 236 | key_file.close() 237 | 238 | clientId = json_data.get('clientId') 239 | projectId = json_data.get('projectId') 240 | if not clientId and not projectId: 241 | raise errors.GitkitClientError('Missing projectId or clientId in server configuration.') 242 | 243 | return cls( 244 | clientId, 245 | json_data['serviceAccountEmail'], 246 | key, 247 | json_data['widgetUrl'], 248 | json_data['cookieName'], 249 | None, 250 | projectId) 251 | 252 | def VerifyGitkitToken(self, jwt): 253 | """Verifies a Gitkit token string. 254 | 255 | Args: 256 | jwt: string, the token to be checked 257 | 258 | Returns: 259 | GitkitUser, if the token is valid. None otherwise. 260 | """ 261 | certs = self.rpc_helper.GetPublicCert() 262 | crypt.MAX_TOKEN_LIFETIME_SECS = 30 * 86400 # 30 days 263 | parsed = None 264 | for aud in filter(lambda x: x is not None, [self.project_id, self.client_id]): 265 | try: 266 | parsed = crypt.verify_signed_jwt_with_certs(jwt, certs, aud) 267 | except crypt.AppIdentityError as e: 268 | if "Wrong recipient" not in e.message: 269 | return None 270 | if parsed: 271 | return GitkitUser.FromToken(parsed) 272 | return None # Gitkit token audience doesn't match projectId or clientId in server configuration 273 | 274 | def GetUserByEmail(self, email): 275 | """Gets user info by email. 276 | 277 | Args: 278 | email: string, the user email. 279 | 280 | Returns: 281 | GitkitUser, containing the user info. 282 | """ 283 | user = self.rpc_helper.GetAccountInfoByEmail(email) 284 | return GitkitUser.FromApiResponse(user) 285 | 286 | def GetUserById(self, local_id): 287 | """Gets user info by id. 288 | 289 | Args: 290 | local_id: string, the user id at Gitkit server. 291 | 292 | Returns: 293 | GitkitUser, containing the user info. 294 | """ 295 | user = self.rpc_helper.GetAccountInfoById(local_id) 296 | return GitkitUser.FromApiResponse(user) 297 | 298 | def UploadUsers(self, hash_algorithm, hash_key, accounts): 299 | """Uploads multiple users to Gitkit server. 300 | 301 | Args: 302 | hash_algorithm: string, the hash algorithm. 303 | hash_key: array, raw key of the hash algorithm. 304 | accounts: list of GitkitUser. 305 | 306 | Returns: 307 | A dict of failed accounts. The key is the index of the 'accounts' list, 308 | starting from 0. 309 | """ 310 | return self.rpc_helper.UploadAccount(hash_algorithm, 311 | base64.urlsafe_b64encode(hash_key), 312 | [GitkitUser.ToRequest(i) for i in accounts]) 313 | 314 | def GetAllUsers(self, pagination_size=10): 315 | """Gets all user info from Gitkit server. 316 | 317 | Args: 318 | pagination_size: int, how many users should be returned per request. 319 | The account info are retrieved in pagination. 320 | 321 | Yields: 322 | A generator to iterate all users. 323 | """ 324 | next_page_token, accounts = self.rpc_helper.DownloadAccount( 325 | None, pagination_size) 326 | while accounts: 327 | for account in accounts: 328 | yield GitkitUser.FromApiResponse(account) 329 | next_page_token, accounts = self.rpc_helper.DownloadAccount( 330 | next_page_token, pagination_size) 331 | 332 | def DeleteUser(self, local_id): 333 | """Deletes a user at Gitkit server. 334 | 335 | Args: 336 | local_id: string, id of the user to be deleted 337 | 338 | Returns: 339 | a dict, containing 'error' key if the API failed. 340 | """ 341 | return self.rpc_helper.DeleteAccount(local_id) 342 | 343 | def GetOobResult(self, param, user_ip, gitkit_token=None): 344 | """Gets out-of-band code for ResetPassword/ChangeEmail request. 345 | 346 | Args: 347 | param: dict of HTTP POST params 348 | user_ip: string, end user's IP address 349 | gitkit_token: string, the gitkit token if user logged in 350 | 351 | Returns: 352 | A dict of { 353 | email: user email who initializes the request 354 | new_email: the requested new email, for ChangeEmail action only 355 | oob_link: the generated link to be send to user's email 356 | oob_code: the one time out-of-band code 357 | action: OobAction 358 | response_body: the http body to be returned to Gitkit widget 359 | } 360 | """ 361 | if 'action' in param: 362 | try: 363 | if param['action'] == GitkitClient.RESET_PASSWORD_ACTION: 364 | request = self._PasswordResetRequest(param, user_ip) 365 | oob_code, oob_link = self._BuildOobLink(request, 366 | param['action']) 367 | return { 368 | 'action': GitkitClient.RESET_PASSWORD_ACTION, 369 | 'email': param['email'], 370 | 'oob_link': oob_link, 371 | 'oob_code': oob_code, 372 | 'response_body': simplejson.dumps({'success': True}) 373 | } 374 | elif param['action'] == GitkitClient.CHANGE_EMAIL_ACTION: 375 | if not gitkit_token: 376 | return self._FailureOobResponse('login is required') 377 | request = self._ChangeEmailRequest(param, user_ip, gitkit_token) 378 | oob_code, oob_link = self._BuildOobLink(request, 379 | param['action']) 380 | return { 381 | 'action': GitkitClient.CHANGE_EMAIL_ACTION, 382 | 'email': param['oldEmail'], 383 | 'new_email': param['newEmail'], 384 | 'oob_link': oob_link, 385 | 'oob_code': oob_code, 386 | 'response_body': simplejson.dumps({'success': True}) 387 | } 388 | except errors.GitkitClientError as error: 389 | return self._FailureOobResponse(error.value) 390 | return self._FailureOobResponse('unknown request type') 391 | 392 | def GetEmailVerificationLink(self, email): 393 | """Get the url to verify user's email 394 | 395 | Args: 396 | email: string, user's email to be verified 397 | 398 | Returns: 399 | The email verification link. 400 | """ 401 | param = { 402 | 'email': email, 403 | 'requestType': 'VERIFY_EMAIL' 404 | } 405 | return self._BuildOobLink(param, "verifyEmail")[1] 406 | 407 | def _FailureOobResponse(self, error_msg): 408 | """Generates failed response for out-of-band operation. 409 | 410 | Args: 411 | error_msg: string, error message 412 | 413 | Returns: 414 | A dict representing errors. 415 | """ 416 | return {'response_body': simplejson.dumps({'error': error_msg})} 417 | 418 | def _BuildOobLink(self, param, mode): 419 | """Builds out-of-band URL. 420 | 421 | Gitkit API GetOobCode() is called and the returning code is combined 422 | with Gitkit widget URL to building the out-of-band url. 423 | 424 | Args: 425 | param: dict of request. 426 | mode: string, Gitkit widget mode to handle the oob action after user 427 | clicks the oob url in the email. 428 | 429 | Raises: 430 | GitkitClientError: if oob code is not returned. 431 | 432 | Returns: 433 | A string of oob url. 434 | """ 435 | code = self.rpc_helper.GetOobCode(param) 436 | if code: 437 | parsed = list(parse.urlparse(self.widget_url)) 438 | 439 | query = dict(parse.parse_qsl(parsed[4])) 440 | query.update({'mode': mode, 'oobCode': code}) 441 | 442 | try: 443 | parsed[4] = parse.urlencode(query) 444 | except AttributeError: 445 | parsed[4] = urllib.urlencode(query) 446 | 447 | return code, parse.urlunparse(parsed) 448 | raise errors.GitkitClientError('invalid request') 449 | 450 | def _PasswordResetRequest(self, param, user_ip): 451 | return { 452 | 'email': param['email'], 453 | 'userIp': user_ip, 454 | 'captchaResp': param['response'], 455 | 'requestType': 'PASSWORD_RESET'} 456 | 457 | def _ChangeEmailRequest(self, param, user_ip, id_token): 458 | return { 459 | 'email': param['oldEmail'], 460 | 'newEmail': param['newEmail'], 461 | 'userIp': user_ip, 462 | 'idToken': id_token, 463 | 'requestType': 'NEW_EMAIL_ACCEPT'} 464 | -------------------------------------------------------------------------------- /identitytoolkit/rpchelper.py: -------------------------------------------------------------------------------- 1 | # Copyright 2014 Google Inc. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | """Google Identity Toolkit remote API wrapper. 16 | 17 | Used by gitkitclient.py to handle http interactions with Gitkit server. Third 18 | party developers do not need to call this class directly. 19 | """ 20 | 21 | import time 22 | try: 23 | import urllib.request as urllib_request 24 | from urllib import parse 25 | except ImportError: 26 | import urlparse as parse 27 | import urllib2 as urllib_request 28 | import urllib 29 | 30 | import httplib2 31 | from oauth2client import client 32 | from oauth2client import crypt 33 | from oauth2client.client import GoogleCredentials 34 | import simplejson 35 | 36 | import identitytoolkit.errors as errors 37 | 38 | 39 | class RpcHelper(object): 40 | """Helper class to invoke Gitkit remote API.""" 41 | 42 | GITKIT_SCOPE = 'https://www.googleapis.com/auth/identitytoolkit' 43 | TOKEN_ENDPOINT = 'https://accounts.google.com/o/oauth2/token' 44 | MAX_TOKEN_LIFETIME_SECS = 3600 # 1 hour in seconds 45 | 46 | def __init__(self, service_account_email, service_account_key, 47 | google_api_url, http): 48 | self.credentials = None 49 | if service_account_email and service_account_key: 50 | self.service_account_email = service_account_email 51 | self.service_account_key = service_account_key 52 | else: 53 | self.service_account_email = '' 54 | self.service_account_key = '' 55 | try: 56 | self.credentials = GoogleCredentials.get_application_default() \ 57 | .create_scoped(RpcHelper.GITKIT_SCOPE) 58 | except Exception as e: 59 | print('WARNING: unable to retrieve service account credentials.') 60 | self.google_api_url = google_api_url + 'identitytoolkit/v3/relyingparty/' 61 | 62 | if http is None: 63 | self.http = httplib2.Http(client.MemoryCache()) 64 | else: 65 | self.http = http 66 | 67 | def GetAccountInfoByEmail(self, email): 68 | """Gets account info of an email. 69 | 70 | Args: 71 | email: string, user email. 72 | 73 | Returns: 74 | A dict of user attribute. 75 | """ 76 | response = self._InvokeGitkitApi('getAccountInfo', {'email': [email]}) 77 | # pylint does not recognize the return type of simplejson.loads 78 | # pylint: disable=maybe-no-member 79 | return response['users'][0] 80 | 81 | def GetAccountInfoById(self, user_id): 82 | """Gets account info of a user id. 83 | 84 | Args: 85 | user_id: string, user id. 86 | 87 | Returns: 88 | A dict of user attribute. 89 | """ 90 | response = self._InvokeGitkitApi('getAccountInfo', {'localId': [user_id]}) 91 | # pylint does not recognize the return type of simplejson.loads 92 | # pylint: disable=maybe-no-member 93 | return response['users'][0] 94 | 95 | def GetOobCode(self, request): 96 | """Gets out-of-band code requested by user. 97 | 98 | Args: 99 | request: dict, the request details. 100 | 101 | Returns: 102 | Out of band code string. 103 | """ 104 | response = self._InvokeGitkitApi('getOobConfirmationCode', request) 105 | # pylint does not recognize the return type of simplejson.loads 106 | # pylint: disable=maybe-no-member 107 | return response.get('oobCode', None) 108 | 109 | def DownloadAccount(self, next_page_token=None, max_results=None): 110 | """Downloads multiple accounts from Gitkit server. 111 | 112 | Args: 113 | next_page_token: string, pagination token. 114 | max_results: pagination size. 115 | 116 | Returns: 117 | An array of accounts. 118 | """ 119 | param = {} 120 | if next_page_token: 121 | param['nextPageToken'] = next_page_token 122 | if max_results: 123 | param['maxResults'] = max_results 124 | response = self._InvokeGitkitApi('downloadAccount', param) 125 | # pylint does not recognize the return type of simplejson.loads 126 | # pylint: disable=maybe-no-member 127 | return response.get('nextPageToken', None), response.get('users', {}) 128 | 129 | def UploadAccount(self, hash_algorithm, hash_key, accounts): 130 | """Uploads multiple accounts to Gitkit server. 131 | 132 | Args: 133 | hash_algorithm: string, algorithm to hash password. 134 | hash_key: string, base64-encoded key of the algorithm. 135 | accounts: array of accounts to be uploaded. 136 | 137 | Returns: 138 | Response of the API. 139 | """ 140 | param = { 141 | 'hashAlgorithm': hash_algorithm, 142 | 'signerKey': hash_key, 143 | 'users': accounts 144 | } 145 | # pylint does not recognize the return type of simplejson.loads 146 | # pylint: disable=maybe-no-member 147 | return self._InvokeGitkitApi('uploadAccount', param) 148 | 149 | def DeleteAccount(self, local_id): 150 | """Deletes an account. 151 | 152 | Args: 153 | local_id: string, user id to be deleted. 154 | 155 | Returns: 156 | API response. 157 | """ 158 | # pylint does not recognize the return type of simplejson.loads 159 | # pylint: disable=maybe-no-member 160 | return self._InvokeGitkitApi('deleteAccount', {'localId': local_id}) 161 | 162 | def GetProjectConfig(self): 163 | """Gets project config. 164 | 165 | Returns: 166 | API response. 167 | """ 168 | # pylint does not recognize the return type of simplejson.loads 169 | # pylint: disable=maybe-no-member 170 | return self._InvokeGitkitApi('getProjectConfig') 171 | 172 | def GetPublicCert(self): 173 | """Download Gitkit public cert. 174 | 175 | Returns: 176 | dict of public certs. 177 | """ 178 | 179 | cert_url = self.google_api_url + 'publicKeys' 180 | 181 | resp, content = self.http.request(cert_url) 182 | if resp.status == 200: 183 | return simplejson.loads(content) 184 | else: 185 | raise errors.GitkitServerError('Error response for cert url: %s' % 186 | content) 187 | 188 | def _InvokeGitkitApi(self, method, params=None, need_service_account=True): 189 | """Invokes Gitkit API, with optional access token for service account. 190 | 191 | Args: 192 | method: string, the api method name. 193 | params: dict of optional parameters for the API. 194 | need_service_account: false if service account is not needed. 195 | 196 | Raises: 197 | GitkitClientError: if the request is bad. 198 | GitkitServerError: if Gitkit can not handle the request. 199 | 200 | Returns: 201 | API response as dict. 202 | """ 203 | body = simplejson.dumps(params) if params else None 204 | req = urllib_request.Request(self.google_api_url + method) 205 | req.add_header('Content-type', 'application/json') 206 | if need_service_account: 207 | if self.credentials: 208 | access_token = self.credentials.get_access_token().access_token 209 | elif self.service_account_email and self.service_account_key: 210 | access_token = self._GetAccessToken() 211 | else: 212 | raise errors.GitkitClientError('Missing service account credentials') 213 | req.add_header('Authorization', 'Bearer ' + access_token) 214 | try: 215 | binary_body = body.encode('utf-8') if body else None 216 | raw_response = urllib_request.urlopen(req, binary_body).read() 217 | except urllib_request.HTTPError as err: 218 | if err.code == 400: 219 | raw_response = err.read() 220 | else: 221 | raise 222 | return self._CheckGitkitError(raw_response) 223 | 224 | def _GetAccessToken(self): 225 | """Gets oauth2 access token for Gitkit API using service account. 226 | 227 | Returns: 228 | string, oauth2 access token. 229 | """ 230 | d = { 231 | 'assertion': self._GenerateAssertion(), 232 | 'grant_type': 'urn:ietf:params:oauth:grant-type:jwt-bearer', 233 | } 234 | try: 235 | body = parse.urlencode(d) 236 | except AttributeError: 237 | body = urllib.urlencode(d) 238 | req = urllib_request.Request(RpcHelper.TOKEN_ENDPOINT) 239 | req.add_header('Content-type', 'application/x-www-form-urlencoded') 240 | binary_body = body.encode('utf-8') 241 | raw_response = urllib_request.urlopen(req, binary_body) 242 | return simplejson.loads(raw_response.read())['access_token'] 243 | 244 | def _GenerateAssertion(self): 245 | """Generates the signed assertion that will be used in the request. 246 | 247 | Returns: 248 | string, signed Json Web Token (JWT) assertion. 249 | """ 250 | now = int(time.time()) 251 | payload = { 252 | 'aud': RpcHelper.TOKEN_ENDPOINT, 253 | 'scope': 'https://www.googleapis.com/auth/identitytoolkit', 254 | 'iat': now, 255 | 'exp': now + RpcHelper.MAX_TOKEN_LIFETIME_SECS, 256 | 'iss': self.service_account_email 257 | } 258 | return crypt.make_signed_jwt( 259 | crypt.Signer.from_string(self.service_account_key), 260 | payload) 261 | 262 | def _CheckGitkitError(self, raw_response): 263 | """Raises error if API invocation failed. 264 | 265 | Args: 266 | raw_response: string, the http response. 267 | 268 | Raises: 269 | GitkitClientError: if the error code is 4xx. 270 | GitkitServerError: if the response if malformed. 271 | 272 | Returns: 273 | Successful response as dict. 274 | """ 275 | try: 276 | response = simplejson.loads(raw_response) 277 | if 'error' not in response: 278 | return response 279 | else: 280 | error = response['error'] 281 | if 'code' in error: 282 | code = error['code'] 283 | if str(code).startswith('4'): 284 | raise errors.GitkitClientError(error['message']) 285 | else: 286 | raise errors.GitkitServerError(error['message']) 287 | except simplejson.JSONDecodeError: 288 | pass 289 | raise errors.GitkitServerError('null error code from Gitkit server') 290 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.md 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | import sys 3 | 4 | install_requires = [ 5 | 'oauth2client>=1.3.2', 6 | 'pyOpenSSL>=0.14', 7 | 'simplejson>=2.3.2', 8 | ] 9 | tests_require = list(install_requires) 10 | 11 | # Python 2 requires Mock to run tests 12 | if sys.version_info < (3, 0): 13 | tests_require += ['pbr==1.6', 'Mock'] 14 | 15 | packages = ['identitytoolkit',] 16 | 17 | setup( 18 | name = 'identity-toolkit-python-client', 19 | packages = packages, 20 | license="Apache 2.0", 21 | version = '0.1.11', 22 | description = 'Google Identity Toolkit python client library', 23 | author = 'Jin Liu', 24 | url = 'https://github.com/google/identity-toolkit-python-client', 25 | download_url = 'https://github.com/google/identity-toolkit-python-client/archive/master.zip', 26 | keywords = ['identity', 'google', 'login', 'toolkit'], # arbitrary keywords 27 | classifiers = [ 28 | 'Development Status :: 5 - Production/Stable', 29 | 'Intended Audience :: Developers', 30 | 'License :: OSI Approved :: Apache Software License', 31 | 'Operating System :: OS Independent', 32 | 'Programming Language :: Python :: 2.7', 33 | 'Programming Language :: Python :: 3.3', 34 | 'Programming Language :: Python :: 3.4', 35 | 'Topic :: Internet :: WWW/HTTP', 36 | ], 37 | install_requires = install_requires, 38 | tests_require = tests_require, 39 | test_suite = 'tests', 40 | ) 41 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | from tests.test_gitkitclient import GitkitClientTestCase 2 | from tests.test_rpchelper import RpcHelperTestCase 3 | -------------------------------------------------------------------------------- /tests/test_gitkitclient.py: -------------------------------------------------------------------------------- 1 | # Copyright 2014 Google Inc. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | """Unit test for GitkitClient.""" 16 | 17 | import base64 18 | import unittest 19 | try: 20 | from urllib import parse 21 | from unittest import mock 22 | except ImportError: 23 | import urlparse as parse 24 | import mock 25 | 26 | from identitytoolkit import errors 27 | from identitytoolkit import gitkitclient 28 | 29 | 30 | class GitkitClientTestCase(unittest.TestCase): 31 | 32 | def setUp(self): 33 | self.widget_url = 'http://localhost:9000/widget' 34 | self.user_id = '1234' 35 | self.email = 'user@example.com' 36 | self.user_name = 'Joe' 37 | self.user_photo = 'http://idp.com/photo' 38 | self.gitkitclient = gitkitclient.GitkitClient(client_id='client_id', 39 | widget_url=self.widget_url) 40 | 41 | def testGetClientId(self): 42 | expected_client_id = 'client_id' 43 | with mock.patch('identitytoolkit.rpchelper.RpcHelper.GetProjectConfig') as rpc_mock: 44 | rpc_mock.return_value = { 45 | 'idpConfig': [{ 46 | 'provider': 'GOOGLE', 47 | 'clientId': expected_client_id, 48 | }] 49 | } 50 | actual_client_id = self.gitkitclient.GetClientId() 51 | self.assertEqual(actual_client_id, expected_client_id) 52 | 53 | def testGetClientId_throwException(self): 54 | with mock.patch('identitytoolkit.rpchelper.RpcHelper.GetProjectConfig') as rpc_mock: 55 | rpc_mock.return_value = { 56 | 'idpConfig': [] 57 | } 58 | try: 59 | self.gitkitclient.GetClientId() 60 | self.fail('GitkitServerException expected') 61 | except errors.GitkitServerError as error: 62 | self.assertEqual('Google client ID not configured yet.', error.value) 63 | 64 | def testGetBrowserApiKey(self): 65 | expected_api_key = 'api_key' 66 | with mock.patch('identitytoolkit.rpchelper.RpcHelper.GetProjectConfig') as rpc_mock: 67 | rpc_mock.return_value = { 68 | 'apiKey': expected_api_key, 69 | } 70 | actual_api_key = self.gitkitclient.GetBrowserApiKey() 71 | self.assertEqual(actual_api_key, expected_api_key) 72 | 73 | def testGetSignInOptions(self): 74 | with mock.patch('identitytoolkit.rpchelper.RpcHelper.GetProjectConfig') as rpc_mock: 75 | rpc_mock.return_value = { 76 | 'allowPasswordUser': True, 77 | 'idpConfig': [{ 78 | 'provider': 'GOOGLE', 79 | 'enabled': True, 80 | }] 81 | } 82 | sign_in_options = self.gitkitclient.GetSignInOptions() 83 | self.assertEqual(sign_in_options, ['google', 'password']) 84 | 85 | def testGetSignInOptions_throwEmpty(self): 86 | with mock.patch('identitytoolkit.rpchelper.RpcHelper.GetProjectConfig') as rpc_mock: 87 | rpc_mock.return_value = { 88 | 'allowPasswordUser': False, 89 | 'idpConfig': [{ 90 | 'provider': 'GOOGLE', 91 | 'enabled': False, 92 | }] 93 | } 94 | try: 95 | self.gitkitclient.GetSignInOptions() 96 | self.fail('GitkitServerException expected') 97 | except errors.GitkitServerError as error: 98 | self.assertEqual('no sign in option configured', error.value) 99 | 100 | def testVerifyToken(self): 101 | with mock.patch('identitytoolkit.rpchelper.RpcHelper.GetPublicCert') as rpc_mock: 102 | rpc_mock.return_value = {'kid': 'cert'} 103 | with mock.patch('oauth2client.crypt.' 104 | 'verify_signed_jwt_with_certs') as crypt_mock: 105 | crypt_mock.return_value = { 106 | 'localId': self.user_id, 107 | 'email': self.email, 108 | 'display_name': self.user_name 109 | } 110 | gitkit_user = self.gitkitclient.VerifyGitkitToken('token') 111 | self.assertEqual(self.user_id, gitkit_user.user_id) 112 | self.assertEqual(self.email, gitkit_user.email) 113 | self.assertEqual(self.user_name, gitkit_user.name) 114 | self.assertIsNone(gitkit_user.photo_url) 115 | self.assertEqual({}, gitkit_user.provider_info) 116 | 117 | def testGetAccountInfo(self): 118 | with mock.patch('identitytoolkit.rpchelper.RpcHelper._InvokeGitkitApi') as rpc_mock: 119 | rpc_mock.return_value = {'users': [{ 120 | 'email': self.email, 121 | 'localId': self.user_id, 122 | 'displayName': self.user_name, 123 | 'photoUrl': self.user_photo 124 | }]} 125 | gitkit_user = self.gitkitclient.GetUserByEmail(self.email) 126 | self.assertEqual(self.user_id, gitkit_user.user_id) 127 | self.assertEqual(self.email, gitkit_user.email) 128 | self.assertEqual(self.user_name, gitkit_user.name) 129 | self.assertEqual(self.user_photo, gitkit_user.photo_url) 130 | 131 | def testUploadAccount(self): 132 | hash_algorithm = gitkitclient.ALGORITHM_HMAC_SHA256 133 | try: 134 | hash_key = bytes('key123', 'utf-8') 135 | except TypeError: 136 | hash_key = 'key123' 137 | upload_user = gitkitclient.GitkitUser.FromDictionary({ 138 | 'email': self.email, 139 | 'localId': self.user_id, 140 | 'displayName': self.user_name, 141 | 'photoUrl': self.user_photo 142 | }) 143 | with mock.patch('identitytoolkit.rpchelper.RpcHelper._InvokeGitkitApi') as rpc_mock: 144 | rpc_mock.return_value = {} 145 | self.gitkitclient.UploadUsers(hash_algorithm, hash_key, [upload_user]) 146 | expected_param = { 147 | 'hashAlgorithm': hash_algorithm, 148 | 'signerKey': base64.urlsafe_b64encode(hash_key), 149 | 'users': [{ 150 | 'email': self.email, 151 | 'localId': self.user_id, 152 | 'displayName': self.user_name, 153 | 'photoUrl': self.user_photo 154 | }] 155 | } 156 | rpc_mock.assert_called_with('uploadAccount', expected_param) 157 | 158 | def testDownloadAccount(self): 159 | with mock.patch('identitytoolkit.rpchelper.RpcHelper._InvokeGitkitApi') as rpc_mock: 160 | # First paginated request 161 | rpc_mock.return_value = { 162 | 'nextPageToken': '100', 163 | 'users': [ 164 | {'email': self.email, 'localId': self.user_id}, 165 | {'email': 'another@example.com', 'localId': 'another'} 166 | ] 167 | } 168 | iterator = self.gitkitclient.GetAllUsers() 169 | self.assertEqual(self.email, next(iterator).email) 170 | self.assertEqual('another@example.com', next(iterator).email) 171 | 172 | # Should stop since no more result 173 | rpc_mock.return_value = {} 174 | with self.assertRaises(StopIteration): 175 | next(iterator) 176 | 177 | expected_call = [(('downloadAccount', {'maxResults': 10}),), 178 | (('downloadAccount', 179 | {'nextPageToken': '100', 'maxResults': 10}),)] 180 | self.assertEqual(expected_call, rpc_mock.call_args_list) 181 | 182 | def testGetOobResult(self): 183 | code = '1234' 184 | with mock.patch('identitytoolkit.rpchelper.RpcHelper._InvokeGitkitApi') as rpc_mock: 185 | rpc_mock.return_value = {'oobCode': code} 186 | widget_request = { 187 | 'action': 'resetPassword', 188 | 'email': self.email, 189 | 'response': '8888' 190 | } 191 | result = self.gitkitclient.GetOobResult(widget_request, '1.1.1.1') 192 | self.assertEqual('resetPassword', result['action']) 193 | self.assertEqual(self.email, result['email']) 194 | self.assertEqual(code, result['oob_code']) 195 | self.assertEqual('{"success": true}', result['response_body']) 196 | self.assertTrue(result['oob_link'].startswith(self.widget_url)) 197 | url = parse.urlparse(result['oob_link']) 198 | query = parse.parse_qs(url.query) 199 | self.assertEqual('resetPassword', query['mode'][0]) 200 | self.assertEqual(code, query['oobCode'][0]) 201 | 202 | def testGetEmailVerificationLink(self): 203 | code = '1234' 204 | with mock.patch('identitytoolkit.rpchelper.RpcHelper._InvokeGitkitApi') as rpc_mock: 205 | rpc_mock.return_value = {'oobCode': code} 206 | result = self.gitkitclient.GetEmailVerificationLink('user@example.com') 207 | self.assertTrue(result.startswith(self.widget_url)) 208 | url = parse.urlparse(result) 209 | query = parse.parse_qs(url.query) 210 | self.assertEqual('verifyEmail', query['mode'][0]) 211 | self.assertEqual(code, query['oobCode'][0]) 212 | 213 | 214 | if __name__ == '__main__': 215 | unittest.main() 216 | -------------------------------------------------------------------------------- /tests/test_rpchelper.py: -------------------------------------------------------------------------------- 1 | # Copyright 2014 Google Inc. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | """Unit test for RpcHelper.""" 16 | try: 17 | from io import StringIO 18 | from unittest import mock 19 | except ImportError: 20 | from StringIO import StringIO 21 | import mock 22 | 23 | import unittest 24 | import simplejson 25 | import sys 26 | 27 | from identitytoolkit import errors 28 | from identitytoolkit import rpchelper 29 | 30 | 31 | class RpcHelperTestCase(unittest.TestCase): 32 | 33 | def setUp(self): 34 | self.api_url = '/widget' 35 | self.service_email = 'dev@content.google.com' 36 | self.rpchelper = rpchelper.RpcHelper(self.service_email, 37 | 'service_key', 38 | self.api_url, 39 | None) 40 | 41 | def testGitkitClientError(self): 42 | error_response = { 43 | 'error': { 44 | 'code': 400, 45 | 'message': 'invalid email' 46 | } 47 | } 48 | try: 49 | self.rpchelper._CheckGitkitError(simplejson.dumps(error_response)) 50 | self.fail('GitkitClientException expected') 51 | except errors.GitkitClientError as error: 52 | self.assertEqual(error_response['error']['message'], error.value) 53 | 54 | def testGitkitServerError(self): 55 | try: 56 | self.rpchelper._CheckGitkitError('') 57 | self.fail('GitkitServerException expected') 58 | except errors.GitkitServerError as error: 59 | self.assertEqual('null error code from Gitkit server', error.value) 60 | 61 | def testGetAccessToken(self): 62 | self.rpchelper._GenerateAssertion = mock.MagicMock() 63 | if sys.version_info[0] > 2: 64 | str_urlopen = 'urllib.request.urlopen' 65 | else: 66 | str_urlopen = 'urllib2.urlopen' 67 | with mock.patch(str_urlopen) as url_mock: 68 | url_mock.return_value = StringIO('{"access_token": "token"}') 69 | result = self.rpchelper._GetAccessToken() 70 | self.assertEqual('token', result) 71 | 72 | 73 | def testGenerateAssertion(self): 74 | with mock.patch('oauth2client.crypt.Signer.from_string') as signer_mock: 75 | signer_mock.return_value = '' 76 | with mock.patch('oauth2client.crypt.make_signed_jwt') as crypt_mock: 77 | self.rpchelper._GenerateAssertion() 78 | _, args, _ = crypt_mock.mock_calls[0] 79 | payload = args[1] # args[0] is signer 80 | self.assertEqual('https://accounts.google.com/o/oauth2/token', 81 | payload['aud']) 82 | self.assertEqual(self.service_email, payload['iss']) 83 | self.assertEqual('https://www.googleapis.com/auth/identitytoolkit', 84 | payload['scope']) 85 | 86 | if __name__ == '__main__': 87 | unittest.main() 88 | --------------------------------------------------------------------------------