• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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