Path: csiph.com!usenet.pasdenom.info!news.albasani.net!newsfeed.freenet.ag!news2.euro.net!newsgate.cistron.nl!newsgate.news.xs4all.nl!post.news.xs4all.nl!not-for-mail Return-Path: X-Original-To: python-list@python.org Delivered-To: python-list@mail.python.org X-Spam-Status: OK 0.000 X-Spam-Evidence: '*H*': 1.00; '*S*': 0.00; 'python,': 0.02; 'else:': 0.03; 'skip:[ 20': 0.03; 'python3': 0.05; "'',": 0.07; '(especially': 0.07; 'json': 0.07; 'params': 0.07; 'redirected': 0.07; 'variables.': 0.07; 'works.': 0.07; 'api': 0.09; 'python': 0.09; "'w')": 0.09; '*is*': 0.09; 'app,': 0.09; 'os.path': 0.09; 'page)': 0.09; 'received:mail-vc0-f174.google.com': 0.09; 'refresh': 0.09; 'sep': 0.09; 'solution,': 0.09; 'stable.': 0.09; 'url:calendar': 0.09; 'url:github': 0.09; 'cc:addr:python-list': 0.10; 'def': 0.10; 'gui': 0.11; 'files.': 0.13; 'library': 0.15; '"...': 0.16; '(key,': 0.16; '(via': 0.16; '*never*': 0.16; 'at,': 0.16; 'awesome.': 0.16; 'calendar': 0.16; 'decent': 0.16; 'example).': 0.16; 'executable.': 0.16; 'filename):': 0.16; 'javascript)': 0.16; 'parameters,': 0.16; 'query,': 0.16; 'storing': 0.16; 'subject:api': 0.16; 'tokens:': 0.16; 'url.': 0.16; 'urllib.parse': 0.16; 'verbose': 0.16; 'webpage,': 0.16; 'wed,': 0.16; 'wrote:': 0.17; 'fix': 0.17; 'config': 0.17; 'mechanism': 0.17; 'specify': 0.17; 'url:accounts': 0.17; 'examples': 0.18; 'code,': 0.18; 'input': 0.18; 'requests': 0.18; 'sender:addr:gmail.com': 0.18; 'app': 0.19; 'module': 0.19; 'code.': 0.20; 'trying': 0.21; 'bit': 0.21; 'import': 0.21; 'either.': 0.22; 'embedding': 0.22; 'parse': 0.22; 'title,': 0.22; 'skip:_ 20': 0.22; "i'd": 0.22; 'cc:2**0': 0.23; 'specified': 0.23; 'this:': 0.23; 'to:2**1': 0.23; 'cc:no real name:2**0': 0.24; 'cc:addr:python.org': 0.25; 'header:In-Reply-To:1': 0.25; 'looks': 0.26; '(which': 0.26; 'am,': 0.27; 'skip:# 10': 0.27; 'important.': 0.27; 'message-id:@mail.gmail.com': 0.27; "doesn't": 0.28; 'skip:( 20': 0.28; 'asks': 0.29; 'received:209.85.220.174': 0.29; 'talked': 0.29; 'things,': 0.29; 'though.': 0.29; 'value)': 0.29; 'no,': 0.29; 'skip:_ 10': 0.29; "i'm": 0.29; "skip:' 10": 0.30; '(from': 0.30; 'query': 0.30; 'window': 0.30; 'function': 0.30; 'stuff': 0.30; 'code': 0.31; 'problem.': 0.32; 'file': 0.32; "skip:' 20": 0.32; 'could': 0.32; 'anywhere': 0.33; 'page.': 0.33; 'docs': 0.33; 'skip:j 20': 0.33; 'skip:~ 10': 0.33; 'another': 0.33; 'changed': 0.34; 'received:google.com': 0.34; 'text': 0.34; 'server': 0.35; 'whatever': 0.35; '2.0': 0.35; 'returning': 0.35; 'pm,': 0.35; 'received:209.85.220': 0.35; 'received:209.85': 0.35; 'something': 0.35; 'there': 0.35; 'really': 0.36; 'skip:u 20': 0.36; '26,': 0.65; 'harder': 0.65; 'sound': 0.65; 'webpage': 0.65; 'quality': 0.69; 'lack': 0.71; 'now:': 0.71; 'secret': 0.71; 'presented': 0.72; 'url:o': 0.83; "'state'": 0.84; 'configparser': 0.84; 'drm': 0.84; 'expiry': 0.84; 'messed': 0.84; 'payload': 0.84; 'publicly.': 0.84; 'text-based': 0.84; 'url:auth': 0.84; 'choose.': 0.91 DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20120113; h=mime-version:sender:in-reply-to:references:from:date :x-google-sender-auth:message-id:subject:to:cc:content-type; bh=P4wSea7Ff6tKzKgZF6alNo03EspKaq5yt1VLjFsoNp4=; b=A1qX1fcYr1ICWTjn7hWCwOvzr8NBx5ZLtXEfG80b6N+ajS1nbVtV1lzZYuJ/AcgJfu /Dfi36MGhqdEVYDNux86pBIaKHkhE24QbwKj+3iSJLtqHPkTRKC+WgcfeBz1lgOBJR3Z 8BC8/cw6J6wvwymn1j6T9F9uftHi0vN2YPs6+L6aw/zH6LLjCEjgxX6NXzLE2hxMucEa J7Wb6gtyzmhTw9PRZc237kV0ErUcgmxk2mxxZRYd5BqY56eP8wXbPTHpb8HpUI//ZD2R rQri5F0S5MIcwC+qVvmpKeaKySgjuD5u+l/qhfqvYNXyN6KDX2WRYABeG8zt3UBmWcnC 13wg== MIME-Version: 1.0 Sender: kushal.kumaran@gmail.com In-Reply-To: <50621279.8060700@tysdomain.com> References: <5062003A.9020208@tysdomain.com> <50621279.8060700@tysdomain.com> From: Kushal Kumaran Date: Wed, 26 Sep 2012 10:54:23 +0530 X-Google-Sender-Auth: pcZ3j1vytjWvGK27E05pn0SDpZc Subject: Re: google api and oauth2 To: "Littlefield, Tyler" Content-Type: text/plain; charset=UTF-8 Cc: python-list@python.org X-BeenThere: python-list@python.org X-Mailman-Version: 2.1.15 Precedence: list List-Id: General discussion list for the Python programming language List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Newsgroups: comp.lang.python Message-ID: Lines: 175 NNTP-Posting-Host: 2001:888:2000:d::a6 X-Trace: 1348637504 news.xs4all.nl 6858 [2001:888:2000:d::a6]:58340 X-Complaints-To: abuse@xs4all.nl Xref: csiph.com comp.lang.python:30143 (making no attempt to fix messed up quoting, please take a look at your mail client configuration) On Wed, Sep 26, 2012 at 1:52 AM, Littlefield, Tyler wrote: > On 9/25/2012 2:05 PM, Demian Brecht wrote: > > This is a shameless plug, but if you want a much easier to understand method > of accessing protected resources via OAuth2, I have a 55 LOC client > implementation with docs and examples here: > https://github.com/demianbrecht/sanction (Google is one of the tested > providers with an access example). > > > No complaints from me if it works. Honestly I was a bit discouraged at > Google's decent lack of documentation and the quality of the code. > If you are writing a desktop application, read this: https://developers.google.com/accounts/docs/OAuth2#clientside > > Are you trying to access resources client side (through Javascript) or > server side? Either way, the redirect URI *is* important. The first step is > to have your user authorize your application using Google's authorization > page. As one of the query parameters, you must specify the redirect URI > (which must match those registered through Google's app console). > > I'm trying to access it through a desktop Python application, which made me > really confused. There was something else that talked about returning the > tokens in a different way, but it talked about returning them in the title > of the webpage, and since I'd be spawning a browser to request > authorization, I'd have to write something that would pull the window > information and then parse out the token from the title, which doesn't sound > to stable. > After authenticating with google from a web browser, and authorizing your application, the user will be presented with a web page from which they can copy the access token (from the url or a text field presented in the web page) and input into your application using whatever mechanism you choose. If you are a simple text-based app, you can just use input() to ask for the token. If you are a GUI app, you can do other things, such as embedding a web browser into your application and reading the redirected URL. > > Once the user has authorized your application, they're redirected back to > your site (via the specified redirect URI), with a "code" attached as a > query param. Once you get that code, you must exchange that with Google's > token endpoint to retrieve the access and refresh tokens. > > Awesome. I could theoretically just create a webpage on my server to > redirect people to with the query, but I'm still not quite sure how I'd > retrieve that from the desktop application. > > > No, it doesn't matter which library you use. Google's (imho) is overly > verbose and difficult to grok (especially for someone new to either OAuth > 2.0 or Python, or both). The client ID doesn't need to be kept private, but > the secret does. You should *never* put this anywhere that can be read > publicly. > > I plan on storing them both in variables. It's not going to be the best > solution, but I plan to use python -O to create pyo files, which from what I > understand are harder to decompile, and it'll be in a py2exe executable. > Still not to hard to get at, but it's not right there either. > Don't worry about it. There is no way to keep it secret for desktop applications. It is exactly the same as the DRM problem. The google documentation itself admits: "... These applications, in general, cannot keep secrets.". You should store the given tokens persistently, though. > In the past, I made a half-hearted attempt to writing something to upload stuff to google calendar from .ics files. Here's the google-authentication part of the code, if you can use it. I cannot, at the moment, recall why I did not use the oauth2client library from google. #!/usr/bin/env python3 import datetime import json import os.path import configparser import requests import webbrowser import urllib.parse def _store_tokens(auth_data, filename): expiry_time = (datetime.datetime.now() + datetime.timedelta(seconds=int(auth_data['expires_in']))) auth_data['expiry_time'] = expiry_time.isoformat() with open(filename, 'w') as file_stream: json.dump(auth_data, file_stream) def _get_new_token(config, token_file): payload = { 'response_type' : 'code', 'client_id' : config['api']['client_id'], 'redirect_uri' : config['api']['redirect_uri'], 'scope' : 'https://www.googleapis.com/auth/calendar', 'state' : 'init' } url = 'https://accounts.google.com/o/oauth2/auth' params = ['{}={}'.format(key, urllib.parse.quote(value)) for (key, value) in payload.items()] webbrowser.open('{}?{}'.format(url, '&'.join(params)), new=2) auth_code = input('Enter authorization code obtained: ') payload = { 'code' : auth_code, 'client_id' : config['api']['client_id'], 'client_secret' : config['api']['client_secret'], 'redirect_uri' : config['api']['redirect_uri'], 'grant_type' : 'authorization_code' } r = requests.post('https://accounts.google.com/o/oauth2/token', data=payload) auth_data = json.loads(r.text) _store_tokens(auth_data, token_file) return auth_data['access_token'] def _refreshed_token(config, auth_data, token_file): payload = { 'refresh_token' : auth_data['refresh_token'], 'client_id' : config['api']['client_id'], 'client_secret' : config['api']['client_secret'], 'grant_type' : 'refresh_token' } r = requests.post('https://accounts.google.com/o/oauth2/token', data=payload) auth_data = json.loads(r.text) _store_tokens(auth_data, token_file) return auth_data['access_token'] def _get_existing_token(config, token_file): with open(token_file) as token_stream: auth_data = json.load(token_stream) now = datetime.datetime.now().isoformat() if auth_data['expiry_time'] < now: return _refreshed_token(config, auth_data, token_file) return auth_data['access_token'] def auth(config): token_file = os.path.expanduser(config['api']['token_file']) if os.path.exists(token_file): return _get_existing_token(config, token_file) else: return _get_new_token(config, token_file) The "config" object required by the auth function is from a config file that looks like this: [api] client_id = client_secret = redirect_uri = urn:ietf:wg:oauth:2.0:oob token_file = ~/.google-tokens The "scope" value in the _get_new_token function will need to be changed to whatever scope your application needs. This code uses the python requests module for http(s) requests. It will start up a web browser to let the user authenticate and then asks for the token. The google api will issue two tokens: an "access" token and a "refresh" token. The access token will have a short expiry time, after which you need to get another access token issued. You use the refresh token for that reissue request. -- regards, kushal