[Qiita] QiitaにおけるOAuth認証の流れ（PhoneGap/Cordova版）

Qiitaが用意しているAPIでは**v2からOAuth認証をサポート**しています。
公式にもAPIのドキュメントがありますが、具体的な手順が分からない人のためにOAuth認証の実装の仕方をまとめてみました。
ここではPhoneGap/Cordovaを用いた時の実装を示しますが、他の環境・プログラミング言語で実装することを考えている人でも、実装の流れなどは参考になるかもしれません。

# OAuth認証とは

OAuthってなんぞやって言う方は、既に以下のような記事があるので参照してみてください。

* [Basic認証とOAuth](http://qiita.com/r7kamura/items/69904137ea20b6b86822)
* [モバイルアプリのユーザ認証方法についてまとめてみた](http://qiita.com/ledmonster/items/0ee1e757af231aa927b1)

# OAuth認証実装の流れ

最初にQiitaにおけるOAuth認証の流れを示します。

## アプリケーションの登録  

Qiitaの設定画面から「アプリケーション」-「登録中のアプリケーション」と進み、以下のフィールドを入力します。

 * アプリケーションの名前
 * アプリケーションの説明
 * WebサイトのURL
 * **リダイレクト先のURL**  

この中で最も重要であるリダイレクト先のURLには、ユーザがOAuth認証でアプリを認可した時に自動的に移動するURLを指定します。
その他の入力項目はOAuth認証の実装にそこまで影響を与えないので、適宜入力していきます。

入力した内容を保存すると、 *Client ID* と *Client Secret* が得られるのでこれらをメモしておきます。

## OAuth認可画面を開く
OAuth認可画面のURLへ移動します。
OAuth認可画面のURLや指定するパラメータを以下に示します。

|パラメータ|説明|
|----|----|
|URL|https://qiita.com/api/v2/oauth/authorize|
|client_id|「アプリケーションで取得」した *Client ID* |
|scope|アプリからQiitaに対して許可する操作<br>複数指定する場合は値の間に+を入れる|
|state|ユーザがアプリを認可した時の移動先URLにパラメータとして設定される16進数の値<br>認可されたことを確認するために使用する|

*scope*の具体的な値には以下のようなものがあります。
ここで例えば、Qiitaへの読み込み書き込みを許可する認可画面を表示したい場合は、`scope=read_qiita+write_qiita`とします。

|値|説明|
|---|---|
|read_qiita|Qiitaからの読み込みを許可します|
|write_qiita|Qiitaへの書き込みを許可します|
|read_qiita_team|Qiita:Teamからの読み込みを許可します|
|write_qiita_team|Qiita:Teamへの書き込みを許可します|

*client_id*が`my_client_id`、*scope*が`read_qiita`、*state*が`FEDCBA9876543210`であった場合の認可画面のURLは以下のようになります。
`https://qiita.com/api/v2/oauth/authorize?client_id=my_client_id&scope=read_qiita&state=FEDCBA9876543210`

## アプリの認可を検知

ユーザがアプリを認可すると、「アプリケーションの登録」で設定したリダイレクトURLに移動します。
またリダイレクトURLのパラメータには以下の値が設定されます。

|パラメータ|説明|
|---|---|
|state|「OAuth認可画面」でパラメータとして設定したstateの値|
|code|アクセストークンを取得するために必要な値|

*state*の値を**「OAuth認可画面」でパラメータとして設定したstateの値と比較**することで、認可が正しく行われたことを確認することができます。

## アクセストークンの取得
アクセストークンを取得します。
アクセストークンの取得はアクセストークンを取得するためのURLに対して以下のパラメータをつけてPOST送信するだけです。


|パラメータ|説明|
|---|---|
|URL|https://qiita.com/api/v2/access_tokens|
|client_id|「アプリケーションの登録」で取得した *Client ID* |
|client_secret|「アプリケーションの登録」で取得した *Client Secret* |
|code|「アプリの認可の検知」で取得した *code* |

POST送信して認証に成功した場合、以下のパラメータが返されます。

|パラメータ|説明|
|---|---|
|access_token|アクセストークン|

## Qiita APIを利用する
「アクセストークンの取得」で得たアクセストークンを使うことで、認証が必要なQiita APIを利用することが可能になります。
Qiita APIの場合、**HTTPリクエストヘッダにアクセストークンを含める必要がある点**に注意が必要です。

# OAuth認証の実装（PhoneGap/Cordova）

OAuth認証の実装の仕方をPhoneGap/Cordovaを例にして説明します。

## 認可処理

PhoneGapでは認可画面を開くために、**InAppBrowser**プラグインを利用します。
InAppBrowserプラグインがインストールされていない場合はインストールしましょう。

```shell-session
 $ cordova plugin add org.apache.cordova.inappbrowser`
```

最初に、OAuth認可画面のURLを開きます。

```js
cb = window.open(url, "_blank", 'location=no,toolbar=no,clearcache=yes,clearsessioncache=yes');
```

認可画面を開くだけならこれだけでも良いのですが、このままだとInAppBrowserを閉じることができません。
このためページが読み込まれるたびにURLを確認して、アプリが認可されたことを確認したらブラウザを閉じる処理を追加します。
具体的には、Qiitaで設定したリダイレクトURLと一致し、かつパラメータ *state* の値が認証画面を開くときに指定した *state* と一致していることで確認します。

```js
cb.addEventListener('loadstart', function(e) {
    var loc = e.url;    // 読み込みを開始したURL
    // リダイレクトURLへ飛んだことを確認する
    if (loc.indexOf(redirectURL) != -1) {
        // パラメータ取得
        var params = getQueryParams(loc);
        // stateの整合性を確認
        if (params['state'] != state) {
            alert("not match state parameter.");
            return;
        }
        cb.close();
    }
});
```

## アクセストークンの取得

取得したパラメータをアクセストークンを取得ためのURLに対して、パラメータを設定してPOST送信します。
POST送信が成功した場合、アクセストークンが返ってきます。
返ってきたアクセストークンを用いることで、認証を必要とするQiita APIを利用することができるようになります。

```js
$.ajax({
    url: 'https://qiita.com/api/v2/access_tokens',
    type: 'POST',
    headers: {
        'Content-Type': 'application/json',
    },
    dataType: 'json',
    data: JSON.stringify(params),
    success: function(res) {
        // アクセストークン取得成功。
        // res['token']にアクセストークンが保存されている。
    },
    error: function(e) {
        alert("Failed to get access token");  // 認証失敗
    }
});
```

## APIの利用

アクセストークンを取得できたので、Qiitaが提供するAPIを使ってみましょう。
ここではアプリを認可したユーザ名を取得する例を示しています。
**HTTPリクエストヘッダに取得したアクセストークンを指定する**ことで、アプリを認可したユーザ名を取得することができます。

```js
$.ajax({
    url: 'https://qiita.com/api/v2/authenticated_user',
    type: 'GET',
    headers: {
        'Content-Type': 'application/json',
        'Authorization': 'Bearer ' + authToken,   // アクセストークン
    },
    dataType: 'json',
    success: function(res) {
        // 取得成功
        // res['id']にアプリを認可したユーザが保存されている。
    },
    error: function(e) {
        alert("Failed to get authenticated user ID");   // 取得失敗
    }
});
```

# OAuth認証のサンプル

以上の実装の流れからOAuth認証のサンプルを示します。
サンプルのソースコード内にコメントを書いているので、詳細な説明は省きます。
なお、以下の文字列は適宜自分が利用するものに置き換えてください。

|文字列|説明|
|---|---|
|APP_CLIENT_ID|「アプリケーションの登録」で取得した *Client ID* |
|APP_CLIENT_SECRET|「アプリケーションの登録」で取得した *Client Secret* |
|APP_PERMISSION_SCOPE|「OAuth認可画面」で設定する *scope*の値 |
|APP_REDIRECT_URL|「アプリケーションの登録」で設定した *リダイレクト先のURL* |
|APP_REDIRECT_STATE|「アプリケーションの登録」で取得した *State* |

```js:oauth.js
var QiitaOAuth = (function() {
    var obj = function(){};
    obj.prototype = {
        init: function() {
            // 認証向けのパラメータ
            this.authURL = 'https://qiita.com/api/v2/oauth/authorize';   // 認証用URL
            this.clientID = APP_CLIENT_ID;                               // Client ID
            this.clientSecret = APP_CLIENT_SECRET;                       // Client Secret
            this.redirectURL = APP_REDIRECT_URL;             // 認可時のリダイレクト先のURL
            this.scope = APP_PERMISSION_SCOPE;                       // アクセス範囲
            this.state = APP_REDIRECT_STATE;                             // 認可時に正当性を確認するための値
        
            this.cb = undefined;
        },
        login: function(successCallback) {
            // 認可画面を開く
            var url = this.getAuthURL();
            this.cb = window.open(url, "_blank", 'location=no,toolbar=no,clearcache=yes,clearsessioncache=yes');
            this.loginSuccess = successCallback;    // 認可が完了した時に呼ばれる関数
            this.redirectParams = undefined;        // アクセストークンを取得するために必要なパラメータ
        
            var self = this;
            // アプリを認可したイベントを捕捉するため、ページ読み込み開始のイベントを捕捉するように設定
            self.cb.addEventListener('loadstart', function(e) {
                var loc = e.url;    // 読み込みを開始したURL
                // リダイレクトURLへ飛んだことを確認する
                if (loc.indexOf(self.redirectURL) != -1) {
                    // パラメータ取得
                    var params = getQueryParams(loc);
                    // stateの整合性を確認
                    if (params['state'] != self.state) {
                        alert("not match state parameter.");
                        return;
                    }
                    self.cb.close();
                    self.redirectParams = {
                        'client_id': self.clientID,
                        'client_secret': self.clientSecret,
                        'code': params['code']
                    };
                    self.loginSuccess(self.redirectParams);
                }
            });
        },
        // 認証用URLの作成
        getAuthURL: function() {
            var url;
            url = this.authURL + "?" + $.param({
                client_id: this.clientID,
                scope: this.scope,
                state: this.state
            });
            return url;
        }
    };
    return obj;
})();


// URLからパラメータを取得する関数
function getQueryParams(url)
{
    var params = {};
    var queryStart = url.indexOf('?');

    // パラメータが存在しない場合
    if (queryStart == -1) { return params; }

    var query = url.substring(queryStart + 1);
    var p = query.split('&');

    // パラメータを取得
    for (var i = 0; i < p.length; ++i) {
        var elm = p[i].split('=');
        var key = decodeURIComponent(elm[0]);
        var value = decodeURIComponent(elm[1]);
        params[key] = value;
    }

    return params;
}

// アプリが認可されたときに呼ばれる、アクセストークンを取得する関数
function onOAuthSuccess(params)
{
    $.ajax({
        url: 'https://qiita.com/api/v2/access_tokens',
        type: 'POST',
        headers: {
            'Content-Type': 'application/json',
        },
        dataType: 'json',
        data: JSON.stringify(params),
        success: function(res) {
            // 取得成功
            getAuthUser(res['token']);
        },
        error: function(e) {
            // 取得失敗
            alert("Failed to get access token");
        }
    });
}

// アプリを認可したユーザを取得する関数
function getAuthUser(authToken)
{
    $.ajax({
        url: 'https://qiita.com/api/v2/authenticated_user',
        type: 'GET',
        headers: {
            'Content-Type': 'application/json',
            'Authorization': 'Bearer ' + authToken,   // 取得したアクセストークンを指定
        },
        dataType: 'json',
        success: function(res) {
            // 取得成功
            authUser = res['id'];
        },
        error: function(e) {
            // 取得失敗
            alert("Failed to get authenticated user ID");
        }
    });
}

var qoa = new QiitaOAuth();
qoa.init();
qoa.login(onOAuthSuccess);
```

# おわりに

実装が面倒そうに思えるOAuth認証ですが、手続きの流れがわかってしまえば実装は簡単です。
今後OAuth認証を実装される方にとってこの記事が参考になれば幸いです。

# 参考情報

* [Basic認証とOAuth](http://qiita.com/r7kamura/items/69904137ea20b6b86822)
* [モバイルアプリのユーザ認証方法についてまとめてみた](http://qiita.com/ledmonster/items/0ee1e757af231aa927b1)
* http://www.adobe.com/jp/devnet/phonegap/articles/phonegap-apps-powered-by-developercom.html