Python でシンプルに OAuth 2 する (urllib + oauthlib)

Python から OAuth 2 な API を利用する際に、`urllib` + `oauthlib` を使った場合の具体的なやり方がググっても出てこなかったので、自分でやってみた。

今回は例として [Netatmo API](https://dev.netatmo.com/apidocumentation/oauth) について記述しているが、OAuth 2 に対応した API であればやり方はほとんど変わらないはず。

## なぜ urllib + oauthlib か
`urllib` は HTTP リクエストを送るライブラリとしてよくまとまっていて、`oauthlib` は OAuth に関する処理を行うライブラリとしてよくまとまっていて、Python で OAuth 2 クライアントを扱うならこの2つを組み合わせるのが最もシンプルだと思ったから。

ただし Simple ≠ Easy なので、Easy を求める人は [Requests-OAuthlib](https://requests-oauthlib.readthedocs.io/en/latest/index.html) あたりを使ったらいいと思う。

参考： [Python の HTTP クライアントは urllib\.request で十分 \- Qiita](https://qiita.com/hoto17296/items/8fcf55cc6cd823a18217)

## 事前準備
`oauthlib` は標準モジュールではないのでインストールしておく。

``` console
$ pip install oauthlib
```

全体を通して `urllib` と `json` を使うので import しておく。

``` python
import urllib
import json
```

認証のために必要な情報を入手しておく。

``` python
NETATMO_API_CLIENT_ID = 'xxxxxxxxxxxxxxxx'
NETATMO_API_CLIENT_SECRET = 'xxxxxxxxxxxxxxxx'
NETATMO_API_SCOPE = ['read_station']
NETATMO_API_REDIRECT_URL = 'http://localhost/callback'
```

今回は `GET /getmeasure` API を利用したいだけなので scope は `read_station` さえあればよい。

## Access Token を取得する
まずは `WebApplicationClient` オブジェクトを作る。

``` python
from oauthlib.oauth2 import WebApplicationClient

oauth = WebApplicationClient(NETATMO_API_CLIENT_ID)
```

次に、Authorization Code を取得するための URL を生成する。

``` python
url, headers, body = oauth.prepare_authorization_request('https://api.netatmo.com/oauth2/authorize', redirect_url=NETATMO_API_REDIRECT_URL, scope=NETATMO_API_SCOPE)
print(url)  
```

ユーザにアクセス許可を求めるための URL が表示される。

Web アプリケーションであればユーザをこの URL にリダイレクトさせて、ユーザが accept すれば Authorization Code が指定した Redirect URL に飛んでくる。

ここでは仮に、Redirect URL に以下のようなパラメータ付きのアクセスが来たものとして進める。

``` python
authorization_response = 'https://localhost/callback?state=xxxxxxxx&code=xxxxxxxx'
```

Authorization Code が得られたので、次はこれを用いて Access Token を取得する。

``` python
url, headers, body = oauth.prepare_token_request('https://api.netatmo.com/oauth2/token', authorization_response=authorization_response, client_secret=NETATMO_API_CLIENT_SECRET)
req = urllib.request.Request(url, body.encode(), headers=headers)
with urllib.request.urlopen(req) as res:
    oauth.parse_request_body_response(res.read())
```

`oauth.prepare_token_request` では Access Token を取得するためのリクエストパラメータなどを準備してくれる。このとき `authorization_response` から Authorization Code を自力で parse する必要はなく、そのまま引数で渡すだけでいい。

Netatmo API の場合はここで Client Secret を指定する必要があるが、ほとんどの API で同様だと思う。

Access Token 取得リクエストの結果はそのまま `oauth.parse_request_body_response` に渡せば `oauth.access_token` などに勝手に設定してくれる。

これで認証が完了した。

## Access Token を使って API にアクセスする
`GET /getmeasure` にアクセスしてみる。
https://dev.netatmo.com/apidocumentation/weather#getmeasure

``` python
columns = ['temperature', 'co2', 'humidity', 'pressure', 'noise']
params = {
    'date_begin': 1577836800,  # 2020-01-01 00:00:00 UTC
    'device_id': 'xx:xx:xx:xx:xx:xx',
    'scale': '30min',
    'type': ','.join(columns),
    'optimize': 'false',
}
url = 'https://api.netatmo.com/api/getmeasure?' + urllib.parse.urlencode(params)
url, headers, body = oauth.add_token(url)
req = urllib.request.Request(url, headers=headers)
with urllib.request.urlopen(req) as res:
    data = json.load(res)
```

`oauth.add_token` で得られる `headers` には `Authorization` ヘッダが追加されている。

## Access Token を更新する
Netatmo API は Access Token の有効期限が3時間と短いため、これを超える場合は Refresh Token を用いて Access Token を更新する必要がある。

``` python
url, headers, body = oauth.prepare_refresh_token_request('https://api.netatmo.com/oauth2/token', client_id=NETATMO_API_CLIENT_ID, client_secret=NETATMO_API_CLIENT_SECRET)
req = urllib.request.Request(url, body.encode(), headers=headers)
with urllib.request.urlopen(req) as res:
    oauth.parse_request_body_response(res.read())
```

`oauth.prepare_refresh_token_request` では Access Token を更新するためのリクエストパラメータなどを準備してくれる。(`oauth` 内部で保持している Refresh Token などを自動でリクエストパラメータとして設定してくれる)

Netatmo API の場合、ここで Client ID と Client Secret を追加で指定する必要がある。