1# Copyright 2014 Google Inc. All rights reserved. 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 17Do 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. 20""" 21 22from __future__ import print_function 23 24import logging 25import socket 26import sys 27 28from six.moves import BaseHTTPServer 29from six.moves import http_client 30from six.moves import input 31from six.moves import urllib 32 33from oauth2client import client 34from oauth2client import util 35 36 37__author__ = 'jcgregorio@google.com (Joe Gregorio)' 38__all__ = ['argparser', 'run_flow', 'message_if_missing'] 39 40_CLIENT_SECRETS_MESSAGE = """WARNING: Please configure OAuth 2.0 41 42To make this sample run you will need to populate the client_secrets.json file 43found at: 44 45 {file_path} 46 47with information from the APIs Console <https://code.google.com/apis/console>. 48 49""" 50 51_FAILED_START_MESSAGE = """ 52Failed to start a local webserver listening on either port 8080 53or port 8090. Please check your firewall settings and locally 54running programs that may be blocking or using those ports. 55 56Falling back to --noauth_local_webserver and continuing with 57authorization. 58""" 59 60_BROWSER_OPENED_MESSAGE = """ 61Your browser has been opened to visit: 62 63 {address} 64 65If your browser is on a different machine then exit and re-run this 66application with the command-line parameter 67 68 --noauth_local_webserver 69""" 70 71_GO_TO_LINK_MESSAGE = """ 72Go to the following link in your browser: 73 74 {address} 75""" 76 77 78def _CreateArgumentParser(): 79 try: 80 import argparse 81 except ImportError: # pragma: NO COVER 82 return None 83 parser = argparse.ArgumentParser(add_help=False) 84 parser.add_argument('--auth_host_name', default='localhost', 85 help='Hostname when running a local web server.') 86 parser.add_argument('--noauth_local_webserver', action='store_true', 87 default=False, help='Do not run a local web server.') 88 parser.add_argument('--auth_host_port', default=[8080, 8090], type=int, 89 nargs='*', help='Port web server should listen on.') 90 parser.add_argument( 91 '--logging_level', default='ERROR', 92 choices=['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'], 93 help='Set the logging level of detail.') 94 return parser 95 96# argparser is an ArgumentParser that contains command-line options expected 97# by tools.run(). Pass it in as part of the 'parents' argument to your own 98# ArgumentParser. 99argparser = _CreateArgumentParser() 100 101 102class ClientRedirectServer(BaseHTTPServer.HTTPServer): 103 """A server to handle OAuth 2.0 redirects back to localhost. 104 105 Waits for a single request and parses the query parameters 106 into query_params and then stops serving. 107 """ 108 query_params = {} 109 110 111class ClientRedirectHandler(BaseHTTPServer.BaseHTTPRequestHandler): 112 """A handler for OAuth 2.0 redirects back to localhost. 113 114 Waits for a single request and parses the query parameters 115 into the servers query_params and then stops serving. 116 """ 117 118 def do_GET(self): 119 """Handle a GET request. 120 121 Parses the query parameters and prints a message 122 if the flow has completed. Note that we can't detect 123 if an error occurred. 124 """ 125 self.send_response(http_client.OK) 126 self.send_header("Content-type", "text/html") 127 self.end_headers() 128 query = self.path.split('?', 1)[-1] 129 query = dict(urllib.parse.parse_qsl(query)) 130 self.server.query_params = query 131 self.wfile.write( 132 b"<html><head><title>Authentication Status</title></head>") 133 self.wfile.write( 134 b"<body><p>The authentication flow has completed.</p>") 135 self.wfile.write(b"</body></html>") 136 137 def log_message(self, format, *args): 138 """Do not log messages to stdout while running as cmd. line program.""" 139 140 141@util.positional(3) 142def run_flow(flow, storage, flags=None, http=None): 143 """Core code for a command-line application. 144 145 The ``run()`` function is called from your application and runs 146 through all the steps to obtain credentials. It takes a ``Flow`` 147 argument and attempts to open an authorization server page in the 148 user's default web browser. The server asks the user to grant your 149 application access to the user's data. If the user grants access, 150 the ``run()`` function returns new credentials. The new credentials 151 are also stored in the ``storage`` argument, which updates the file 152 associated with the ``Storage`` object. 153 154 It presumes it is run from a command-line application and supports the 155 following flags: 156 157 ``--auth_host_name`` (string, default: ``localhost``) 158 Host name to use when running a local web server to handle 159 redirects during OAuth authorization. 160 161 ``--auth_host_port`` (integer, default: ``[8080, 8090]``) 162 Port to use when running a local web server to handle redirects 163 during OAuth authorization. Repeat this option to specify a list 164 of values. 165 166 ``--[no]auth_local_webserver`` (boolean, default: ``True``) 167 Run a local web server to handle redirects during OAuth 168 authorization. 169 170 The tools module defines an ``ArgumentParser`` the already contains the 171 flag definitions that ``run()`` requires. You can pass that 172 ``ArgumentParser`` to your ``ArgumentParser`` constructor:: 173 174 parser = argparse.ArgumentParser( 175 description=__doc__, 176 formatter_class=argparse.RawDescriptionHelpFormatter, 177 parents=[tools.argparser]) 178 flags = parser.parse_args(argv) 179 180 Args: 181 flow: Flow, an OAuth 2.0 Flow to step through. 182 storage: Storage, a ``Storage`` to store the credential in. 183 flags: ``argparse.Namespace``, (Optional) The command-line flags. This 184 is the object returned from calling ``parse_args()`` on 185 ``argparse.ArgumentParser`` as described above. Defaults 186 to ``argparser.parse_args()``. 187 http: An instance of ``httplib2.Http.request`` or something that 188 acts like it. 189 190 Returns: 191 Credentials, the obtained credential. 192 """ 193 if flags is None: 194 flags = argparser.parse_args() 195 logging.getLogger().setLevel(getattr(logging, flags.logging_level)) 196 if not flags.noauth_local_webserver: 197 success = False 198 port_number = 0 199 for port in flags.auth_host_port: 200 port_number = port 201 try: 202 httpd = ClientRedirectServer((flags.auth_host_name, port), 203 ClientRedirectHandler) 204 except socket.error: 205 pass 206 else: 207 success = True 208 break 209 flags.noauth_local_webserver = not success 210 if not success: 211 print(_FAILED_START_MESSAGE) 212 213 if not flags.noauth_local_webserver: 214 oauth_callback = 'http://{host}:{port}/'.format( 215 host=flags.auth_host_name, port=port_number) 216 else: 217 oauth_callback = client.OOB_CALLBACK_URN 218 flow.redirect_uri = oauth_callback 219 authorize_url = flow.step1_get_authorize_url() 220 221 if not flags.noauth_local_webserver: 222 import webbrowser 223 webbrowser.open(authorize_url, new=1, autoraise=True) 224 print(_BROWSER_OPENED_MESSAGE.format(address=authorize_url)) 225 else: 226 print(_GO_TO_LINK_MESSAGE.format(address=authorize_url)) 227 228 code = None 229 if not flags.noauth_local_webserver: 230 httpd.handle_request() 231 if 'error' in httpd.query_params: 232 sys.exit('Authentication request was rejected.') 233 if 'code' in httpd.query_params: 234 code = httpd.query_params['code'] 235 else: 236 print('Failed to find "code" in the query parameters ' 237 'of the redirect.') 238 sys.exit('Try running with --noauth_local_webserver.') 239 else: 240 code = input('Enter verification code: ').strip() 241 242 try: 243 credential = flow.step2_exchange(code, http=http) 244 except client.FlowExchangeError as e: 245 sys.exit('Authentication has failed: {0}'.format(e)) 246 247 storage.put(credential) 248 credential.set_store(storage) 249 print('Authentication successful.') 250 251 return credential 252 253 254def message_if_missing(filename): 255 """Helpful message to display if the CLIENT_SECRETS file is missing.""" 256 return _CLIENT_SECRETS_MESSAGE.format(file_path=filename) 257