1 """Command-line utility for fetching/inspecting credentials. 2 3 oauth2l (pronounced "oauthtool") is a small utility for fetching 4 credentials, or inspecting existing credentials. Here we demonstrate 5 some sample use: 6 7 $ oauth2l fetch userinfo.email bigquery compute 8 Fetched credentials of type: 9 oauth2client.client.OAuth2Credentials 10 Access token: 11 ya29.abcdefghijklmnopqrstuvwxyz123yessirree 12 $ oauth2l header userinfo.email 13 Authorization: Bearer ya29.zyxwvutsrqpnmolkjihgfedcba 14 $ oauth2l validate thisisnotatoken 15 <exit status: 1> 16 $ oauth2l validate ya29.zyxwvutsrqpnmolkjihgfedcba 17 $ oauth2l scopes ya29.abcdefghijklmnopqrstuvwxyz123yessirree 18 https://www.googleapis.com/auth/bigquery 19 https://www.googleapis.com/auth/compute 20 https://www.googleapis.com/auth/userinfo.email 21 22 The `header` command is designed to be easy to use with `curl`: 23 24 $ curl "$(oauth2l header bigquery)" \ 25 'https://www.googleapis.com/bigquery/v2/projects' 26 27 The token can also be printed in other formats, for easy chaining 28 into other programs: 29 30 $ oauth2l fetch -f json_compact userinfo.email 31 <one-line JSON object with credential information> 32 $ oauth2l fetch -f bare drive 33 ya29.suchT0kenManyCredentialsW0Wokyougetthepoint 34 35 """ 36 37 import httplib 38 import json 39 import logging 40 import os 41 import pkgutil 42 import sys 43 import textwrap 44 45 import gflags as flags 46 from google.apputils import appcommands 47 import oauth2client.client 48 49 import apitools.base.py as apitools_base 50 from apitools.base.py import cli as apitools_cli 51 52 FLAGS = flags.FLAGS 53 # We could use a generated client here, but it's used for precisely 54 # one URL, with one parameter and no worries about URL encoding. Let's 55 # go with simple. 56 _OAUTH2_TOKENINFO_TEMPLATE = ( 57 'https://www.googleapis.com/oauth2/v2/tokeninfo' 58 '?access_token={access_token}' 59 ) 60 61 62 flags.DEFINE_string( 63 'client_secrets', '', 64 'If specified, use the client ID/secret from the named ' 65 'file, which should be a client_secrets.json file as downloaded ' 66 'from the Developer Console.') 67 flags.DEFINE_string( 68 'credentials_filename', '', 69 '(optional) Filename for fetching/storing credentials.') 70 flags.DEFINE_string( 71 'service_account_json_keyfile', '', 72 'Filename for a JSON service account key downloaded from the Developer ' 73 'Console.') 74 75 76 def GetDefaultClientInfo(): 77 client_secrets = json.loads(pkgutil.get_data( 78 'apitools.data', 'apitools_client_secrets.json'))['installed'] 79 return { 80 'client_id': client_secrets['client_id'], 81 'client_secret': client_secrets['client_secret'], 82 'user_agent': 'apitools/0.2 oauth2l/0.1', 83 } 84 85 86 def GetClientInfoFromFlags(): 87 """Fetch client info from FLAGS.""" 88 if FLAGS.client_secrets: 89 client_secrets_path = os.path.expanduser(FLAGS.client_secrets) 90 if not os.path.exists(client_secrets_path): 91 raise ValueError('Cannot find file: %s' % FLAGS.client_secrets) 92 with open(client_secrets_path) as client_secrets_file: 93 client_secrets = json.load(client_secrets_file) 94 if 'installed' not in client_secrets: 95 raise ValueError('Provided client ID must be for an installed app') 96 client_secrets = client_secrets['installed'] 97 return { 98 'client_id': client_secrets['client_id'], 99 'client_secret': client_secrets['client_secret'], 100 'user_agent': 'apitools/0.2 oauth2l/0.1', 101 } 102 else: 103 return GetDefaultClientInfo() 104 105 106 def _ExpandScopes(scopes): 107 scope_prefix = 'https://www.googleapis.com/auth/' 108 return [s if s.startswith('https://') else scope_prefix + s 109 for s in scopes] 110 111 112 def _PrettyJson(data): 113 return json.dumps(data, sort_keys=True, indent=4, separators=(',', ': ')) 114 115 116 def _CompactJson(data): 117 return json.dumps(data, sort_keys=True, separators=(',', ':')) 118 119 120 def _Format(fmt, credentials): 121 """Format credentials according to fmt.""" 122 if fmt == 'bare': 123 return credentials.access_token 124 elif fmt == 'header': 125 return 'Authorization: Bearer %s' % credentials.access_token 126 elif fmt == 'json': 127 return _PrettyJson(json.loads(credentials.to_json())) 128 elif fmt == 'json_compact': 129 return _CompactJson(json.loads(credentials.to_json())) 130 elif fmt == 'pretty': 131 format_str = textwrap.dedent('\n'.join([ 132 'Fetched credentials of type:', 133 ' {credentials_type.__module__}.{credentials_type.__name__}', 134 'Access token:', 135 ' {credentials.access_token}', 136 ])) 137 return format_str.format(credentials=credentials, 138 credentials_type=type(credentials)) 139 raise ValueError('Unknown format: {}'.format(fmt)) 140 141 _FORMATS = set(('bare', 'header', 'json', 'json_compact', 'pretty')) 142 143 144 def _GetTokenScopes(access_token): 145 """Return the list of valid scopes for the given token as a list.""" 146 url = _OAUTH2_TOKENINFO_TEMPLATE.format(access_token=access_token) 147 response = apitools_base.MakeRequest( 148 apitools_base.GetHttp(), apitools_base.Request(url)) 149 if response.status_code not in [httplib.OK, httplib.BAD_REQUEST]: 150 raise apitools_base.HttpError.FromResponse(response) 151 if response.status_code == httplib.BAD_REQUEST: 152 return [] 153 return json.loads(response.content)['scope'].split(' ') 154 155 156 def _ValidateToken(access_token): 157 """Return True iff the provided access token is valid.""" 158 return bool(_GetTokenScopes(access_token)) 159 160 161 def FetchCredentials(scopes, client_info=None, credentials_filename=None): 162 """Fetch a credential for the given client_info and scopes.""" 163 client_info = client_info or GetClientInfoFromFlags() 164 scopes = _ExpandScopes(scopes) 165 if not scopes: 166 raise ValueError('No scopes provided') 167 credentials_filename = credentials_filename or FLAGS.credentials_filename 168 # TODO(craigcitro): Remove this logging nonsense once we quiet the 169 # spurious logging in oauth2client. 170 old_level = logging.getLogger().level 171 logging.getLogger().setLevel(logging.ERROR) 172 credentials = apitools_base.GetCredentials( 173 'oauth2l', scopes, credentials_filename=credentials_filename, 174 service_account_json_keyfile=FLAGS.service_account_json_keyfile, 175 oauth2client_args='', **client_info) 176 logging.getLogger().setLevel(old_level) 177 if not _ValidateToken(credentials.access_token): 178 credentials.refresh(apitools_base.GetHttp()) 179 return credentials 180 181 182 class _Email(apitools_cli.NewCmd): 183 184 """Get user email.""" 185 186 usage = 'email <access_token>' 187 188 def RunWithArgs(self, access_token): 189 """Print the email address for this token, if possible.""" 190 userinfo = apitools_base.GetUserinfo( 191 oauth2client.client.AccessTokenCredentials(access_token, 192 'oauth2l/1.0')) 193 user_email = userinfo.get('email') 194 if user_email: 195 print user_email 196 197 198 class _Fetch(apitools_cli.NewCmd): 199 200 """Fetch credentials.""" 201 202 usage = 'fetch <scope> [<scope> ...]' 203 204 def __init__(self, name, flag_values): 205 super(_Fetch, self).__init__(name, flag_values) 206 flags.DEFINE_enum( 207 'credentials_format', 'pretty', sorted(_FORMATS), 208 'Output format for token.', 209 short_name='f', flag_values=flag_values) 210 211 def RunWithArgs(self, *scopes): 212 """Fetch a valid access token and display it.""" 213 credentials = FetchCredentials(scopes) 214 print _Format(FLAGS.credentials_format.lower(), credentials) 215 216 217 class _Header(apitools_cli.NewCmd): 218 219 """Print credentials for a header.""" 220 221 usage = 'header <scope> [<scope> ...]' 222 223 def RunWithArgs(self, *scopes): 224 """Fetch a valid access token and display it formatted for a header.""" 225 print _Format('header', FetchCredentials(scopes)) 226 227 228 class _Scopes(apitools_cli.NewCmd): 229 230 """Get the list of scopes for a token.""" 231 232 usage = 'scopes <access_token>' 233 234 def RunWithArgs(self, access_token): 235 """Print the list of scopes for a valid token.""" 236 scopes = _GetTokenScopes(access_token) 237 if not scopes: 238 return 1 239 for scope in sorted(scopes): 240 print scope 241 242 243 class _Userinfo(apitools_cli.NewCmd): 244 245 """Get userinfo.""" 246 247 usage = 'userinfo <access_token>' 248 249 def __init__(self, name, flag_values): 250 super(_Userinfo, self).__init__(name, flag_values) 251 flags.DEFINE_enum( 252 'format', 'json', sorted(('json', 'json_compact')), 253 'Output format for userinfo.', 254 short_name='f', flag_values=flag_values) 255 256 def RunWithArgs(self, access_token): 257 """Print the userinfo for this token (if we have the right scopes).""" 258 userinfo = apitools_base.GetUserinfo( 259 oauth2client.client.AccessTokenCredentials(access_token, 260 'oauth2l/1.0')) 261 if FLAGS.format == 'json': 262 print _PrettyJson(userinfo) 263 else: 264 print _CompactJson(userinfo) 265 266 267 class _Validate(apitools_cli.NewCmd): 268 269 """Validate a token.""" 270 271 usage = 'validate <access_token>' 272 273 def RunWithArgs(self, access_token): 274 """Validate an access token. Exits with 0 if valid, 1 otherwise.""" 275 return 1 - (_ValidateToken(access_token)) 276 277 278 def run_main(): # pylint:disable=invalid-name 279 """Function to be used as setuptools script entry point.""" 280 # Put the flags for this module somewhere the flags module will look 281 # for them. 282 283 # pylint:disable=protected-access 284 new_name = flags._GetMainModule() 285 sys.modules[new_name] = sys.modules['__main__'] 286 for flag in FLAGS.FlagsByModuleDict().get(__name__, []): 287 FLAGS._RegisterFlagByModule(new_name, flag) 288 for key_flag in FLAGS.KeyFlagsByModuleDict().get(__name__, []): 289 FLAGS._RegisterKeyFlagForModule(new_name, key_flag) 290 # pylint:enable=protected-access 291 292 # Now set __main__ appropriately so that appcommands will be 293 # happy. 294 sys.modules['__main__'] = sys.modules[__name__] 295 appcommands.Run() 296 sys.modules['__main__'] = sys.modules.pop(new_name) 297 298 299 def main(unused_argv): 300 appcommands.AddCmd('email', _Email) 301 appcommands.AddCmd('fetch', _Fetch) 302 appcommands.AddCmd('header', _Header) 303 appcommands.AddCmd('scopes', _Scopes) 304 appcommands.AddCmd('userinfo', _Userinfo) 305 appcommands.AddCmd('validate', _Validate) 306 307 308 if __name__ == '__main__': 309 appcommands.Run() 310