Skip to content

Commit f290bd8

Browse files
proppyJon Wayne Parrott
authored andcommitted
add google-oauthlib-tool (googleapis#1)
* add google-oauthlib-tool Commandline tool to generate credentials using 3LO oauth2 flow. See: googleapis/google-auth-library-python#152
1 parent 8bd215f commit f290bd8

File tree

6 files changed

+299
-0
lines changed

6 files changed

+299
-0
lines changed

.coveragerc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,4 @@ exclude_lines =
1111
def __repr__
1212
# Don't complain if tests don't hit defensive assertion code:
1313
raise NotImplementedError
14+
if __name__ == '__main__':

google_auth_oauthlib/tool/__init__.py

Whitespace-only changes.
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
# Copyright (C) 2017 Google Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""Command-line tool for obtaining authorization and credentials from a user.
16+
17+
This tool uses the OAuth 2.0 Authorization Code grant as described in
18+
`section 1.3.1 of RFC6749`_ and implemeted by
19+
:class:`google_auth_oauthlib.flow.Flow`.
20+
21+
This tool is intended for assist developers in obtaining credentials
22+
for testing applications where it may not be possible or easy to run a
23+
complete OAuth 2.0 authorization flow, especially in the case of code
24+
samples or embedded devices without input / display capabilities.
25+
26+
This is not intended for production use where a combination of
27+
companion and on-device applications should complete the OAuth 2.0
28+
authorization flow to get authorization from the users.
29+
30+
.. _section 1.3.1 of RFC6749: https://tools.ietf.org/html/rfc6749#section-1.3.1
31+
"""
32+
33+
import json
34+
import os
35+
import os.path
36+
37+
import click
38+
39+
import google_auth_oauthlib.flow
40+
41+
42+
APP_NAME = 'google-oauthlib-tool'
43+
DEFAULT_CREDENTIALS_FILENAME = 'credentials.json'
44+
45+
46+
@click.command()
47+
@click.option(
48+
'--client-secrets',
49+
metavar='<client_secret_json_file>',
50+
required=True,
51+
help='Path to OAuth2 client secret JSON file.')
52+
@click.option(
53+
'--scope',
54+
multiple=True,
55+
metavar='<oauth2 scope>',
56+
required=True,
57+
help='API scopes to authorize access for.')
58+
@click.option(
59+
'--save',
60+
is_flag=True,
61+
metavar='<save_mode>',
62+
show_default=True,
63+
default=False,
64+
help='Save the credentials to file.')
65+
@click.option(
66+
'--credentials',
67+
metavar='<oauth2_credentials>',
68+
show_default=True,
69+
default=os.path.join(
70+
click.get_app_dir(APP_NAME),
71+
DEFAULT_CREDENTIALS_FILENAME
72+
),
73+
help='Path to store OAuth2 credentials.')
74+
@click.option(
75+
'--headless',
76+
is_flag=True,
77+
metavar='<headless_mode>',
78+
show_default=True, default=False,
79+
help='Run a console based flow.')
80+
def main(client_secrets, scope, save, credentials, headless):
81+
"""Command-line tool for obtaining authorization and credentials from a user.
82+
83+
This tool uses the OAuth 2.0 Authorization Code grant as described
84+
in section 1.3.1 of RFC6749:
85+
https://tools.ietf.org/html/rfc6749#section-1.3.1
86+
87+
This tool is intended for assist developers in obtaining credentials
88+
for testing applications where it may not be possible or easy to run a
89+
complete OAuth 2.0 authorization flow, especially in the case of code
90+
samples or embedded devices without input / display capabilities.
91+
92+
This is not intended for production use where a combination of
93+
companion and on-device applications should complete the OAuth 2.0
94+
authorization flow to get authorization from the users.
95+
96+
"""
97+
98+
flow = google_auth_oauthlib.flow.InstalledAppFlow.from_client_secrets_file(
99+
client_secrets,
100+
scopes=scope
101+
)
102+
103+
if not headless:
104+
creds = flow.run_local_server()
105+
else:
106+
creds = flow.run_console()
107+
108+
creds_data = {
109+
'token': creds.token,
110+
'refresh_token': creds.refresh_token,
111+
'token_uri': creds.token_uri,
112+
'client_id': creds.client_id,
113+
'client_secret': creds.client_secret,
114+
'scopes': creds.scopes
115+
}
116+
117+
if save:
118+
del creds_data['token']
119+
120+
config_path = os.path.dirname(credentials)
121+
if not os.path.isdir(config_path):
122+
os.makedirs(config_path)
123+
124+
with open(credentials, 'w') as outfile:
125+
json.dump(creds_data, outfile)
126+
127+
click.echo('credentials saved: %s' % credentials)
128+
129+
else:
130+
click.echo(json.dumps(creds_data))
131+
132+
133+
if __name__ == '__main__':
134+
# pylint doesn't realize that click has changed the function signature.
135+
main() # pylint: disable=no-value-for-parameter

setup.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@
1818
from setuptools import setup
1919

2020

21+
TOOL_DEPENDENCIES = (
22+
'click'
23+
)
24+
2125
DEPENDENCIES = (
2226
'google-auth',
2327
'requests-oauthlib>=0.7.0',
@@ -38,6 +42,15 @@
3842
url='https://github.com/GoogleCloudPlatform/google-auth-library-python',
3943
packages=find_packages(exclude=('tests*',)),
4044
install_requires=DEPENDENCIES,
45+
extras_require={
46+
'tool': TOOL_DEPENDENCIES,
47+
},
48+
entry_points={
49+
'console_scripts': [
50+
'google-oauthlib-tool'
51+
'=google_auth_oauthlib.tool.__main__:main [tool]',
52+
],
53+
},
4154
license='Apache 2.0',
4255
keywords='google auth oauth client oauthlib',
4356
classifiers=(

tests/test_tool.py

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
# Copyright 2017 Google Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import io
16+
import json
17+
import os.path
18+
import tempfile
19+
20+
import click.testing
21+
import google.oauth2.credentials
22+
import mock
23+
import pytest
24+
25+
import google_auth_oauthlib.flow
26+
import google_auth_oauthlib.tool.__main__ as cli
27+
28+
DATA_DIR = os.path.join(os.path.dirname(__file__), 'data')
29+
CLIENT_SECRETS_FILE = os.path.join(DATA_DIR, 'client_secrets.json')
30+
31+
32+
class TestMain(object):
33+
@pytest.fixture
34+
def runner(self):
35+
return click.testing.CliRunner()
36+
37+
@pytest.fixture
38+
def dummy_credentials(self):
39+
return google.oauth2.credentials.Credentials(
40+
token='dummy_access_token',
41+
refresh_token='dummy_refresh_token',
42+
token_uri='dummy_token_uri',
43+
client_id='dummy_client_id',
44+
client_secret='dummy_client_secret',
45+
scopes=['dummy_scope1', 'dummy_scope2']
46+
)
47+
48+
@pytest.fixture
49+
def local_server_mock(self, dummy_credentials):
50+
run_local_server_patch = mock.patch.object(
51+
google_auth_oauthlib.flow.InstalledAppFlow,
52+
'run_local_server',
53+
autospec=True)
54+
55+
with run_local_server_patch as flow:
56+
flow.return_value = dummy_credentials
57+
yield flow
58+
59+
@pytest.fixture
60+
def console_mock(self, dummy_credentials):
61+
run_console_patch = mock.patch.object(
62+
google_auth_oauthlib.flow.InstalledAppFlow,
63+
'run_console',
64+
autospec=True)
65+
66+
with run_console_patch as flow:
67+
flow.return_value = dummy_credentials
68+
yield flow
69+
70+
def test_help(self, runner):
71+
result = runner.invoke(cli.main, ['--help'])
72+
assert not result.exception
73+
assert 'RFC6749' in result.output
74+
assert 'OAuth 2.0 authorization flow' in result.output
75+
assert 'not intended for production use' in result.output
76+
assert result.exit_code == 0
77+
78+
def test_defaults(self, runner, dummy_credentials, local_server_mock):
79+
result = runner.invoke(cli.main, [
80+
'--client-secrets', CLIENT_SECRETS_FILE,
81+
'--scope', 'somescope',
82+
])
83+
local_server_mock.assert_called_with(mock.ANY)
84+
assert not result.exception
85+
assert result.exit_code == 0
86+
creds_data = json.loads(result.output)
87+
creds = google.oauth2.credentials.Credentials(**creds_data)
88+
assert creds.token == dummy_credentials.token
89+
assert creds.refresh_token == dummy_credentials.refresh_token
90+
assert creds.token_uri == dummy_credentials.token_uri
91+
assert creds.client_id == dummy_credentials.client_id
92+
assert creds.client_secret == dummy_credentials.client_secret
93+
assert creds.scopes == dummy_credentials.scopes
94+
95+
def test_headless(self, runner, dummy_credentials, console_mock):
96+
result = runner.invoke(cli.main, [
97+
'--client-secrets', CLIENT_SECRETS_FILE,
98+
'--scope', 'somescope',
99+
'--headless'
100+
])
101+
console_mock.assert_called_with(mock.ANY)
102+
assert not result.exception
103+
assert dummy_credentials.refresh_token in result.output
104+
assert result.exit_code == 0
105+
106+
def test_save_new_dir(self, runner, dummy_credentials, local_server_mock):
107+
credentials_tmpdir = tempfile.mkdtemp()
108+
credentials_path = os.path.join(
109+
credentials_tmpdir,
110+
'new-directory',
111+
'credentials.json'
112+
)
113+
result = runner.invoke(cli.main, [
114+
'--client-secrets', CLIENT_SECRETS_FILE,
115+
'--scope', 'somescope',
116+
'--credentials', credentials_path,
117+
'--save'
118+
])
119+
local_server_mock.assert_called_with(mock.ANY)
120+
assert not result.exception
121+
assert 'saved' in result.output
122+
assert result.exit_code == 0
123+
with io.open(credentials_path) as f: # pylint: disable=invalid-name
124+
creds_data = json.load(f)
125+
assert 'access_token' not in creds_data
126+
127+
creds = google.oauth2.credentials.Credentials(
128+
token=None, **creds_data)
129+
assert creds.token is None
130+
assert creds.refresh_token == dummy_credentials.refresh_token
131+
assert creds.token_uri == dummy_credentials.token_uri
132+
assert creds.client_id == dummy_credentials.client_id
133+
assert creds.client_secret == dummy_credentials.client_secret
134+
135+
def test_save_existing_dir(self, runner, local_server_mock):
136+
credentials_tmpdir = tempfile.mkdtemp()
137+
result = runner.invoke(cli.main, [
138+
'--client-secrets', CLIENT_SECRETS_FILE,
139+
'--scope', 'somescope',
140+
'--credentials', os.path.join(
141+
credentials_tmpdir,
142+
'credentials.json'
143+
),
144+
'--save'
145+
])
146+
local_server_mock.assert_called_with(mock.ANY)
147+
assert not result.exception
148+
assert 'saved' in result.output
149+
assert result.exit_code == 0

tox.ini

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ deps =
77
pytest
88
pytest-cov
99
futures
10+
click
1011
commands =
1112
py.test --cov=google_auth_oauthlib --cov=tests {posargs:tests}
1213

0 commit comments

Comments
 (0)