Update upstream source from tag 'upstream/0.4.2'
Update to upstream version '0.4.2'
with Debian dir eae626092763c1b3f3c8b08e0fcd1890a773d0f0
Shayan Doust
3 years ago
0 | Apache License | |
0 | Apache License | |
1 | 1 | Version 2.0, January 2004 |
2 | http://www.apache.org/licenses/ | |
2 | https://www.apache.org/licenses/ | |
3 | 3 | |
4 | 4 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION |
5 | 5 | |
191 | 191 | you may not use this file except in compliance with the License. |
192 | 192 | You may obtain a copy of the License at |
193 | 193 | |
194 | http://www.apache.org/licenses/LICENSE-2.0 | |
194 | https://www.apache.org/licenses/LICENSE-2.0 | |
195 | 195 | |
196 | 196 | Unless required by applicable law or agreed to in writing, software |
197 | 197 | distributed under the License is distributed on an "AS IS" BASIS, |
0 | # -*- coding: utf-8 -*- | |
1 | # | |
2 | # Copyright 2020 Google LLC | |
3 | # | |
4 | # Licensed under the Apache License, Version 2.0 (the "License"); | |
5 | # you may not use this file except in compliance with the License. | |
6 | # You may obtain a copy of the License at | |
7 | # | |
8 | # https://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 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
13 | # See the License for the specific language governing permissions and | |
14 | # limitations under the License. | |
15 | ||
16 | # Generated by synthtool. DO NOT EDIT! | |
0 | 17 | include README.rst LICENSE |
18 | recursive-include google *.json *.proto | |
1 | 19 | recursive-include tests * |
20 | global-exclude *.py[co] | |
21 | global-exclude __pycache__ | |
22 | ||
23 | # Exclude scripts for samples readmegen | |
24 | prune scripts/readme-gen⏎ |
0 | 0 | Metadata-Version: 2.1 |
1 | 1 | Name: google-auth-oauthlib |
2 | Version: 0.4.1 | |
2 | Version: 0.4.2 | |
3 | 3 | Summary: Google Authentication Library |
4 | 4 | Home-page: https://github.com/GoogleCloudPlatform/google-auth-library-python-oauthlib |
5 | 5 | Author: Google Cloud Platform |
47 | 47 | |
48 | 48 | Keywords: google auth oauth client oauthlib |
49 | 49 | Platform: UNKNOWN |
50 | Classifier: Programming Language :: Python :: 2 | |
51 | Classifier: Programming Language :: Python :: 2.7 | |
52 | 50 | Classifier: Programming Language :: Python :: 3 |
53 | Classifier: Programming Language :: Python :: 3.4 | |
54 | Classifier: Programming Language :: Python :: 3.5 | |
55 | 51 | Classifier: Programming Language :: Python :: 3.6 |
52 | Classifier: Programming Language :: Python :: 3.7 | |
53 | Classifier: Programming Language :: Python :: 3.8 | |
54 | Classifier: Programming Language :: Python :: 3.9 | |
56 | 55 | Classifier: Development Status :: 3 - Alpha |
57 | 56 | Classifier: Intended Audience :: Developers |
58 | 57 | Classifier: License :: OSI Approved :: Apache Software License |
61 | 60 | Classifier: Operating System :: MacOS :: MacOS X |
62 | 61 | Classifier: Operating System :: OS Independent |
63 | 62 | Classifier: Topic :: Internet :: WWW/HTTP |
63 | Requires-Python: >=3.6 | |
64 | 64 | Provides-Extra: tool |
54 | 54 | import hashlib |
55 | 55 | import json |
56 | 56 | import logging |
57 | ||
57 | 58 | try: |
58 | 59 | from secrets import SystemRandom |
59 | 60 | except ImportError: # pragma: NO COVER |
93 | 94 | """ |
94 | 95 | |
95 | 96 | def __init__( |
96 | self, oauth2session, client_type, client_config, | |
97 | redirect_uri=None, code_verifier=None, | |
98 | autogenerate_code_verifier=False): | |
97 | self, | |
98 | oauth2session, | |
99 | client_type, | |
100 | client_config, | |
101 | redirect_uri=None, | |
102 | code_verifier=None, | |
103 | autogenerate_code_verifier=False, | |
104 | ): | |
99 | 105 | """ |
100 | 106 | Args: |
101 | 107 | oauth2session (requests_oauthlib.OAuth2Session): |
149 | 155 | https://developers.google.com/api-client-library/python/guide |
150 | 156 | /aaa_client_secrets |
151 | 157 | """ |
152 | if 'web' in client_config: | |
153 | client_type = 'web' | |
154 | elif 'installed' in client_config: | |
155 | client_type = 'installed' | |
158 | if "web" in client_config: | |
159 | client_type = "web" | |
160 | elif "installed" in client_config: | |
161 | client_type = "installed" | |
156 | 162 | else: |
157 | raise ValueError( | |
158 | 'Client secrets must be for a web or installed app.') | |
163 | raise ValueError("Client secrets must be for a web or installed app.") | |
159 | 164 | |
160 | 165 | # these args cannot be passed to requests_oauthlib.OAuth2Session |
161 | code_verifier = kwargs.pop('code_verifier', None) | |
162 | autogenerate_code_verifier = kwargs.pop( | |
163 | 'autogenerate_code_verifier', None) | |
164 | ||
165 | session, client_config = ( | |
166 | google_auth_oauthlib.helpers.session_from_client_config( | |
167 | client_config, scopes, **kwargs)) | |
168 | ||
169 | redirect_uri = kwargs.get('redirect_uri', None) | |
166 | code_verifier = kwargs.pop("code_verifier", None) | |
167 | autogenerate_code_verifier = kwargs.pop("autogenerate_code_verifier", None) | |
168 | ||
169 | ( | |
170 | session, | |
171 | client_config, | |
172 | ) = google_auth_oauthlib.helpers.session_from_client_config( | |
173 | client_config, scopes, **kwargs | |
174 | ) | |
175 | ||
176 | redirect_uri = kwargs.get("redirect_uri", None) | |
170 | 177 | |
171 | 178 | return cls( |
172 | 179 | session, |
174 | 181 | client_config, |
175 | 182 | redirect_uri, |
176 | 183 | code_verifier, |
177 | autogenerate_code_verifier | |
184 | autogenerate_code_verifier, | |
178 | 185 | ) |
179 | 186 | |
180 | 187 | @classmethod |
192 | 199 | Returns: |
193 | 200 | Flow: The constructed Flow instance. |
194 | 201 | """ |
195 | with open(client_secrets_file, 'r') as json_file: | |
202 | with open(client_secrets_file, "r") as json_file: | |
196 | 203 | client_config = json.load(json_file) |
197 | 204 | |
198 | 205 | return cls.from_client_config(client_config, scopes=scopes, **kwargs) |
231 | 238 | :class:`Flow` instance to obtain the token, you will need to |
232 | 239 | specify the ``state`` when constructing the :class:`Flow`. |
233 | 240 | """ |
234 | kwargs.setdefault('access_type', 'offline') | |
241 | kwargs.setdefault("access_type", "offline") | |
235 | 242 | if self.autogenerate_code_verifier: |
236 | chars = ascii_letters+digits+'-._~' | |
243 | chars = ascii_letters + digits + "-._~" | |
237 | 244 | rnd = SystemRandom() |
238 | 245 | random_verifier = [rnd.choice(chars) for _ in range(0, 128)] |
239 | self.code_verifier = ''.join(random_verifier) | |
246 | self.code_verifier = "".join(random_verifier) | |
240 | 247 | |
241 | 248 | if self.code_verifier: |
242 | 249 | code_hash = hashlib.sha256() |
243 | 250 | code_hash.update(str.encode(self.code_verifier)) |
244 | 251 | unencoded_challenge = code_hash.digest() |
245 | 252 | b64_challenge = urlsafe_b64encode(unencoded_challenge) |
246 | code_challenge = b64_challenge.decode().split('=')[0] | |
247 | kwargs.setdefault('code_challenge', code_challenge) | |
248 | kwargs.setdefault('code_challenge_method', 'S256') | |
253 | code_challenge = b64_challenge.decode().split("=")[0] | |
254 | kwargs.setdefault("code_challenge", code_challenge) | |
255 | kwargs.setdefault("code_challenge_method", "S256") | |
249 | 256 | url, state = self.oauth2session.authorization_url( |
250 | self.client_config['auth_uri'], **kwargs) | |
257 | self.client_config["auth_uri"], **kwargs | |
258 | ) | |
251 | 259 | |
252 | 260 | return url, state |
253 | 261 | |
274 | 282 | :meth:`credentials` to obtain a |
275 | 283 | :class:`~google.auth.credentials.Credentials` instance. |
276 | 284 | """ |
277 | kwargs.setdefault('client_secret', self.client_config['client_secret']) | |
278 | kwargs.setdefault('code_verifier', self.code_verifier) | |
279 | return self.oauth2session.fetch_token( | |
280 | self.client_config['token_uri'], **kwargs) | |
285 | kwargs.setdefault("client_secret", self.client_config["client_secret"]) | |
286 | kwargs.setdefault("code_verifier", self.code_verifier) | |
287 | return self.oauth2session.fetch_token(self.client_config["token_uri"], **kwargs) | |
281 | 288 | |
282 | 289 | @property |
283 | 290 | def credentials(self): |
294 | 301 | ValueError: If there is no access token in the session. |
295 | 302 | """ |
296 | 303 | return google_auth_oauthlib.helpers.credentials_from_session( |
297 | self.oauth2session, self.client_config) | |
304 | self.oauth2session, self.client_config | |
305 | ) | |
298 | 306 | |
299 | 307 | def authorized_session(self): |
300 | 308 | """Returns a :class:`requests.Session` authorized with credentials. |
307 | 315 | google.auth.transport.requests.AuthorizedSession: The constructed |
308 | 316 | session. |
309 | 317 | """ |
310 | return google.auth.transport.requests.AuthorizedSession( | |
311 | self.credentials) | |
318 | return google.auth.transport.requests.AuthorizedSession(self.credentials) | |
312 | 319 | |
313 | 320 | |
314 | 321 | class InstalledAppFlow(Flow): |
352 | 359 | https://developers.google.com/api-client-library/python/auth |
353 | 360 | /installed-app |
354 | 361 | """ |
355 | _OOB_REDIRECT_URI = 'urn:ietf:wg:oauth:2.0:oob' | |
362 | ||
363 | _OOB_REDIRECT_URI = "urn:ietf:wg:oauth:2.0:oob" | |
356 | 364 | |
357 | 365 | _DEFAULT_AUTH_PROMPT_MESSAGE = ( |
358 | 'Please visit this URL to authorize this application: {url}') | |
366 | "Please visit this URL to authorize this application: {url}" | |
367 | ) | |
359 | 368 | """str: The message to display when prompting the user for |
360 | 369 | authorization.""" |
361 | _DEFAULT_AUTH_CODE_MESSAGE = ( | |
362 | 'Enter the authorization code: ') | |
370 | _DEFAULT_AUTH_CODE_MESSAGE = "Enter the authorization code: " | |
363 | 371 | """str: The message to display when prompting the user for the |
364 | 372 | authorization code. Used only by the console strategy.""" |
365 | 373 | |
366 | 374 | _DEFAULT_WEB_SUCCESS_MESSAGE = ( |
367 | 'The authentication flow has completed. You may close this window.') | |
375 | "The authentication flow has completed. You may close this window." | |
376 | ) | |
368 | 377 | |
369 | 378 | def run_console( |
370 | self, | |
371 | authorization_prompt_message=_DEFAULT_AUTH_PROMPT_MESSAGE, | |
372 | authorization_code_message=_DEFAULT_AUTH_CODE_MESSAGE, | |
373 | **kwargs): | |
379 | self, | |
380 | authorization_prompt_message=_DEFAULT_AUTH_PROMPT_MESSAGE, | |
381 | authorization_code_message=_DEFAULT_AUTH_CODE_MESSAGE, | |
382 | **kwargs | |
383 | ): | |
374 | 384 | """Run the flow using the console strategy. |
375 | 385 | |
376 | 386 | The console strategy instructs the user to open the authorization URL |
390 | 400 | google.oauth2.credentials.Credentials: The OAuth 2.0 credentials |
391 | 401 | for the user. |
392 | 402 | """ |
393 | kwargs.setdefault('prompt', 'consent') | |
403 | kwargs.setdefault("prompt", "consent") | |
394 | 404 | |
395 | 405 | self.redirect_uri = self._OOB_REDIRECT_URI |
396 | 406 | |
405 | 415 | return self.credentials |
406 | 416 | |
407 | 417 | def run_local_server( |
408 | self, host='localhost', port=8080, | |
409 | authorization_prompt_message=_DEFAULT_AUTH_PROMPT_MESSAGE, | |
410 | success_message=_DEFAULT_WEB_SUCCESS_MESSAGE, | |
411 | open_browser=True, | |
412 | **kwargs): | |
418 | self, | |
419 | host="localhost", | |
420 | port=8080, | |
421 | authorization_prompt_message=_DEFAULT_AUTH_PROMPT_MESSAGE, | |
422 | success_message=_DEFAULT_WEB_SUCCESS_MESSAGE, | |
423 | open_browser=True, | |
424 | **kwargs | |
425 | ): | |
413 | 426 | """Run the flow using the server strategy. |
414 | 427 | |
415 | 428 | The server strategy instructs the user to open the authorization URL in |
438 | 451 | for the user. |
439 | 452 | """ |
440 | 453 | wsgi_app = _RedirectWSGIApp(success_message) |
454 | # Fail fast if the address is occupied | |
455 | wsgiref.simple_server.WSGIServer.allow_reuse_address = False | |
441 | 456 | local_server = wsgiref.simple_server.make_server( |
442 | host, port, wsgi_app, handler_class=_WSGIRequestHandler) | |
443 | ||
444 | self.redirect_uri = 'http://{}:{}/'.format( | |
445 | host, local_server.server_port) | |
457 | host, port, wsgi_app, handler_class=_WSGIRequestHandler | |
458 | ) | |
459 | ||
460 | self.redirect_uri = "http://{}:{}/".format(host, local_server.server_port) | |
446 | 461 | auth_url, _ = self.authorization_url(**kwargs) |
447 | 462 | |
448 | 463 | if open_browser: |
454 | 469 | |
455 | 470 | # Note: using https here because oauthlib is very picky that |
456 | 471 | # OAuth 2.0 should only occur over https. |
457 | authorization_response = wsgi_app.last_request_uri.replace( | |
458 | 'http', 'https') | |
472 | authorization_response = wsgi_app.last_request_uri.replace("http", "https") | |
459 | 473 | self.fetch_token(authorization_response=authorization_response) |
474 | ||
475 | # This closes the socket | |
476 | local_server.server_close() | |
460 | 477 | |
461 | 478 | return self.credentials |
462 | 479 | |
466 | 483 | |
467 | 484 | Uses a named logger instead of printing to stderr. |
468 | 485 | """ |
486 | ||
469 | 487 | def log_message(self, format, *args): |
470 | 488 | # pylint: disable=redefined-builtin |
471 | 489 | # (format is the argument name defined in the superclass.) |
498 | 516 | Returns: |
499 | 517 | Iterable[bytes]: The response body. |
500 | 518 | """ |
501 | start_response('200 OK', [('Content-type', 'text/plain')]) | |
519 | start_response("200 OK", [("Content-type", "text/plain")]) | |
502 | 520 | self.last_request_uri = wsgiref.util.request_uri(environ) |
503 | return [self._success_message.encode('utf-8')] | |
521 | return [self._success_message.encode("utf-8")] |
26 | 26 | import google.oauth2.credentials |
27 | 27 | import requests_oauthlib |
28 | 28 | |
29 | _REQUIRED_CONFIG_KEYS = frozenset(('auth_uri', 'token_uri', 'client_id')) | |
29 | _REQUIRED_CONFIG_KEYS = frozenset(("auth_uri", "token_uri", "client_id")) | |
30 | 30 | |
31 | 31 | |
32 | 32 | def session_from_client_config(client_config, scopes, **kwargs): |
54 | 54 | /aaa_client_secrets |
55 | 55 | """ |
56 | 56 | |
57 | if 'web' in client_config: | |
58 | config = client_config['web'] | |
59 | elif 'installed' in client_config: | |
60 | config = client_config['installed'] | |
57 | if "web" in client_config: | |
58 | config = client_config["web"] | |
59 | elif "installed" in client_config: | |
60 | config = client_config["installed"] | |
61 | 61 | else: |
62 | raise ValueError( | |
63 | 'Client secrets must be for a web or installed app.') | |
62 | raise ValueError("Client secrets must be for a web or installed app.") | |
64 | 63 | |
65 | 64 | if not _REQUIRED_CONFIG_KEYS.issubset(config.keys()): |
66 | raise ValueError('Client secrets is not in the correct format.') | |
65 | raise ValueError("Client secrets is not in the correct format.") | |
67 | 66 | |
68 | 67 | session = requests_oauthlib.OAuth2Session( |
69 | client_id=config['client_id'], | |
70 | scope=scopes, | |
71 | **kwargs) | |
68 | client_id=config["client_id"], scope=scopes, **kwargs | |
69 | ) | |
72 | 70 | |
73 | 71 | return session, client_config |
74 | 72 | |
93 | 91 | https://developers.google.com/api-client-library/python/guide |
94 | 92 | /aaa_client_secrets |
95 | 93 | """ |
96 | with open(client_secrets_file, 'r') as json_file: | |
94 | with open(client_secrets_file, "r") as json_file: | |
97 | 95 | client_config = json.load(json_file) |
98 | 96 | |
99 | 97 | return session_from_client_config(client_config, scopes, **kwargs) |
125 | 123 | |
126 | 124 | if not session.token: |
127 | 125 | raise ValueError( |
128 | 'There is no access token for this session, did you call ' | |
129 | 'fetch_token?') | |
126 | "There is no access token for this session, did you call " "fetch_token?" | |
127 | ) | |
130 | 128 | |
131 | 129 | credentials = google.oauth2.credentials.Credentials( |
132 | session.token['access_token'], | |
133 | refresh_token=session.token.get('refresh_token'), | |
134 | id_token=session.token.get('id_token'), | |
135 | token_uri=client_config.get('token_uri'), | |
136 | client_id=client_config.get('client_id'), | |
137 | client_secret=client_config.get('client_secret'), | |
138 | scopes=session.scope) | |
139 | credentials.expiry = datetime.datetime.utcfromtimestamp( | |
140 | session.token['expires_at']) | |
130 | session.token["access_token"], | |
131 | refresh_token=session.token.get("refresh_token"), | |
132 | id_token=session.token.get("id_token"), | |
133 | token_uri=client_config.get("token_uri"), | |
134 | client_id=client_config.get("client_id"), | |
135 | client_secret=client_config.get("client_secret"), | |
136 | scopes=session.scope, | |
137 | ) | |
138 | credentials.expiry = datetime.datetime.utcfromtimestamp(session.token["expires_at"]) | |
141 | 139 | return credentials |
38 | 38 | import google_auth_oauthlib.flow |
39 | 39 | |
40 | 40 | |
41 | APP_NAME = 'google-oauthlib-tool' | |
42 | DEFAULT_CREDENTIALS_FILENAME = 'credentials.json' | |
41 | APP_NAME = "google-oauthlib-tool" | |
42 | DEFAULT_CREDENTIALS_FILENAME = "credentials.json" | |
43 | 43 | |
44 | 44 | |
45 | 45 | @click.command() |
46 | 46 | @click.option( |
47 | '--client-secrets', | |
48 | metavar='<client_secret_json_file>', | |
47 | "--client-secrets", | |
48 | metavar="<client_secret_json_file>", | |
49 | 49 | required=True, |
50 | help='Path to OAuth2 client secret JSON file.') | |
50 | help="Path to OAuth2 client secret JSON file.", | |
51 | ) | |
51 | 52 | @click.option( |
52 | '--scope', | |
53 | "--scope", | |
53 | 54 | multiple=True, |
54 | metavar='<oauth2 scope>', | |
55 | metavar="<oauth2 scope>", | |
55 | 56 | required=True, |
56 | help='API scopes to authorize access for.') | |
57 | help="API scopes to authorize access for.", | |
58 | ) | |
57 | 59 | @click.option( |
58 | '--save', | |
60 | "--save", | |
59 | 61 | is_flag=True, |
60 | metavar='<save_mode>', | |
62 | metavar="<save_mode>", | |
61 | 63 | show_default=True, |
62 | 64 | default=False, |
63 | help='Save the credentials to file.') | |
65 | help="Save the credentials to file.", | |
66 | ) | |
64 | 67 | @click.option( |
65 | '--credentials', | |
66 | metavar='<oauth2_credentials>', | |
68 | "--credentials", | |
69 | metavar="<oauth2_credentials>", | |
67 | 70 | show_default=True, |
68 | default=os.path.join( | |
69 | click.get_app_dir(APP_NAME), | |
70 | DEFAULT_CREDENTIALS_FILENAME | |
71 | ), | |
72 | help='Path to store OAuth2 credentials.') | |
71 | default=os.path.join(click.get_app_dir(APP_NAME), DEFAULT_CREDENTIALS_FILENAME), | |
72 | help="Path to store OAuth2 credentials.", | |
73 | ) | |
73 | 74 | @click.option( |
74 | '--headless', | |
75 | "--headless", | |
75 | 76 | is_flag=True, |
76 | metavar='<headless_mode>', | |
77 | show_default=True, default=False, | |
78 | help='Run a console based flow.') | |
77 | metavar="<headless_mode>", | |
78 | show_default=True, | |
79 | default=False, | |
80 | help="Run a console based flow.", | |
81 | ) | |
79 | 82 | def main(client_secrets, scope, save, credentials, headless): |
80 | 83 | """Command-line tool for obtaining authorization and credentials from a user. |
81 | 84 | |
95 | 98 | """ |
96 | 99 | |
97 | 100 | flow = google_auth_oauthlib.flow.InstalledAppFlow.from_client_secrets_file( |
98 | client_secrets, | |
99 | scopes=scope | |
101 | client_secrets, scopes=scope | |
100 | 102 | ) |
101 | 103 | |
102 | 104 | if not headless: |
105 | 107 | creds = flow.run_console() |
106 | 108 | |
107 | 109 | creds_data = { |
108 | 'token': creds.token, | |
109 | 'refresh_token': creds.refresh_token, | |
110 | 'token_uri': creds.token_uri, | |
111 | 'client_id': creds.client_id, | |
112 | 'client_secret': creds.client_secret, | |
113 | 'scopes': creds.scopes | |
110 | "token": creds.token, | |
111 | "refresh_token": creds.refresh_token, | |
112 | "token_uri": creds.token_uri, | |
113 | "client_id": creds.client_id, | |
114 | "client_secret": creds.client_secret, | |
115 | "scopes": creds.scopes, | |
114 | 116 | } |
115 | 117 | |
116 | 118 | if save: |
117 | del creds_data['token'] | |
119 | del creds_data["token"] | |
118 | 120 | |
119 | 121 | config_path = os.path.dirname(credentials) |
120 | 122 | if config_path and not os.path.isdir(config_path): |
121 | 123 | os.makedirs(config_path) |
122 | 124 | |
123 | with open(credentials, 'w') as outfile: | |
125 | with open(credentials, "w") as outfile: | |
124 | 126 | json.dump(creds_data, outfile) |
125 | 127 | |
126 | click.echo('credentials saved: %s' % credentials) | |
128 | click.echo("credentials saved: %s" % credentials) | |
127 | 129 | |
128 | 130 | else: |
129 | 131 | click.echo(json.dumps(creds_data)) |
130 | 132 | |
131 | 133 | |
132 | if __name__ == '__main__': | |
134 | if __name__ == "__main__": | |
133 | 135 | # pylint doesn't realize that click has changed the function signature. |
134 | 136 | main() # pylint: disable=no-value-for-parameter |
0 | 0 | Metadata-Version: 2.1 |
1 | 1 | Name: google-auth-oauthlib |
2 | Version: 0.4.1 | |
2 | Version: 0.4.2 | |
3 | 3 | Summary: Google Authentication Library |
4 | 4 | Home-page: https://github.com/GoogleCloudPlatform/google-auth-library-python-oauthlib |
5 | 5 | Author: Google Cloud Platform |
47 | 47 | |
48 | 48 | Keywords: google auth oauth client oauthlib |
49 | 49 | Platform: UNKNOWN |
50 | Classifier: Programming Language :: Python :: 2 | |
51 | Classifier: Programming Language :: Python :: 2.7 | |
52 | 50 | Classifier: Programming Language :: Python :: 3 |
53 | Classifier: Programming Language :: Python :: 3.4 | |
54 | Classifier: Programming Language :: Python :: 3.5 | |
55 | 51 | Classifier: Programming Language :: Python :: 3.6 |
52 | Classifier: Programming Language :: Python :: 3.7 | |
53 | Classifier: Programming Language :: Python :: 3.8 | |
54 | Classifier: Programming Language :: Python :: 3.9 | |
56 | 55 | Classifier: Development Status :: 3 - Alpha |
57 | 56 | Classifier: Intended Audience :: Developers |
58 | 57 | Classifier: License :: OSI Approved :: Apache Software License |
61 | 60 | Classifier: Operating System :: MacOS :: MacOS X |
62 | 61 | Classifier: Operating System :: OS Independent |
63 | 62 | Classifier: Topic :: Internet :: WWW/HTTP |
63 | Requires-Python: >=3.6 | |
64 | 64 | Provides-Extra: tool |
14 | 14 | google_auth_oauthlib.egg-info/top_level.txt |
15 | 15 | google_auth_oauthlib/tool/__init__.py |
16 | 16 | google_auth_oauthlib/tool/__main__.py |
17 | tests/__init__.py | |
18 | tests/test_flow.py | |
19 | tests/test_helpers.py | |
20 | tests/test_interactive.py | |
21 | tests/test_tool.py | |
22 | tests/data/client_secrets.json⏎ | |
17 | tests/unit/test_flow.py | |
18 | tests/unit/test_helpers.py | |
19 | tests/unit/test_interactive.py | |
20 | tests/unit/test_tool.py | |
21 | tests/unit/data/client_secrets.json⏎ |
17 | 17 | from setuptools import setup |
18 | 18 | |
19 | 19 | |
20 | TOOL_DEPENDENCIES = ( | |
21 | 'click' | |
22 | ) | |
20 | TOOL_DEPENDENCIES = "click" | |
23 | 21 | |
24 | DEPENDENCIES = ( | |
25 | 'google-auth', | |
26 | 'requests-oauthlib>=0.7.0', | |
27 | ) | |
22 | DEPENDENCIES = ("google-auth", "requests-oauthlib>=0.7.0") | |
28 | 23 | |
29 | 24 | |
30 | with io.open('README.rst', 'r') as fh: | |
25 | with io.open("README.rst", "r") as fh: | |
31 | 26 | long_description = fh.read() |
32 | 27 | |
33 | 28 | |
29 | version = "0.4.2" | |
30 | ||
34 | 31 | setup( |
35 | name='google-auth-oauthlib', | |
36 | version = '0.4.1', | |
37 | author='Google Cloud Platform', | |
38 | author_email='jonwayne+google-auth@google.com', | |
39 | description='Google Authentication Library', | |
32 | name="google-auth-oauthlib", | |
33 | version=version, | |
34 | author="Google Cloud Platform", | |
35 | author_email="jonwayne+google-auth@google.com", | |
36 | description="Google Authentication Library", | |
40 | 37 | long_description=long_description, |
41 | url='https://github.com/GoogleCloudPlatform/google-auth-library-python-oauthlib', | |
42 | packages=find_packages(exclude=('tests*',)), | |
38 | url="https://github.com/GoogleCloudPlatform/google-auth-library-python-oauthlib", | |
39 | packages=find_packages(exclude=("tests*",)), | |
43 | 40 | install_requires=DEPENDENCIES, |
44 | extras_require={ | |
45 | 'tool': TOOL_DEPENDENCIES, | |
41 | extras_require={"tool": TOOL_DEPENDENCIES}, | |
42 | entry_points={ | |
43 | "console_scripts": [ | |
44 | "google-oauthlib-tool" "=google_auth_oauthlib.tool.__main__:main [tool]" | |
45 | ] | |
46 | 46 | }, |
47 | entry_points={ | |
48 | 'console_scripts': [ | |
49 | 'google-oauthlib-tool' | |
50 | '=google_auth_oauthlib.tool.__main__:main [tool]', | |
51 | ], | |
52 | }, | |
53 | license='Apache 2.0', | |
54 | keywords='google auth oauth client oauthlib', | |
55 | classifiers=( | |
56 | 'Programming Language :: Python :: 2', | |
57 | 'Programming Language :: Python :: 2.7', | |
58 | 'Programming Language :: Python :: 3', | |
59 | 'Programming Language :: Python :: 3.4', | |
60 | 'Programming Language :: Python :: 3.5', | |
61 | 'Programming Language :: Python :: 3.6', | |
62 | 'Development Status :: 3 - Alpha', | |
63 | 'Intended Audience :: Developers', | |
64 | 'License :: OSI Approved :: Apache Software License', | |
65 | 'Operating System :: POSIX', | |
66 | 'Operating System :: Microsoft :: Windows', | |
67 | 'Operating System :: MacOS :: MacOS X', | |
68 | 'Operating System :: OS Independent', | |
69 | 'Topic :: Internet :: WWW/HTTP', | |
70 | ), | |
47 | python_requires=">=3.6", | |
48 | license="Apache 2.0", | |
49 | keywords="google auth oauth client oauthlib", | |
50 | classifiers=[ | |
51 | "Programming Language :: Python :: 3", | |
52 | "Programming Language :: Python :: 3.6", | |
53 | "Programming Language :: Python :: 3.7", | |
54 | "Programming Language :: Python :: 3.8", | |
55 | "Programming Language :: Python :: 3.9", | |
56 | "Development Status :: 3 - Alpha", | |
57 | "Intended Audience :: Developers", | |
58 | "License :: OSI Approved :: Apache Software License", | |
59 | "Operating System :: POSIX", | |
60 | "Operating System :: Microsoft :: Windows", | |
61 | "Operating System :: MacOS :: MacOS X", | |
62 | "Operating System :: OS Independent", | |
63 | "Topic :: Internet :: WWW/HTTP", | |
64 | ], | |
71 | 65 | ) |
0 | { | |
1 | "web": { | |
2 | "client_id": "example.apps.googleusercontent.com", | |
3 | "project_id": "example", | |
4 | "auth_uri": "https://accounts.google.com/o/oauth2/auth", | |
5 | "token_uri": "https://accounts.google.com/o/oauth2/token", | |
6 | "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", | |
7 | "client_secret": "itsasecrettoeveryone", | |
8 | "redirect_uris": [ | |
9 | "urn:ietf:wg:oauth:2.0:oob", | |
10 | "http://localhost" | |
11 | ] | |
12 | } | |
13 | } |
0 | # Copyright 2017 Google Inc. | |
1 | # | |
2 | # Licensed under the Apache License, Version 2.0 (the "License"); | |
3 | # you may not use this file except in compliance with the License. | |
4 | # You may obtain a copy of the License at | |
5 | # | |
6 | # http://www.apache.org/licenses/LICENSE-2.0 | |
7 | # | |
8 | # Unless required by applicable law or agreed to in writing, software | |
9 | # distributed under the License is distributed on an "AS IS" BASIS, | |
10 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
11 | # See the License for the specific language governing permissions and | |
12 | # limitations under the License. | |
13 | ||
14 | import concurrent.futures | |
15 | import datetime | |
16 | from functools import partial | |
17 | import json | |
18 | import os | |
19 | import re | |
20 | ||
21 | import mock | |
22 | import pytest | |
23 | import requests | |
24 | from six.moves import urllib | |
25 | ||
26 | from google_auth_oauthlib import flow | |
27 | ||
28 | DATA_DIR = os.path.join(os.path.dirname(__file__), 'data') | |
29 | CLIENT_SECRETS_FILE = os.path.join(DATA_DIR, 'client_secrets.json') | |
30 | ||
31 | with open(CLIENT_SECRETS_FILE, 'r') as fh: | |
32 | CLIENT_SECRETS_INFO = json.load(fh) | |
33 | ||
34 | ||
35 | class TestFlow(object): | |
36 | def test_from_client_secrets_file(self): | |
37 | instance = flow.Flow.from_client_secrets_file( | |
38 | CLIENT_SECRETS_FILE, scopes=mock.sentinel.scopes) | |
39 | assert instance.client_config == CLIENT_SECRETS_INFO['web'] | |
40 | assert (instance.oauth2session.client_id == | |
41 | CLIENT_SECRETS_INFO['web']['client_id']) | |
42 | assert instance.oauth2session.scope == mock.sentinel.scopes | |
43 | ||
44 | def test_from_client_secrets_file_with_redirect_uri(self): | |
45 | instance = flow.Flow.from_client_secrets_file( | |
46 | CLIENT_SECRETS_FILE, scopes=mock.sentinel.scopes, | |
47 | redirect_uri=mock.sentinel.redirect_uri | |
48 | ) | |
49 | assert (instance.redirect_uri == | |
50 | instance.oauth2session.redirect_uri == | |
51 | mock.sentinel.redirect_uri) | |
52 | ||
53 | def test_from_client_config_installed(self): | |
54 | client_config = {'installed': CLIENT_SECRETS_INFO['web']} | |
55 | instance = flow.Flow.from_client_config( | |
56 | client_config, scopes=mock.sentinel.scopes) | |
57 | assert instance.client_config == client_config['installed'] | |
58 | assert (instance.oauth2session.client_id == | |
59 | client_config['installed']['client_id']) | |
60 | assert instance.oauth2session.scope == mock.sentinel.scopes | |
61 | ||
62 | def test_from_client_config_with_redirect_uri(self): | |
63 | client_config = {'installed': CLIENT_SECRETS_INFO['web']} | |
64 | instance = flow.Flow.from_client_config( | |
65 | client_config, scopes=mock.sentinel.scopes, | |
66 | redirect_uri=mock.sentinel.redirect_uri | |
67 | ) | |
68 | assert (instance.redirect_uri == | |
69 | instance.oauth2session.redirect_uri == | |
70 | mock.sentinel.redirect_uri) | |
71 | ||
72 | def test_from_client_config_bad_format(self): | |
73 | with pytest.raises(ValueError): | |
74 | flow.Flow.from_client_config({}, scopes=mock.sentinel.scopes) | |
75 | ||
76 | @pytest.fixture | |
77 | def instance(self): | |
78 | yield flow.Flow.from_client_config( | |
79 | CLIENT_SECRETS_INFO, scopes=mock.sentinel.scopes) | |
80 | ||
81 | def test_redirect_uri(self, instance): | |
82 | instance.redirect_uri = mock.sentinel.redirect_uri | |
83 | assert (instance.redirect_uri == | |
84 | instance.oauth2session.redirect_uri == | |
85 | mock.sentinel.redirect_uri) | |
86 | ||
87 | def test_authorization_url(self, instance): | |
88 | scope = 'scope_one' | |
89 | instance.oauth2session.scope = [scope] | |
90 | authorization_url_patch = mock.patch.object( | |
91 | instance.oauth2session, 'authorization_url', | |
92 | wraps=instance.oauth2session.authorization_url) | |
93 | ||
94 | with authorization_url_patch as authorization_url_spy: | |
95 | url, _ = instance.authorization_url(prompt='consent') | |
96 | ||
97 | assert CLIENT_SECRETS_INFO['web']['auth_uri'] in url | |
98 | assert scope in url | |
99 | authorization_url_spy.assert_called_with( | |
100 | CLIENT_SECRETS_INFO['web']['auth_uri'], | |
101 | access_type='offline', | |
102 | prompt='consent') | |
103 | ||
104 | def test_authorization_url_code_verifier(self, instance): | |
105 | scope = 'scope_one' | |
106 | instance.oauth2session.scope = [scope] | |
107 | instance.code_verifier = 'amanaplanacanalpanama' | |
108 | authorization_url_patch = mock.patch.object( | |
109 | instance.oauth2session, 'authorization_url', | |
110 | wraps=instance.oauth2session.authorization_url) | |
111 | ||
112 | with authorization_url_patch as authorization_url_spy: | |
113 | url, _ = instance.authorization_url(prompt='consent') | |
114 | ||
115 | assert CLIENT_SECRETS_INFO['web']['auth_uri'] in url | |
116 | assert scope in url | |
117 | authorization_url_spy.assert_called_with( | |
118 | CLIENT_SECRETS_INFO['web']['auth_uri'], | |
119 | access_type='offline', | |
120 | prompt='consent', | |
121 | code_challenge='2yN0TOdl0gkGwFOmtfx3f913tgEaLM2d2S0WlmG1Z6Q', | |
122 | code_challenge_method='S256') | |
123 | ||
124 | def test_authorization_url_access_type(self, instance): | |
125 | scope = 'scope_one' | |
126 | instance.oauth2session.scope = [scope] | |
127 | instance.code_verifier = 'amanaplanacanalpanama' | |
128 | authorization_url_patch = mock.patch.object( | |
129 | instance.oauth2session, 'authorization_url', | |
130 | wraps=instance.oauth2session.authorization_url) | |
131 | ||
132 | with authorization_url_patch as authorization_url_spy: | |
133 | url, _ = instance.authorization_url(access_type='meep') | |
134 | ||
135 | assert CLIENT_SECRETS_INFO['web']['auth_uri'] in url | |
136 | assert scope in url | |
137 | authorization_url_spy.assert_called_with( | |
138 | CLIENT_SECRETS_INFO['web']['auth_uri'], | |
139 | access_type='meep', | |
140 | code_challenge='2yN0TOdl0gkGwFOmtfx3f913tgEaLM2d2S0WlmG1Z6Q', | |
141 | code_challenge_method='S256') | |
142 | ||
143 | def test_authorization_url_generated_verifier(self): | |
144 | scope = 'scope_one' | |
145 | instance = flow.Flow.from_client_config( | |
146 | CLIENT_SECRETS_INFO, scopes=[scope], | |
147 | autogenerate_code_verifier=True) | |
148 | authorization_url_path = mock.patch.object( | |
149 | instance.oauth2session, 'authorization_url', | |
150 | wraps=instance.oauth2session.authorization_url) | |
151 | ||
152 | with authorization_url_path as authorization_url_spy: | |
153 | instance.authorization_url() | |
154 | ||
155 | _, kwargs = authorization_url_spy.call_args_list[0] | |
156 | assert kwargs['code_challenge_method'] == 'S256' | |
157 | assert len(instance.code_verifier) == 128 | |
158 | assert len(kwargs['code_challenge']) == 43 | |
159 | valid_verifier = r'^[A-Za-z0-9-._~]*$' | |
160 | valid_challenge = r'^[A-Za-z0-9-_]*$' | |
161 | assert re.match(valid_verifier, instance.code_verifier) | |
162 | assert re.match(valid_challenge, kwargs['code_challenge']) | |
163 | ||
164 | def test_fetch_token(self, instance): | |
165 | instance.code_verifier = 'amanaplanacanalpanama' | |
166 | fetch_token_patch = mock.patch.object( | |
167 | instance.oauth2session, 'fetch_token', autospec=True, | |
168 | return_value=mock.sentinel.token) | |
169 | ||
170 | with fetch_token_patch as fetch_token_mock: | |
171 | token = instance.fetch_token(code=mock.sentinel.code) | |
172 | ||
173 | assert token == mock.sentinel.token | |
174 | fetch_token_mock.assert_called_with( | |
175 | CLIENT_SECRETS_INFO['web']['token_uri'], | |
176 | client_secret=CLIENT_SECRETS_INFO['web']['client_secret'], | |
177 | code=mock.sentinel.code, | |
178 | code_verifier='amanaplanacanalpanama') | |
179 | ||
180 | def test_credentials(self, instance): | |
181 | instance.oauth2session.token = { | |
182 | 'access_token': mock.sentinel.access_token, | |
183 | 'refresh_token': mock.sentinel.refresh_token, | |
184 | 'id_token': mock.sentinel.id_token, | |
185 | 'expires_at': 643969200.0 | |
186 | } | |
187 | ||
188 | credentials = instance.credentials | |
189 | ||
190 | assert credentials.token == mock.sentinel.access_token | |
191 | assert credentials.expiry == datetime.datetime(1990, 5, 29, 8, 20, 0) | |
192 | assert credentials._refresh_token == mock.sentinel.refresh_token | |
193 | assert credentials.id_token == mock.sentinel.id_token | |
194 | assert (credentials._client_id == | |
195 | CLIENT_SECRETS_INFO['web']['client_id']) | |
196 | assert (credentials._client_secret == | |
197 | CLIENT_SECRETS_INFO['web']['client_secret']) | |
198 | assert (credentials._token_uri == | |
199 | CLIENT_SECRETS_INFO['web']['token_uri']) | |
200 | ||
201 | def test_authorized_session(self, instance): | |
202 | instance.oauth2session.token = { | |
203 | 'access_token': mock.sentinel.access_token, | |
204 | 'refresh_token': mock.sentinel.refresh_token, | |
205 | 'id_token': mock.sentinel.id_token, | |
206 | 'expires_at': 643969200.0 | |
207 | } | |
208 | ||
209 | session = instance.authorized_session() | |
210 | ||
211 | assert session.credentials.token == mock.sentinel.access_token | |
212 | ||
213 | ||
214 | class TestInstalledAppFlow(object): | |
215 | SCOPES = ['email', 'profile'] | |
216 | REDIRECT_REQUEST_PATH = '/?code=code&state=state' | |
217 | ||
218 | @pytest.fixture | |
219 | def instance(self): | |
220 | yield flow.InstalledAppFlow.from_client_config( | |
221 | CLIENT_SECRETS_INFO, scopes=self.SCOPES) | |
222 | ||
223 | @pytest.fixture | |
224 | def mock_fetch_token(self, instance): | |
225 | def set_token(*args, **kwargs): | |
226 | instance.oauth2session.token = { | |
227 | 'access_token': mock.sentinel.access_token, | |
228 | 'refresh_token': mock.sentinel.refresh_token, | |
229 | 'id_token': mock.sentinel.id_token, | |
230 | 'expires_at': 643969200.0 | |
231 | } | |
232 | ||
233 | fetch_token_patch = mock.patch.object( | |
234 | instance.oauth2session, 'fetch_token', autospec=True, | |
235 | side_effect=set_token) | |
236 | ||
237 | with fetch_token_patch as fetch_token_mock: | |
238 | yield fetch_token_mock | |
239 | ||
240 | @mock.patch('google_auth_oauthlib.flow.input', autospec=True) | |
241 | def test_run_console(self, input_mock, instance, mock_fetch_token): | |
242 | input_mock.return_value = mock.sentinel.code | |
243 | instance.code_verifier = 'amanaplanacanalpanama' | |
244 | credentials = instance.run_console() | |
245 | ||
246 | assert credentials.token == mock.sentinel.access_token | |
247 | assert credentials._refresh_token == mock.sentinel.refresh_token | |
248 | assert credentials.id_token == mock.sentinel.id_token | |
249 | ||
250 | mock_fetch_token.assert_called_with( | |
251 | CLIENT_SECRETS_INFO['web']['token_uri'], | |
252 | client_secret=CLIENT_SECRETS_INFO['web']['client_secret'], | |
253 | code=mock.sentinel.code, | |
254 | code_verifier='amanaplanacanalpanama') | |
255 | ||
256 | @pytest.mark.webtest | |
257 | @mock.patch('google_auth_oauthlib.flow.webbrowser', autospec=True) | |
258 | def test_run_local_server( | |
259 | self, webbrowser_mock, instance, mock_fetch_token): | |
260 | auth_redirect_url = urllib.parse.urljoin( | |
261 | 'http://localhost:60452', | |
262 | self.REDIRECT_REQUEST_PATH) | |
263 | ||
264 | with concurrent.futures.ThreadPoolExecutor(max_workers=1) as pool: | |
265 | future = pool.submit(partial( | |
266 | instance.run_local_server, port=60452)) | |
267 | ||
268 | while not future.done(): | |
269 | try: | |
270 | requests.get(auth_redirect_url) | |
271 | except requests.ConnectionError: # pragma: NO COVER | |
272 | pass | |
273 | ||
274 | credentials = future.result() | |
275 | ||
276 | assert credentials.token == mock.sentinel.access_token | |
277 | assert credentials._refresh_token == mock.sentinel.refresh_token | |
278 | assert credentials.id_token == mock.sentinel.id_token | |
279 | assert webbrowser_mock.open.called | |
280 | ||
281 | expected_auth_response = auth_redirect_url.replace('http', 'https') | |
282 | mock_fetch_token.assert_called_with( | |
283 | CLIENT_SECRETS_INFO['web']['token_uri'], | |
284 | client_secret=CLIENT_SECRETS_INFO['web']['client_secret'], | |
285 | authorization_response=expected_auth_response, | |
286 | code_verifier=None) | |
287 | ||
288 | @pytest.mark.webtest | |
289 | @mock.patch('google_auth_oauthlib.flow.webbrowser', autospec=True) | |
290 | def test_run_local_server_code_verifier( | |
291 | self, webbrowser_mock, instance, mock_fetch_token): | |
292 | auth_redirect_url = urllib.parse.urljoin( | |
293 | 'http://localhost:60452', | |
294 | self.REDIRECT_REQUEST_PATH) | |
295 | instance.code_verifier = 'amanaplanacanalpanama' | |
296 | ||
297 | with concurrent.futures.ThreadPoolExecutor(max_workers=1) as pool: | |
298 | future = pool.submit(partial( | |
299 | instance.run_local_server, port=60452)) | |
300 | ||
301 | while not future.done(): | |
302 | try: | |
303 | requests.get(auth_redirect_url) | |
304 | except requests.ConnectionError: # pragma: NO COVER | |
305 | pass | |
306 | ||
307 | credentials = future.result() | |
308 | ||
309 | assert credentials.token == mock.sentinel.access_token | |
310 | assert credentials._refresh_token == mock.sentinel.refresh_token | |
311 | assert credentials.id_token == mock.sentinel.id_token | |
312 | assert webbrowser_mock.open.called | |
313 | ||
314 | expected_auth_response = auth_redirect_url.replace('http', 'https') | |
315 | mock_fetch_token.assert_called_with( | |
316 | CLIENT_SECRETS_INFO['web']['token_uri'], | |
317 | client_secret=CLIENT_SECRETS_INFO['web']['client_secret'], | |
318 | authorization_response=expected_auth_response, | |
319 | code_verifier='amanaplanacanalpanama') | |
320 | ||
321 | @mock.patch('google_auth_oauthlib.flow.webbrowser', autospec=True) | |
322 | @mock.patch('wsgiref.simple_server.make_server', autospec=True) | |
323 | def test_run_local_server_no_browser( | |
324 | self, make_server_mock, webbrowser_mock, instance, | |
325 | mock_fetch_token): | |
326 | ||
327 | def assign_last_request_uri(host, port, wsgi_app, **kwargs): | |
328 | wsgi_app.last_request_uri = self.REDIRECT_REQUEST_PATH | |
329 | return mock.Mock() | |
330 | ||
331 | make_server_mock.side_effect = assign_last_request_uri | |
332 | ||
333 | instance.run_local_server(open_browser=False) | |
334 | ||
335 | assert not webbrowser_mock.open.called |
0 | # Copyright 2017 Google Inc. | |
1 | # | |
2 | # Licensed under the Apache License, Version 2.0 (the "License"); | |
3 | # you may not use this file except in compliance with the License. | |
4 | # You may obtain a copy of the License at | |
5 | # | |
6 | # http://www.apache.org/licenses/LICENSE-2.0 | |
7 | # | |
8 | # Unless required by applicable law or agreed to in writing, software | |
9 | # distributed under the License is distributed on an "AS IS" BASIS, | |
10 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
11 | # See the License for the specific language governing permissions and | |
12 | # limitations under the License. | |
13 | ||
14 | import datetime | |
15 | import json | |
16 | import os | |
17 | ||
18 | import mock | |
19 | import pytest | |
20 | ||
21 | from google_auth_oauthlib import helpers | |
22 | ||
23 | DATA_DIR = os.path.join(os.path.dirname(__file__), 'data') | |
24 | CLIENT_SECRETS_FILE = os.path.join(DATA_DIR, 'client_secrets.json') | |
25 | ||
26 | with open(CLIENT_SECRETS_FILE, 'r') as fh: | |
27 | CLIENT_SECRETS_INFO = json.load(fh) | |
28 | ||
29 | ||
30 | def test_session_from_client_config_web(): | |
31 | session, config = helpers.session_from_client_config( | |
32 | CLIENT_SECRETS_INFO, scopes=mock.sentinel.scopes) | |
33 | ||
34 | assert config == CLIENT_SECRETS_INFO | |
35 | assert session.client_id == CLIENT_SECRETS_INFO['web']['client_id'] | |
36 | assert session.scope == mock.sentinel.scopes | |
37 | ||
38 | ||
39 | def test_session_from_client_config_installed(): | |
40 | info = {'installed': CLIENT_SECRETS_INFO['web']} | |
41 | session, config = helpers.session_from_client_config( | |
42 | info, scopes=mock.sentinel.scopes) | |
43 | assert config == info | |
44 | assert session.client_id == info['installed']['client_id'] | |
45 | assert session.scope == mock.sentinel.scopes | |
46 | ||
47 | ||
48 | def test_session_from_client_config_bad_format(): | |
49 | with pytest.raises(ValueError): | |
50 | helpers.session_from_client_config({}, scopes=[]) | |
51 | ||
52 | ||
53 | def test_session_from_client_config_missing_keys(): | |
54 | with pytest.raises(ValueError): | |
55 | helpers.session_from_client_config({'web': {}}, scopes=[]) | |
56 | ||
57 | ||
58 | def test_session_from_client_secrets_file(): | |
59 | session, config = helpers.session_from_client_secrets_file( | |
60 | CLIENT_SECRETS_FILE, scopes=mock.sentinel.scopes) | |
61 | assert config == CLIENT_SECRETS_INFO | |
62 | assert session.client_id == CLIENT_SECRETS_INFO['web']['client_id'] | |
63 | assert session.scope == mock.sentinel.scopes | |
64 | ||
65 | ||
66 | @pytest.fixture | |
67 | def session(): | |
68 | session, _ = helpers.session_from_client_config( | |
69 | CLIENT_SECRETS_INFO, scopes=mock.sentinel.scopes) | |
70 | yield session | |
71 | ||
72 | ||
73 | def test_credentials_from_session(session): | |
74 | session.token = { | |
75 | 'access_token': mock.sentinel.access_token, | |
76 | 'refresh_token': mock.sentinel.refresh_token, | |
77 | 'id_token': mock.sentinel.id_token, | |
78 | 'expires_at': 643969200.0 | |
79 | } | |
80 | ||
81 | credentials = helpers.credentials_from_session( | |
82 | session, CLIENT_SECRETS_INFO['web']) | |
83 | ||
84 | assert credentials.token == mock.sentinel.access_token | |
85 | assert credentials.expiry == datetime.datetime(1990, 5, 29, 8, 20, 0) | |
86 | assert credentials._refresh_token == mock.sentinel.refresh_token | |
87 | assert credentials.id_token == mock.sentinel.id_token | |
88 | assert credentials._client_id == CLIENT_SECRETS_INFO['web']['client_id'] | |
89 | assert (credentials._client_secret == | |
90 | CLIENT_SECRETS_INFO['web']['client_secret']) | |
91 | assert credentials._token_uri == CLIENT_SECRETS_INFO['web']['token_uri'] | |
92 | ||
93 | ||
94 | def test_bad_credentials(session): | |
95 | with pytest.raises(ValueError): | |
96 | helpers.credentials_from_session(session) |
0 | # Copyright 2019 Google LLC | |
1 | # | |
2 | # Licensed under the Apache License, Version 2.0 (the "License"); | |
3 | # you may not use this file except in compliance with the License. | |
4 | # You may obtain a copy of the License at | |
5 | # | |
6 | # https://www.apache.org/licenses/LICENSE-2.0 | |
7 | # | |
8 | # Unless required by applicable law or agreed to in writing, software | |
9 | # distributed under the License is distributed on an "AS IS" BASIS, | |
10 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
11 | # See the License for the specific language governing permissions and | |
12 | # limitations under the License. | |
13 | ||
14 | import mock | |
15 | ||
16 | ||
17 | def test_get_user_credentials(): | |
18 | from google_auth_oauthlib import flow | |
19 | from google_auth_oauthlib import interactive as module_under_test | |
20 | ||
21 | mock_flow_instance = mock.create_autospec( | |
22 | flow.InstalledAppFlow, | |
23 | instance=True | |
24 | ) | |
25 | ||
26 | with mock.patch( | |
27 | "google_auth_oauthlib.flow.InstalledAppFlow", autospec=True | |
28 | ) as mock_flow: | |
29 | mock_flow.from_client_config.return_value = mock_flow_instance | |
30 | module_under_test.get_user_credentials( | |
31 | ["scopes"], | |
32 | "some-client-id", | |
33 | "shh-secret", | |
34 | ) | |
35 | ||
36 | mock_flow.from_client_config.assert_called_once_with( | |
37 | mock.ANY, | |
38 | scopes=["scopes"], | |
39 | ) | |
40 | actual_client_config = mock_flow.from_client_config.call_args[0][0] | |
41 | assert actual_client_config["installed"]["client_id"] == "some-client-id" | |
42 | assert actual_client_config["installed"]["client_secret"] == "shh-secret" | |
43 | mock_flow_instance.run_console.assert_called_once() |
0 | # Copyright 2017 Google Inc. | |
1 | # | |
2 | # Licensed under the Apache License, Version 2.0 (the "License"); | |
3 | # you may not use this file except in compliance with the License. | |
4 | # You may obtain a copy of the License at | |
5 | # | |
6 | # http://www.apache.org/licenses/LICENSE-2.0 | |
7 | # | |
8 | # Unless required by applicable law or agreed to in writing, software | |
9 | # distributed under the License is distributed on an "AS IS" BASIS, | |
10 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
11 | # See the License for the specific language governing permissions and | |
12 | # limitations under the License. | |
13 | ||
14 | import io | |
15 | import json | |
16 | import os.path | |
17 | import tempfile | |
18 | ||
19 | import click.testing | |
20 | import google.oauth2.credentials | |
21 | import mock | |
22 | import pytest | |
23 | ||
24 | import google_auth_oauthlib.flow | |
25 | import google_auth_oauthlib.tool.__main__ as cli | |
26 | ||
27 | DATA_DIR = os.path.join(os.path.dirname(__file__), 'data') | |
28 | CLIENT_SECRETS_FILE = os.path.join(DATA_DIR, 'client_secrets.json') | |
29 | ||
30 | ||
31 | class TestMain(object): | |
32 | @pytest.fixture | |
33 | def runner(self): | |
34 | return click.testing.CliRunner() | |
35 | ||
36 | @pytest.fixture | |
37 | def dummy_credentials(self): | |
38 | return google.oauth2.credentials.Credentials( | |
39 | token='dummy_access_token', | |
40 | refresh_token='dummy_refresh_token', | |
41 | token_uri='dummy_token_uri', | |
42 | client_id='dummy_client_id', | |
43 | client_secret='dummy_client_secret', | |
44 | scopes=['dummy_scope1', 'dummy_scope2'] | |
45 | ) | |
46 | ||
47 | @pytest.fixture | |
48 | def local_server_mock(self, dummy_credentials): | |
49 | run_local_server_patch = mock.patch.object( | |
50 | google_auth_oauthlib.flow.InstalledAppFlow, | |
51 | 'run_local_server', | |
52 | autospec=True) | |
53 | ||
54 | with run_local_server_patch as flow: | |
55 | flow.return_value = dummy_credentials | |
56 | yield flow | |
57 | ||
58 | @pytest.fixture | |
59 | def console_mock(self, dummy_credentials): | |
60 | run_console_patch = mock.patch.object( | |
61 | google_auth_oauthlib.flow.InstalledAppFlow, | |
62 | 'run_console', | |
63 | autospec=True) | |
64 | ||
65 | with run_console_patch as flow: | |
66 | flow.return_value = dummy_credentials | |
67 | yield flow | |
68 | ||
69 | def test_help(self, runner): | |
70 | result = runner.invoke(cli.main, ['--help']) | |
71 | assert not result.exception | |
72 | assert 'RFC6749' in result.output | |
73 | assert 'OAuth 2.0 authorization flow' in result.output | |
74 | assert 'not intended for production use' in result.output | |
75 | assert result.exit_code == 0 | |
76 | ||
77 | def test_defaults(self, runner, dummy_credentials, local_server_mock): | |
78 | result = runner.invoke(cli.main, [ | |
79 | '--client-secrets', CLIENT_SECRETS_FILE, | |
80 | '--scope', 'somescope', | |
81 | ]) | |
82 | local_server_mock.assert_called_with(mock.ANY) | |
83 | assert not result.exception | |
84 | assert result.exit_code == 0 | |
85 | creds_data = json.loads(result.output) | |
86 | creds = google.oauth2.credentials.Credentials(**creds_data) | |
87 | assert creds.token == dummy_credentials.token | |
88 | assert creds.refresh_token == dummy_credentials.refresh_token | |
89 | assert creds.token_uri == dummy_credentials.token_uri | |
90 | assert creds.client_id == dummy_credentials.client_id | |
91 | assert creds.client_secret == dummy_credentials.client_secret | |
92 | assert creds.scopes == dummy_credentials.scopes | |
93 | ||
94 | def test_headless(self, runner, dummy_credentials, console_mock): | |
95 | result = runner.invoke(cli.main, [ | |
96 | '--client-secrets', CLIENT_SECRETS_FILE, | |
97 | '--scope', 'somescope', | |
98 | '--headless' | |
99 | ]) | |
100 | console_mock.assert_called_with(mock.ANY) | |
101 | assert not result.exception | |
102 | assert dummy_credentials.refresh_token in result.output | |
103 | assert result.exit_code == 0 | |
104 | ||
105 | def test_save_new_dir(self, runner, dummy_credentials, local_server_mock): | |
106 | credentials_tmpdir = tempfile.mkdtemp() | |
107 | credentials_path = os.path.join( | |
108 | credentials_tmpdir, | |
109 | 'new-directory', | |
110 | 'credentials.json' | |
111 | ) | |
112 | result = runner.invoke(cli.main, [ | |
113 | '--client-secrets', CLIENT_SECRETS_FILE, | |
114 | '--scope', 'somescope', | |
115 | '--credentials', credentials_path, | |
116 | '--save' | |
117 | ]) | |
118 | local_server_mock.assert_called_with(mock.ANY) | |
119 | assert not result.exception | |
120 | assert 'saved' in result.output | |
121 | assert result.exit_code == 0 | |
122 | with io.open(credentials_path) as f: # pylint: disable=invalid-name | |
123 | creds_data = json.load(f) | |
124 | assert 'access_token' not in creds_data | |
125 | ||
126 | creds = google.oauth2.credentials.Credentials( | |
127 | token=None, **creds_data) | |
128 | assert creds.token is None | |
129 | assert creds.refresh_token == dummy_credentials.refresh_token | |
130 | assert creds.token_uri == dummy_credentials.token_uri | |
131 | assert creds.client_id == dummy_credentials.client_id | |
132 | assert creds.client_secret == dummy_credentials.client_secret | |
133 | ||
134 | def test_save_existing_dir(self, runner, local_server_mock): | |
135 | credentials_tmpdir = tempfile.mkdtemp() | |
136 | result = runner.invoke(cli.main, [ | |
137 | '--client-secrets', CLIENT_SECRETS_FILE, | |
138 | '--scope', 'somescope', | |
139 | '--credentials', os.path.join( | |
140 | credentials_tmpdir, | |
141 | 'credentials.json' | |
142 | ), | |
143 | '--save' | |
144 | ]) | |
145 | local_server_mock.assert_called_with(mock.ANY) | |
146 | assert not result.exception | |
147 | assert 'saved' in result.output | |
148 | assert result.exit_code == 0 |
0 | { | |
1 | "web": { | |
2 | "client_id": "example.apps.googleusercontent.com", | |
3 | "project_id": "example", | |
4 | "auth_uri": "https://accounts.google.com/o/oauth2/auth", | |
5 | "token_uri": "https://accounts.google.com/o/oauth2/token", | |
6 | "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", | |
7 | "client_secret": "itsasecrettoeveryone", | |
8 | "redirect_uris": [ | |
9 | "urn:ietf:wg:oauth:2.0:oob", | |
10 | "http://localhost" | |
11 | ] | |
12 | } | |
13 | } |
0 | # Copyright 2017 Google Inc. | |
1 | # | |
2 | # Licensed under the Apache License, Version 2.0 (the "License"); | |
3 | # you may not use this file except in compliance with the License. | |
4 | # You may obtain a copy of the License at | |
5 | # | |
6 | # http://www.apache.org/licenses/LICENSE-2.0 | |
7 | # | |
8 | # Unless required by applicable law or agreed to in writing, software | |
9 | # distributed under the License is distributed on an "AS IS" BASIS, | |
10 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
11 | # See the License for the specific language governing permissions and | |
12 | # limitations under the License. | |
13 | ||
14 | import concurrent.futures | |
15 | import datetime | |
16 | from functools import partial | |
17 | import json | |
18 | import os | |
19 | import re | |
20 | import random | |
21 | import socket | |
22 | ||
23 | import mock | |
24 | import pytest | |
25 | import requests | |
26 | from six.moves import urllib | |
27 | ||
28 | from google_auth_oauthlib import flow | |
29 | ||
30 | DATA_DIR = os.path.join(os.path.dirname(__file__), "data") | |
31 | CLIENT_SECRETS_FILE = os.path.join(DATA_DIR, "client_secrets.json") | |
32 | ||
33 | with open(CLIENT_SECRETS_FILE, "r") as fh: | |
34 | CLIENT_SECRETS_INFO = json.load(fh) | |
35 | ||
36 | ||
37 | class TestFlow(object): | |
38 | def test_from_client_secrets_file(self): | |
39 | instance = flow.Flow.from_client_secrets_file( | |
40 | CLIENT_SECRETS_FILE, scopes=mock.sentinel.scopes | |
41 | ) | |
42 | assert instance.client_config == CLIENT_SECRETS_INFO["web"] | |
43 | assert ( | |
44 | instance.oauth2session.client_id == CLIENT_SECRETS_INFO["web"]["client_id"] | |
45 | ) | |
46 | assert instance.oauth2session.scope == mock.sentinel.scopes | |
47 | ||
48 | def test_from_client_secrets_file_with_redirect_uri(self): | |
49 | instance = flow.Flow.from_client_secrets_file( | |
50 | CLIENT_SECRETS_FILE, | |
51 | scopes=mock.sentinel.scopes, | |
52 | redirect_uri=mock.sentinel.redirect_uri, | |
53 | ) | |
54 | assert ( | |
55 | instance.redirect_uri | |
56 | == instance.oauth2session.redirect_uri | |
57 | == mock.sentinel.redirect_uri | |
58 | ) | |
59 | ||
60 | def test_from_client_config_installed(self): | |
61 | client_config = {"installed": CLIENT_SECRETS_INFO["web"]} | |
62 | instance = flow.Flow.from_client_config( | |
63 | client_config, scopes=mock.sentinel.scopes | |
64 | ) | |
65 | assert instance.client_config == client_config["installed"] | |
66 | assert ( | |
67 | instance.oauth2session.client_id == client_config["installed"]["client_id"] | |
68 | ) | |
69 | assert instance.oauth2session.scope == mock.sentinel.scopes | |
70 | ||
71 | def test_from_client_config_with_redirect_uri(self): | |
72 | client_config = {"installed": CLIENT_SECRETS_INFO["web"]} | |
73 | instance = flow.Flow.from_client_config( | |
74 | client_config, | |
75 | scopes=mock.sentinel.scopes, | |
76 | redirect_uri=mock.sentinel.redirect_uri, | |
77 | ) | |
78 | assert ( | |
79 | instance.redirect_uri | |
80 | == instance.oauth2session.redirect_uri | |
81 | == mock.sentinel.redirect_uri | |
82 | ) | |
83 | ||
84 | def test_from_client_config_bad_format(self): | |
85 | with pytest.raises(ValueError): | |
86 | flow.Flow.from_client_config({}, scopes=mock.sentinel.scopes) | |
87 | ||
88 | @pytest.fixture | |
89 | def instance(self): | |
90 | yield flow.Flow.from_client_config( | |
91 | CLIENT_SECRETS_INFO, scopes=mock.sentinel.scopes | |
92 | ) | |
93 | ||
94 | def test_redirect_uri(self, instance): | |
95 | instance.redirect_uri = mock.sentinel.redirect_uri | |
96 | assert ( | |
97 | instance.redirect_uri | |
98 | == instance.oauth2session.redirect_uri | |
99 | == mock.sentinel.redirect_uri | |
100 | ) | |
101 | ||
102 | def test_authorization_url(self, instance): | |
103 | scope = "scope_one" | |
104 | instance.oauth2session.scope = [scope] | |
105 | authorization_url_patch = mock.patch.object( | |
106 | instance.oauth2session, | |
107 | "authorization_url", | |
108 | wraps=instance.oauth2session.authorization_url, | |
109 | ) | |
110 | ||
111 | with authorization_url_patch as authorization_url_spy: | |
112 | url, _ = instance.authorization_url(prompt="consent") | |
113 | ||
114 | assert CLIENT_SECRETS_INFO["web"]["auth_uri"] in url | |
115 | assert scope in url | |
116 | authorization_url_spy.assert_called_with( | |
117 | CLIENT_SECRETS_INFO["web"]["auth_uri"], | |
118 | access_type="offline", | |
119 | prompt="consent", | |
120 | ) | |
121 | ||
122 | def test_authorization_url_code_verifier(self, instance): | |
123 | scope = "scope_one" | |
124 | instance.oauth2session.scope = [scope] | |
125 | instance.code_verifier = "amanaplanacanalpanama" | |
126 | authorization_url_patch = mock.patch.object( | |
127 | instance.oauth2session, | |
128 | "authorization_url", | |
129 | wraps=instance.oauth2session.authorization_url, | |
130 | ) | |
131 | ||
132 | with authorization_url_patch as authorization_url_spy: | |
133 | url, _ = instance.authorization_url(prompt="consent") | |
134 | ||
135 | assert CLIENT_SECRETS_INFO["web"]["auth_uri"] in url | |
136 | assert scope in url | |
137 | authorization_url_spy.assert_called_with( | |
138 | CLIENT_SECRETS_INFO["web"]["auth_uri"], | |
139 | access_type="offline", | |
140 | prompt="consent", | |
141 | code_challenge="2yN0TOdl0gkGwFOmtfx3f913tgEaLM2d2S0WlmG1Z6Q", | |
142 | code_challenge_method="S256", | |
143 | ) | |
144 | ||
145 | def test_authorization_url_access_type(self, instance): | |
146 | scope = "scope_one" | |
147 | instance.oauth2session.scope = [scope] | |
148 | instance.code_verifier = "amanaplanacanalpanama" | |
149 | authorization_url_patch = mock.patch.object( | |
150 | instance.oauth2session, | |
151 | "authorization_url", | |
152 | wraps=instance.oauth2session.authorization_url, | |
153 | ) | |
154 | ||
155 | with authorization_url_patch as authorization_url_spy: | |
156 | url, _ = instance.authorization_url(access_type="meep") | |
157 | ||
158 | assert CLIENT_SECRETS_INFO["web"]["auth_uri"] in url | |
159 | assert scope in url | |
160 | authorization_url_spy.assert_called_with( | |
161 | CLIENT_SECRETS_INFO["web"]["auth_uri"], | |
162 | access_type="meep", | |
163 | code_challenge="2yN0TOdl0gkGwFOmtfx3f913tgEaLM2d2S0WlmG1Z6Q", | |
164 | code_challenge_method="S256", | |
165 | ) | |
166 | ||
167 | def test_authorization_url_generated_verifier(self): | |
168 | scope = "scope_one" | |
169 | instance = flow.Flow.from_client_config( | |
170 | CLIENT_SECRETS_INFO, scopes=[scope], autogenerate_code_verifier=True | |
171 | ) | |
172 | authorization_url_path = mock.patch.object( | |
173 | instance.oauth2session, | |
174 | "authorization_url", | |
175 | wraps=instance.oauth2session.authorization_url, | |
176 | ) | |
177 | ||
178 | with authorization_url_path as authorization_url_spy: | |
179 | instance.authorization_url() | |
180 | ||
181 | _, kwargs = authorization_url_spy.call_args_list[0] | |
182 | assert kwargs["code_challenge_method"] == "S256" | |
183 | assert len(instance.code_verifier) == 128 | |
184 | assert len(kwargs["code_challenge"]) == 43 | |
185 | valid_verifier = r"^[A-Za-z0-9-._~]*$" | |
186 | valid_challenge = r"^[A-Za-z0-9-_]*$" | |
187 | assert re.match(valid_verifier, instance.code_verifier) | |
188 | assert re.match(valid_challenge, kwargs["code_challenge"]) | |
189 | ||
190 | def test_fetch_token(self, instance): | |
191 | instance.code_verifier = "amanaplanacanalpanama" | |
192 | fetch_token_patch = mock.patch.object( | |
193 | instance.oauth2session, | |
194 | "fetch_token", | |
195 | autospec=True, | |
196 | return_value=mock.sentinel.token, | |
197 | ) | |
198 | ||
199 | with fetch_token_patch as fetch_token_mock: | |
200 | token = instance.fetch_token(code=mock.sentinel.code) | |
201 | ||
202 | assert token == mock.sentinel.token | |
203 | fetch_token_mock.assert_called_with( | |
204 | CLIENT_SECRETS_INFO["web"]["token_uri"], | |
205 | client_secret=CLIENT_SECRETS_INFO["web"]["client_secret"], | |
206 | code=mock.sentinel.code, | |
207 | code_verifier="amanaplanacanalpanama", | |
208 | ) | |
209 | ||
210 | def test_credentials(self, instance): | |
211 | instance.oauth2session.token = { | |
212 | "access_token": mock.sentinel.access_token, | |
213 | "refresh_token": mock.sentinel.refresh_token, | |
214 | "id_token": mock.sentinel.id_token, | |
215 | "expires_at": 643969200.0, | |
216 | } | |
217 | ||
218 | credentials = instance.credentials | |
219 | ||
220 | assert credentials.token == mock.sentinel.access_token | |
221 | assert credentials.expiry == datetime.datetime(1990, 5, 29, 8, 20, 0) | |
222 | assert credentials._refresh_token == mock.sentinel.refresh_token | |
223 | assert credentials.id_token == mock.sentinel.id_token | |
224 | assert credentials._client_id == CLIENT_SECRETS_INFO["web"]["client_id"] | |
225 | assert credentials._client_secret == CLIENT_SECRETS_INFO["web"]["client_secret"] | |
226 | assert credentials._token_uri == CLIENT_SECRETS_INFO["web"]["token_uri"] | |
227 | ||
228 | def test_authorized_session(self, instance): | |
229 | instance.oauth2session.token = { | |
230 | "access_token": mock.sentinel.access_token, | |
231 | "refresh_token": mock.sentinel.refresh_token, | |
232 | "id_token": mock.sentinel.id_token, | |
233 | "expires_at": 643969200.0, | |
234 | } | |
235 | ||
236 | session = instance.authorized_session() | |
237 | ||
238 | assert session.credentials.token == mock.sentinel.access_token | |
239 | ||
240 | ||
241 | class TestInstalledAppFlow(object): | |
242 | SCOPES = ["email", "profile"] | |
243 | REDIRECT_REQUEST_PATH = "/?code=code&state=state" | |
244 | ||
245 | @pytest.fixture | |
246 | def instance(self): | |
247 | yield flow.InstalledAppFlow.from_client_config( | |
248 | CLIENT_SECRETS_INFO, scopes=self.SCOPES | |
249 | ) | |
250 | ||
251 | @pytest.fixture | |
252 | def port(self): | |
253 | # Creating a new server at the same port will result in | |
254 | # a 'Address already in use' error for a brief | |
255 | # period of time after the socket has been closed. | |
256 | # Work around this in the tests by choosing a random port. | |
257 | # https://stackoverflow.com/questions/6380057/python-binding-socket-address-already-in-use | |
258 | yield random.randrange(60400, 60900) | |
259 | ||
260 | @pytest.fixture | |
261 | def socket(self, port): | |
262 | s = socket.socket() | |
263 | s.bind(("localhost", port)) | |
264 | yield s | |
265 | s.close() | |
266 | ||
267 | @pytest.fixture | |
268 | def mock_fetch_token(self, instance): | |
269 | def set_token(*args, **kwargs): | |
270 | instance.oauth2session.token = { | |
271 | "access_token": mock.sentinel.access_token, | |
272 | "refresh_token": mock.sentinel.refresh_token, | |
273 | "id_token": mock.sentinel.id_token, | |
274 | "expires_at": 643969200.0, | |
275 | } | |
276 | ||
277 | fetch_token_patch = mock.patch.object( | |
278 | instance.oauth2session, "fetch_token", autospec=True, side_effect=set_token | |
279 | ) | |
280 | ||
281 | with fetch_token_patch as fetch_token_mock: | |
282 | yield fetch_token_mock | |
283 | ||
284 | @mock.patch("google_auth_oauthlib.flow.input", autospec=True) | |
285 | def test_run_console(self, input_mock, instance, mock_fetch_token): | |
286 | input_mock.return_value = mock.sentinel.code | |
287 | instance.code_verifier = "amanaplanacanalpanama" | |
288 | credentials = instance.run_console() | |
289 | ||
290 | assert credentials.token == mock.sentinel.access_token | |
291 | assert credentials._refresh_token == mock.sentinel.refresh_token | |
292 | assert credentials.id_token == mock.sentinel.id_token | |
293 | ||
294 | mock_fetch_token.assert_called_with( | |
295 | CLIENT_SECRETS_INFO["web"]["token_uri"], | |
296 | client_secret=CLIENT_SECRETS_INFO["web"]["client_secret"], | |
297 | code=mock.sentinel.code, | |
298 | code_verifier="amanaplanacanalpanama", | |
299 | ) | |
300 | ||
301 | @pytest.mark.webtest | |
302 | @mock.patch("google_auth_oauthlib.flow.webbrowser", autospec=True) | |
303 | def test_run_local_server(self, webbrowser_mock, instance, mock_fetch_token, port): | |
304 | auth_redirect_url = urllib.parse.urljoin( | |
305 | f"http://localhost:{port}", self.REDIRECT_REQUEST_PATH | |
306 | ) | |
307 | ||
308 | with concurrent.futures.ThreadPoolExecutor(max_workers=1) as pool: | |
309 | future = pool.submit(partial(instance.run_local_server, port=port)) | |
310 | ||
311 | while not future.done(): | |
312 | try: | |
313 | requests.get(auth_redirect_url) | |
314 | except requests.ConnectionError: # pragma: NO COVER | |
315 | pass | |
316 | ||
317 | credentials = future.result() | |
318 | ||
319 | assert credentials.token == mock.sentinel.access_token | |
320 | assert credentials._refresh_token == mock.sentinel.refresh_token | |
321 | assert credentials.id_token == mock.sentinel.id_token | |
322 | assert webbrowser_mock.open.called | |
323 | ||
324 | expected_auth_response = auth_redirect_url.replace("http", "https") | |
325 | mock_fetch_token.assert_called_with( | |
326 | CLIENT_SECRETS_INFO["web"]["token_uri"], | |
327 | client_secret=CLIENT_SECRETS_INFO["web"]["client_secret"], | |
328 | authorization_response=expected_auth_response, | |
329 | code_verifier=None, | |
330 | ) | |
331 | ||
332 | @pytest.mark.webtest | |
333 | @mock.patch("google_auth_oauthlib.flow.webbrowser", autospec=True) | |
334 | def test_run_local_server_code_verifier( | |
335 | self, webbrowser_mock, instance, mock_fetch_token, port | |
336 | ): | |
337 | auth_redirect_url = urllib.parse.urljoin( | |
338 | f"http://localhost:{port}", self.REDIRECT_REQUEST_PATH | |
339 | ) | |
340 | instance.code_verifier = "amanaplanacanalpanama" | |
341 | ||
342 | with concurrent.futures.ThreadPoolExecutor(max_workers=1) as pool: | |
343 | future = pool.submit(partial(instance.run_local_server, port=port)) | |
344 | ||
345 | while not future.done(): | |
346 | try: | |
347 | requests.get(auth_redirect_url) | |
348 | except requests.ConnectionError: # pragma: NO COVER | |
349 | pass | |
350 | ||
351 | credentials = future.result() | |
352 | ||
353 | assert credentials.token == mock.sentinel.access_token | |
354 | assert credentials._refresh_token == mock.sentinel.refresh_token | |
355 | assert credentials.id_token == mock.sentinel.id_token | |
356 | assert webbrowser_mock.open.called | |
357 | ||
358 | expected_auth_response = auth_redirect_url.replace("http", "https") | |
359 | mock_fetch_token.assert_called_with( | |
360 | CLIENT_SECRETS_INFO["web"]["token_uri"], | |
361 | client_secret=CLIENT_SECRETS_INFO["web"]["client_secret"], | |
362 | authorization_response=expected_auth_response, | |
363 | code_verifier="amanaplanacanalpanama", | |
364 | ) | |
365 | ||
366 | @mock.patch("google_auth_oauthlib.flow.webbrowser", autospec=True) | |
367 | @mock.patch("wsgiref.simple_server.make_server", autospec=True) | |
368 | def test_run_local_server_no_browser( | |
369 | self, make_server_mock, webbrowser_mock, instance, mock_fetch_token | |
370 | ): | |
371 | def assign_last_request_uri(host, port, wsgi_app, **kwargs): | |
372 | wsgi_app.last_request_uri = self.REDIRECT_REQUEST_PATH | |
373 | return mock.Mock() | |
374 | ||
375 | make_server_mock.side_effect = assign_last_request_uri | |
376 | ||
377 | instance.run_local_server(open_browser=False) | |
378 | ||
379 | assert not webbrowser_mock.open.called | |
380 | ||
381 | @pytest.mark.webtest | |
382 | @mock.patch("google_auth_oauthlib.flow.webbrowser", autospec=True) | |
383 | def test_run_local_server_occupied_port( | |
384 | self, webbrowser_mock, instance, mock_fetch_token, port, socket | |
385 | ): | |
386 | # socket fixture is already bound to http://localhost:port | |
387 | instance.run_local_server | |
388 | with pytest.raises(OSError) as exc: | |
389 | instance.run_local_server(port=port) | |
390 | assert "address already in use" in exc.strerror.lower() |
0 | # Copyright 2017 Google Inc. | |
1 | # | |
2 | # Licensed under the Apache License, Version 2.0 (the "License"); | |
3 | # you may not use this file except in compliance with the License. | |
4 | # You may obtain a copy of the License at | |
5 | # | |
6 | # http://www.apache.org/licenses/LICENSE-2.0 | |
7 | # | |
8 | # Unless required by applicable law or agreed to in writing, software | |
9 | # distributed under the License is distributed on an "AS IS" BASIS, | |
10 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
11 | # See the License for the specific language governing permissions and | |
12 | # limitations under the License. | |
13 | ||
14 | import datetime | |
15 | import json | |
16 | import os | |
17 | ||
18 | import mock | |
19 | import pytest | |
20 | ||
21 | from google_auth_oauthlib import helpers | |
22 | ||
23 | DATA_DIR = os.path.join(os.path.dirname(__file__), "data") | |
24 | CLIENT_SECRETS_FILE = os.path.join(DATA_DIR, "client_secrets.json") | |
25 | ||
26 | with open(CLIENT_SECRETS_FILE, "r") as fh: | |
27 | CLIENT_SECRETS_INFO = json.load(fh) | |
28 | ||
29 | ||
30 | def test_session_from_client_config_web(): | |
31 | session, config = helpers.session_from_client_config( | |
32 | CLIENT_SECRETS_INFO, scopes=mock.sentinel.scopes | |
33 | ) | |
34 | ||
35 | assert config == CLIENT_SECRETS_INFO | |
36 | assert session.client_id == CLIENT_SECRETS_INFO["web"]["client_id"] | |
37 | assert session.scope == mock.sentinel.scopes | |
38 | ||
39 | ||
40 | def test_session_from_client_config_installed(): | |
41 | info = {"installed": CLIENT_SECRETS_INFO["web"]} | |
42 | session, config = helpers.session_from_client_config( | |
43 | info, scopes=mock.sentinel.scopes | |
44 | ) | |
45 | assert config == info | |
46 | assert session.client_id == info["installed"]["client_id"] | |
47 | assert session.scope == mock.sentinel.scopes | |
48 | ||
49 | ||
50 | def test_session_from_client_config_bad_format(): | |
51 | with pytest.raises(ValueError): | |
52 | helpers.session_from_client_config({}, scopes=[]) | |
53 | ||
54 | ||
55 | def test_session_from_client_config_missing_keys(): | |
56 | with pytest.raises(ValueError): | |
57 | helpers.session_from_client_config({"web": {}}, scopes=[]) | |
58 | ||
59 | ||
60 | def test_session_from_client_secrets_file(): | |
61 | session, config = helpers.session_from_client_secrets_file( | |
62 | CLIENT_SECRETS_FILE, scopes=mock.sentinel.scopes | |
63 | ) | |
64 | assert config == CLIENT_SECRETS_INFO | |
65 | assert session.client_id == CLIENT_SECRETS_INFO["web"]["client_id"] | |
66 | assert session.scope == mock.sentinel.scopes | |
67 | ||
68 | ||
69 | @pytest.fixture | |
70 | def session(): | |
71 | session, _ = helpers.session_from_client_config( | |
72 | CLIENT_SECRETS_INFO, scopes=mock.sentinel.scopes | |
73 | ) | |
74 | yield session | |
75 | ||
76 | ||
77 | def test_credentials_from_session(session): | |
78 | session.token = { | |
79 | "access_token": mock.sentinel.access_token, | |
80 | "refresh_token": mock.sentinel.refresh_token, | |
81 | "id_token": mock.sentinel.id_token, | |
82 | "expires_at": 643969200.0, | |
83 | } | |
84 | ||
85 | credentials = helpers.credentials_from_session(session, CLIENT_SECRETS_INFO["web"]) | |
86 | ||
87 | assert credentials.token == mock.sentinel.access_token | |
88 | assert credentials.expiry == datetime.datetime(1990, 5, 29, 8, 20, 0) | |
89 | assert credentials._refresh_token == mock.sentinel.refresh_token | |
90 | assert credentials.id_token == mock.sentinel.id_token | |
91 | assert credentials._client_id == CLIENT_SECRETS_INFO["web"]["client_id"] | |
92 | assert credentials._client_secret == CLIENT_SECRETS_INFO["web"]["client_secret"] | |
93 | assert credentials._token_uri == CLIENT_SECRETS_INFO["web"]["token_uri"] | |
94 | ||
95 | ||
96 | def test_bad_credentials(session): | |
97 | with pytest.raises(ValueError): | |
98 | helpers.credentials_from_session(session) |
0 | # Copyright 2019 Google LLC | |
1 | # | |
2 | # Licensed under the Apache License, Version 2.0 (the "License"); | |
3 | # you may not use this file except in compliance with the License. | |
4 | # You may obtain a copy of the License at | |
5 | # | |
6 | # https://www.apache.org/licenses/LICENSE-2.0 | |
7 | # | |
8 | # Unless required by applicable law or agreed to in writing, software | |
9 | # distributed under the License is distributed on an "AS IS" BASIS, | |
10 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
11 | # See the License for the specific language governing permissions and | |
12 | # limitations under the License. | |
13 | ||
14 | import mock | |
15 | ||
16 | ||
17 | def test_get_user_credentials(): | |
18 | from google_auth_oauthlib import flow | |
19 | from google_auth_oauthlib import interactive as module_under_test | |
20 | ||
21 | mock_flow_instance = mock.create_autospec(flow.InstalledAppFlow, instance=True) | |
22 | ||
23 | with mock.patch( | |
24 | "google_auth_oauthlib.flow.InstalledAppFlow", autospec=True | |
25 | ) as mock_flow: | |
26 | mock_flow.from_client_config.return_value = mock_flow_instance | |
27 | module_under_test.get_user_credentials( | |
28 | ["scopes"], "some-client-id", "shh-secret" | |
29 | ) | |
30 | ||
31 | mock_flow.from_client_config.assert_called_once_with(mock.ANY, scopes=["scopes"]) | |
32 | actual_client_config = mock_flow.from_client_config.call_args[0][0] | |
33 | assert actual_client_config["installed"]["client_id"] == "some-client-id" | |
34 | assert actual_client_config["installed"]["client_secret"] == "shh-secret" | |
35 | mock_flow_instance.run_console.assert_called_once() |
0 | # Copyright 2017 Google Inc. | |
1 | # | |
2 | # Licensed under the Apache License, Version 2.0 (the "License"); | |
3 | # you may not use this file except in compliance with the License. | |
4 | # You may obtain a copy of the License at | |
5 | # | |
6 | # http://www.apache.org/licenses/LICENSE-2.0 | |
7 | # | |
8 | # Unless required by applicable law or agreed to in writing, software | |
9 | # distributed under the License is distributed on an "AS IS" BASIS, | |
10 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
11 | # See the License for the specific language governing permissions and | |
12 | # limitations under the License. | |
13 | ||
14 | import io | |
15 | import json | |
16 | import os.path | |
17 | import tempfile | |
18 | ||
19 | import click.testing | |
20 | import google.oauth2.credentials | |
21 | import mock | |
22 | import pytest | |
23 | ||
24 | import google_auth_oauthlib.flow | |
25 | import google_auth_oauthlib.tool.__main__ as cli | |
26 | ||
27 | DATA_DIR = os.path.join(os.path.dirname(__file__), "data") | |
28 | CLIENT_SECRETS_FILE = os.path.join(DATA_DIR, "client_secrets.json") | |
29 | ||
30 | ||
31 | class TestMain(object): | |
32 | @pytest.fixture | |
33 | def runner(self): | |
34 | return click.testing.CliRunner() | |
35 | ||
36 | @pytest.fixture | |
37 | def dummy_credentials(self): | |
38 | return google.oauth2.credentials.Credentials( | |
39 | token="dummy_access_token", | |
40 | refresh_token="dummy_refresh_token", | |
41 | token_uri="dummy_token_uri", | |
42 | client_id="dummy_client_id", | |
43 | client_secret="dummy_client_secret", | |
44 | scopes=["dummy_scope1", "dummy_scope2"], | |
45 | ) | |
46 | ||
47 | @pytest.fixture | |
48 | def local_server_mock(self, dummy_credentials): | |
49 | run_local_server_patch = mock.patch.object( | |
50 | google_auth_oauthlib.flow.InstalledAppFlow, | |
51 | "run_local_server", | |
52 | autospec=True, | |
53 | ) | |
54 | ||
55 | with run_local_server_patch as flow: | |
56 | flow.return_value = dummy_credentials | |
57 | yield flow | |
58 | ||
59 | @pytest.fixture | |
60 | def console_mock(self, dummy_credentials): | |
61 | run_console_patch = mock.patch.object( | |
62 | google_auth_oauthlib.flow.InstalledAppFlow, "run_console", autospec=True | |
63 | ) | |
64 | ||
65 | with run_console_patch as flow: | |
66 | flow.return_value = dummy_credentials | |
67 | yield flow | |
68 | ||
69 | def test_help(self, runner): | |
70 | result = runner.invoke(cli.main, ["--help"]) | |
71 | assert not result.exception | |
72 | assert "RFC6749" in result.output | |
73 | assert "OAuth 2.0 authorization flow" in result.output | |
74 | assert "not intended for production use" in result.output | |
75 | assert result.exit_code == 0 | |
76 | ||
77 | def test_defaults(self, runner, dummy_credentials, local_server_mock): | |
78 | result = runner.invoke( | |
79 | cli.main, ["--client-secrets", CLIENT_SECRETS_FILE, "--scope", "somescope"] | |
80 | ) | |
81 | local_server_mock.assert_called_with(mock.ANY) | |
82 | assert not result.exception | |
83 | assert result.exit_code == 0 | |
84 | creds_data = json.loads(result.output) | |
85 | creds = google.oauth2.credentials.Credentials(**creds_data) | |
86 | assert creds.token == dummy_credentials.token | |
87 | assert creds.refresh_token == dummy_credentials.refresh_token | |
88 | assert creds.token_uri == dummy_credentials.token_uri | |
89 | assert creds.client_id == dummy_credentials.client_id | |
90 | assert creds.client_secret == dummy_credentials.client_secret | |
91 | assert creds.scopes == dummy_credentials.scopes | |
92 | ||
93 | def test_headless(self, runner, dummy_credentials, console_mock): | |
94 | result = runner.invoke( | |
95 | cli.main, | |
96 | [ | |
97 | "--client-secrets", | |
98 | CLIENT_SECRETS_FILE, | |
99 | "--scope", | |
100 | "somescope", | |
101 | "--headless", | |
102 | ], | |
103 | ) | |
104 | console_mock.assert_called_with(mock.ANY) | |
105 | assert not result.exception | |
106 | assert dummy_credentials.refresh_token in result.output | |
107 | assert result.exit_code == 0 | |
108 | ||
109 | def test_save_new_dir(self, runner, dummy_credentials, local_server_mock): | |
110 | credentials_tmpdir = tempfile.mkdtemp() | |
111 | credentials_path = os.path.join( | |
112 | credentials_tmpdir, "new-directory", "credentials.json" | |
113 | ) | |
114 | result = runner.invoke( | |
115 | cli.main, | |
116 | [ | |
117 | "--client-secrets", | |
118 | CLIENT_SECRETS_FILE, | |
119 | "--scope", | |
120 | "somescope", | |
121 | "--credentials", | |
122 | credentials_path, | |
123 | "--save", | |
124 | ], | |
125 | ) | |
126 | local_server_mock.assert_called_with(mock.ANY) | |
127 | assert not result.exception | |
128 | assert "saved" in result.output | |
129 | assert result.exit_code == 0 | |
130 | with io.open(credentials_path) as f: # pylint: disable=invalid-name | |
131 | creds_data = json.load(f) | |
132 | assert "access_token" not in creds_data | |
133 | ||
134 | creds = google.oauth2.credentials.Credentials(token=None, **creds_data) | |
135 | assert creds.token is None | |
136 | assert creds.refresh_token == dummy_credentials.refresh_token | |
137 | assert creds.token_uri == dummy_credentials.token_uri | |
138 | assert creds.client_id == dummy_credentials.client_id | |
139 | assert creds.client_secret == dummy_credentials.client_secret | |
140 | ||
141 | def test_save_existing_dir(self, runner, local_server_mock): | |
142 | credentials_tmpdir = tempfile.mkdtemp() | |
143 | result = runner.invoke( | |
144 | cli.main, | |
145 | [ | |
146 | "--client-secrets", | |
147 | CLIENT_SECRETS_FILE, | |
148 | "--scope", | |
149 | "somescope", | |
150 | "--credentials", | |
151 | os.path.join(credentials_tmpdir, "credentials.json"), | |
152 | "--save", | |
153 | ], | |
154 | ) | |
155 | local_server_mock.assert_called_with(mock.ANY) | |
156 | assert not result.exception | |
157 | assert "saved" in result.output | |
158 | assert result.exit_code == 0 |