1# 2# Copyright 2015 Google Inc. 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# http://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"""Command-line utility for fetching/inspecting credentials. 17 18oauth2l (pronounced "oauthtool") is a small utility for fetching 19credentials, or inspecting existing credentials. Here we demonstrate 20some sample use: 21 22 $ oauth2l fetch userinfo.email bigquery compute 23 Fetched credentials of type: 24 oauth2client.client.OAuth2Credentials 25 Access token: 26 ya29.abcdefghijklmnopqrstuvwxyz123yessirree 27 $ oauth2l header userinfo.email 28 Authorization: Bearer ya29.zyxwvutsrqpnmolkjihgfedcba 29 $ oauth2l validate thisisnotatoken 30 <exit status: 1> 31 $ oauth2l validate ya29.zyxwvutsrqpnmolkjihgfedcba 32 $ oauth2l scopes ya29.abcdefghijklmnopqrstuvwxyz123yessirree 33 https://www.googleapis.com/auth/bigquery 34 https://www.googleapis.com/auth/compute 35 https://www.googleapis.com/auth/userinfo.email 36 37The `header` command is designed to be easy to use with `curl`: 38 39 $ curl -H "$(oauth2l header bigquery)" \\ 40 'https://www.googleapis.com/bigquery/v2/projects' 41 ... lists all projects ... 42 43The token can also be printed in other formats, for easy chaining 44into other programs: 45 46 $ oauth2l fetch -f json_compact userinfo.email 47 <one-line JSON object with credential information> 48 $ oauth2l fetch -f bare drive 49 ya29.suchT0kenManyCredentialsW0Wokyougetthepoint 50 51""" 52 53from __future__ import print_function 54 55import argparse 56import json 57import logging 58import os 59import pkgutil 60import sys 61import textwrap 62 63import oauth2client.client 64from six.moves import http_client 65 66import apitools.base.py as apitools_base 67 68# We could use a generated client here, but it's used for precisely 69# one URL, with one parameter and no worries about URL encoding. Let's 70# go with simple. 71_OAUTH2_TOKENINFO_TEMPLATE = ( 72 'https://www.googleapis.com/oauth2/v2/tokeninfo' 73 '?access_token={access_token}' 74) 75 76 77def GetDefaultClientInfo(): 78 client_secrets_json = pkgutil.get_data( 79 'apitools.data', 'apitools_client_secrets.json').decode('utf8') 80 client_secrets = json.loads(client_secrets_json)['installed'] 81 return { 82 'client_id': client_secrets['client_id'], 83 'client_secret': client_secrets['client_secret'], 84 'user_agent': 'apitools/0.2 oauth2l/0.1', 85 } 86 87 88def GetClientInfoFromFlags(client_secrets): 89 """Fetch client info from args.""" 90 if client_secrets: 91 client_secrets_path = os.path.expanduser(client_secrets) 92 if not os.path.exists(client_secrets_path): 93 raise ValueError( 94 'Cannot find file: {0}'.format(client_secrets)) 95 with open(client_secrets_path) as client_secrets_file: 96 client_secrets = json.load(client_secrets_file) 97 if 'installed' not in client_secrets: 98 raise ValueError('Provided client ID must be for an installed app') 99 client_secrets = client_secrets['installed'] 100 return { 101 'client_id': client_secrets['client_id'], 102 'client_secret': client_secrets['client_secret'], 103 'user_agent': 'apitools/0.2 oauth2l/0.1', 104 } 105 else: 106 return GetDefaultClientInfo() 107 108 109def _ExpandScopes(scopes): 110 scope_prefix = 'https://www.googleapis.com/auth/' 111 return [s if s.startswith('https://') else scope_prefix + s 112 for s in scopes] 113 114 115def _PrettyJson(data): 116 return json.dumps(data, sort_keys=True, indent=4, separators=(',', ': ')) 117 118 119def _CompactJson(data): 120 return json.dumps(data, sort_keys=True, separators=(',', ':')) 121 122 123def _AsText(text_or_bytes): 124 if isinstance(text_or_bytes, bytes): 125 return text_or_bytes.decode('utf8') 126 return text_or_bytes 127 128 129def _Format(fmt, credentials): 130 """Format credentials according to fmt.""" 131 if fmt == 'bare': 132 return credentials.access_token 133 elif fmt == 'header': 134 return 'Authorization: Bearer %s' % credentials.access_token 135 elif fmt == 'json': 136 return _PrettyJson(json.loads(_AsText(credentials.to_json()))) 137 elif fmt == 'json_compact': 138 return _CompactJson(json.loads(_AsText(credentials.to_json()))) 139 elif fmt == 'pretty': 140 format_str = textwrap.dedent('\n'.join([ 141 'Fetched credentials of type:', 142 ' {credentials_type.__module__}.{credentials_type.__name__}', 143 'Access token:', 144 ' {credentials.access_token}', 145 ])) 146 return format_str.format(credentials=credentials, 147 credentials_type=type(credentials)) 148 raise ValueError('Unknown format: {0}'.format(fmt)) 149 150_FORMATS = set(('bare', 'header', 'json', 'json_compact', 'pretty')) 151 152 153def _GetTokenScopes(access_token): 154 """Return the list of valid scopes for the given token as a list.""" 155 url = _OAUTH2_TOKENINFO_TEMPLATE.format(access_token=access_token) 156 response = apitools_base.MakeRequest( 157 apitools_base.GetHttp(), apitools_base.Request(url)) 158 if response.status_code not in [http_client.OK, http_client.BAD_REQUEST]: 159 raise apitools_base.HttpError.FromResponse(response) 160 if response.status_code == http_client.BAD_REQUEST: 161 return [] 162 return json.loads(_AsText(response.content))['scope'].split(' ') 163 164 165def _ValidateToken(access_token): 166 """Return True iff the provided access token is valid.""" 167 return bool(_GetTokenScopes(access_token)) 168 169 170def _FetchCredentials(args, client_info=None, credentials_filename=None): 171 """Fetch a credential for the given client_info and scopes.""" 172 client_info = client_info or GetClientInfoFromFlags(args.client_secrets) 173 scopes = _ExpandScopes(args.scope) 174 if not scopes: 175 raise ValueError('No scopes provided') 176 credentials_filename = credentials_filename or args.credentials_filename 177 # TODO(craigcitro): Remove this logging nonsense once we quiet the 178 # spurious logging in oauth2client. 179 old_level = logging.getLogger().level 180 logging.getLogger().setLevel(logging.ERROR) 181 credentials = apitools_base.GetCredentials( 182 'oauth2l', scopes, credentials_filename=credentials_filename, 183 service_account_json_keyfile=args.service_account_json_keyfile, 184 oauth2client_args='', **client_info) 185 logging.getLogger().setLevel(old_level) 186 if not _ValidateToken(credentials.access_token): 187 credentials.refresh(apitools_base.GetHttp()) 188 return credentials 189 190 191def _Email(args): 192 """Print the email address for this token, if possible.""" 193 userinfo = apitools_base.GetUserinfo( 194 oauth2client.client.AccessTokenCredentials(args.access_token, 195 'oauth2l/1.0')) 196 user_email = userinfo.get('email') 197 if user_email: 198 print(user_email) 199 200 201def _Fetch(args): 202 """Fetch a valid access token and display it.""" 203 credentials = _FetchCredentials(args) 204 print(_Format(args.credentials_format.lower(), credentials)) 205 206 207def _Header(args): 208 """Fetch an access token and display it formatted as an HTTP header.""" 209 print(_Format('header', _FetchCredentials(args))) 210 211 212def _Scopes(args): 213 """Print the list of scopes for a valid token.""" 214 scopes = _GetTokenScopes(args.access_token) 215 if not scopes: 216 return 1 217 for scope in sorted(scopes): 218 print(scope) 219 220 221def _Userinfo(args): 222 """Print the userinfo for this token, if possible.""" 223 userinfo = apitools_base.GetUserinfo( 224 oauth2client.client.AccessTokenCredentials(args.access_token, 225 'oauth2l/1.0')) 226 if args.format == 'json': 227 print(_PrettyJson(userinfo)) 228 else: 229 print(_CompactJson(userinfo)) 230 231 232def _Validate(args): 233 """Validate an access token. Exits with 0 if valid, 1 otherwise.""" 234 return 1 - (_ValidateToken(args.access_token)) 235 236 237def _GetParser(): 238 """Returns argparse argument parser.""" 239 shared_flags = argparse.ArgumentParser(add_help=False) 240 shared_flags.add_argument( 241 '--client_secrets', 242 default='', 243 help=('If specified, use the client ID/secret from the named ' 244 'file, which should be a client_secrets.json file ' 245 'downloaded from the Developer Console.')) 246 shared_flags.add_argument( 247 '--credentials_filename', 248 default='', 249 help='(optional) Filename for fetching/storing credentials.') 250 shared_flags.add_argument( 251 '--service_account_json_keyfile', 252 default='', 253 help=('Filename for a JSON service account key downloaded from ' 254 'the Google Developer Console.')) 255 256 parser = argparse.ArgumentParser( 257 description=__doc__, 258 formatter_class=argparse.RawDescriptionHelpFormatter, 259 ) 260 subparsers = parser.add_subparsers(dest='command') 261 262 # email 263 email = subparsers.add_parser('email', help=_Email.__doc__, 264 parents=[shared_flags]) 265 email.set_defaults(func=_Email) 266 email.add_argument( 267 'access_token', 268 help=('Access token to print associated email address for. Must have ' 269 'the userinfo.email scope.')) 270 271 # fetch 272 fetch = subparsers.add_parser('fetch', help=_Fetch.__doc__, 273 parents=[shared_flags]) 274 fetch.set_defaults(func=_Fetch) 275 fetch.add_argument( 276 '-f', '--credentials_format', 277 default='pretty', choices=sorted(_FORMATS), 278 help='Output format for token.') 279 fetch.add_argument( 280 'scope', 281 nargs='*', 282 help='Scope to fetch. May be provided multiple times.') 283 284 # header 285 header = subparsers.add_parser('header', help=_Header.__doc__, 286 parents=[shared_flags]) 287 header.set_defaults(func=_Header) 288 header.add_argument( 289 'scope', 290 nargs='*', 291 help='Scope to header. May be provided multiple times.') 292 293 # scopes 294 scopes = subparsers.add_parser('scopes', help=_Scopes.__doc__, 295 parents=[shared_flags]) 296 scopes.set_defaults(func=_Scopes) 297 scopes.add_argument( 298 'access_token', 299 help=('Scopes associated with this token will be printed.')) 300 301 # userinfo 302 userinfo = subparsers.add_parser('userinfo', help=_Userinfo.__doc__, 303 parents=[shared_flags]) 304 userinfo.set_defaults(func=_Userinfo) 305 userinfo.add_argument( 306 '-f', '--format', 307 default='json', choices=('json', 'json_compact'), 308 help='Output format for userinfo.') 309 userinfo.add_argument( 310 'access_token', 311 help=('Access token to print associated email address for. Must have ' 312 'the userinfo.email scope.')) 313 314 # validate 315 validate = subparsers.add_parser('validate', help=_Validate.__doc__, 316 parents=[shared_flags]) 317 validate.set_defaults(func=_Validate) 318 validate.add_argument( 319 'access_token', 320 help='Access token to validate.') 321 322 return parser 323 324 325def main(argv=None): 326 argv = argv or sys.argv 327 # Invoke the newly created parser. 328 args = _GetParser().parse_args(argv[1:]) 329 try: 330 exit_code = args.func(args) 331 except BaseException as e: 332 print('Error encountered in {0} operation: {1}'.format( 333 args.command, e)) 334 return 1 335 return exit_code 336 337 338if __name__ == '__main__': 339 sys.exit(main(sys.argv)) 340