OAuth 2.0 認証クライアントの簡単な実装例（何故かレシピ風）

# 今日の料理

19日の記事「[OAuth 2.0 認証のクライアントを作る時の注意点](http://qiita.com/chyiro/items/848171700d48e9542d02)」では、クライアント作る時の注意点を紹介しました。今回はその実践篇として、簡単なサンプルプログラムを紹介したいと思います。

というわけで、今日は Facebook にログインして、自分の誕生日を抽出するためのアクセストークンを取得する簡単な Java プログラムを作ります。

# 材料
* 言語環境
  * Java SE 8u66
* 追加ライブラリ
  * Apache Oltu 1.0.1
* サービス
  * Facebook
* IDE
  * IntelliJ IDEA 15.0.2

# 下準備
料理の前に、味付けのポイント（前回の話に出た注意点）を予めを把握しておきましょう。

## クライアントの処理対象リソースと操作内容
プログラムのリソース処理をユーザの誕生日情報取得だけなので、ここでは Facebook の Graph API を使用します[^graphapi]。実際必要となる scope は `user_birthday` になります[^profile]。

## 認可コードリダイレクト先の指定制限
Facebook Login のドキュメント[^login]によると、SDK を使用していないネイティブデスクトップアプリにログインが必要な場合、`https://www.facebook.com/dialog/oauth?client_id={app-id}&amp;redirect_uri={redirect-uri}`
の形でエンドポイントを指定することで、Facebook の用意したログイン画面[^loginforapp]を使用できます。

ただし、この場合の **redirect_uri** は `https://www.facebook.com/connect/login_success.html` に指定する必要があります。

さらに、デスクトップアプリの場合、**response_type** は `token` に指定しなければなりません。その影響で認証フローが変わるが詳細はあとで説明します。

## リフレッシュトークンのサポート有無と有効期限

Facebook はリフレッシュトークンをサポートしません。その代わりにアクセストークンの有効期間を延長するフローと、アクセストークンの有効性を検証するツール[^token_debugger]があります。

# 作り方

## 1）アプリ情報の登録
Facebook の開発者向けサイト [facebook for developers](https://developers.facebook.com/) に新規アプリを追加します。
デスクトップアプリに対応するプラットフォームはないので、advance setup で作成してから、アプリのサイドメニューから**設定**（英語表示時は**Settings**）を開いて以下の項目を設定しておきます。

* **Advanced** の内容
<img width="541" alt="Advenced の設定" src="https://qiita-image-store.s3.amazonaws.com/0/30021/430e4b2f-f6e4-c1ba-7d6a-b9fba51f9d2c.png">


* **Advanced** ＞ **Client OAuth Settings** の内容
<img width="541" alt="クライアントの OAuth 設定" src="https://qiita-image-store.s3.amazonaws.com/0/30021/00a1db27-3be3-dfc6-66a5-46141775be11.png">

アプリの Dashboard の App ID は接続時必要になるのでメモしておきます[^unused_secret]。
<img width="541" alt="キャプチャ3.PNG" src="https://qiita-image-store.s3.amazonaws.com/0/30021/fa205c40-5bc7-4c95-fae0-b4b65a2ef311.png">


## 2）Web ブラウザの準備
ログイン画面を出すには Web ブラウザが必要ですが、ここでは JavaFX の `WebView` を使用します。手間を省けるため、IntelliJ IDEA のプロジェクトテンプレート「JavaFX Application」そのまま使います。

デフォルトの構成をそのまま使います。プロジェクトはこんな感じです。
<img width="318" alt="プロジェクト構成" src="https://qiita-image-store.s3.amazonaws.com/0/30021/6e5c6c1b-d346-26f5-9a77-9441a7e3636b.png">

ただ、デフォルトのウィンドウサイズが小さくて不便なので、`Main` を少し変えます。

```java
    @Override
    public void start(Stage primaryStage) throws Exception{
        Parent root = FXMLLoader.load(getClass().getResource("sample.fxml"));
        primaryStage.setTitle("Hello OAuth"); // World じゃないよ
        primaryStage.setScene(new Scene(root)); // サイズはお任せで
        primaryStage.show();
    }
```

そして `Controller` に`WebView` 追加します。

```java
public class Controller {
    @FXML
    private WebView webView;

    @FXML
    private void initialize() {
        WebEngine engine = webView.getEngine();
        engine.getLoadWorker().stateProperty().addListener(newChangeListener());
        engine.load(buildLoginUrlString());
    }

    private ChangeListener<State> newChangeListener() { ... }  // あとで埋める
    private String buildLoginUrlString() { ... }  // あとで埋める
}
```

`sample.fxml` のバインディングはこんな感じで、`WebView` さえあれば十分です。サイズもここで入れておきます。

```xml
<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.scene.web.*?>
<?import javafx.scene.layout.*?>

<GridPane alignment="center" hgap="10" vgap="10" xmlns:fx="http://javafx.com/fxml/1" xmlns="http://javafx.com/javafx/8" fx:controller="sample.Controller">
   <children>
      <WebView prefWidth="1024" prefHeight="768" fx:id="webView"/>
   </children>
</GridPane>
```

## 3）ログイン用URL文字列の作成
上に説明したように、ログイン画面を出すために `https://www.facebook.com/dialog/oauth?client_id={app-id}&amp;redirect_uri={redirect-uri}`
の形でエンドポイントを使用します。その他必要なパラメータ **scope** と **response_type** も同じクエリに入れます。

リダイレクト先のURLをエンコードする必要があるので、ここでは自前で作らず、ライブラリに任せます。

```java
    private String buildLoginUrlString() {
        try {
            return OAuthClientRequest
                    .authorizationLocation(FACEBOOK_OAUTH_LOGIN_DIALOG)
                    .setClientId(CLIENT_ID)
                    .setRedirectURI(REDIRECT_URI)
                    .setResponseType("token")
                    .setScope("user_birthday")
                    .buildQueryMessage()
                    .getLocationUri();
        } catch (OAuthSystemException e) {
            throw new RuntimeException("ここでは考慮しない");
        }
    }
```

## 4) アクセストークン取得
えっ、いきなりアクセストークン？認可コードもなしで？
ええ、その通りです。

上の説明に出た **response_type** を `token` に指定する影響で、ユーザがログインしたあと、デスクトップアプリに返すのは認可コードではなくアクセストークンになります。

実際の返す方は、**redirect_uri** で指定したページにリダイレクトする際に、URLに付け加える形になります。形式は `YOUR_REDIRECT_URI#access_token={access_token}&expires_in={expires_in}` です。

`WebView` を初期化する際に、`ChangeListener` を指定するのはこの処理を実現するためです。その `ChangeListener` の実装はこんな感じです。

```java
    private ChangeListener<State> newChangeListener() {
        return (value, oldState, newState) -> {
            if (newState == State.SUCCEEDED) {
                handleUrlString(webView.getEngine().getLocation());
            }
        };
    }
```

`handleUrlString` は文字列解析のメソッドです。

```java
    private void handleUrlString(String urlString) {
        if (urlString.startsWith(REDIRECT_URI)) {
            // クエリから詳細を抽出
            String queryStr = urlString.replaceFirst(REDIRECT_URI + "#", "");
            Map<String, String> queryDetail = Arrays.stream(queryStr.split("&")).collect(
                    Collectors.toMap(
                            (String s) -> s.substring(0, s.indexOf("=")),
                            (String s) -> s.substring(s.indexOf("=") + 1)
                    ));

            if (queryDetail.containsKey("access_token")) {
                String token = queryDetail.get("access_token");
                String expire = queryDetail.get("expires_in");
                System.out.println("access_token:\n" + token);
                System.out.println("expires_in: (sec)\n" + expire);
            } else {
                throw new IllegalStateException("ここでは考慮しない");
            }
        }
    }
```

# できあがり
実際表示されたログイン画面です。
<img width="576" alt="ログイン画面" src="https://qiita-image-store.s3.amazonaws.com/0/30021/07a0b364-68f5-dd2e-f219-6be37e5e8cea.png">

ログインしたあと、情報へのアクセスの許可が求められます。画面が小さい？そんなこと知りません～
<img width="576" alt="キャプチャ.PNG" src="https://qiita-image-store.s3.amazonaws.com/0/30021/f6dc8007-4c3c-b1d5-f971-7ea4e4dea0bc.png">

実行した IntelliJ IDEA のコンソールにアクセストークンと有効期間が出力されます。
<img width="508" alt="console.PNG" src="https://qiita-image-store.s3.amazonaws.com/0/30021/2bd99fe0-3cc3-2d4c-5f55-44a128ef2654.png">

# 試食
取得したトークンを [Graph API Explorer](https://developers.facebook.com/tools/explorer/) で試します。
アプリケーション、Access Token、使用 API と結果フィールドを指定して、**Submit** を押してたら、
<img width="686" alt="apiexp.PNG" src="https://qiita-image-store.s3.amazonaws.com/0/30021/e5bf5f1e-619a-1aaf-3f18-9f9fb70edfb3.png">
誕生日の日付を抽出できました。なんと、抽出した日付が今日と一緒ですね！誕生日おめでとう！

[^graphapi]: Facebook のドキュメントに Atlas API と Marketing API があるが、アーキテクチャ上は Graph API の一部ようです。
[^profile]: ユーザ公開プロフィールにアクセスするための `public_profile` は自動的に付与されます。
[^unused_secret]: 今回のサンプルは使用する認証フローの関係上 App Secret を使用していません。
[^login]: [Manually Build a Login Flow](https://developers.facebook.com/docs/facebook-login/manually-build-a-login-flow) です。ドキュメントは英語しか無いみたいです。
[^loginforapp]: [Facebook Login for Apps](https://developers.facebook.com/docs/facebook-login/overview#logindialog) のことです。
[^token_debugger]: [Access Token Debugger](https://developers.facebook.com/tools/debug/accesstoken/) のことです。