blob: 12c68bf0f0f3819d4c25675d2a3140a6ad436274 [file] [log] [blame]
Joe Gregoriofff42f22013-04-05 12:53:46 -04001# Copyright (C) 2013 Google Inc.
Joe Gregorio695fdc12011-01-16 16:46:55 -05002#
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 tools for authenticating via OAuth 2.0
16
Joe Gregorio7c22ab22011-02-16 15:32:39 -050017Do the OAuth 2.0 Web Server dance for a command line application. Stores the
18generated credentials in a common file that is used by other example apps in
19the same directory.
Joe Gregorio695fdc12011-01-16 16:46:55 -050020"""
21
22__author__ = 'jcgregorio@google.com (Joe Gregorio)'
Joe Gregorio700f13f2013-07-02 13:47:15 -040023__all__ = ['argparser', 'run_flow', 'run', 'message_if_missing']
Joe Gregorio695fdc12011-01-16 16:46:55 -050024
Joe Gregorio695fdc12011-01-16 16:46:55 -050025
Joe Gregorio9e5fe4d2011-03-10 09:33:47 -050026import BaseHTTPServer
Joe Gregorio79daca02013-03-29 16:25:52 -040027import argparse
Joe Gregoriofff42f22013-04-05 12:53:46 -040028import httplib2
Joe Gregorio79daca02013-03-29 16:25:52 -040029import logging
30import os
Joe Gregorio9e5fe4d2011-03-10 09:33:47 -050031import socket
32import sys
Joe Gregorio2fdd2952012-01-17 09:03:28 -050033import webbrowser
Joe Gregorio9e5fe4d2011-03-10 09:33:47 -050034
Joe Gregoriofff42f22013-04-05 12:53:46 -040035from oauth2client import client
36from oauth2client import file
Joe Gregorio68a8cfe2012-08-03 16:17:40 -040037from oauth2client import util
Joe Gregorio9e5fe4d2011-03-10 09:33:47 -050038
39try:
Joe Gregorio9da2ad82011-09-11 14:04:44 -040040 from urlparse import parse_qsl
Joe Gregorio9e5fe4d2011-03-10 09:33:47 -050041except ImportError:
Joe Gregorio9da2ad82011-09-11 14:04:44 -040042 from cgi import parse_qsl
Joe Gregorio9e5fe4d2011-03-10 09:33:47 -050043
Joe Gregorio79daca02013-03-29 16:25:52 -040044_CLIENT_SECRETS_MESSAGE = """WARNING: Please configure OAuth 2.0
Joe Gregorio9e5fe4d2011-03-10 09:33:47 -050045
Joe Gregorio79daca02013-03-29 16:25:52 -040046To make this sample run you will need to populate the client_secrets.json file
47found at:
Joe Gregorio9e5fe4d2011-03-10 09:33:47 -050048
Joe Gregorio79daca02013-03-29 16:25:52 -040049 %s
Joe Gregorio9e5fe4d2011-03-10 09:33:47 -050050
Joe Gregorio79daca02013-03-29 16:25:52 -040051with information from the APIs Console <https://code.google.com/apis/console>.
Joe Gregorio9e5fe4d2011-03-10 09:33:47 -050052
Joe Gregorio79daca02013-03-29 16:25:52 -040053"""
54
55# run_parser is an ArgumentParser that contains command-line options expected
56# by tools.run(). Pass it in as part of the 'parents' argument to your own
57# ArgumentParser.
58argparser = argparse.ArgumentParser(add_help=False)
59argparser.add_argument('--auth_host_name', default='localhost',
60 help='Hostname when running a local web server.')
61argparser.add_argument('--noauth_local_webserver', action='store_true',
62 default=False, help='Do not run a local web server.')
63argparser.add_argument('--auth_host_port', default=[8080, 8090], type=int,
64 nargs='*', help='Port web server should listen on.')
65argparser.add_argument('--logging_level', default='ERROR',
66 choices=['DEBUG', 'INFO', 'WARNING', 'ERROR',
67 'CRITICAL'],
68 help='Set the logging level of detail.')
Joe Gregorio9e5fe4d2011-03-10 09:33:47 -050069
70
71class ClientRedirectServer(BaseHTTPServer.HTTPServer):
72 """A server to handle OAuth 2.0 redirects back to localhost.
73
74 Waits for a single request and parses the query parameters
75 into query_params and then stops serving.
76 """
77 query_params = {}
78
79
80class ClientRedirectHandler(BaseHTTPServer.BaseHTTPRequestHandler):
81 """A handler for OAuth 2.0 redirects back to localhost.
82
83 Waits for a single request and parses the query parameters
84 into the servers query_params and then stops serving.
85 """
86
87 def do_GET(s):
Joe Gregorio9da2ad82011-09-11 14:04:44 -040088 """Handle a GET request.
Joe Gregorio9e5fe4d2011-03-10 09:33:47 -050089
90 Parses the query parameters and prints a message
91 if the flow has completed. Note that we can't detect
92 if an error occurred.
93 """
94 s.send_response(200)
95 s.send_header("Content-type", "text/html")
96 s.end_headers()
97 query = s.path.split('?', 1)[-1]
98 query = dict(parse_qsl(query))
99 s.server.query_params = query
100 s.wfile.write("<html><head><title>Authentication Status</title></head>")
101 s.wfile.write("<body><p>The authentication flow has completed.</p>")
102 s.wfile.write("</body></html>")
103
104 def log_message(self, format, *args):
105 """Do not log messages to stdout while running as command line program."""
106 pass
107
108
Joe Gregorio79daca02013-03-29 16:25:52 -0400109@util.positional(3)
Joe Gregorio700f13f2013-07-02 13:47:15 -0400110def run_flow(flow, storage, flags, http=None):
Joe Gregorio695fdc12011-01-16 16:46:55 -0500111 """Core code for a command-line application.
Joe Gregoriofffa7d72011-02-18 17:20:39 -0500112
Joe Gregoriod98b2482012-10-24 08:49:12 -0400113 The run() function is called from your application and runs through all the
114 steps to obtain credentials. It takes a Flow argument and attempts to open an
115 authorization server page in the user's default web browser. The server asks
116 the user to grant your application access to the user's data. If the user
117 grants access, the run() function returns new credentials. The new credentials
118 are also stored in the Storage argument, which updates the file associated
119 with the Storage object.
120
121 It presumes it is run from a command-line application and supports the
122 following flags:
123
Joe Gregorio7a5f3e42012-11-06 10:12:52 -0500124 --auth_host_name: Host name to use when running a local web server
125 to handle redirects during OAuth authorization.
126 (default: 'localhost')
Joe Gregoriod98b2482012-10-24 08:49:12 -0400127
Joe Gregorio7a5f3e42012-11-06 10:12:52 -0500128 --auth_host_port: Port to use when running a local web server to handle
129 redirects during OAuth authorization.;
130 repeat this option to specify a list of values
131 (default: '[8080, 8090]')
132 (an integer)
Joe Gregoriod98b2482012-10-24 08:49:12 -0400133
Joe Gregorio7a5f3e42012-11-06 10:12:52 -0500134 --[no]auth_local_webserver: Run a local web server to handle redirects
Joe Gregoriod98b2482012-10-24 08:49:12 -0400135 during OAuth authorization.
Joe Gregorio7a5f3e42012-11-06 10:12:52 -0500136 (default: 'true')
Joe Gregoriod98b2482012-10-24 08:49:12 -0400137
Joe Gregorio79daca02013-03-29 16:25:52 -0400138 The tools module defines an ArgumentParser the already contains the flag
139 definitions that run() requires. You can pass that ArgumentParser to your
140 ArgumentParser constructor:
141
142 parser = argparse.ArgumentParser(description=__doc__,
143 formatter_class=argparse.RawDescriptionHelpFormatter,
144 parents=[tools.run_parser])
145 flags = parser.parse_args(argv)
Joe Gregoriod98b2482012-10-24 08:49:12 -0400146
Joe Gregoriofffa7d72011-02-18 17:20:39 -0500147 Args:
148 flow: Flow, an OAuth 2.0 Flow to step through.
149 storage: Storage, a Storage to store the credential in.
Joe Gregorio79daca02013-03-29 16:25:52 -0400150 flags: argparse.ArgumentParser, the command-line flags.
Joe Gregorio8e000ed2012-02-07 15:41:44 -0500151 http: An instance of httplib2.Http.request
152 or something that acts like it.
Joe Gregoriofffa7d72011-02-18 17:20:39 -0500153
154 Returns:
155 Credentials, the obtained credential.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500156 """
Joe Gregorio79daca02013-03-29 16:25:52 -0400157 logging.getLogger().setLevel(getattr(logging, flags.logging_level))
158 if not flags.noauth_local_webserver:
Joe Gregorio9e5fe4d2011-03-10 09:33:47 -0500159 success = False
160 port_number = 0
Joe Gregorio79daca02013-03-29 16:25:52 -0400161 for port in flags.auth_host_port:
Joe Gregorio9e5fe4d2011-03-10 09:33:47 -0500162 port_number = port
163 try:
Joe Gregorio79daca02013-03-29 16:25:52 -0400164 httpd = ClientRedirectServer((flags.auth_host_name, port),
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400165 ClientRedirectHandler)
Joe Gregorio9e5fe4d2011-03-10 09:33:47 -0500166 except socket.error, e:
167 pass
168 else:
169 success = True
170 break
Joe Gregorio79daca02013-03-29 16:25:52 -0400171 flags.noauth_local_webserver = not success
Joe Gregorio01c86b12012-06-07 14:31:32 -0400172 if not success:
173 print 'Failed to start a local webserver listening on either port 8080'
174 print 'or port 9090. Please check your firewall settings and locally'
175 print 'running programs that may be blocking or using those ports.'
176 print
177 print 'Falling back to --noauth_local_webserver and continuing with',
178 print 'authorization.'
179 print
Joe Gregorio9e5fe4d2011-03-10 09:33:47 -0500180
Joe Gregorio79daca02013-03-29 16:25:52 -0400181 if not flags.noauth_local_webserver:
182 oauth_callback = 'http://%s:%s/' % (flags.auth_host_name, port_number)
Joe Gregorio9e5fe4d2011-03-10 09:33:47 -0500183 else:
Joe Gregoriofff42f22013-04-05 12:53:46 -0400184 oauth_callback = client.OOB_CALLBACK_URN
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400185 flow.redirect_uri = oauth_callback
186 authorize_url = flow.step1_get_authorize_url()
Joe Gregorio695fdc12011-01-16 16:46:55 -0500187
Joe Gregorio79daca02013-03-29 16:25:52 -0400188 if not flags.noauth_local_webserver:
Joe Gregorio2fdd2952012-01-17 09:03:28 -0500189 webbrowser.open(authorize_url, new=1, autoraise=True)
190 print 'Your browser has been opened to visit:'
191 print
192 print ' ' + authorize_url
193 print
Joe Gregorio8097e2a2011-05-17 11:11:34 -0400194 print 'If your browser is on a different machine then exit and re-run this'
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400195 print 'application with the command-line parameter '
Joe Gregorio2fdd2952012-01-17 09:03:28 -0500196 print
197 print ' --noauth_local_webserver'
198 print
199 else:
200 print 'Go to the following link in your browser:'
201 print
202 print ' ' + authorize_url
Joe Gregorio8097e2a2011-05-17 11:11:34 -0400203 print
204
Joe Gregorio562b7312011-09-15 09:06:38 -0400205 code = None
Joe Gregorio79daca02013-03-29 16:25:52 -0400206 if not flags.noauth_local_webserver:
Joe Gregorio9e5fe4d2011-03-10 09:33:47 -0500207 httpd.handle_request()
208 if 'error' in httpd.query_params:
209 sys.exit('Authentication request was rejected.')
210 if 'code' in httpd.query_params:
211 code = httpd.query_params['code']
Joe Gregorio562b7312011-09-15 09:06:38 -0400212 else:
213 print 'Failed to find "code" in the query parameters of the redirect.'
214 sys.exit('Try running with --noauth_local_webserver.')
Joe Gregorio9e5fe4d2011-03-10 09:33:47 -0500215 else:
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400216 code = raw_input('Enter verification code: ').strip()
Joe Gregorio695fdc12011-01-16 16:46:55 -0500217
Joe Gregoriofffa7d72011-02-18 17:20:39 -0500218 try:
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400219 credential = flow.step2_exchange(code, http=http)
Joe Gregoriofff42f22013-04-05 12:53:46 -0400220 except client.FlowExchangeError, e:
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400221 sys.exit('Authentication has failed: %s' % e)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500222
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400223 storage.put(credential)
224 credential.set_store(storage)
225 print 'Authentication successful.'
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500226
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400227 return credential
Joe Gregorio79daca02013-03-29 16:25:52 -0400228
229
230def message_if_missing(filename):
231 """Helpful message to display if the CLIENT_SECRETS file is missing."""
232
233 return _CLIENT_SECRETS_MESSAGE % filename
Joe Gregorio700f13f2013-07-02 13:47:15 -0400234
235try:
236 from old_run import run
Joe Gregoriob983a632013-07-30 13:20:12 -0400237 from old_run import FLAGS
Joe Gregorio700f13f2013-07-02 13:47:15 -0400238except ImportError:
239 def run(*args, **kwargs):
240 raise NotImplementedError(
241 'The gflags library must be installed to use tools.run(). '
242 'Please install gflags or preferrably switch to using '
243 'tools.run_flow().')