Django + Shibboleth with RemoteUserMiddleware

#背景
Shibboleth認証でDjangoアプリにログインしたい。
RemoteUserMiddlewareを使えばできると聞いてやってみたらいろいろぶち当たったので解決方法を書く。

#環境
- Windows Server 2016
- Django 3.0
- Python 3.7.7（[後々のために](https://qiita.com/hiro-jp/items/1a15ac51165f5af395f3)、3.8系は避けること！）
- Apache 2.4 + mod_shib + mod_wsgi
- Shibboleth Service Provider 3.x

#前提
- かんたんなDjangoアプリはあるものとする。
- createsuperuserで管理者権限ユーザを作ってあるものとする。
- Shibbolethはインストールしてあるものとする。（関連記事参照）

関連記事：[Apache + OneLogin SSO with Shibboleth](https://qiita.com/hiro-jp/items/096bbaf647a9e4a3ae18)

#流れ
Shibboleth認証が通ったらID情報が環境変数REMOTE_USERに入る場合を想定（この想定についていけない人は[この記事](https://qiita.com/hiro-jp/items/096bbaf647a9e4a3ae18#3-shibboleth2xml%E3%81%AE%E8%A8%AD%E5%AE%9A)から）。このREMOTE_USERをusernameとしてDjangoアプリに（自動で）ログインする（Djangoアプリ側ではユーザ名もパスワードも要求されない）。デフォルトの設定では、REMOTE_USERのID情報がUserオブジェクトに存在しない（初見の）場合、自動的にUserオブジェクトを作成してログインする（この設定は変更できる）。

詳細：[Django Documentation - Authentication using REMOTE_USER](https://docs.djangoproject.com/en/3.0/howto/auth-remote-user/)

#ApacheのLocationを追加

```httpd.conf
<Location /Shibboleth.sso>
  SetHandler shib
</Location>

<Location /> # ←'/'でなく、'/secure/'などとすれば、secure以下が管理下に置かれる。
    AuthType shibboleth
    ShibRequestSetting requireSession true
    Require shibboleth
    Require shib-session
    ShibUseHeaders On
</Location>
```

#DjangoにRemoteUserMiddlewareを設定
詳細：https://docs.djangoproject.com/en/3.0/howto/auth-remote-user/

MIDDLEWAREにRemoteUserMiddlewareを追加（下記）。

```python:settigs.py
MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.auth.middleware.RemoteUserMiddleware', # ←ここに追加
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
```

AUTHENTICATION_BACKENDSにRemoteUserBackendを追加（下記）。

```python:settings.py
AUTHENTICATION_BACKENDS = (
    'django.contrib.auth.backends.RemoteUserBackend',
)
```

#attribute-mapの設定
IdPからのassertionのうち、アプリ側で使いたいID情報をキャッチするように設定する。今回は、日本語における「（姓に対する）名」をキャッチするため下記のようにした。

```xml:attribute-map.xml
...
    <Attribute name="urn:oid:2.5.4.42" id="givenName"/> # ← 追加

    <!-- New standard identifier attributes for SAML. -->

    <Attribute name="urn:oasis:names:tc:SAML:attribute:subject-id" id="subject-id">
        <AttributeDecoder xsi:type="ScopedAttributeDecoder" caseSensitive="false"/>
    </Attribute>
...
```

`name=urn:oid:2.5.4.42`って何ぞや、という話は[なんだか難しい話](https://ja.wikipedia.org/wiki/%E3%82%AA%E3%83%96%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88%E8%AD%98%E5%88%A5%E5%AD%90)のようなのでかいつまんで理解しておく。できるだけ平たく言うと、日本語における「（姓に対する）名」、英語における「Given name」や「First name」、フランス語における「Prénom」、アラビア語における「الاسم الخاص」といった概念を、共通の識別子として`urn:oid`形式であらわすと`urn:oid:2.5.4.42`になるようだ（[ムツカシイこと考える人](https://www.itu.int/itu-t/recommendations/rec.aspx?rec=X.520)がいるなあ…）。このほか、「メールアドレス」「email address」「L'adresse courriel」「عنوان البريد الإلكتروني」が指す概念を共通の識別子として`urn:oid`形式で表すと`urn:oid:0.9.2342.19200300.100.1.3`と決められている。

ともかく、IdPからのSAML Assertionにはこういったurnで表されるAttribute nameをkeyとして、それぞれのvalueが送られてくる。それをShibboleth SP側で何と呼ぶかを決めるのが`id`であり、これは勝手に決めてよい。

まとめると、`<Attribute name="urn:oid:2.5.4.42" id="givenName"/>`は、IdPから送られてくる`urn:oid:2.5.4.42`をkeyとするvalueを、今後`givenName`という`id`で表す、という記述。

#REMOTE_USERの設定
IdPからのassertionのうち、アプリ側のログインに使いたいID情報をREMOTE_USERに格納するように設定する。今回は`givenName`と名付けたassertionを使うことにした。

```xml:shibboleth2.xml
...
    <ApplicationDefaults entityID="https://localhost/"
        REMOTE_USER = "givenName"
        cipherSuites="DEFAULT:!EXP:!LOW:!aNULL:!eNULL:!DES:!IDEA:!SEED:!RC4:!3DES:!kRSA:!SSLv2:!SSLv3:!TLSv1:!TLSv1.1">
...
```

以上。

**関連記事**：
- [RemoteUserMiddlewareとDjango認証を併用する方法](https://qiita.com/hiro-jp/items/e29b514ddf17045d3bd3)
- [Django RemoteUserMiddleware ログイン時にユーザ情報を更新する方法](https://qiita.com/hiro-jp/items/4ca6d8c9c9a0690b4d7b)
- [Shibboleth SPにおけるIdP独自attributeへの対応方法](https://qiita.com/hiro-jp/items/cbf1a8ec68132dd25b85)