なぜか動かない！OAuth連携するサイトの開発で意外と見落としがちなこと

# はじめに

TwitterなどとOAuth連携するWebサイトの開発でハマっているがあったので紹介します。


# OAuthとは

あるサービスにTwitterやFacebookのアカウントを用いてログインするときに、以下の様な確認画面が出てきて認証を行います。
OAuthはこれを支える裏側の認証フローの仕組みで、ユーザフレンドリなアカウント情報のやり取りを実現します。

【参考】OAuth画面
![aaaaaaaaa.png](https://qiita-image-store.s3.amazonaws.com/0/6463/468425d9-8f87-ae45-2779-56ee64997aa6.png)



そもそもOAuthってどうして必要なのかという点についてはこの辺りを参考に。
> http://www.ari-hiro.com/blog/2012/12/30/oauth2-summary

# 処理シーケンス

OAuthは複雑な処理フローに見てますが実際のブラウザの動きから抑えれば難しくないです。
トラブルシューティングする際はどこで問題が発生したかが重要な切り分け材料となるので、概念レベルで全体像を把握しておきます。

![OAuth2-Blog-Post-Sequence-Diagram.png](https://qiita-image-store.s3.amazonaws.com/0/6463/fe074a9b-85ab-5a1b-3d03-fc4053ae9ac5.png)

> 【参考】SECURE YOUR REST API WITH OAUTH2 IMPLICIT GRANT
> http://www.ibuildings.com/blog/2013/03/secure-your-rest-api-oauth2-implicit-grant


また、上記がとっつきにくい場合はこちらの記事の方が理解しやすくお勧めです。
> OAuth 2.0でWebサービスの利用方法はどう変わるか
> http://www.atmarkit.co.jp/fsmart/articles/oauth2/01.html



# ハマりどころ

## リダイレクトすると404や503エラー

Twitterの開発者メニューで登録するリダイレクトIPですが、開発環境のIPと本番環境で同じIPでは動きません。

例えば、開発環境では```127.0.0.1```で上手くいっても、本番環境では上手く動きません。
この```127.0.0.1```は自分自身を示すIPなので、本番サーバからリダイレクトされたIPが```127.0.0.1```だった場合、自分自身のローカルPCにアクセスしにいくため正しく動作しません。

開発が終了したら本番環境のサーバIPを登録しましょう。
その際、一度本番環境サーバのIPを設定すると開発機では動かなくなるので注意しましょう。

## **セッションが消える！**

### **ホスト名一致しない編**

リダイレクトのIP（ホスト名）と同一のIP(ホスト名）でログイン処理〜認証情報を取得処理を行う必要があります。
特に開発時は`localhost`で動作確認している方が多いと思いますが、`OAuth認証`の疎通テストではログイン画面とリダイレクト先のホスト名が異なってしまうと、同一セッションとみなされなくなり上手く動かないため注意です。
理由は同一セッションかどうかを判定するセッションIDは`Cookie`に保持され、その`Cookie`の値はホスト名の文字列に紐付いて管理されるためです。
DNSで解決したIP単位で保持されず、単純にURLバーの値に紐付いて保持されるので注意です。

以下のStackoverflowにも似たような質問が飛んでました。
> IP and domain create different session
> http://stackoverflow.com/questions/11836914/ip-and-domain-create-different-session

対応としては開発マシンのIPを登録した場合は、ログイン画面からそのIPで疎通確認を行います。
例えば、開発時にリダイレクト先URLのIPを`127.0.0.1`にした場合は`localhost`でログイン画面にアクセスするのではなく`127.0.0.1`でアクセスします。


### **プロトコルの壁編**
①HTTPS（自分のアプリ）→②HTTPS（認証処理）→③HTTP（自分のアプリ）だと、1⇔3間でセッションIDが変わってしまいます。
（セッションIDはCookieに保存されていますが、**Cookieの仕様で異なるプロトコルをまたがっての利用を許容しない**ため値が変わります）

そのため1のタイミングでセッションに認証情報を保持させても、3のタイミングでは別セッション扱いになるので1で載せた情報を取得できず、あたかもセッションが霧散したように見えます。

この理由に併せて、OAuthのサンプル実装では認証用のオブジェクトをセッションスコープで保持して連携する方式になっていることが多いので、サンプルコード通りに実装したのに連携が失敗するという現象が発生します。

Twitter4jを用いたServletのOAuthのサンプルコードは以下のサイトを参考に。
> GAE/JとTwitter4JでTwitterのOAuth
> https://zenjiro.wordpress.com/2010/06/19/gaej%E3%81%A8twitter4j%E3%81%A7twitter%E3%81%AEoauth/

HTTP⇔HTTPS間のセッションの話は以下を参考に。
>There is a reason that cookies are not allow to cross protocol boundaries - it is an attack vector! 
> http://stackoverflow.com/questions/4635425/tomcat-keep-session-when-moving-from-https-to-http

## 解決方法
最も確実でシンプルな対応方法は、**全ての通信をHTTPSで動くようにする**ことです。
HTTPS通信の設定方法は後述します。

ちなみにHTTPS通信の開始タイミングですが、ユーザ名/パスワードをPOSTするタイミングではなく入力フォームをGETさせる段階でHTTPSにすべきです。（つまりログイン画面からログアウト画面まで全てHTTPSで通信させる）

理由は下記に記載されています。
>ログイン画面がhttpsではない理由なんて無い！
>http://might1976.doorblog.jp/archives/51035103.html


## よく見かけたNG対応集

セキュリティ的にも実装的にも問題がある手法ですが、たまに暫定対応で突っ走ったコードを見ることが多かったので共有します。
全て自アプリケーションはHTTPプロトコルのみで動いている前提です。

### 1.アプリケーションスコープ
セッションではなくアプリケーションスコープに載せれば動いた！という声をたまに聞きますが、複数人が同時にOAuth認証でログインすると不具合を起こすのでNGです。
アプリケーションスコープを利用するとセッションスコープと違って値が消えない理由は、セッションIDがどのような値になってもアプリケーションスコープの値は常に取得可能だからです。

### 2.Cookieと組み合わせる
認証サーバ連携前のセッションIDを```Cookie```に入れて、連携後の処理でCookieからセッションIDを復元、セッションスコープやアプリケーションスコープに格納された認証情報を取得する方法も考えられますが、以下の理由で厳しいです。

#### 2.1. Cookie＆セッションスコープ連携 
標準のServletAPIでは自分のセッションIDに紐付かないセッションにアクセス出来ないので実装できません。


#### 2.2. Cookie＆アプリケーションスコープ連携
アプリケーションスコープに保持するためのキーを「適当なプレフィクス＋セッションID」にして紐付ける手法です。
セッションIDをキーにすることで一意性が担保出来るため表面上の動作は問題なく動きます。
が、HTTP通信が残るため万が一にも連携用のセッションIDが盗聴される可能性があるのでセキュリティホールになります。


# TomcatのHTTPS設定

TomcatでHTTPS通信を行わせる手法を説明します。

## 作業フロー

0. JDKの付属ツール```keytool```でSSL証明書を作成
0. Tomcatの定義ファイルである```server.xml```を編集
0. 動作確認その1
0. IDE（Eclipse）への環境の反映
0. 動作確認その2

以下の記事の```.keystore```ファイルの作成＋```server.xml```編集を参考にしました。
>サーブレット 11章　認証方式　―TECHSCORE
>http://www.techscore.com/tech/Java/JavaEE/Servlet/11-3/

## 環境確認

環境変数には以下が設定されていることとします。
```JAVA_HOME```と```JRE_HOME```と```CATALINA_HOME```の3つの変数です。
```JRE_HOME```はTomcatの起動時に必要です。

Windowsで環境変数を設定するときは、```Windowsキー``` ＋ ```Pause```でシステム画面を開いて、システム詳細設定＞環境変数(N)を選択し、各変数を設定します。

IDE（Eclipse）経由でしかTomcatを起動しない場合は、JAVA_HOMEの設定のみで問題無いです。
（その場合は.keystoreファイル作成後、Eclipse設定までスキップして下さい）


## .keystoreファイル作成

姓名、組織単位名、組織名、などはなんでも良いです。
\[いいえ\]の問には```y```を入力します。
最後の鍵パスワード入力無しでスキップ可能です。

```bat:.keystoreの生成
# コマンドの実行
"%JAVA_HOME%/bin/keytool" -genkey -keyalg RSA -alias example.com -keystore .keystore -storepass tomcat
姓名は何ですか。
 [Unknown]:  tomcat
組織単位名は何ですか。
 [Unknown]:
組織名は何ですか。
 [Unknown]:
都市名または地域名は何ですか。
 [tokyo]:  tokyo
都道府県名または州名は何ですか。
 [osaki]:  tokyo
この単位に該当する2文字の国コードは何ですか。
 [jp]:  jp
N=tomcat, OU=Unknown, O=Unknown, L=tokyo, ST=tokyo, C=jpでよろしいですか。
 [いいえ]:  y

www.test.com>の鍵パスワードを入力してください
       (キーストアのパスワードと同じ場合はRETURNを押してください):
```

このコマンドでカレントディレクトリに```.keystore```ファイルが生成されます。
目検で確認するか以下のコマンドdえ確認して下さい

```bat:JAVA_HOME版.keystore生成確認
$ dir /b "%JAVA_HOME%\.keystore"
.keystore
```

## 配備
生成された```.keystore```ファイルをTomcatのホームディレクトリに手動コピーします。

```bat:CATALINA_HOME版.keystoreコピー確認
$  dir /b "%CATALINA_HOME%\.keystore"
.keystore
```

## 設定

Tomcatの```server.xml```を編集します。
confフォルダ以下に```server.xml```は存在します。

編集内容は以下です。

* 8443ポート記載部分のコメントアウトを外して有効にします
* さらに```keystoreFile```と```keystorePass```の2属性を追加
keystorePassの設定値は先ほど設定したパスワードと同じものを設定します

```xml:server.xml
【消す→】    <!--
            <Connector port="8443" protocol="org.apache.coyote.http11.Http11NioProtocol"
                       maxThreads="150" SSLEnabled="true" scheme="https" secure="true"
                       clientAuth="false" sslProtocol="TLS"
                       keystoreFile=".keystore" keystorePass="tomcat"/> 【←ここ追加】
            -->　【←消す】　
```

## 起動

Tomcatを起動します。
```bat:Tomcatの起動
$ "%CATALINA_HOME%/bin/startup.bat"
```

以下のように8443のProtocolHandlerが起動するログが出ていれば上手く読み込めています。

```log:Tomcat起動ログ
24-Jun-2015 11:14:59.250 INFO [main] org.apache.coyote.AbstractProtocol.start Starting ProtocolHandler ["http-apr-8080"]
24-Jun-2015 11:14:59.260 INFO [main] org.apache.coyote.AbstractProtocol.start Starting ProtocolHandler ["http-nio-8443"]　【←これが出てればOK】
24-Jun-2015 11:14:59.267 INFO [main] org.apache.coyote.AbstractProtocol.start Starting ProtocolHandler ["ajp-apr-8009"]
```

## 確認

以下にURLにアクセスする。
アクセスURLの```http```が```https```に、ポートが```8443```なのに注意すること。

```sh:アクセスURL
https://${server_ip}:8443/
```

以下のようなマネージャー画面にログイン出来れば成功です。

![tomcataccess.png](https://qiita-image-store.s3.amazonaws.com/0/6463/db203c7c-c383-966a-88fa-75fe6638c205.png)

## Eclipse経由でTomcatを起動する場合の設定

EclipseのWTP(Web Tools Platform)で開発していると、前述の対応を実施してもEclipseからTomcatを起動した時に8443ポートが有効になっていません。
これに対応するには以下2つの対応が必要です。

### Serversのserver.xml書き換え

これは```Servers```のPJにTomcatの```server.xml```がコピーされているための措置です。
Tomcatのconfフォルダ以下と同様にこちらも8443部分のコメントを外すことと、```keystoreFile```と```keystorePass```の2属性を追加書き換えが必要です。

【参考】Servers Project(EclipseのWTPで自動生成されるPJ)
![eclipse_servers.png](https://qiita-image-store.s3.amazonaws.com/0/6463/aaa87926-f6ca-e388-9e6b-05bfdef69628.png)


【参考】Servers以下のserver.xmlもTomcat/conf以下と同様に編集する
![eclipse_編集例.png](https://qiita-image-store.s3.amazonaws.com/0/6463/c0283c99-5821-d4bb-1fe6-11e3937af2c5.png)


### EclipseのWSTプラグインに.keystoreファイルをコピー

更に```C:\XXX\pleiades\workspace\.metadata\.plugins\org.eclipse.wst.server.core\tmp0```以下に```.keystore```をコピーします。
これはEclipse経由でTomcatを起動すると、定義ファイルのロードがEclipseのメタデータのフォルダをロードする仕様だからです。

ちなみに、OS Xだと以下のパスになります。
```
/Users/<username>/Documents/workspace/.metadata/.plugins/org.eclipse.wst.server.core/tmp0/conf/keystore.key
```

以上でEclipse経由でTomcat Serverを立ち上げてもHTTPSによるアクセスが可能となります。

Eclipse経由でTomcatを起動して、ブラウザ経由でアクセス可能か確認して下さい。

```sh:アクセスURL
https://${server_ip}:8443/
```

以下参考
> Eclipse WTP: How do I enable SSL on Tomcat?
> http://stackoverflow.com/questions/951890/eclipse-wtp-how-do-i-enable-ssl-on-tomcat


## 本番環境への設定

ローカルでの開発環境と同様に```.keystore```ファイルを本番環境のサーバデプロイ ＋ ```server.xml```を編集して完了です。

# HTTPとHTTPSの混在サイトについて

ECの様にレスポンスタイムが命！というところはオープンに出来るページをHTTP通信にしてプロトコルを混在させているサイトもあります。
その場合は[【メモ】httpとhttpsの混在環境でクッキーを利用する - Qiita](http://qiita.com/sksmnagisa/items/0f48541f795beabe886e)で説明されているような処理を加えているものと考えられます。

とは言っても、おそらくサーバサイド ＋ クライアントサイドを合算して数十ms程度の節約にしかなら無いと思うので、相当神経質なサイト以外は対応の優先度は低いかなと思います。


# おしまい

OAuthを使うときはHTTPS通信が基本！
そうでなくても基本的にHTTPS通信しましょ！