chrome.identity APIを使ってGoogle App Engine OAuth2を使う

2015-04-22T00:00:00+09:00 Java Google App Engine JavaScript Chrome Extension

参考: http://www.ciiycode.com/0z6ziWjWxjjP/google-app-engine-oauth2-provider

Google Account OAuth1.0aがdeprecated化したのがつい先日くらいの話なので、そのまま放置しておくと問題にもなりそうなので検証してみる

概要

Google App EngineにはOAuthServiceっていうAPIがあるのでOAuth2認証を利用する事も出来る。OAuth1.0aの場合はOAuthServiceでgetCurrentUserメソッドを使う場合等にはクエリーストリングにOAuth認証に必要なパラメーターを設置する必要があるがOAuth2の場合にはAuthorizationヘッダーに取得したアクセストークンを指定するような形式になる。又、OAuth1.0aの時とは違いクライアント側で取得のトークンは有効期限は自前でトークンを削除しない限りは問題無いが、OAuth2の場合には一定時間でトークンの有効期限が設定されているので定期的にチェックしつつ再取得する必要性がある

でOAuth2を利用するにあたって https://console.developers.google.com からプロジェクトのクライアントID及びクライアントシークレットを取得する必要がある。今回のはInstalled Applicationを使うのでクライアントのタイプはそっちで登録する

まぁ下準備はこれくらい

サーバー側のOAuth2への対応修正

確証は無いけどOAuthService.getCurrentUserやisUserAdmin等のメソッドの引数にscopeを指定しないと正常にUserインスタンスを取得できない模様なので

OAuthService service = OAuthServiceFactory.getOAuthService();
User user = null;

try {
    user = service.getCurrentUser("https://www.googleapis.com/auth/userinfo.email");
} catch (OAuthRequestException e) {
    e.printStackTrace();
}

っていう感じで取得したりするような処理に書き換える必要がある模様。これを行わないでクライアント側で取得したトークンを使ってリクエストを投げるとInvalidOAuthTokenException等の例外が送出されてしまう模様。但し、これを修正してしまうとOAuth1.0aの際に指定していたscopeと一致しないような場合とかだとOAuth1.0aがすぐに通らなくなるので

んまぁサーバー側のGoogle App Engineの修正はこれだけ。修正してコンパイル後に修正版をデプロイしておく

chrome.identity APIを使ったクライアント側を実装

Chrome Extensionにはidentityパーミッションを付与すればchrome.identityが使えるようになる。chrome.identityを使うとGoogle Accountやそれ以外のOAuth2フローによるアクセストークンなどの取得が出来るようになる。但し、chrome.identity.getAuthTokenを使う場合だとChrome Syncの有効が必要になるっぽい気がするのでchrome.identity.launchWebAuthFlowのAPIを使ってGoogle Account認証を行ってそれからアクセストークンを取得して、その取得したアクセストークンを利用してサーバー側のGoogle App Engine側にAuthorizationヘッダーを付与してリクエストしてOAuth2によるユーザー処理を行えるようにする

※微妙と長いので

var client_id     = "console.developers.google.comから取得したクライアントID";
var client_secret = "console.developers.google.comから取得したクライアンシークレット";

var access_token  = localStorage["access_token"];
var refresh_token = localStorage["refresh_token"];

if (access_token && refresh_token) {
  load_data();
} else {
  start_authorization();
}

// POSTする際にObjectをクエリー化するだけ
function paramsToQueryString(params) {
  var queryStrings = [];
  for (var key in params) {
    var value = params[key];
    queryStrings.push(encodeURIComponent(key) + "=" + encodeURIComponent(value));
  }

  return queryStrings.join('&');
}

// トークンの有効期限が切れてる場合に新しいアクセストークンを発行してもらう
function refresh_access_token() {
  var params = {
    "client_id": client_id,
    "client_secret": client_secret,
    "grant_type": "refresh_token",
    "refresh_token": refresh_token
  };

  return fetch("https://www.googleapis.com/oauth2/v3/token", {
    method: "POST",
    headers: {
      "Content-Type": "application/x-www-form-urlencoded"
    },
    body: paramsToQueryString(params)
  }).then(function(res) {
    if (res.status != 200) {
      throw new Error("failed token refresh");
    }

    return res.json();
  }).then(function(data) {
    // 注意として取得したレスポンスにはrefresh_tokenは無いのでそれも保管しちゃいけない(undefinedになるので)
    access_token = localStorage["access_token"] = data.access_token;
  });
}

// 現在取得されているアクセストークンが有効化どうかをチェックする
//現在有効では無い場合はInvalid tokenエラーになるのでエラーになったらrefresh_access_tokenをコールする
function check_token() {
  return fetch("https://www.googleapis.com/oauth2/v1/tokeninfo?access_token=" + access_token).then(function(res) {
    if (res.status != 200) {
      throw new Error("invalid token");
    }

    return res.json();
  }).catch(function(error) {
    return refresh_access_token();
  });
}

function load_data() {
  var fn = function() {
    // OAuthでプロテクトされている所は自前のを使う。
    // でリクエストする際に「Authorization Bearer トークン」を付与する
    fetch("https://oauth-demo-example.appspot.com/protected/resources", {
      headers: {
        "Authorization": "Bearer " + access_token
      }
    }).then(function(res) {
      return res.json();
    }).then(function(data) {
        console.log(data);
    });
  };

  // トークンをチェックしてからAPIをコールする
  check_token().then(function(json) {
    fn();
  });
}

function start_authorization() {
  var url = "https://accounts.google.com/o/oauth2/auth?client_id=" + client_id + "&redirect_uri=urn:ietf:wg:oauth:2.0:oob&response_type=code&scope=https://www.googleapis.com/auth/userinfo.email";

  chrome.identity.launchWebAuthFlow(
    { url: url, interactive: true },
    function() {
      // ログイン許可を出すと画面に認証コードが出るのでそれを入力させる
      var code = prompt("please input authorization code");

      var params = {
        "client_id": client_id,
        "client_secret": client_secret,
        "code": code,
        "grant_type": "authorization_code",
        "redirect_uri": "urn:ietf:wg:oauth:2.0:oob"
      };

      // 入力した認証コードを使用してアクセストークンを取得
      fetch("https://www.googleapis.com/oauth2/v3/token", {
        method: "POST",
        headers: {
          "Content-Type": "application/x-www-form-urlencoded"
        },
        body: paramsToQueryString(params)
      }).then(function(res) {
        if (res.status != 200) {
          throw new Error("failed retrieve oauth token");
        }

        return res.json();
      }).then(function(data) {
        access_token  = localStorage["access_token"]  = data.access_token;
        refresh_token = localStorage["refresh_token"] = data.refresh_token;
        load_data();
      }).catch(function(error) {
        alert(error);
      });

      return true;
    }
  );
}

まぁそんな感じで要点をまとめると

  • アクセストークンが取得されてないならchrome.identity.launchWebAuthFlowを使って認証及び認証コードの取得を行う。その認証コードを用いてアクセストークンを取得
  • 上記を実行後、OAuth2で保護されてるリソースへAuthorization: Bearerを用いてリクエストする(※アクセストークンの取得の際にtoken_type?かなんかで識別可能。Bearerの場合はBearerを使う)。その前にアクセストークンが有効かどうかをチェックした後に行う
  • アクセストークンが有効切れになっているような場合(checktokenにてのリクエストで400エラーが返ってくる場合等)にはrefreshaccess_tokenを行ってアクセストークンの更新を行う。

で動き的なのとしては

みたいに許可しますか的なウィンドウが出るので許可すると

みたいに認証コードが出るのでそのウィンドウを消すと

というダイアログが出るので先ほどの認証コードをコピペする。でOKを押すとアクセストークンの取得が行われてそのトークンを用いてプロテクトリソースにアクセスする(console.logしてるので)

っていう感じでfetch apiを使ってやってるので色々とPromise関係でフローがややこしくなってるので上記コードでも若干バグる事があると思うので慎重利用しなきゃならんとは思うのですが…

んまぁそんな感じでGoogle App EngineのOAuthServiceを用いてOAuth2認証リソースを利用したいような感じをchrome.identity APIを使ってやる実験はとりあえず出来るっていう事で

Google AppEngineでGoogle Analytics Core Reporting API doma2をやってみる (5) - OriginalStates -