OLD | NEW |
1 # Copyright 2015 The Chromium Authors. All rights reserved. | 1 # Copyright 2015 The Chromium Authors. All rights reserved. |
2 # Use of this source code is governed by a BSD-style license that can be | 2 # Use of this source code is governed by a BSD-style license that can be |
3 # found in the LICENSE file. | 3 # found in the LICENSE file. |
4 | 4 |
5 """Google OAuth2 related functions.""" | 5 """Google OAuth2 related functions.""" |
6 | 6 |
7 import BaseHTTPServer | 7 import BaseHTTPServer |
8 import collections | 8 import collections |
9 import datetime | 9 import datetime |
10 import functools | 10 import functools |
| 11 import hashlib |
11 import json | 12 import json |
12 import logging | 13 import logging |
13 import optparse | 14 import optparse |
14 import os | 15 import os |
15 import socket | 16 import socket |
16 import sys | 17 import sys |
17 import threading | 18 import threading |
18 import urllib | 19 import urllib |
19 import urlparse | 20 import urlparse |
20 import webbrowser | 21 import webbrowser |
(...skipping 34 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
55 os.path.expanduser('~'), '.depot_tools_oauth2_tokens') | 56 os.path.expanduser('~'), '.depot_tools_oauth2_tokens') |
56 | 57 |
57 | 58 |
58 # Authentication configuration extracted from command line options. | 59 # Authentication configuration extracted from command line options. |
59 # See doc string for 'make_auth_config' for meaning of fields. | 60 # See doc string for 'make_auth_config' for meaning of fields. |
60 AuthConfig = collections.namedtuple('AuthConfig', [ | 61 AuthConfig = collections.namedtuple('AuthConfig', [ |
61 'use_oauth2', # deprecated, will be always True | 62 'use_oauth2', # deprecated, will be always True |
62 'save_cookies', # deprecated, will be removed | 63 'save_cookies', # deprecated, will be removed |
63 'use_local_webserver', | 64 'use_local_webserver', |
64 'webserver_port', | 65 'webserver_port', |
| 66 'refresh_token_json', |
65 ]) | 67 ]) |
66 | 68 |
67 | 69 |
68 # OAuth access token with its expiration time (UTC datetime or None if unknown). | 70 # OAuth access token with its expiration time (UTC datetime or None if unknown). |
69 AccessToken = collections.namedtuple('AccessToken', [ | 71 AccessToken = collections.namedtuple('AccessToken', [ |
70 'token', | 72 'token', |
71 'expires_at', | 73 'expires_at', |
72 ]) | 74 ]) |
73 | 75 |
74 | 76 |
| 77 # Refresh token passed via --auth-refresh-token-json. |
| 78 RefreshToken = collections.namedtuple('RefreshToken', [ |
| 79 'client_id', |
| 80 'client_secret', |
| 81 'refresh_token', |
| 82 ]) |
| 83 |
| 84 |
75 class AuthenticationError(Exception): | 85 class AuthenticationError(Exception): |
76 """Raised on errors related to authentication.""" | 86 """Raised on errors related to authentication.""" |
77 | 87 |
78 | 88 |
79 class LoginRequiredError(AuthenticationError): | 89 class LoginRequiredError(AuthenticationError): |
80 """Interaction with the user is required to authenticate.""" | 90 """Interaction with the user is required to authenticate.""" |
81 | 91 |
82 def __init__(self, token_cache_key): | 92 def __init__(self, token_cache_key): |
83 # HACK(vadimsh): It is assumed here that the token cache key is a hostname. | 93 # HACK(vadimsh): It is assumed here that the token cache key is a hostname. |
84 msg = ( | 94 msg = ( |
85 'You are not logged in. Please login first by running:\n' | 95 'You are not logged in. Please login first by running:\n' |
86 ' depot-tools-auth login %s' % token_cache_key) | 96 ' depot-tools-auth login %s' % token_cache_key) |
87 super(LoginRequiredError, self).__init__(msg) | 97 super(LoginRequiredError, self).__init__(msg) |
88 | 98 |
89 | 99 |
90 def make_auth_config( | 100 def make_auth_config( |
91 use_oauth2=None, | 101 use_oauth2=None, |
92 save_cookies=None, | 102 save_cookies=None, |
93 use_local_webserver=None, | 103 use_local_webserver=None, |
94 webserver_port=None): | 104 webserver_port=None, |
| 105 refresh_token_json=None): |
95 """Returns new instance of AuthConfig. | 106 """Returns new instance of AuthConfig. |
96 | 107 |
97 If some config option is None, it will be set to a reasonable default value. | 108 If some config option is None, it will be set to a reasonable default value. |
98 This function also acts as an authoritative place for default values of | 109 This function also acts as an authoritative place for default values of |
99 corresponding command line options. | 110 corresponding command line options. |
100 """ | 111 """ |
101 default = lambda val, d: val if val is not None else d | 112 default = lambda val, d: val if val is not None else d |
102 return AuthConfig( | 113 return AuthConfig( |
103 default(use_oauth2, _should_use_oauth2()), | 114 default(use_oauth2, _should_use_oauth2()), |
104 default(save_cookies, True), | 115 default(save_cookies, True), |
105 default(use_local_webserver, not _is_headless()), | 116 default(use_local_webserver, not _is_headless()), |
106 default(webserver_port, 8090)) | 117 default(webserver_port, 8090), |
| 118 default(refresh_token_json, '')) |
107 | 119 |
108 | 120 |
109 def add_auth_options(parser, default_config=None): | 121 def add_auth_options(parser, default_config=None): |
110 """Appends OAuth related options to OptionParser.""" | 122 """Appends OAuth related options to OptionParser.""" |
111 default_config = default_config or make_auth_config() | 123 default_config = default_config or make_auth_config() |
112 parser.auth_group = optparse.OptionGroup(parser, 'Auth options') | 124 parser.auth_group = optparse.OptionGroup(parser, 'Auth options') |
113 parser.add_option_group(parser.auth_group) | 125 parser.add_option_group(parser.auth_group) |
114 | 126 |
115 # OAuth2 vs password switch. | 127 # OAuth2 vs password switch. |
116 auth_default = 'use OAuth2' if default_config.use_oauth2 else 'use password' | 128 auth_default = 'use OAuth2' if default_config.use_oauth2 else 'use password' |
(...skipping 24 matching lines...) Expand all Loading... |
141 action='store_false', | 153 action='store_false', |
142 dest='use_local_webserver', | 154 dest='use_local_webserver', |
143 default=default_config.use_local_webserver, | 155 default=default_config.use_local_webserver, |
144 help='Do not run a local web server when performing OAuth2 login flow.') | 156 help='Do not run a local web server when performing OAuth2 login flow.') |
145 parser.auth_group.add_option( | 157 parser.auth_group.add_option( |
146 '--auth-host-port', | 158 '--auth-host-port', |
147 type=int, | 159 type=int, |
148 default=default_config.webserver_port, | 160 default=default_config.webserver_port, |
149 help='Port a local web server should listen on. Used only if ' | 161 help='Port a local web server should listen on. Used only if ' |
150 '--auth-no-local-webserver is not set. [default: %default]') | 162 '--auth-no-local-webserver is not set. [default: %default]') |
| 163 parser.auth_group.add_option( |
| 164 '--auth-refresh-token-json', |
| 165 default=default_config.refresh_token_json, |
| 166 help='Path to a JSON file with role account refresh token to use.') |
151 | 167 |
152 | 168 |
153 def extract_auth_config_from_options(options): | 169 def extract_auth_config_from_options(options): |
154 """Given OptionParser parsed options, extracts AuthConfig from it. | 170 """Given OptionParser parsed options, extracts AuthConfig from it. |
155 | 171 |
156 OptionParser should be populated with auth options by 'add_auth_options'. | 172 OptionParser should be populated with auth options by 'add_auth_options'. |
157 """ | 173 """ |
158 return make_auth_config( | 174 return make_auth_config( |
159 use_oauth2=options.use_oauth2, | 175 use_oauth2=options.use_oauth2, |
160 save_cookies=False if options.use_oauth2 else options.save_cookies, | 176 save_cookies=False if options.use_oauth2 else options.save_cookies, |
161 use_local_webserver=options.use_local_webserver, | 177 use_local_webserver=options.use_local_webserver, |
162 webserver_port=options.auth_host_port) | 178 webserver_port=options.auth_host_port, |
| 179 refresh_token_json=options.auth_refresh_token_json) |
163 | 180 |
164 | 181 |
165 def auth_config_to_command_options(auth_config): | 182 def auth_config_to_command_options(auth_config): |
166 """AuthConfig -> list of strings with command line options. | 183 """AuthConfig -> list of strings with command line options. |
167 | 184 |
168 Omits options that are set to default values. | 185 Omits options that are set to default values. |
169 """ | 186 """ |
170 if not auth_config: | 187 if not auth_config: |
171 return [] | 188 return [] |
172 defaults = make_auth_config() | 189 defaults = make_auth_config() |
173 opts = [] | 190 opts = [] |
174 if auth_config.use_oauth2 != defaults.use_oauth2: | 191 if auth_config.use_oauth2 != defaults.use_oauth2: |
175 opts.append('--oauth2' if auth_config.use_oauth2 else '--no-oauth2') | 192 opts.append('--oauth2' if auth_config.use_oauth2 else '--no-oauth2') |
176 if auth_config.save_cookies != auth_config.save_cookies: | 193 if auth_config.save_cookies != auth_config.save_cookies: |
177 if not auth_config.save_cookies: | 194 if not auth_config.save_cookies: |
178 opts.append('--no-cookies') | 195 opts.append('--no-cookies') |
179 if auth_config.use_local_webserver != defaults.use_local_webserver: | 196 if auth_config.use_local_webserver != defaults.use_local_webserver: |
180 if not auth_config.use_local_webserver: | 197 if not auth_config.use_local_webserver: |
181 opts.append('--auth-no-local-webserver') | 198 opts.append('--auth-no-local-webserver') |
182 if auth_config.webserver_port != defaults.webserver_port: | 199 if auth_config.webserver_port != defaults.webserver_port: |
183 opts.extend(['--auth-host-port', str(auth_config.webserver_port)]) | 200 opts.extend(['--auth-host-port', str(auth_config.webserver_port)]) |
| 201 if auth_config.refresh_token_json != defaults.refresh_token_json: |
| 202 opts.extend([ |
| 203 '--auth-refresh-token-json', str(auth_config.refresh_token_json)]) |
184 return opts | 204 return opts |
185 | 205 |
186 | 206 |
187 def get_authenticator_for_host(hostname, config): | 207 def get_authenticator_for_host(hostname, config): |
188 """Returns Authenticator instance to access given host. | 208 """Returns Authenticator instance to access given host. |
189 | 209 |
190 Args: | 210 Args: |
191 hostname: a naked hostname or http(s)://<hostname>[/] URL. Used to derive | 211 hostname: a naked hostname or http(s)://<hostname>[/] URL. Used to derive |
192 a cache key for token cache. | 212 a cache key for token cache. |
193 config: AuthConfig instance. | 213 config: AuthConfig instance. |
(...skipping 21 matching lines...) Expand all Loading... |
215 config: AuthConfig object that holds authentication configuration. | 235 config: AuthConfig object that holds authentication configuration. |
216 """ | 236 """ |
217 | 237 |
218 def __init__(self, token_cache_key, config): | 238 def __init__(self, token_cache_key, config): |
219 assert isinstance(config, AuthConfig) | 239 assert isinstance(config, AuthConfig) |
220 assert config.use_oauth2 | 240 assert config.use_oauth2 |
221 self._access_token = None | 241 self._access_token = None |
222 self._config = config | 242 self._config = config |
223 self._lock = threading.Lock() | 243 self._lock = threading.Lock() |
224 self._token_cache_key = token_cache_key | 244 self._token_cache_key = token_cache_key |
| 245 self._external_token = None |
| 246 if config.refresh_token_json: |
| 247 self._external_token = _read_refresh_token_json(config.refresh_token_json) |
225 | 248 |
226 def login(self): | 249 def login(self): |
227 """Performs interactive login flow if necessary. | 250 """Performs interactive login flow if necessary. |
228 | 251 |
229 Raises: | 252 Raises: |
230 AuthenticationError on error or if interrupted. | 253 AuthenticationError on error or if interrupted. |
231 """ | 254 """ |
| 255 if self._external_token: |
| 256 raise AuthenticationError( |
| 257 'Can\'t run login flow when using --auth-refresh-token-json.') |
232 return self.get_access_token( | 258 return self.get_access_token( |
233 force_refresh=True, allow_user_interaction=True) | 259 force_refresh=True, allow_user_interaction=True) |
234 | 260 |
235 def logout(self): | 261 def logout(self): |
236 """Revokes the refresh token and deletes it from the cache. | 262 """Revokes the refresh token and deletes it from the cache. |
237 | 263 |
238 Returns True if actually revoked a token. | 264 Returns True if had some credentials cached. |
239 """ | 265 """ |
240 revoked = False | |
241 with self._lock: | 266 with self._lock: |
242 self._access_token = None | 267 self._access_token = None |
243 storage = self._get_storage() | 268 storage = self._get_storage() |
244 credentials = storage.get() | 269 credentials = storage.get() |
245 if credentials: | 270 had_creds = bool(credentials) |
246 credentials.revoke(httplib2.Http()) | 271 if credentials and credentials.refresh_token and credentials.revoke_uri: |
247 revoked = True | 272 try: |
| 273 credentials.revoke(httplib2.Http()) |
| 274 except client.TokenRevokeError as e: |
| 275 logging.warning('Failed to revoke refresh token: %s', e) |
248 storage.delete() | 276 storage.delete() |
249 return revoked | 277 return had_creds |
250 | 278 |
251 def has_cached_credentials(self): | 279 def has_cached_credentials(self): |
252 """Returns True if long term credentials (refresh token) are in cache. | 280 """Returns True if long term credentials (refresh token) are in cache. |
253 | 281 |
254 Doesn't make network calls. | 282 Doesn't make network calls. |
255 | 283 |
256 If returns False, get_access_token() later will ask for interactive login by | 284 If returns False, get_access_token() later will ask for interactive login by |
257 raising LoginRequiredError. | 285 raising LoginRequiredError. |
258 | 286 |
259 If returns True, most probably get_access_token() won't ask for interactive | 287 If returns True, most probably get_access_token() won't ask for interactive |
260 login, though it is not guaranteed, since cached token can be already | 288 login, though it is not guaranteed, since cached token can be already |
261 revoked and there's no way to figure this out without actually trying to use | 289 revoked and there's no way to figure this out without actually trying to use |
262 it. | 290 it. |
263 """ | 291 """ |
264 with self._lock: | 292 with self._lock: |
265 credentials = self._get_storage().get() | 293 return bool(self._get_cached_credentials()) |
266 return credentials and not credentials.invalid | |
267 | 294 |
268 def get_access_token(self, force_refresh=False, allow_user_interaction=False): | 295 def get_access_token(self, force_refresh=False, allow_user_interaction=False): |
269 """Returns AccessToken, refreshing it if necessary. | 296 """Returns AccessToken, refreshing it if necessary. |
270 | 297 |
271 Args: | 298 Args: |
272 force_refresh: forcefully refresh access token even if it is not expired. | 299 force_refresh: forcefully refresh access token even if it is not expired. |
273 allow_user_interaction: True to enable blocking for user input if needed. | 300 allow_user_interaction: True to enable blocking for user input if needed. |
274 | 301 |
275 Raises: | 302 Raises: |
276 AuthenticationError on error or if authentication flow was interrupted. | 303 AuthenticationError on error or if authentication flow was interrupted. |
(...skipping 64 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
341 else: | 368 else: |
342 return (resp, content) | 369 return (resp, content) |
343 | 370 |
344 http.request = new_request | 371 http.request = new_request |
345 return http | 372 return http |
346 | 373 |
347 ## Private methods. | 374 ## Private methods. |
348 | 375 |
349 def _get_storage(self): | 376 def _get_storage(self): |
350 """Returns oauth2client.Storage with cached tokens.""" | 377 """Returns oauth2client.Storage with cached tokens.""" |
| 378 # Do not mix cache keys for different externally provided tokens. |
| 379 if self._external_token: |
| 380 token_hash = hashlib.sha1(self._external_token.refresh_token).hexdigest() |
| 381 cache_key = '%s:refresh_tok:%s' % (self._token_cache_key, token_hash) |
| 382 else: |
| 383 cache_key = self._token_cache_key |
351 return multistore_file.get_credential_storage_custom_string_key( | 384 return multistore_file.get_credential_storage_custom_string_key( |
352 OAUTH_TOKENS_CACHE, self._token_cache_key) | 385 OAUTH_TOKENS_CACHE, cache_key) |
| 386 |
| 387 def _get_cached_credentials(self): |
| 388 """Returns oauth2client.Credentials loaded from storage.""" |
| 389 storage = self._get_storage() |
| 390 credentials = storage.get() |
| 391 |
| 392 # Is using --auth-refresh-token-json? |
| 393 if self._external_token: |
| 394 # Cached credentials are valid and match external token -> use them. It is |
| 395 # important to reuse credentials from the storage because they contain |
| 396 # cached access token. |
| 397 valid = ( |
| 398 credentials and not credentials.invalid and |
| 399 credentials.refresh_token == self._external_token.refresh_token and |
| 400 credentials.client_id == self._external_token.client_id and |
| 401 credentials.client_secret == self._external_token.client_secret) |
| 402 if valid: |
| 403 return credentials |
| 404 # Construct new credentials from externally provided refresh token, |
| 405 # associate them with cache storage (so that access_token will be placed |
| 406 # in the cache later too). |
| 407 credentials = client.OAuth2Credentials( |
| 408 access_token=None, |
| 409 client_id=self._external_token.client_id, |
| 410 client_secret=self._external_token.client_secret, |
| 411 refresh_token=self._external_token.refresh_token, |
| 412 token_expiry=None, |
| 413 token_uri='https://accounts.google.com/o/oauth2/token', |
| 414 user_agent=None, |
| 415 revoke_uri=None) |
| 416 credentials.set_store(storage) |
| 417 storage.put(credentials) |
| 418 return credentials |
| 419 |
| 420 # Not using external refresh token -> return whatever is cached. |
| 421 return credentials if (credentials and not credentials.invalid) else None |
353 | 422 |
354 def _load_access_token(self): | 423 def _load_access_token(self): |
355 """Returns cached AccessToken if it is not expired yet.""" | 424 """Returns cached AccessToken if it is not expired yet.""" |
356 credentials = self._get_storage().get() | 425 creds = self._get_cached_credentials() |
357 if not credentials or credentials.invalid: | 426 if not creds or not creds.access_token or creds.access_token_expired: |
358 return None | 427 return None |
359 if not credentials.access_token or credentials.access_token_expired: | 428 return AccessToken(str(creds.access_token), creds.token_expiry) |
360 return None | |
361 return AccessToken(str(credentials.access_token), credentials.token_expiry) | |
362 | 429 |
363 def _create_access_token(self, allow_user_interaction=False): | 430 def _create_access_token(self, allow_user_interaction=False): |
364 """Mints and caches a new access token, launching OAuth2 dance if necessary. | 431 """Mints and caches a new access token, launching OAuth2 dance if necessary. |
365 | 432 |
366 Uses cached refresh token, if present. In that case user interaction is not | 433 Uses cached refresh token, if present. In that case user interaction is not |
367 required and function will finish quietly. Otherwise it will launch 3-legged | 434 required and function will finish quietly. Otherwise it will launch 3-legged |
368 OAuth2 flow, that needs user interaction. | 435 OAuth2 flow, that needs user interaction. |
369 | 436 |
370 Args: | 437 Args: |
371 allow_user_interaction: if True, allow interaction with the user (e.g. | 438 allow_user_interaction: if True, allow interaction with the user (e.g. |
372 reading standard input, or launching a browser). | 439 reading standard input, or launching a browser). |
373 | 440 |
374 Returns: | 441 Returns: |
375 AccessToken. | 442 AccessToken. |
376 | 443 |
377 Raises: | 444 Raises: |
378 AuthenticationError on error or if authentication flow was interrupted. | 445 AuthenticationError on error or if authentication flow was interrupted. |
379 LoginRequiredError if user interaction is required, but | 446 LoginRequiredError if user interaction is required, but |
380 allow_user_interaction is False. | 447 allow_user_interaction is False. |
381 """ | 448 """ |
382 storage = self._get_storage() | 449 credentials = self._get_cached_credentials() |
383 credentials = None | |
384 | 450 |
385 # 3-legged flow with (perhaps cached) refresh token. | 451 # 3-legged flow with (perhaps cached) refresh token. |
386 credentials = storage.get() | |
387 refreshed = False | 452 refreshed = False |
388 if credentials and not credentials.invalid: | 453 if credentials and not credentials.invalid: |
389 try: | 454 try: |
390 credentials.refresh(httplib2.Http()) | 455 credentials.refresh(httplib2.Http()) |
391 refreshed = True | 456 refreshed = True |
392 except client.Error as err: | 457 except client.Error as err: |
393 logging.warning( | 458 logging.warning( |
394 'OAuth error during access token refresh: %s. ' | 459 'OAuth error during access token refresh (%s). ' |
395 'Attempting a full authentication flow.', err) | 460 'Attempting a full authentication flow.', err) |
396 | 461 |
397 # Refresh token is missing or invalid, go through the full flow. | 462 # Refresh token is missing or invalid, go through the full flow. |
398 if not refreshed: | 463 if not refreshed: |
| 464 # Can't refresh externally provided token. |
| 465 if self._external_token: |
| 466 raise AuthenticationError( |
| 467 'Token provided via --auth-refresh-token-json is no longer valid.') |
399 if not allow_user_interaction: | 468 if not allow_user_interaction: |
400 raise LoginRequiredError(self._token_cache_key) | 469 raise LoginRequiredError(self._token_cache_key) |
401 credentials = _run_oauth_dance(self._config) | 470 credentials = _run_oauth_dance(self._config) |
402 | 471 |
403 logging.info( | 472 logging.info( |
404 'OAuth access_token refreshed. Expires in %s.', | 473 'OAuth access_token refreshed. Expires in %s.', |
405 credentials.token_expiry - datetime.datetime.utcnow()) | 474 credentials.token_expiry - datetime.datetime.utcnow()) |
| 475 storage = self._get_storage() |
406 credentials.set_store(storage) | 476 credentials.set_store(storage) |
407 storage.put(credentials) | 477 storage.put(credentials) |
408 return AccessToken(str(credentials.access_token), credentials.token_expiry) | 478 return AccessToken(str(credentials.access_token), credentials.token_expiry) |
409 | 479 |
410 | 480 |
411 ## Private functions. | 481 ## Private functions. |
412 | 482 |
413 | 483 |
414 def _should_use_oauth2(): | 484 def _should_use_oauth2(): |
415 """Default value for use_oauth2 config option. | 485 """Default value for use_oauth2 config option. |
416 | 486 |
417 Used to selectively enable OAuth2 by default. | 487 Used to selectively enable OAuth2 by default. |
418 """ | 488 """ |
419 return os.path.exists(os.path.join(DEPOT_TOOLS_DIR, 'USE_OAUTH2')) | 489 return os.path.exists(os.path.join(DEPOT_TOOLS_DIR, 'USE_OAUTH2')) |
420 | 490 |
421 | 491 |
422 def _is_headless(): | 492 def _is_headless(): |
423 """True if machine doesn't seem to have a display.""" | 493 """True if machine doesn't seem to have a display.""" |
424 return sys.platform == 'linux2' and not os.environ.get('DISPLAY') | 494 return sys.platform == 'linux2' and not os.environ.get('DISPLAY') |
425 | 495 |
426 | 496 |
| 497 def _read_refresh_token_json(path): |
| 498 """Returns RefreshToken by reading it from the JSON file.""" |
| 499 try: |
| 500 with open(path, 'r') as f: |
| 501 data = json.load(f) |
| 502 return RefreshToken( |
| 503 client_id=str(data.get('client_id', OAUTH_CLIENT_ID)), |
| 504 client_secret=str(data.get('client_secret', OAUTH_CLIENT_SECRET)), |
| 505 refresh_token=str(data['refresh_token'])) |
| 506 except (IOError, ValueError) as e: |
| 507 raise AuthenticationError( |
| 508 'Failed to read refresh token from %s: %s' % (path, e)) |
| 509 except KeyError as e: |
| 510 raise AuthenticationError( |
| 511 'Failed to read refresh token from %s: missing key %s' % (path, e)) |
| 512 |
| 513 |
427 def _needs_refresh(access_token): | 514 def _needs_refresh(access_token): |
428 """True if AccessToken should be refreshed.""" | 515 """True if AccessToken should be refreshed.""" |
429 if access_token.expires_at is not None: | 516 if access_token.expires_at is not None: |
430 # Allow 5 min of clock skew between client and backend. | 517 # Allow 5 min of clock skew between client and backend. |
431 now = datetime.datetime.utcnow() + datetime.timedelta(seconds=300) | 518 now = datetime.datetime.utcnow() + datetime.timedelta(seconds=300) |
432 return now >= access_token.expires_at | 519 return now >= access_token.expires_at |
433 # Token without expiration time never expires. | 520 # Token without expiration time never expires. |
434 return False | 521 return False |
435 | 522 |
436 | 523 |
(...skipping 103 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
540 self.end_headers() | 627 self.end_headers() |
541 query = self.path.split('?', 1)[-1] | 628 query = self.path.split('?', 1)[-1] |
542 query = dict(urlparse.parse_qsl(query)) | 629 query = dict(urlparse.parse_qsl(query)) |
543 self.server.query_params = query | 630 self.server.query_params = query |
544 self.wfile.write('<html><head><title>Authentication Status</title></head>') | 631 self.wfile.write('<html><head><title>Authentication Status</title></head>') |
545 self.wfile.write('<body><p>The authentication flow has completed.</p>') | 632 self.wfile.write('<body><p>The authentication flow has completed.</p>') |
546 self.wfile.write('</body></html>') | 633 self.wfile.write('</body></html>') |
547 | 634 |
548 def log_message(self, _format, *args): | 635 def log_message(self, _format, *args): |
549 """Do not log messages to stdout while running as command line program.""" | 636 """Do not log messages to stdout while running as command line program.""" |
OLD | NEW |