blob: 839f29692a21ed7c88e0a19f10958603076bd410 [file] [log] [blame]
Joe Gregorio695fdc12011-01-16 16:46:55 -05001# Copyright (C) 2010 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 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 Gregorio7c22ab22011-02-16 15:32:39 -050023__all__ = ['run']
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
28import logging
29import os
Joe Gregorio9e5fe4d2011-03-10 09:33:47 -050030import socket
31import sys
Joe Gregorio2fdd2952012-01-17 09:03:28 -050032import webbrowser
Joe Gregorio9e5fe4d2011-03-10 09:33:47 -050033
Joe Gregorio68a8cfe2012-08-03 16:17:40 -040034from oauth2client.client import FlowExchangeError
35from oauth2client.client import OOB_CALLBACK_URN
36from oauth2client import util
Joe Gregorio9e5fe4d2011-03-10 09:33:47 -050037
38try:
Joe Gregorio9da2ad82011-09-11 14:04:44 -040039 from urlparse import parse_qsl
Joe Gregorio9e5fe4d2011-03-10 09:33:47 -050040except ImportError:
Joe Gregorio9da2ad82011-09-11 14:04:44 -040041 from cgi import parse_qsl
Joe Gregorio9e5fe4d2011-03-10 09:33:47 -050042
Joe Gregorio79daca02013-03-29 16:25:52 -040043_CLIENT_SECRETS_MESSAGE = """WARNING: Please configure OAuth 2.0
Joe Gregorio9e5fe4d2011-03-10 09:33:47 -050044
Joe Gregorio79daca02013-03-29 16:25:52 -040045To make this sample run you will need to populate the client_secrets.json file
46found at:
Joe Gregorio9e5fe4d2011-03-10 09:33:47 -050047
Joe Gregorio79daca02013-03-29 16:25:52 -040048 %s
Joe Gregorio9e5fe4d2011-03-10 09:33:47 -050049
Joe Gregorio79daca02013-03-29 16:25:52 -040050with information from the APIs Console <https://code.google.com/apis/console>.
Joe Gregorio9e5fe4d2011-03-10 09:33:47 -050051
Joe Gregorio79daca02013-03-29 16:25:52 -040052"""
53
54# run_parser is an ArgumentParser that contains command-line options expected
55# by tools.run(). Pass it in as part of the 'parents' argument to your own
56# ArgumentParser.
57argparser = argparse.ArgumentParser(add_help=False)
58argparser.add_argument('--auth_host_name', default='localhost',
59 help='Hostname when running a local web server.')
60argparser.add_argument('--noauth_local_webserver', action='store_true',
61 default=False, help='Do not run a local web server.')
62argparser.add_argument('--auth_host_port', default=[8080, 8090], type=int,
63 nargs='*', help='Port web server should listen on.')
64argparser.add_argument('--logging_level', default='ERROR',
65 choices=['DEBUG', 'INFO', 'WARNING', 'ERROR',
66 'CRITICAL'],
67 help='Set the logging level of detail.')
Joe Gregorio9e5fe4d2011-03-10 09:33:47 -050068
69
70class ClientRedirectServer(BaseHTTPServer.HTTPServer):
71 """A server to handle OAuth 2.0 redirects back to localhost.
72
73 Waits for a single request and parses the query parameters
74 into query_params and then stops serving.
75 """
76 query_params = {}
77
78
79class ClientRedirectHandler(BaseHTTPServer.BaseHTTPRequestHandler):
80 """A handler for OAuth 2.0 redirects back to localhost.
81
82 Waits for a single request and parses the query parameters
83 into the servers query_params and then stops serving.
84 """
85
86 def do_GET(s):
Joe Gregorio9da2ad82011-09-11 14:04:44 -040087 """Handle a GET request.
Joe Gregorio9e5fe4d2011-03-10 09:33:47 -050088
89 Parses the query parameters and prints a message
90 if the flow has completed. Note that we can't detect
91 if an error occurred.
92 """
93 s.send_response(200)
94 s.send_header("Content-type", "text/html")
95 s.end_headers()
96 query = s.path.split('?', 1)[-1]
97 query = dict(parse_qsl(query))
98 s.server.query_params = query
99 s.wfile.write("<html><head><title>Authentication Status</title></head>")
100 s.wfile.write("<body><p>The authentication flow has completed.</p>")
101 s.wfile.write("</body></html>")
102
103 def log_message(self, format, *args):
104 """Do not log messages to stdout while running as command line program."""
105 pass
106
107
Joe Gregorio79daca02013-03-29 16:25:52 -0400108@util.positional(3)
109def run(flow, storage, flags, http=None):
Joe Gregorio695fdc12011-01-16 16:46:55 -0500110 """Core code for a command-line application.
Joe Gregoriofffa7d72011-02-18 17:20:39 -0500111
Joe Gregoriod98b2482012-10-24 08:49:12 -0400112 The run() function is called from your application and runs through all the
113 steps to obtain credentials. It takes a Flow argument and attempts to open an
114 authorization server page in the user's default web browser. The server asks
115 the user to grant your application access to the user's data. If the user
116 grants access, the run() function returns new credentials. The new credentials
117 are also stored in the Storage argument, which updates the file associated
118 with the Storage object.
119
120 It presumes it is run from a command-line application and supports the
121 following flags:
122
Joe Gregorio7a5f3e42012-11-06 10:12:52 -0500123 --auth_host_name: Host name to use when running a local web server
124 to handle redirects during OAuth authorization.
125 (default: 'localhost')
Joe Gregoriod98b2482012-10-24 08:49:12 -0400126
Joe Gregorio7a5f3e42012-11-06 10:12:52 -0500127 --auth_host_port: Port to use when running a local web server to handle
128 redirects during OAuth authorization.;
129 repeat this option to specify a list of values
130 (default: '[8080, 8090]')
131 (an integer)
Joe Gregoriod98b2482012-10-24 08:49:12 -0400132
Joe Gregorio7a5f3e42012-11-06 10:12:52 -0500133 --[no]auth_local_webserver: Run a local web server to handle redirects
Joe Gregoriod98b2482012-10-24 08:49:12 -0400134 during OAuth authorization.
Joe Gregorio7a5f3e42012-11-06 10:12:52 -0500135 (default: 'true')
Joe Gregoriod98b2482012-10-24 08:49:12 -0400136
Joe Gregorio79daca02013-03-29 16:25:52 -0400137 The tools module defines an ArgumentParser the already contains the flag
138 definitions that run() requires. You can pass that ArgumentParser to your
139 ArgumentParser constructor:
140
141 parser = argparse.ArgumentParser(description=__doc__,
142 formatter_class=argparse.RawDescriptionHelpFormatter,
143 parents=[tools.run_parser])
144 flags = parser.parse_args(argv)
Joe Gregoriod98b2482012-10-24 08:49:12 -0400145
Joe Gregoriofffa7d72011-02-18 17:20:39 -0500146 Args:
147 flow: Flow, an OAuth 2.0 Flow to step through.
148 storage: Storage, a Storage to store the credential in.
Joe Gregorio79daca02013-03-29 16:25:52 -0400149 flags: argparse.ArgumentParser, the command-line flags.
Joe Gregorio8e000ed2012-02-07 15:41:44 -0500150 http: An instance of httplib2.Http.request
151 or something that acts like it.
Joe Gregoriofffa7d72011-02-18 17:20:39 -0500152
153 Returns:
154 Credentials, the obtained credential.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500155 """
Joe Gregorio79daca02013-03-29 16:25:52 -0400156 logging.getLogger().setLevel(getattr(logging, flags.logging_level))
157 if not flags.noauth_local_webserver:
Joe Gregorio9e5fe4d2011-03-10 09:33:47 -0500158 success = False
159 port_number = 0
Joe Gregorio79daca02013-03-29 16:25:52 -0400160 for port in flags.auth_host_port:
Joe Gregorio9e5fe4d2011-03-10 09:33:47 -0500161 port_number = port
162 try:
Joe Gregorio79daca02013-03-29 16:25:52 -0400163 httpd = ClientRedirectServer((flags.auth_host_name, port),
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400164 ClientRedirectHandler)
Joe Gregorio9e5fe4d2011-03-10 09:33:47 -0500165 except socket.error, e:
166 pass
167 else:
168 success = True
169 break
Joe Gregorio79daca02013-03-29 16:25:52 -0400170 flags.noauth_local_webserver = not success
Joe Gregorio01c86b12012-06-07 14:31:32 -0400171 if not success:
172 print 'Failed to start a local webserver listening on either port 8080'
173 print 'or port 9090. Please check your firewall settings and locally'
174 print 'running programs that may be blocking or using those ports.'
175 print
176 print 'Falling back to --noauth_local_webserver and continuing with',
177 print 'authorization.'
178 print
Joe Gregorio9e5fe4d2011-03-10 09:33:47 -0500179
Joe Gregorio79daca02013-03-29 16:25:52 -0400180 if not flags.noauth_local_webserver:
181 oauth_callback = 'http://%s:%s/' % (flags.auth_host_name, port_number)
Joe Gregorio9e5fe4d2011-03-10 09:33:47 -0500182 else:
Joe Gregoriof2326c02012-02-09 12:18:44 -0500183 oauth_callback = OOB_CALLBACK_URN
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400184 flow.redirect_uri = oauth_callback
185 authorize_url = flow.step1_get_authorize_url()
Joe Gregorio695fdc12011-01-16 16:46:55 -0500186
Joe Gregorio79daca02013-03-29 16:25:52 -0400187 if not flags.noauth_local_webserver:
Joe Gregorio2fdd2952012-01-17 09:03:28 -0500188 webbrowser.open(authorize_url, new=1, autoraise=True)
189 print 'Your browser has been opened to visit:'
190 print
191 print ' ' + authorize_url
192 print
Joe Gregorio8097e2a2011-05-17 11:11:34 -0400193 print 'If your browser is on a different machine then exit and re-run this'
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400194 print 'application with the command-line parameter '
Joe Gregorio2fdd2952012-01-17 09:03:28 -0500195 print
196 print ' --noauth_local_webserver'
197 print
198 else:
199 print 'Go to the following link in your browser:'
200 print
201 print ' ' + authorize_url
Joe Gregorio8097e2a2011-05-17 11:11:34 -0400202 print
203
Joe Gregorio562b7312011-09-15 09:06:38 -0400204 code = None
Joe Gregorio79daca02013-03-29 16:25:52 -0400205 if not flags.noauth_local_webserver:
Joe Gregorio9e5fe4d2011-03-10 09:33:47 -0500206 httpd.handle_request()
207 if 'error' in httpd.query_params:
208 sys.exit('Authentication request was rejected.')
209 if 'code' in httpd.query_params:
210 code = httpd.query_params['code']
Joe Gregorio562b7312011-09-15 09:06:38 -0400211 else:
212 print 'Failed to find "code" in the query parameters of the redirect.'
213 sys.exit('Try running with --noauth_local_webserver.')
Joe Gregorio9e5fe4d2011-03-10 09:33:47 -0500214 else:
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400215 code = raw_input('Enter verification code: ').strip()
Joe Gregorio695fdc12011-01-16 16:46:55 -0500216
Joe Gregoriofffa7d72011-02-18 17:20:39 -0500217 try:
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400218 credential = flow.step2_exchange(code, http=http)
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400219 except FlowExchangeError, e:
220 sys.exit('Authentication has failed: %s' % e)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500221
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400222 storage.put(credential)
223 credential.set_store(storage)
224 print 'Authentication successful.'
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500225
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400226 return credential
Joe Gregorio79daca02013-03-29 16:25:52 -0400227
228
229def message_if_missing(filename):
230 """Helpful message to display if the CLIENT_SECRETS file is missing."""
231
232 return _CLIENT_SECRETS_MESSAGE % filename