PythonでCLIからOAuth2を利用してQiitaのアクセストークンを取得してみた

Qiita APIのCLIツールを作成しているときに、アクセストークンの設定方法どうしようかなぁと悩んで、OAuth2つかってみようと思いたって試してみました。


### 結論

OAuth2を利用してアクセストークンを取得することはできたのですが、クライアントシークレットという暗号キーを含める必要があったので、よろしくないなぁと思いました。


## 実装

GitHubにもソースをアップしていますので、ご参考ください。
https://github.com/kai-kou/qiita-oauth2-with-python-cli

以下の記事でFaceBookでOAuth2を利用した方法を説明されていたので、それを参考にさせてもらいました。

ひとことでまとめると「ローカルにHTTPサーバたてて、リダイレクトを受ける。」です。

LOGGING IN TO FACEBOOK OAUTH2 VIA COMMAND LINE USING PYTHON
https://www.pmg.com/blog/logging-facebook-oauth2-via-command-line-using-python/


QiitaのOAuth2認証については公式ドキュメントが参考になります。

Qiita API v2ドキュメント - Qiita:Developer
https://qiita.com/api/v2/docs#%E8%AA%8D%E8%A8%BC%E8%AA%8D%E5%8F%AF


Qiita APIへアクセスするのに```requests``` を利用しています。あとは標準ライブラリだけで実現できます。

```sh
> mkdir 任意のディレクトリ
> cd 任意のディレクトリ
> python -m venv venv
> . venv/bin/activate

> pip install requests
> touch qiita_api_auth.py
> touch main.py
```

Qiitaでの認証・認可後にレダイレクトを受け付けるHTTPサーバの実装です。```BaseHTTPRequestHandler``` クラスの```do_GET``` メソッドでGETを受け付けて、URLに含まれる```code``` を取得します。取得できたらQiita APIを利用してアクセストークンを作成します。

```python:qiita_api_auth.py
from http.server import BaseHTTPRequestHandler, HTTPServer

from urllib.request import urlopen, HTTPError

from webbrowser import open_new

import requests

import json

import random, string

QIITA_API_BASE_URL = 'https://qiita.com/api/v2/'


class HTTPServerHandler(BaseHTTPRequestHandler):
  """
  QiitaのOAuth2認証でリダイレクトを受け入れるHTTPサーバ
  """
  def __init__(self, request, address, server, client_id, client_secret):
    self._client_id = client_id
    self._client_secret = client_secret
    super().__init__(request, address, server)


  def do_GET(self):
    self.send_response(200)
    self.send_header('Content-type', 'text/html')
    self.end_headers()

    # リダイレクトURLからコードが取得できたらアクセストークンを取得する
    if 'code' in self.path:
      params = self.path.split('&')
      code = params[0].replace('/?code=', '')
      state = params[0].replace('state=', '')
      url = f'{QIITA_API_BASE_URL}access_tokens'
      headers = {'Content-Type': 'application/json'}
      params = {
        'client_id': self._client_id,
        'client_secret': self._client_secret,
        'code': code
      }
      response = requests.request(
        method='POST',
        url=url,
        headers=headers,
        data=json.dumps(params))
      self.wfile.write(bytes('<html><h1>Please close the window.</h1></html>', 'utf-8'))
      self.server.access_token = None
      if response.status_code == 201:
        self.server.access_token = response.json()['token']


class QiitaAccessTokenHandler:
  """
  QiitaのOAuth2認証を利用してアクセストークンを取得するクラス
  """
  def __init__(self, client_id, client_secret, scope=['read_qiita', 'write_qiita']):
    self._client_id = client_id
    self._client_secret = client_secret
    self._scope = '+'.join(scope)


  def get_access_token(self):
    state = self._randomname(40)
    access_url = f'{QIITA_API_BASE_URL}oauth/authorize?client_id={self._client_id}&scope={self._scope}&state={state}'
    open_new(access_url)
    httpServer = HTTPServer(
      ('localhost', 8080),
      lambda request, address, server: HTTPServerHandler(
        request, address, server, self._client_id, self._client_secret))
    httpServer.handle_request()
    return httpServer.access_token


  def _randomname(self, n):
    randlst = [random.choice(string.ascii_letters + string.digits) for i in range(n)]
    return ''.join(randlst)

```

上記クラスを呼び出す実装です。Qiitaでアプリケーションを作成し生成された```Client ID``` と```Client Secret``` を環境変数に設定して利用しています。

```python:main.py
import os

from qiita_api_auth import QiitaAccessTokenHandler


def get_access_token(init=False):
  client_id = os.getenv('QIITA_CLIENT_ID')
  client_secret = os.getenv('QIITA_CLIENT_SECRET')

  # OAuth2認証でアクセストークンを取得する
  token_handler = QiitaAccessTokenHandler(client_id, client_secret)
  return token_handler.get_access_token()


if __name__ == '__main__':
  access_token = get_access_token()
  print(f'アクセストークンとれたよー: {access_token}')
```

## 認証してみる

### Qiitaでアプリケーションを作成する

Qiitaにログインした状態で、下記URLへアクセスして、アプリケーションを登録します。

https://qiita.com/settings/applications/new

<img width="1062" alt="スクリーンショット 2018-11-10 12.04.06.png" src="https://qiita-image-store.s3.amazonaws.com/0/48549/05c746ec-f82e-375a-10b7-86590a475f2d.png">

#### アプリケーションの名前

任意で入力してください。

#### アプリケーションの説明

必須ではありませんが、認可時に表示されますので、どういったアプリ・サービス・ツールなのか明記しておくとわかりやすいですね。

#### WebサイトのURL

アプリ・サービス・ツールのサイトがあればそのURLを指定します。
なければ、とりあえずQiitaのマイページのURLでも設定しましょう。

#### リダイレクト先のURL

認可後に、Qiitaからリダイレクトする際のURLになります。
ローカルにHTTPサーバを立ち上げるので、そのURLを指定します。

- リダイレクト先のURL: ```http://localhost:8080```

#### クライアント情報の取得と設定

アプリケーションが作成後、アプリケーション一覧にある「編集ボタン」から編集ページを開くと、```Client ID``` と```Client Secret``` が表示されているのでそれを利用します。
<img width="1060" alt="スクリーンショット_2018-11-10_12_08_57.png" src="https://qiita-image-store.s3.amazonaws.com/0/48549/f67444c8-263b-d637-6c7e-44a0271decd9.png">

**```Client ID``` と```Client Secret``` は他人に知られてしまうと、悪用される恐れがありますので、取扱にはご注意ください。**

環境変数に```Client ID``` と```Client Secret``` を設定します。

```sh
# bash
> export QIITA_CLIENT_ID=<Client ID>
> export QIITA_CLIENT_SECRET=<Client Secret>

# fish
> set -x QIITA_CLIENT_ID <Client ID>
> set -x QIITA_CLIENT_SECRET <Client Secret>
```

### 実行する

実行すると、Qiitaの認証ページがブラウザで表示されます。
<img width="1059" alt="スクリーンショット 2018-11-10 12.14.39.png" src="https://qiita-image-store.s3.amazonaws.com/0/48549/d9c3e1c9-b23b-c7e3-4a49-77e71e6de63e.png">

「許可する」をクリックすると、先程設定したリダイレクト先のURLへリダイレクトします。ローカルでHTTPサーバを立ち上げているので、そこでURLに含まれるquery文字列から```code``` が取得できます。

取得した```code``` と環境変数に設定した```Client ID``` と```Client Secret``` をパラメータにしてQiita APIからアクセストークンが取得できます。

```sh
> python main.py
```
<img width="827" alt="スクリーンショット_2018-11-10_12_15_20.png" src="https://qiita-image-store.s3.amazonaws.com/0/48549/d1facdfe-f5e8-1ec2-713a-b1f6b9c49f6b.png">
やったぜ。

## まとめ

HTTPサーバをローカルで立ち上げてしまえば、リダイレクト先を簡単に用意できることが確認できました。
ただCLIツールなどで利用する場合、```Client Secret``` をソースに含める必要があるため、アプリなどと違い、非常に簡単に参照できてしまいます。勝手にIDとSecretを利用・悪用されるリスクを考えると、一般公開するようなツールだと利用は難しいところです。
検証や個人ツールでアクセストークンを作成するのが面倒なときに利用するのが良いかもしれませんね。

OAuth クライアント シークレット（Facebook）の漏えい - Google ヘルプ
https://support.google.com/faqs/answer/7126515?hl=ja

security - OAuth2のclient_secretは、漏れると何がマズいか？　また、どうやって隠すか？ - スタック・オーバーフロー
https://ja.stackoverflow.com/questions/26469/oauth2%E3%81%AEclient-secret%E3%81%AF-%E6%BC%8F%E3%82%8C%E3%82%8B%E3%81%A8%E4%BD%95%E3%81%8C%E3%83%9E%E3%82%BA%E3%81%84%E3%81%8B-%E3%81%BE%E3%81%9F-%E3%81%A9%E3%81%86%E3%82%84%E3%81%A3%E3%81%A6%E9%9A%A0%E3%81%99%E3%81%8B


## 参考

LOGGING IN TO FACEBOOK OAUTH2 VIA COMMAND LINE USING PYTHON
https://www.pmg.com/blog/logging-facebook-oauth2-via-command-line-using-python/

21.22. http.server — HTTP サーバ &#8212; Python 3.6.5 ドキュメント
https://docs.python.jp/3.6/library/http.server.html

BaseHTTPServer – web サーバを実装するベースクラス
http://ja.pymotw.com/2/BaseHTTPServer/

Pythonで簡単にHTTPサーバを作る
https://qiita.com/shinido/items/b4fdc907a37424bcf15b

デバッグ用HTTP ServerをPythonで起動する
https://ishiis.net/2016/10/10/python-http-server/

OAuth クライアント シークレット（Facebook）の漏えい - Google ヘルプ
https://support.google.com/faqs/answer/7126515?hl=ja

security - OAuth2のclient_secretは、漏れると何がマズいか？　また、どうやって隠すか？ - スタック・オーバーフロー
https://ja.stackoverflow.com/questions/26469/oauth2%E3%81%AEclient-secret%E3%81%AF-%E6%BC%8F%E3%82%8C%E3%82%8B%E3%81%A8%E4%BD%95%E3%81%8C%E3%83%9E%E3%82%BA%E3%81%84%E3%81%8B-%E3%81%BE%E3%81%9F-%E3%81%A9%E3%81%86%E3%82%84%E3%81%A3%E3%81%A6%E9%9A%A0%E3%81%99%E3%81%8B