読者です 読者をやめる 読者になる 読者になる

家族ToDo(仮)開発日誌

やりたいことや行きたい場所を家族で共有するためのAndoridアプリの開発日誌(兼Android開発学習メモ)です。

開発日誌 (9) : セキュリティと利便性を両立するOAuth2.0認証シーケンス (その2)

今回の進捗

  • Web側のユーザ登録・認証処理仮実装
  • Android端末(Genymotion)からローカル端末の自前Webを参照するためのhosts設定
  • Andorid端末から自前Webを経由してOAuthし、結果をDB登録した後、独自認証キーを返却するまでの一連の通し動作確認

作業時間: 4h

ユーザ登録・認証処理の仮実装

前回の日誌で示したシーケンスのフィジビリティをとるために、最低限必要なWeb側の機能をやっつけ実装する。

↓の図の1〜7までは前回までで終わっているので、8〜10が対象となる。

f:id:ymkn:20140217001435p:plain

8: 独自認証キーの発行

いわゆるセッションキーであるので、それなりに気をつけたい。気をつけるべきポイントは下記の通り。(なんか個人サイトだけど、IPAが言っているのと同じことを書いているような気がしたのでIPAのサイトを調べ直したりしてない)

1.推測困難なセッション ID を利用する
2.セッション ID を URL に含めない
3.HTTPS 通信で利用する Cookie には secure 属性を付与する
4-1.ログイン後にセッションを新規に開始する
4-2.ログイン後にセッション ID とは別の秘密情報を持ち、各ページでその値をチェックする

参考: セッション管理の不備と対策 | Webセキュリティの小部屋

1、2についてはアクセストークンをSeedにして乱数生成すればいいだろう。3はCookie使わないのでアレだが、通信はHTTPSで行うのは必須。4-1、4-2はいずれかを対応すれば良いので、今回は4-1を採用する。Android端末から認証のために認証キーが送られてきて認証が成功する度に、新規の認証キーを生成して払い出すようにすれば良い。

9: アクセストークンとメールアドレス、認証キーをDBに保存

googleから得られたJSONjson_decode関数を使ってデコードし、結果をPDOでDBにINSERTする。認証時も同様にPDOでメールアドレスを条件にSELECTし、見つかったら認証キーとともにユーザ情報をJSONエンコードしてカスタムスキーマのURLへリダイレクトする。

以下、30分くらいはまったのでメモ。

PDOでエラー時に例外を生成するようにする

PDOの属性PDO::ATTR_ERRMODEにPDO::ERRMODE_EXCEPTIONを指定すれば良い。デフォルトでは何もエラーはかないので大変困る。エラー内容を確認できなかったのでPDO::prepareメソッドでPDOStatementクラスがうまく生成されない原因に気づくのに時間が掛かった。ちなみに原因はINSERT文中の列名の誤り。prepareメソッドでそんなところまでチェックするんだね。すごい。

json_decode関数はstdClassを返す

そもそもstdClassって何?と最新のPHP事情について行けていない自分がいるが、身も蓋もない言い方をすれば、連想配列のキーをpublicフィールドとして持つ単純クラス、ということでしょうか。JavaなんかのObjectクラスに相当するとか聞いたけど、メソッドとかもっているかまでは調べてない。

ちなみに第2引数にtrue (trueって・・) を指定するとイメージ(?)通り連想配列を返してくれる。

10: カスタムスキーマを使い、認証キーを返却

カスタムスキーマなURLにリダイレクトしてResponseとしてJSON吐けばいいだけじゃん、とか思ってたら、うまくリダイレクトできなくて悲しい。というのも、Androidの実行環境としてGenymotionを使っているので、localhost:8888/ほげほげとかににリダイレクトしようとすると、当然ながらAndroid端末内のlocalhostへリダイレクトしてしまうのだった。

GoogleのWeb Application向けのredirect URLにIPアドレスを指定できない

じゃあ母艦のIPアドレスを直接指定すればいいじゃんと192.168.ほげほげとかを指定すると、今度はGoogleのOAuthリダイレクトURLのチェックではじかれるのだった。またか・・Googleよ。kazokutodo.example.comなどドメインの形式であればOKのようなので、Google側はそれで登録した。

Genymotion上のAndroid端末のrootを取得する

問題はAndroidから上記URLを使ってリダイレクトさせる方法だが、hosts書き換えれば一発OKであるものの、Android上のhostsを書き換える方法に苦戦。rootはGenymotionのAndroid上にインストール済みのGenymotion Configurationアプリを使用することで簡単に取得できるのだが、hostsをAndroid端末上で修正する方法がわからないのであった。エディタとか無いしね。

Genymotion上のAndroid端末にGoogle Playストアをインストールする

というわけでHost Editorアプリをインストールしたかったのだが、APKからのインストールでもGoogle Playアプリが必要とのことで、下記サイトを参考にインストールを敢行。今まで使っていたAndroid 4.1.1の端末にはうまくインストールできなかったので、4.2.2の端末を作り直してそちらでやり直すとうまくいった。

参考:[Android]GenymotionにGoogle Playを入れる | hello-world.jp.net

晴れてAndroidから母艦のWebサーバへリダイレクトできるようになった。

日誌の整理が必要かも

しかし開発日誌と銘打ってはいるものの殴り書きのメモに近い状態になってきたので、ここらで一休みして記事の整理をしていく必要がある気がした。コード片の記載もスクリーンショットも少ないので、単なる妄想日記に見えないこともないし。

開発日誌 (8) : セキュリティと利便性を両立するOAuth2.0認証シーケンス

OAuth 開発日誌 Android PHP

今回の進捗

  • Web側でのOAuth2.0による認証のサンプル実装
  • セキュリティに配慮した認証シーケンスの検討

作業時間: 5h

Webアプリ(PHP)経由でOAuth

前回、Androidアプリから直接GoogleにOAuth2.0で認証しようとしたが、下記が課題だった。

  • GoogleのOAuth 2.0ではCallback URLにカスタムスキーマが使用できない(http固定)ため、コールバック時にアプリ選択ダイアログが表示されてしまいうざい(普通httpにはブラウザが割り当てられているので)
  • 毎回のGoogle認証を避けるためには端末側にアクセストークンを持たせる必要があるが、漏れたときのことを考えると頭が痛い (OpenID Connect対応していないサービスが多い現状を踏まえ)

なぜGoogleがredirect urlをhttp://localhost固定にしたのか、その理由は探し出せなかったが、前できていたのをわざわざ変更したのだから、おそらくセキュリティ的な理由*1があるからと思う。

ちなみに、もう一つのリダイレクトURLにurn:ietf:wg:oauth:2.0:oobというのがあるが、これはブラウザに表示されたPINをアプリにコピペするという方式で、これまためんどくさい。やったことあるならわかると思うが、スマホ上での文字列のコピペは大変苦労する。論外である。

参考: Using OAuth 2.0 for Installed Applications - Google Accounts Authentication and Authorization — Google Developers

とまあそんなわけで結局Androidアプリから直接OAuthするのはあきらめ、自前のWebアプリを作りそいつ側にOAuthをやらせ、Androidアプリは必要なユーザ情報だけを受け取るような感じにしようと思う。これでセキュリティと利便性を両立しようという目論見だ。

まず、ユーザ登録時。

f:id:ymkn:20140217001435p:plain

で、ユーザ登録後の認証時。

f:id:ymkn:20140217001441p:plain

ポイントは下記の通り。

利便性

  • AndroidアプリへのコールバックURLはWebアプリが発行するので、カスタムスキーマが使える (=余計なアプリ選択ダイアログは出ない)
  • もちろん、PINのコピペなど不要
  • Androidアプリ側の作りがシンプルになる

セキュリティ

  • OAuthのシーケンスはWebアプリ(jibunstyle.net)で閉じるので、Androidアプリ(KazokuToDo App)側にアクセストークンを持たせなくて良い (Webアプリに脆弱性が無い限りアクセストークンが漏洩しない)
  • Androidアプリ側にはWebアプリ側で発行した専用の認証キーだけを持たせるので、端末に保存されている情報を抜かれたとしても、OAuthに関する情報は何一つ漏洩しない。

まあ、デメリットもある。

  1. Webアプリ作らないといけない (開発工数増加)
  2. ユーザにとって良くわからんサイト(自前Webアプリ)を経由するので、怪しい
  3. 許可する対象がAndroidアプリではなく、自前のWebアプリとなっており、怪しい。
  4. 障害点が増える。自前のWebアプリが死んてたらGoogleが生きていても認証不可。

2と3はリダイレクト前にガイダンス画面(こちらは家族ToDoアプリの認証ページです。下のボタン押してGoogleのサイトに飛んだことを確認し認証を許可して下さい。)を表示することで緩和するとか。1と4は仕方が無いね。

でもまあ、2と3のデメリットを受け入れるなら、素直にWebViewを使ってAndroid端末側だけで頑張った方がいいのかね。「使ってたAndroidアプリと違うWebアプリに対して認証許可」するのがいいか、「アプリ内ブラウザでGoogleアカウントのID・パスが抜かれているかもしれない懸念を抱きながら認証許可」するのがいいか。うーん。・・セキュリティ的には前者の方がましか。

しかし、調べれば調べるほどOAuth使うとセキュリティ的に考慮しなきゃいけないところが多くて不安だな。なんか普通にユーザ登録させる機能だけにした方がよほど安全な気がしてきた。

Webアプリ側でのOAuth 2.0のサンプル実装

技術調査がてら実装してみた。時間ないので使った開発環境や技術だけ。細かいことは後日書く。

*1:OAuthのコールバック用に設定したカスタムスキーマがばれると、悪意のある他のアプリが同じカスタムスキーマを受け取れるので、アクセストークンとか盗まれてしまうから?

開発日誌 (7) : scribeでGoogle OAuth 2.0 (client_secretなしで認証)

今回の進捗

  • Google OAuth 2.0対応のscribeを入手、ビルド (client_secretが空文字列でもOKに修正)
  • カスタムスキーマなCallback URLをうまく扱ってくれない件について調査。http://localhostしかない?

作業時間: 3h

Google OAuth 2.0対応のscribeでやりなおし

GoogleのOAuth 2.0がらみの記事を読みあさっていて気づいたのだが、そもそも、今まで使っていた本家のscribeに組み込まれているAuthorization URLはなんか古かった。つまりOAuth 1.xを使っていた。というわけで本家のFork ListからGoogleのOAuth 2.0に対応したよさげなForkを選択。

Kobee1203/scribe-java · GitHub

残念ながらバイナリの配布がなさそうなので、 とりあえず全ソースを取得、Netbeansにインポートしたらボタン一つでビルドできた。できあがったjarをAndroid Studioにインポート。

いままでServiceBuilder.providerメソッドに与えていたGoogleApiクラスをGoogleApi20クラスに変更。これでうまくいくかと思いきやUnsupportedOperationExceptionが発生。どうやらscribeでは1.0と2.0でお作法が異なる模様。

java - How can I use Scribe with a not-yet implemented Api? - Stack Overflow

下記サンプルを見ると、getRequestTokenは使わず、いきなりgetAuthorizationUrlを呼んでいたりする。サンプルを参考に処理を書き換え。

scribe-java/src/test/java/org/scribe/examples/GoogleExample20.java at master · bistri/scribe-java · GitHub

これでいけると思いきや、secretKeyが空文字であることが災いして、scribeのチェック処理で死ぬ。どうやらscribeはsecretKeyが必須の前提で作られている模様。仕方が無いのでNetbeansで当該チェック処理をコメントアウトして再ビルド、うまく通るようになった。

カスタムCallbackURLが使えない

Intent Filterの設定もしているのに、うまくアプリに戻ってくれない。適当に調べた感じでは、OAuth 2.0では非標準なスキーマは使えなくなったようだ。うへ。

iOS向けでは下記のようにすることもできるようだが・・。

Naveen Agarwal - Google+ - How to use custom URI schemes in the redirect URI for… https://plus.google.com/+NaveenAgarwal/posts/AztHNnQh7w6

残念ながら、Androidでうまくいかない。どうやらデフォルトのCallback URLであるhttp://localhostを使用する以外に無いようだ。しかしhttpはブラウザに割り当てられているのでアプリ選択ダイアログが出てしまう。

StackOverflowの下記トピックでは、結論としてWebViewを使うことにしたとのこと。うーん。もともとアプリ組み込みのフォームではユーザに安心感を与えられないので標準ブラウザを使う方法を探したという経緯があったのだが・・。

Best way to use Google APIs using OAuth 2.0 on Android - Stack Overflow http://stackoverflow.com/questions/6966965/best-way-to-use-google-apis-using-oauth-2-0-on-android

もう少し追跡調査をしてみるつもりだが、現状これは致し方がないのかもしれない。AndroidだけならAccountManagerをつかえという話なのかも。

次回へ続く。

開発日誌 (6) : Google Developers Consoleにアプリ登録

今回の進捗

  • Google Developers Consoleにアプリ登録してClient IDを取得
  • モデルクラスの整理

作業時間: 1h

client_secretがもらえない

本来はClient IDの登録結果画面にclient_secretも表示されるようなのだが、どう探してもない。いろいろ検索すると、Installed Application向けのClient IDの場合、client_secretは発行されないように変更された模様。

参考: oauth 2.0 - Google APIs Console - missing client secret - Stack Overflow


最新の認証方法は下記URL記載のこと。後で読む。

Authorization | Android Developers

何が困るって、第5回でうまくいったライブラリを使う方法が使えなくなるのではないかと言うこと。というわけで次回に続く。本日は時間切れ。

Intent解決 (Intent Resolution)

Android 画面

第5回でOAuthする際、デフォルトのWebブラウザのActivityを呼び出してGoogleの認証画面を表示しているわけだが、これはAndroidのIntent解決機能を利用している。

要はstartActivityに渡すIntentを生成する際、アプリ内のActivity間遷移では第1引数がContextクラスであるコンストラクタを使っていたが、String (アクション名)をとるコンストラクタで第2引数にURLを渡し生成したIntentを使うと、指定したアクション名とURLに対応する規定のアプリが起動するという仕掛け。

で、自分がどのカテゴリに対応したActivityであるかは、上記のポイントでも書いたようにAndroidManifest.xml内でIntent Filterを定義することで表明できる。

参考:インテント解決とインテントフィルター - Android 開発入門

開発日誌 (5) : OAuthでログイン (Googleからメールアドレスとユーザ名を取得)

Android 開発日誌 OAuth

今回の進捗

  • JavaのOAuthライブラリscribeの導入
  • ログイン画面の追加 (テンプレから不要要素削除しただけの張りぼて)
  • OAuthを使ってGoogleからユーザ情報とメールアドレスの取得
  • 取得した情報のメイン画面への表示


作業時間: 3h

Googleアカウントを使ってログイン (OAuth 2.0)

何かサービスを作る度にユーザ管理・認証のしかけを再発明しているのにうんざりしていたが、最近OAuthが一般的になってきておりこれらを外部サービスに丸投げできるのがうれしい。

厳密にはOAuthはService Providerのサービスに対する認可機能まで持っているので、単に認証だけに使いたい場合にはオーバースペックというか実装具合によってはSPのサービスよび放題なのである種の脆弱性にもなり得ると言うことで最近はOAuth 2.0から認証部分(とベーシックなユーザ情報とか)だけを取り出してまとめたOpenID Connectという規格がはやり始めている模様。ちょっと調べたがサンプルが少なかったのでとりあえずOAuth 2.0ベースで実装を進め、後にOpenID Connectに修正する方向で進める。

OAuthで目的の認証情報を得るにはSPの提供する認証画面をたたいてコールバックを受けなければならない。そのため多少複雑な実装*1が必要となる。

とかく面倒なのは嫌いなので簡単にできるライブラリがないか探したところ、scribe-javaというのがある模様。下記サイトにはこれを用いたGoogleに対するOAuthの実装例まであるのでとりあえずこれを丸パクりして動作確認をしてみる。

参考: AndroidからGoogle OAuthでプロフィール情報にアクセスする方法

ポイントととしては、

  • UIスレッドから呼ばない (別スレッド処理・非同期処理が必要)
  • SPからのコールバックに使うActivityには、AndroidManifest.xmlandroid:launchMode="singleTask"属性を付与し、Intent Filterでコールバック先と指定していたURLを割り当ててあげる必要がある (サンプルでは単にoauth://callback/としているが、他アプリとかち合わないように名前空間でも入れておいた方が良い)

サンプルコードに載っていないsecretTokenの保存・復元処理だけ書き足して実行すると、見事に動作。ユーザ情報の入ったJSONを取得できた。あとはこのJSONを解釈してUserクラスのインスタンスを作って、Intentを使用してメイン画面へ運べばよい。

↓メイン画面で表示してみた(モザイクだらけで分けわからんな)。

f:id:ymkn:20140203000346p:plain
f:id:ymkn:20140203000357p:plain
f:id:ymkn:20140202235739p:plain

Activityに依存しているコードとそうでないコードがごっちゃになっていることと、できればスレッドではなく非同期メソッド (Login画面のテンプレコードで使われているやつ) を使ってみたいのでこの辺はToDoとして次回へ。

*1:第4回でやったようなJSONのデコード・エンコードに比べれば、という程度だが

開発日誌 (4) : アプリのデータをJSONで保存する

Android DI dagger 開発日誌

今回の進捗

  • JSONライブラリJSONICの導入
  • android.content.Contextクラスを用いた端末上でのデータファイルIO

作業時間: 2.5h

端末ローカルへのデータ保存(JSON形式)

ネットワーク接続が不安定な状況を考えると、アプリのデータは端末ローカルとサーバ側の両方に保存するようにしたい。基本としては端末側にデータ保存を行い、定期的にサーバ側と同期をとるわけだ。

その際にローカル側に作成するデータファイルの形式について少し悩んだが、最終的にWeb APIで同期を行うことを考えるとやはりテキストデータであることが望ましい。テキストデータと言えばCSVXMLあたりが一般的と思うが、データとの親和性と取り扱いの容易さを考えると最近はもうJSONでいいだろう。

というわけでAndroidで使えるJSONライブラリを探してみた。下記サイトによれば、JSONICが一番シンプルで使いやすそうである。標準でもJSONObjectなるクラスが用意されているようだが、プリミティブすぎて取り扱いが面倒そうであった。

端末上でのファイルIO

android.content.ContextクラスのopenFileInput()、openFileOutput()を使用すれば、Android端末上のアプリローカルな領域に対するIO Streamが取得できる模様。

Androidでデータを簡単に保存する方法(ファイル入出力編) | Tech Booster

保存時と読み込み時のコードは下記のような感じ。はじめJSONからArrayListに復元できるかと思ったがうまくいかなかったのでToDoの配列を経由している。

public class LocalToDoProvider implements ToDoProvider {
    Context context;

    public LocalToDoProvider(Context context) {
        this.context = context;
    }

    @Override
    public ArrayList<ToDo> provideToDosOfUser(User user) {
        InputStream stream = null;
        ArrayList<ToDo> todoList = new ArrayList<ToDo>();

        try {

            String jsondata = FileUtils.readFileToString(this.context.getFileStreamPath("net.jibunstyle.kazokutodo.localcache"));

            ToDo[] todos = (ToDo[])JSON.decode(jsondata, ToDo[].class);
            todoList = new ArrayList<ToDo>(Arrays.asList(todos));
        } catch (IOException ex) {
            // TODO: handle
        } finally {
            try { stream.close(); } catch (Exception e) {}
        }
        return todoList;
    }

    @Override
    public void registerToDos(ArrayList<ToDo> todoList) {
        PrintWriter writer = null;
        try {
            writer = new PrintWriter(context.openFileOutput("net.jibunstyle.kazokutodo.localcache", Context.MODE_PRIVATE));
            writer.write(JSON.encode(todoList));
            writer.close();
        } catch (IOException ex) {
            try { writer.close(); } catch (Exception e) {}
        }

    }
}

これで端末側でデータを保存・復元できるようになった。後に実装する予定のWeb APIに渡す際は、このJSON文字列をそのまま投げつければ良い。

daggerでInjectするオブジェクトの生成時に引数を渡す

上記クラスはファイルIOのためにContextクラスが必要なため、コンストラクタで受け取るようにしている。ただ、このクラスはdaggerによってインスタンス化を行っているため、下記サイトを参考に、まずModuleクラスのコンストラクタでContextをわたし、provideメソッドにて目的クラスのインスタンスを生成する際にそのContextを渡すようにしている。

Daggerを触ってみた - ほげほげ(仮)

文章で書くと全くわからんな。要は下記のようになっている。

@Module(injects = KazokuToDoController.class)
public class KazokuToDoModule {
    private String type = "local";
    private Context context = null;

    public KazokuToDoModule(String type, Context context) {
        this.type = type;
        this.context = context;
    }

(中略)

    @Provides
    public ToDoProvider provideWishListProvider() {
        ToDoProvider provider = null;
        if ("net.jibunstyle.kazokutodo".equals(this.type)) {
            // TODO: 本物を返す
        } else {
            provider = new LocalToDoProvider(context);
        }
        return provider;
    }