Google App Engine 上で認証が必要な Google Data Feeds を取得する方法

Jeff Scudder, Google Developer Programs
April 17, 2008
翻訳 Takashi Matsuo <tmatsuo@shehas.net> 2008年6月5日, 2008年7月3日更新
元の文書が更新されています。この翻訳は少し古いです。

はじめに

みなさん Google App Engine をどのように使おうか、ワクワクしながら考えをめぐらせている事と思います。中には私たちの Google Data AtomPub API を使って Google の他サービスと連携するようなアプリケーションを作ろうとしている方もいるでしょう。Google のサービスの一部は GData API を準備しており(面白そうな例として YouTube や Google Calendar、Blogger などがあります。完全なリストはここにあります。)、その API を使う事によりユーザ固有のデータを読み出したり編集したりできるのです。

この記事では、特定ユーザの情報を要求したり取得する過程を一通り体験するのに、Google Documents List Data API を使います。Google Accounts の認証には Users API を、アプリケーションのページを生成するために Google App Engine の webapp フレームワークを使います。

Google's AuthSub APIs

Google Data サービスの中にはデータを読み出すのにユーザの許可が要るものがあり、また全ての Google Data サービスはデータの書き込み前にユーザの許可が必要です。Google は ユーザがあなたのアプリケーションに対してサービスにアクセスする許可を与えるために AuthSub を使っています。

AuthSub を使うと、ユーザはまず google.com の安全な(訳注: https 接続の)ページで自分のパスワードを入力し、次にあなたのアプリケーションにリダイレクトされます。あなたのアプリケーションは、ユーザがアカウントマネージメントで許可を取り消すまでの間、サービスにアクセスできるトークンを受け取ります。

この記事では、ユーザのログイン用リンクを準備する方法、複数のリクエストで利用できるセッショントークンを取得する方法、またデータストアにトークンを格納し、ユーザが一度サイトを離れてもトークンを再利用できるようにする方法を学びます。

gdata-python-client ライブラリの使用

Google は GData API からデータを取得したりトークンを管理するのを楽にしてくれる GData Python client ライブラリを提供しています。最近リリースしたバージョンでは Google App Engine のアプリケーションから利用できるようになりました。この記事ではこのライブラリを使用しますが、もちろんあなたのアプリケーションに適した別のものを使っても構いません。gdata-python-client ライブラリをダウンロードしてください

このライブラリを Google App Engine のアプリケーションで使用するには、単純にライブラリのソースファイルをアプリケーションのディレクトリに置き、いつものようにインポートしてください。アップロードする必要があるのは src/gdatasrc/atom です。そして、gdata.service.http_request_handlergdata.urlfetch を必ず指定してください。これだけです!

Step 1: トークンをリクエストするためのリンクを作成する

保護された Google Data feeds にアクセスする許可をユーザから得るために、アプリケーションは AuthSub という API を使います。やり方はかなり簡単です - 保護された Google Data feeds へのアクセスをユーザにリクエストする時には、あなたのアプリケーションはまずユーザを google.com 上のセキュアなページにリダイレクトさせ、そこでユーザはサインインし、アクセスを許可したり拒否したりできます。その後、ユーザは、新しく取得したトークンが URL に埋め込まれた形で、あなたのアプリケーションにリダイレクトされて戻って来ます。

あなたのアプリケーションは AuthSub を使う時に、二つの事を指定する必要があります: アクセスしたいフィードのためのベース URL と、ユーザが(訳注: google.com のセキュアページで)許可を与えた後に戻ってくる事になる、あなたのアプリケーション内の リダイレクト URL です。

トークンをリクエストするためのリンクを作成するために、Google Data client ライブラリに含まれる gdata.service モジュールを使いましょう。このモジュールには、ベースのフィード URL と戻りアドレスから、自動的に正しい URL を生成してくれる GenerateAuthSubURL メソッドがあります。下記のコード中では、ユーザの Blogger GData フィードへのアクセスをリクエストする URL を生成するために、このメソッドを使用しています。

app.yaml ファイルでは、それぞれのステップ毎に違う URL を使うような URL マッピングを定義しましょう。下記のようになります:

application: gdata-feedfetcher
version: 1
runtime: python
api_version: 1

handlers:
- url: /step1.*
  script: step1.py

- url: /step2.*
  script: step2.py

...

アプリで AuthSub を使うための最初のステップを説明するために、下記のような step1.py を作成しましょう。

# Change the value of HOST_NAME to the name given to point to your app.
HOST_NAME
= 'gdata-feedfetcher.appspot.com'


import wsgiref.handlers
from google.appengine.ext import webapp
import gdata.service


class Fetcher(webapp.RequestHandler):
 
def get(self):
     
# Instantiate an instance of the GDataService()
     client
= gdata.service.GDataService()

     authsub_next_url
= ('http://%s/step1'
         
'?token_scope=http://docs.google.com/feeds/' % HOST_NAME)

     
# Generate the AuthSub URL and write a page that includes the link
     self
.response.out.write("""<html><body>
         <a href="
%s">Request token for the Google Documents Scope</a>
         </body></html>"""
% client.GenerateAuthSubURL(authsub_next_url,
             
'http://docs.google.com/feeds/', secure=False, session=True))


def main():
  application
= webapp.WSGIApplication([('/.*', Fetcher),], debug=True)
  wsgiref
.handlers.CGIHandler().run(application)


if __name__ == '__main__':
  main
()

この例で、GenerateAuthSubURL に渡されるひとつ目の URL はユーザを我々のアプリケーションに戻すためのもので、ふたつ目は我々のアプリがどのサービスへのアクセス許可を求めているのかを表す Google Documents List feed のベース URL です。このリンクをクリックして、アプリケーションへ許可を与えると、ブラウザはリダイレクトされ、一度しか使えない AuthSub トークンが URL パラメータとして appspot URL の内部に埋め込まれた形で、元のページに戻ってきます。

Step 2: トークンの取得・アップデート

特定の Google Data サービスへの承認リクエスト URL を作った後、問題のフィードにアクセスするために、我々のアプリに渡ってくるトークンを使用する方法が必要になります。今度は、Google Documents List API から渡ってきた初期トークンを取得して、そのトークンを永続的なセッショントークンにアップグレードする必要があります。Google Documents List API サービスに対してユーザを URL 'http://gdata-feedfetcher.appspot.com/?token_scope=http://docs.google.com/feeds/' にリダイレクトさせるように伝えた事を思い出してください。さあ上記の単純なコード例を拡張して、いくつかのタスクを追加しましょう。

ユーザがサインインした Google Data サービス からの戻りリクエストを処理するための機能を書いていきましょう。Google Data サービスからは下記のような URL が渡ってきます:

http://gdata-feedfetcher.appspot.com/?token_scope=http://docs.google.com/feeds/&token=CKF50YzIHxCT85KMAg

これは我々の戻り URL に、アプリからユーザデータへのアクセスを許可してくれるサービス用の初期トークンが付加されただけのものです。下記のコードはまず、この URL からサービス名とトークンを切り出します。次に document list サービス用のトークンをアップグレードするようにリクエストします。

これを達成するのにふたつのメソッドを使っています。まず ManageAuth() メソッドがユーザが我々のアプリケーションにサインインしているかどうか、また document list サービスからのトークンを受け取っているかどうかを調べて、UpgradeToken メソッドを呼び出します。

UpgradeToken は Google のトークン管理サービスを呼び出し、寿命が長いセッショントークンを取得します。これはクライアントライブラリの組み込み関数である、UpgradeToSessionToken を使って行われます。

下記はセッショントークンへのアップグレードする方法を示す step2.py のコードです。

注意:Google App Engine では、外部の URL にアクセスするのに URLFetch API を使用しなければなりません。我々の Google Data Python client library では、gdata.service はデフォルトでは URLFetch API を使いません。これを指定するために、gdata.urlfetch をインポートしておき、次のように書く必要があります: gdata.service.http_request_handler = gdata.urlfetch

import wsgiref.handlers
import urllib
from google.appengine.ext import webapp
from google.appengine.api import users
import gdata.service
import gdata.urlfetch

gdata
.service.http_request_handler = gdata.urlfetch

# Change the value of HOST_NAME to the name given to point to your app.
HOST_NAME
= 'gdata-feedfetcher.appspot.com'

class Fetcher(webapp.RequestHandler):

 
# Initialize some global variables we will use
 
def __init__(self):
   
# Stores the page's current user
    self
.current_user = None
   
# Stores the token_scope information
    self
.token_scope = None
   
# Stores the Google Data Client
    self
.client = None
   
# The one time use token value from the URL after the AuthSub redirect.
    self
.token = None

 
def get(self):
   
# Write our pages title
    self
.response.out.write("""<html><head><title>
        Google Data Feed Fetcher: read Google Data API Atom feeds</title>"""
)
   
# Get the current user
    self
.current_user = users.GetCurrentUser()

    self
.response.out.write('<body>')
   
# Allow the user to sign in or sign out
   
if self.current_user:
      self
.response.out.write('<a href="%s">Sign Out</a><br>' % (
          users
.CreateLogoutURL(self.request.uri)))
   
else:
      self
.response.out.write('<a href="%s">Sign In</a><br>' % (
          users
.CreateLoginURL(self.request.uri)))

   
for param in self.request.query.split('&'):
     
# Get the token scope variable we specified when generating the URL
     
if param.startswith('token_scope'):
        self
.token_scope = urllib.unquote_plus(param.split('=')[1])
     
# Google Data will return a token, get that
     
elif param.startswith('token'):
        self
.token = param.split('=')[1]

   
# Manage our Authentication for the user
    self
.ManageAuth()

    self
.response.out.write('<div id="main">')

    self
.response.out.write(
       
'<div id="sidebar"><div id="scopes"><h4>Request a token</h4><ul>')
    self
.response.out.write('<li><a href="%s">Google Documents</a></li>' % (
        self
.client.GenerateAuthSubURL(
           
'http://%s/step2?token_scope=http://docs.google.com/feeds/' % (
                HOST_NAME
),
           
'http://docs.google.com/feeds/', secure=False, session=True)))
    self
.response.out.write('</ul></div><br/><div id="tokens">')

 
def ManageAuth(self):
    self
.client = gdata.service.GDataService()
   
if self.token and self.current_user:
     
# Upgrade to a session token and store the session token.
      self
.UpgradeToken()

 
def UpgradeToken(self):
    self
.client.SetAuthSubToken(self.token)
   
# Sets the session token in self.client.auth_token
    self
.client.UpgradeToSessionToken()

def main():
  application
= webapp.WSGIApplication([('/.*', Fetcher),], debug=True)
  wsgiref
.handlers.CGIHandler().run(application)

if __name__ == '__main__':
  main
()

gdata.serviceUpgradeToSessionToken() メソッドを使用して初期トークンをアップグレードする際に、gdata.service クライアントの変数 auth_token にセッショントークンの値がセットされます。次の章では、実際にユーザのフィードを取得するのに使用するため、このトークンを保存する方法を案内します。

Step 3: セッショントークンを保存する

セッショントークンを保存しない場合、ユーザは我々のアプリケーションを使用するその度ごとに AuthSub のリダイレクトを使った承認を行わなければなりません。承認プロセスを毎回行うのを回避するためには、Google App Engine Datastore にセッショントークンを保存する必要があります。将来リクエストを成功させるために保存しなければならない情報は、ユーザの email 、セッショントークン、それから、ターゲット URL という形で保存する、トークンが効果を持つサービススコープです。

この目的のために、ベースモデル db.Model を拡張してデータモデルを書きます。

from google.appengine.ext import db

class StoredToken(db.Model):
  user_email
= db.StringProperty(required=True)
  session_token
= db.StringProperty(required=True)
  target_url
= db.StringProperty(required=True)

次にUpgradeToken を書き換えて、セッショントークンを保存するようにしましょう。この関数は UpgradeAndStoreToken と名前を変えます。

  def UpgradeAndStoreToken(self):
    self
.client.SetAuthSubToken(self.token)
    self
.client.UpgradeToSessionToken()
   
if self.current_user:
     
# Create a new token object for the data store which associates the
     
# session token with the requested URL and the current user.
      new_token
= StoredToken(user_email=self.current_user.email(),
          session_token
=self.client.GetAuthSubToken(), target_url=self.token_scope)
      new_token
.put()

この変更の後で、ManageAuth() の中にある関数名を変えるのも忘れないように!

Step 4: 保存したトークンを使用する

これで、我々のアプリケーションから、ユーザの document list feed を取得できるようにするために必要な準備は全て行いました。最後の仕上げは、Google Docs から実際にユーザのフィードを取得して、我々のサイトに表示する事です!

まずはユーザのトークンを探し出す関数を書きましょう。この関数では、ユーザは document list フィードを要求していて、我々はすでにその URL を self.feed_url に保存してあるものとします。

  def LookupToken(self):
   
if self.feed_url and self.current_user:
      stored_tokens
= StoredToken.gql('WHERE user_email = :1',
          self
.current_user.email())
     
for token in stored_tokens:
       
if self.feed_url.startswith(token.target_url):
          self
.client.SetAuthSubToken(token.session_token)
          return

これでユーザのセッショントークンを手に入れたので、さっそくフィードを取得しましょう!

  def FetchFeed(self):
   
# Attempt to fetch the feed.
   
if not self.feed_url:
      self
.response.out.write(
         
'No feed_url was specified for the app to fetch.<br/>')
      self
.response.out.write('Here\'s an example query which will show the'
         
' XML for the feed listing your Google Documents <a '
         
'href="http://%s/step4'
         
'?feed_url=http://docs.google.com/feeds/documents/private/full">'
         
'http://%s/step4'
         
'?feed_url=http://docs.google.com/feeds/documents/private/full'
         
'</a>' % (HOST_NAME, HOST_NAME))
     
return
   
if not self.client:
      self
.client = gdata.service.GDataService()
   
try:
      response
= self.client.Get(self.feed_url, converter=str)
      self
.response.out.write(cgi.escape(response))
   
except gdata.service.RequestError, request_error:
     
# If fetching fails, then tell the user that they need to login to
     
# authorize this app by logging in at the following URL.
     
if request_error[0]['status'] == 401:
       
# Get the URL of the current page so that our AuthSub request will
       
# send the user back to here.
       
next = self.request.uri
        auth_sub_url
= self.client.GenerateAuthSubURL(next, self.feed_url,
            secure
=False, session=True)
        self
.response.out.write('<a href="%s">' % (auth_sub_url))
        self
.response.out.write(
           
'Click here to authorize this application to view the feed</a>')
     
else:
        self
.response.out.write(
           
'Something else went wrong, here is the error object: %s ' % (
                str
(request_error[0])))

上記のコードは cgi モジュールを使用しているので、スクリプトのはじめの方の import を書いているところに import cgi と追加してください

これで目的のフィードを取得するメソッドができましたので、FeedFetcher クラスの get メソッドを書き換えて、ManageAuth() の後で今回のメソッドを呼び出しましょう。

  def get(self):
   
# Write our pages title
    self
.response.out.write("""<html><head><title>
        Google Data Feed Fetcher: read Google Data API Atom feeds</title>"""
)
   
# Get the current user
    self
.current_user = users.GetCurrentUser()

    self
.response.out.write('<body>')
   
# Allow the user to sign in or sign out
   
if self.current_user:
      self
.response.out.write('<a href="%s">Sign Out</a><br>' % (
          users
.CreateLogoutURL(self.request.uri)))
   
else:
      self
.response.out.write('<a href="%s">Sign In</a><br>' % (
          users
.CreateLoginURL(self.request.uri)))

   
for param in self.request.query.split('&'):
     
# Get the token scope variable we specified when generating the URL
     
if param.startswith('token_scope'):
        self
.token_scope = urllib.unquote_plus(param.split('=')[1])
     
# Google Data will return a token, get that
     
elif param.startswith('token'):
        self
.token = param.split('=')[1]
     
# Find out what the target URL is that we should attempt to fetch.
     
elif param.startswith('feed_url'):
        self
.feed_url = urllib.unquote_plus(param.split('=')[1])

   
# If we received a token for a specific feed_url and not a more general
   
# scope, then use the exact feed_url in this request as the scope for the
   
# token.
   
if self.token and self.feed_url and not self.token_scope:
      self
.token_scope = self.feed_url

   
# Manage our Authentication for the user
    self
.ManageAuth()
    self
.LookupToken()

    self
.response.out.write('<div id="main">')
    self
.FetchFeed()
    self
.response.out.write('</div>')

    self
.response.out.write(
       
'<div id="sidebar"><div id="scopes"><h4>Request a token</h4><ul>')
    self
.response.out.write('<li><a href="%s">Google Documents</a></li>' % (
        self
.client.GenerateAuthSubURL(
           
'http://gdata-feedfetcher.appspot.com/step3' +
               
'?token_scope=http://docs.google.com/feeds/',
           
'http://docs.google.com/feeds/', secure=False, session=True)))
    self
.response.out.write('</ul></div><br/><div id="tokens">')

最終的な実際に動くプログラムは http://gdata-feedfetcher.appspot.com/ で見る事ができます。また、完全なソースコード一式は Google Code でホストしている Google App Engine sample code project で見る事ができます。

AuthSub セッショントークンの寿命は長いですが、ユーザや我々のアプリケーションによって破棄される事があります。ある時点で、データストアに保存されたセッショントークンが破棄されたなら、我々のアプリケーションはもう使えないトークンの後始末をするべきでしょう。トークンの状態は querying the token info URL で調べる事ができます。AuthSub ドキュメント を読めば AuthSub のトークン管理について、さらに詳しい情報が手に入ります。この機能は読者の練習のためにとっておきましょう。楽しんでくださいね :)

まとめ

Google Data Python client ライブラリを使えば、Google App Engine のアプリケーションで簡単にユーザの Google Data フィードを扱う事ができます。

Google Data Python client library は、殆ど全ての Google Data サービスをサポートしています。さらに詳しくは、ライブラリの getting started guide を読んだり、ソースコードを参照するために プロジェクトページ にアクセスしたり, また gdata-python-client's Google グループ に質問メールを出す事もできます。

いつもの通り、Google App Engine について質問があれば、オンラインドキュメント を読むか、グーグルグループをたずねてみてください。