2011年5月20日金曜日

Google App EngineにC#プログラムからログインする方法

今回はGoogle App Engine(GAE)にC#プログラムからログインする方法を解説する。

GAEで特定のUrlに対して、特定のユーザのみにアクセスを許可する場合はapp.yamlで下記のように設定をすればよい。

app.yaml
- url: /do/something/
  script: something.py
  login: required

これでGAEアプリのユーザのみが上記Urlへのアクセスが可能となる。ユーザではない、または未ログインのものが上記UrlにアクセスするとGoogleアカウント(またはOpen Id)のログインページへとリダイレクトされる。そこでユーザ名とパスワードを入力してログインすれば上記のUrlへとリダイレクトバックされる。これがブラウザ上の動作であれば問題はないが、プログラム上から認証を必要とするUrlへとアクセスする場合は困る。

認証を必要とするGAEアプリのUrlに対してリクエストを行う場合は下記の手順で行う必要がある。

  1. ClientLoginサービスにログインしAuthトークンを取得する
  2. 取得したAuthトークンを利用しGAEアプリにログインしCookieを取得する
  3. 取得したCookieとともにHttpGETやらHttpPOSTを行う

各項目を順を追って説明する。

ClientLoginサービスにログインしAuthトークンを取得する
string GetAuth()
{
    var request = (HttpWebRequest)WebRequest.Create("http://www.google.com/accounts/ClientLogin");
    var content = "Email=test@gmail.com&Passwd=testpass&service=ah&accountType=HOSTED_OR_GOOGLE";
    var byteArray = Encoding.UTF8.GetBytes(content);
    request.ContentLength = byteArray.Length;
    request.ContentType = "application/x-www-form-urlencoded";
    request.Method = "POST";
    var dataStream = request.GetRequestStream();
    dataStream.Write(byteArray, 0, byteArray.Length);
    dataStream.Close();
    var response = (HttpWebResponse)request.GetResponse();
    var stream = response.GetResponseStream();
    var reader = new StreamReader(stream);
    var loginStuff = reader.ReadToEnd();
    reader.Close();

    var auth = loginStuff.Substring(loginStuff.IndexOf("Auth")).Replace("Auth=", "").TrimEnd('\n');
    return auth;
}
まずhttp://www.google.com/accounts/ClientLoginへと必要な情報をポストする。各項目の詳細な説明は下記を参照して欲しい。
ClientLogin for Installed Applications

今回は最終的にGAEアプリへとログインしたいのでserviceはahとなる。これがカレンダーであればclなどそれぞれに対応したものへと変わる。ログインに成功するとSID、LSIDとともにAuthが返却されるのでAuthのみ取得する。他の二つは不要。

取得したAuthトークンを利用しGAEアプリにログインしCookieを取得する
CookieContainer GetCookies(string auth)
{
    var cookies = new CookieContainer();
    var url = string.Format("http://test.appspot.com/_ah/login?auth={0}",
                            System.Web.HttpUtility.UrlEncode(auth));
    var request = (HttpWebRequest)WebRequest.Create(url);
    request.AllowAutoRedirect = false;
    request.CookieContainer = cookies;
    request.ContentType = "application/x-www-form-urlencoded";
    request.Method = "GET";
    var response = (HttpWebResponse)request.GetResponse();
    var stream = response.GetResponseStream();
    var reader = new StreamReader(stream);
    var result = reader.ReadToEnd(); 
    reader.Close();
    return cookies;
}
先ほど取得したAuthトークンを使用しGAEアプリへとログインする。実際にはhttp://test.appspot.comの部分をアクセスしたいGAEアプリのUrlへと変更して欲しい。ログインに成功するとACSIDというクッキーが返却されているはずだ。余談ながらGAEアプリへのログイン後、POSTではなくGETでよい場合は、下記のようにリクエストすれば指定のUrlへとリダイレクトされる。

http://test.appspot.com/_ah/login?auth={0}&continue=http://test.appspot.com/do/something/

ただ今回はPOSTでリクエストしたいのでAllowAutoRedirect=falseで自動リダイレクトを無効化している。


取得したCookieとともにHttpGETやらHttpPOSTを行う
void PostToGAE()
{
    var auth = GetAuth();
    var cookies = GetCookies(auth);

    var url = string.Format("http://test.appspot.com/do/something/");
    var content = "testvalue=test";
    var request = (HttpWebRequest)WebRequest.Create(url);
    request.KeepAlive = false;
    request.CookieContainer = cookies;
    var byteArray = Encoding.UTF8.GetBytes(content);
    request.ContentLength = byteArray.Length;
    request.ContentType = "application/x-www-form-urlencoded";
    request.Method = "POST";
    var dataStream = request.GetRequestStream();
    dataStream.Write(byteArray, 0, byteArray.Length);
    dataStream.Close();
    HttpWebResponse response = (HttpWebResponse)request.GetResponse();
    var stream = response.GetResponseStream();
    var reader = new StreamReader(stream);
    var result = reader.ReadToEnd();
    reader.Close();
}
取得したCookieをそれ以降のリクエストと一緒に送ってやれば認証されたユーザとしてサーバサイドで認識される。

※見て分かると思うけれど、上記方法は中間でトラフィックを監視している人物がいた場合に、その人物がクッキーを使いまわすだけで簡単になりすましが可能となる。そのため繊細な情報などを扱う場合はhttpsを使用するのが望ましい。httpsを使用した場合は返却されるクッキー名がSACSIDとなる。


仕組みが分かれば至極単純なのが理解できたと思う。上記のコードは動作テスト用に組んだものなので例外処理などまったくの手付かずなので使用される場合は随時手を加えてもらいたい。


余談
私がこの動作テストを行ったときに正常に動作させるまで二日ほどかかってしまった。その要因はサーバサイドのハンドラーをDjango-nonrelで実装していたのだが、POSTリクエストがcsrf protectionに引っかかっていたせいだった。ずっとGAEの認証側の問題だと考えてそちらばかり追っかけていたので、Djangoのほうまで頭が回らなかった。気付いてしまえば単純な問題だけれど、はまるときははまる。仮にDjango-nonrelを使用する場合は忘れずにハンドラーへ@csrf_exemptを付与するようにしてほしい。

下記、はまっているときにstackoverflowへ質問した内容。
Programmatically login to google app engine c#

3 件のコメント:

  1. はじめまして。

    大変参考にさせて頂いています。
    私は、VB.NETでClientLoginを実装しようと作っていますが
    Authを取得してCookieも取得出来るようになりました。

    しかしGAE側がかなりの初心者でして、web.xmlにてGoogelアカウントで認証をしてログイン画面を出す設定にすると上記のアクセスでエラー302が出て失敗してしまいます。
    web.xmlで設定を外すと成功します。(ただしブラウザでもアクセス出来てしまいます。)

    基本的な質問で申し訳ないですが
    ClientLoginは、GAE側で別の方法で管理する必要があるんでしょうか?(Filterをかますなど)

    お時間が有る時にご教示頂けると助かります。

    返信削除
  2. こんばんは

    詳細が不明なので正確にはわからないのですが、ACSID Cookieと一緒にweb.xmlへアクセスした場合に302でレスポンスが返ってきているならば、302はエラーではなくリダイレクト要求なのでどこかに飛ばそうとしていますね。その飛ばし先はどこなのでしょう?飛ばし先がログイン画面の場合は認証自体が上手くいっていないと思われますのでAuthトークンの取得か、GAEアプリのログインに失敗しているのだと思います。多分GAEのアプリはJAVAなのでしょうが私の場合はPythonだったのでもしかすると何か設定が違うのかもしれません。

    ちなみに今少し調べてて知ったのですが、ClientLogin自体は2012年の春で公式にdeprecationされたようで今はOAuth2での認証が推奨されているようです。ClientLoginでも2015年ぐらいまでは使わせてくれるようですが。
    https://developers.google.com/accounts/docs/OAuth2
    一応上記のコードはライブサイトで元気に今も動いています。

    返信削除
    返信
    1. 返信ありがとうございます。
      説明が足りなくて申し訳ありません。

      for JAVAを利用しています、ブラウザのUI画面とWindowsのアプリから呼ばれるAPIの機能をGAEで作っています。
      UIとAPI側にGoogleアカウントで認証という設定をするとAPIが302を発生しますUI画面はGoogleのログイン画面です
      多分、APIもそのログイン画面に遷移しているんだろうと思います。

      OAuth2の話ありがとうございます、大変参考になります。
      それで、ClientLoginの情報が余りないのにもうなずけました、OAuth2で対応してみたいと思います。

      ありがとうございました!

      削除