@ -8,6 +8,7 @@ import BaseHTTPServer
import collections
import datetime
import functools
import hashlib
import json
import logging
import optparse
@ -62,6 +63,7 @@ AuthConfig = collections.namedtuple('AuthConfig', [
' save_cookies ' , # deprecated, will be removed
' use_local_webserver ' ,
' webserver_port ' ,
' refresh_token_json ' ,
] )
@ -72,6 +74,14 @@ AccessToken = collections.namedtuple('AccessToken', [
] )
# Refresh token passed via --auth-refresh-token-json.
RefreshToken = collections . namedtuple ( ' RefreshToken ' , [
' client_id ' ,
' client_secret ' ,
' refresh_token ' ,
] )
class AuthenticationError ( Exception ) :
""" Raised on errors related to authentication. """
@ -91,7 +101,8 @@ def make_auth_config(
use_oauth2 = None ,
save_cookies = None ,
use_local_webserver = None ,
webserver_port = None ) :
webserver_port = None ,
refresh_token_json = None ) :
""" Returns new instance of AuthConfig.
If some config option is None , it will be set to a reasonable default value .
@ -103,7 +114,8 @@ def make_auth_config(
default ( use_oauth2 , _should_use_oauth2 ( ) ) ,
default ( save_cookies , True ) ,
default ( use_local_webserver , not _is_headless ( ) ) ,
default ( webserver_port , 8090 ) )
default ( webserver_port , 8090 ) ,
default ( refresh_token_json , ' ' ) )
def add_auth_options ( parser , default_config = None ) :
@ -148,6 +160,10 @@ def add_auth_options(parser, default_config=None):
default = default_config . webserver_port ,
help = ' Port a local web server should listen on. Used only if '
' --auth-no-local-webserver is not set. [default: %d efault] ' )
parser . auth_group . add_option (
' --auth-refresh-token-json ' ,
default = default_config . refresh_token_json ,
help = ' Path to a JSON file with role account refresh token to use. ' )
def extract_auth_config_from_options ( options ) :
@ -159,7 +175,8 @@ def extract_auth_config_from_options(options):
use_oauth2 = options . use_oauth2 ,
save_cookies = False if options . use_oauth2 else options . save_cookies ,
use_local_webserver = options . use_local_webserver ,
webserver_port = options . auth_host_port )
webserver_port = options . auth_host_port ,
refresh_token_json = options . auth_refresh_token_json )
def auth_config_to_command_options ( auth_config ) :
@ -181,6 +198,9 @@ def auth_config_to_command_options(auth_config):
opts . append ( ' --auth-no-local-webserver ' )
if auth_config . webserver_port != defaults . webserver_port :
opts . extend ( [ ' --auth-host-port ' , str ( auth_config . webserver_port ) ] )
if auth_config . refresh_token_json != defaults . refresh_token_json :
opts . extend ( [
' --auth-refresh-token-json ' , str ( auth_config . refresh_token_json ) ] )
return opts
@ -222,6 +242,9 @@ class Authenticator(object):
self . _config = config
self . _lock = threading . Lock ( )
self . _token_cache_key = token_cache_key
self . _external_token = None
if config . refresh_token_json :
self . _external_token = _read_refresh_token_json ( config . refresh_token_json )
def login ( self ) :
""" Performs interactive login flow if necessary.
@ -229,24 +252,29 @@ class Authenticator(object):
Raises :
AuthenticationError on error or if interrupted .
"""
if self . _external_token :
raise AuthenticationError (
' Can \' t run login flow when using --auth-refresh-token-json. ' )
return self . get_access_token (
force_refresh = True , allow_user_interaction = True )
def logout ( self ) :
""" Revokes the refresh token and deletes it from the cache.
Returns True if actually revoked a token .
Returns True if had some credentials cached .
"""
revoked = False
with self . _lock :
self . _access_token = None
storage = self . _get_storage ( )
credentials = storage . get ( )
if credentials :
credentials . revoke ( httplib2 . Http ( ) )
revoked = True
had_creds = bool ( credentials )
if credentials and credentials . refresh_token and credentials . revoke_uri :
try :
credentials . revoke ( httplib2 . Http ( ) )
except client . TokenRevokeError as e :
logging . warning ( ' Failed to revoke refresh token: %s ' , e )
storage . delete ( )
return revoked
return had_c reds
def has_cached_credentials ( self ) :
""" Returns True if long term credentials (refresh token) are in cache.
@ -262,8 +290,7 @@ class Authenticator(object):
it .
"""
with self . _lock :
credentials = self . _get_storage ( ) . get ( )
return credentials and not credentials . invalid
return bool ( self . _get_cached_credentials ( ) )
def get_access_token ( self , force_refresh = False , allow_user_interaction = False ) :
""" Returns AccessToken, refreshing it if necessary.
@ -348,17 +375,57 @@ class Authenticator(object):
def _get_storage ( self ) :
""" Returns oauth2client.Storage with cached tokens. """
# Do not mix cache keys for different externally provided tokens.
if self . _external_token :
token_hash = hashlib . sha1 ( self . _external_token . refresh_token ) . hexdigest ( )
cache_key = ' %s :refresh_tok: %s ' % ( self . _token_cache_key , token_hash )
else :
cache_key = self . _token_cache_key
return multistore_file . get_credential_storage_custom_string_key (
OAUTH_TOKENS_CACHE , self . _token_cache_key )
OAUTH_TOKENS_CACHE , cache_key )
def _get_cached_credentials ( self ) :
""" Returns oauth2client.Credentials loaded from storage. """
storage = self . _get_storage ( )
credentials = storage . get ( )
# Is using --auth-refresh-token-json?
if self . _external_token :
# Cached credentials are valid and match external token -> use them. It is
# important to reuse credentials from the storage because they contain
# cached access token.
valid = (
credentials and not credentials . invalid and
credentials . refresh_token == self . _external_token . refresh_token and
credentials . client_id == self . _external_token . client_id and
credentials . client_secret == self . _external_token . client_secret )
if valid :
return credentials
# Construct new credentials from externally provided refresh token,
# associate them with cache storage (so that access_token will be placed
# in the cache later too).
credentials = client . OAuth2Credentials (
access_token = None ,
client_id = self . _external_token . client_id ,
client_secret = self . _external_token . client_secret ,
refresh_token = self . _external_token . refresh_token ,
token_expiry = None ,
token_uri = ' https://accounts.google.com/o/oauth2/token ' ,
user_agent = None ,
revoke_uri = None )
credentials . set_store ( storage )
storage . put ( credentials )
return credentials
# Not using external refresh token -> return whatever is cached.
return credentials if ( credentials and not credentials . invalid ) else None
def _load_access_token ( self ) :
""" Returns cached AccessToken if it is not expired yet. """
credentials = self . _get_storage ( ) . get ( )
if not credentials or credentials . invalid :
return None
if not credentials . access_token or credentials . access_token_expired :
creds = self . _get_cached_credentials ( )
if not creds or not creds . access_token or creds . access_token_expired :
return None
return AccessToken ( str ( credentials . access_token ) , credentials . token_expiry )
return AccessToken ( str ( creds . access_token ) , creds . token_expiry )
def _create_access_token ( self , allow_user_interaction = False ) :
""" Mints and caches a new access token, launching OAuth2 dance if necessary.
@ -379,11 +446,9 @@ class Authenticator(object):
LoginRequiredError if user interaction is required , but
allow_user_interaction is False .
"""
storage = self . _get_storage ( )
credentials = None
credentials = self . _get_cached_credentials ( )
# 3-legged flow with (perhaps cached) refresh token.
credentials = storage . get ( )
refreshed = False
if credentials and not credentials . invalid :
try :
@ -391,11 +456,15 @@ class Authenticator(object):
refreshed = True
except client . Error as err :
logging . warning (
' OAuth error during access token refresh : %s . '
' OAuth error during access token refresh ( %s ) . '
' Attempting a full authentication flow. ' , err )
# Refresh token is missing or invalid, go through the full flow.
if not refreshed :
# Can't refresh externally provided token.
if self . _external_token :
raise AuthenticationError (
' Token provided via --auth-refresh-token-json is no longer valid. ' )
if not allow_user_interaction :
raise LoginRequiredError ( self . _token_cache_key )
credentials = _run_oauth_dance ( self . _config )
@ -403,6 +472,7 @@ class Authenticator(object):
logging . info (
' OAuth access_token refreshed. Expires in %s . ' ,
credentials . token_expiry - datetime . datetime . utcnow ( ) )
storage = self . _get_storage ( )
credentials . set_store ( storage )
storage . put ( credentials )
return AccessToken ( str ( credentials . access_token ) , credentials . token_expiry )
@ -424,6 +494,23 @@ def _is_headless():
return sys . platform == ' linux2 ' and not os . environ . get ( ' DISPLAY ' )
def _read_refresh_token_json ( path ) :
""" Returns RefreshToken by reading it from the JSON file. """
try :
with open ( path , ' r ' ) as f :
data = json . load ( f )
return RefreshToken (
client_id = str ( data . get ( ' client_id ' , OAUTH_CLIENT_ID ) ) ,
client_secret = str ( data . get ( ' client_secret ' , OAUTH_CLIENT_SECRET ) ) ,
refresh_token = str ( data [ ' refresh_token ' ] ) )
except ( IOError , ValueError ) as e :
raise AuthenticationError (
' Failed to read refresh token from %s : %s ' % ( path , e ) )
except KeyError as e :
raise AuthenticationError (
' Failed to read refresh token from %s : missing key %s ' % ( path , e ) )
def _needs_refresh ( access_token ) :
""" True if AccessToken should be refreshed. """
if access_token . expires_at is not None :