Python requestsモジュール：`auth`引数がない場合、`.netrc`の認証情報を参照する


# 環境
* Python3.6.5
* requestsモジュール 2.19.1
* Windows10


# やりたいこと
Pythonのrequestsモジュールを使って、認証が必要なサイトにアクセスしたいです。
アクセスしたいサイト（以下、サイトA）での認証は、以下の手順で行われます。

1. クライアントはサーバに、ログイン用のリクエストを投げる。ログイン用のリクエストにはユーザIDとパスワードを渡す。  
⇒ サーバからトークンを受け取る。
2. クライアントはサーバに、認証が必要なリクエストを投げる。その際`Authorization`ヘッダにトークンを設定する。  
⇒ サーバからレスポンスを受け取る。


# 実施したこと

## `Authorization`ヘッダの設定
手順2の処理は、以下のようなコードを書きました。

```python
import requests
# サイトAのURL
sample_url = "https://example.com"
sample_token = "SAMPLE_TOKEN"
res = requests.get(sample_url, headers={"Authorization": sample_token})
```

## ユーザIDとパスワードを`.netrc`に記載
サイトAのユーザIDとパスワードは、`%USERPROFILE%\.netrc`に書きました。

`.netrc`はfptコマンドのための設定ファイルです。
このファイルにユーザIDとパスワードを書いておくと、fptコマンドを実行するときに自動でログインされます。https://linuxjm.osdn.jp/html/netkit/man5/netrc.5.html
また、fptコマンドだけでなくgitコマンドでも自動ログインされます。
https://qiita.com/azusanakano/items/8dc1d7e384b00239d4d9

Pythonには、標準モジュールに`.netrc`ファイルを扱う[netrc](https://docs.python.org/ja/3/library/netrc.html)モジュールがあります。
パスワードをファイルに保存する場合、`.netrc`でなく、自分で指定した設定ファイルに書くという選択肢もありましたが、なんとなく`.netrc`の方が扱いやすいと思い、`.netrc`を選択しました。[^1]

[^1]: パスワードを平文で保存しているこ書いていていることには、目をつぶってください…


`.netrc`ファイルの中身と、`netrc`を読み込むPythonのコードです。

```plain:.netrc
machine example.com
login xxxxxx
password yyyyyy
```

```python
netrc_hosts = netrc.netrc().hosts
host = netrc_hosts['example.com']
user_id = host[0]
password = host[2]

login(user_id, password)
```

# 問題：Authorizationヘッダが設定されない
トークンを`Authorization`ヘッダに設定しているのにもかかわらず、認証エラーが発生しました。
リクエストのAuthorizationヘッダを確認したところ、 `'Basic eHh4eHh4Onl5eXl5eQ=='`という値で、`"SAMPLE_TOKEN"`ではありませんでした。

```python
import requests
# サイトA
sample_url = "https://example.com"
sample_token = "SAMPLE_TOKEN"

##### `.netrc`にユーザ情報が記載されているとき
res = requests.get(sample_url, headers={"Authorization": sample_token})
print(res.request.headers['Authorization'])
# ⇒ 'Basic eHh4eHh4Onl5eXl5eQ=='

##### `.netrc`にユーザ情報が記載されていないとき
res = requests.get(sample_url, headers={"Authorization": sample_token})
print(res.request.headers['Authorization'])
# ⇒ 'SAMPLE_TOKEN'

```

Authorizationヘッダの`Basic `は、Basic認証を表します。
https://developer.mozilla.org/ja/docs/Web/HTTP/Headers/Authorization
`Basic `以降の文字列をBase64でデコードすると、ユーザIDとパスワードが表示されました。

```python
import base64
print(base64.b64decode("eHh4eHh4Onl5eXl5eQ=="))
# ⇒ b'xxxxxx:yyyyyy'
```

`.netrc`に記載されたユーザIDとパスワードで、Basic認証ヘッダが送信されたようです。


# 原因
requestsモジュールの仕様でした。

>If no authentication method is given with the auth argument, Requests will attempt to get the authentication credentials for the URL’s hostname from the user’s netrc file. The netrc file overrides raw HTTP authentication headers set with headers=.
If credentials for the hostname are found, the request is sent with HTTP Basic Auth.

http://docs.python-requests.org/en/master/user/authentication/#netrc-authentication

* 引数`auth`に認証メソッドが渡されない場合、netrcファイルから認証情報を取得する
* netrcファイルは、認証ヘッダを上書きする
* ホスト名に対応する認証が見つかったら、HTTP Basic認証でリクエストが送られる

今回のコードでは、引数`auth`を使っていなかったため、netrcファイルの認証情報が優先されました。

# 解決方法

## 1. `auth`引数

認証クラスを作成して、そのクラスのインスタンスを`auth`引数に渡す

```python
from requests.auth import AuthBase

class SimpleAuth(AuthBase):
    def __init__(self, token):
        self.token = token

    def __call__(self, r):
        r.headers['Authorization'] = self.token
        return r
        
##### `.netrc`にユーザ情報が記載されているとき
res = requests.get(sample_url, auth=SimpleAuth(sample_token))
print(res.request.headers['Authorization'])
# ⇒ 'SAMPLE_TOKEN'
```        

http://docs.python-requests.org/en/master/user/advanced/#custom-authentication 参照

## 2. `Session`クラスの`trust_env`

`Session`クラスの[trust_env](https://2.python-requests.org//en/master/api/#requests.Session.trust_env) を`False`にすると、`.netrc`を無視します。

>Trust environment settings for proxy configuration, default authentication and similar.


```python
s = requests.Session()
s.trust_env = False
res = s.get(sample_url, headers={"Authorization": sample_token})
```

`Session`インスタンスでしか設定できないので、根本的に解決する場合は、解決方法1が良いです。


# まとめ
デフォルトで「netrcの設定ファイルを読み込む」というよく分からない仕様にハマりました。
この仕様は、[GitHubのイシュー](https://github.com/kennethreitz/requests/issues/3929#issuecomment-287721484)でも議論されていました。

>However, here's a framework I'd consider for handling auth in the 3.0 branch. I'd welcome a PR to make this the case.
>1. If the user sets an Authorization header themselves, either via the request or on the Session, we don't bother to look at the netrc file.
>2. If they didn't, we look at the netrc file for basic auth.
>3. If we get redirected, we fall back to only looking at the netrc file (which we already currently do).

もしかしたら、バージョン3では「Authorizationヘッダが設定されていれば、netrcを見ない」という仕様になっているかもしれません。

# 備考

### 認証情報をnetrc設定ファイルに書くことについて
「[Twelve-Factor App](https://12factor.net/ja/)」というSoftware as a Serviceを作り上げるための方法論には、「設定を環境変数に格納する」という考えがあります。

>Twelve-Factor Appは設定を 環境変数 に格納する。 環境変数は、コードを変更することなくデプロイごとに簡単に変更できる。設定ファイルとは異なり、誤ってリポジトリにチェックインされる可能性はほとんどない。また、独自形式の設定ファイルやJava System Propertiesなど他の設定の仕組みとは異なり、環境変数は言語やOSに依存しない標準である。

そもそも、認証情報をnetrc設定ファイルに書かない方がよさそうです。