DjangoでEメールIDでログインする時に、'@'を含むか検証させたい

# はじめに

Djangoを使っていて違和感を感じたのは、EメールアドレスをログインIDにすることはデフォルトの挙動ではないという点でした。どちらかを選択するという考えでもないようで、簡単に切り替える方法は提供されていません。

これを解決するために、強力な汎用機能である"Custom User Model"を利用する方法は様々なところでガイドされています。

問題は、そこからログインページで、ID、パスワードを入力する際に、IDが'@'文字を含まない場合でも処理が進んでしまうことでした。

Custom User Modelを利用している前提で、IDにドメイン名を含まない場合に、処理を進めないようにしてみたいと思います。

# 技術的な問題点

EメールをIDとして利用できるようになっても、inputタグ自体はusernameのまま変化していません。
usernameとしてはあらゆる文字列が想定されるため、基本的には何でも受け入れてしまいます。

ここで困る事は、ドメイン名(e.g. @example.com)を含まない場合でも、Loginボタンが押下できて、ページ遷移が発生し、無駄に処理(DBやLDAPサーバーとの通信)が発生します。

```html:/accounts/login/ページのHTML抜粋
  <div class="col-12">
    <div class="sm-3">
      <label>E-mail</label>
    </div>
    <div class="sm-4">
      
      <input type="text" name="username" autofocus autocapitalize="none" autocomplete="username" maxlength="254" required id="id_username">
    </div>
  </div>
```

期待する動作としては、Eメールアドレスの形式に合わない場合には、画面上に警告を出してくれることです。

# 現在の設定

urls.pyにはカスタマイズしたログインページを表示させたいので、次のような設定が入っています。

```python:myapp/urls.pyからの抜粋
from .forms import RecaptchaLoginForm
...
    path('accounts/login/',
         LoginView.as_view(
             form_class=RecaptchaLoginForm,
         ),
         name='login',
    ),
...
```

このRecaptchaLoginFormクラスは次のようになっています。

```python:myapp/forms.pyからの抜粋
...
from django.contrib.auth.forms import AuthenticationForm
from captcha.fields import ReCaptchaField

class RecaptchaLoginForm(AuthenticationForm):
    captcha = ReCaptchaField()
    pass
...
```

ReCaptchaのために、django-recaptcha 3.0.0を利用しています。


## 変更後のコード

このRecaptchaLoginFormクラスを変更して、親のAuthenticationFormのコードなどを確認すると、usernameフィールドを上書きしてあげればうまく動きそうです。

元のコードを参考に、次のように変更しました。

```python:myapp/forms.pyからの抜粋
...
from django.contrib.auth.forms import AuthenticationForm
from django.forms.fields import EmailField
from django import forms
from captcha.fields import ReCaptchaField

class RecaptchaLoginForm(AuthenticationForm):
    username = EmailField(widget=forms.EmailInput(attrs={'autofocus': True}))
    captcha = ReCaptchaField()
    pass
...
```

この結果、HTMLは次のように変更されました。

```html:/accounts/login/のHTML抜粋
...
  <div class="col-12">
    <div class="sm-3">
      <label>E-mail</label>
    </div>
    <div class="sm-4">
      <input type="email" name="username" autofocus maxlength="254" required id="id_username">
    </div>
  </div>
...
```

これによってSubmitボタンを押下した時に検証され、tooltipが表示されPOST処理が停止します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/78296/2716e3df-417b-528b-94fc-f16bb2ba601e.png)

マウスオーバー時のテキストも変更されますので、これが一番効果的かもしれません。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/78296/d2dc142f-82b5-f561-a1fc-6db08998fd25.png)


# 課題

このTooltipは何も表示されないよりは良いですが、autofocusによってブラウザが入力候補を表示してしまうと視認できずにUXが低下します。

今回の方法はフレームワークの手順に従った適切な対応ですが、templates/registration/login.html に次のようなJavaScriptを埋め込む方法も考えられます。


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/78296/cb91b8cf-b5f9-788b-5679-e80556bbcbdb.png)

```javascript:templates/registration/login.htmlに追加したJavaScriptコード
<script type="text/javascript">
  // validate email input field identified by "#id_username"
  (function () {
      'user strict'
      target = document.querySelector('form[method="post"]');
      target.addEventListener('submit', function(event) {
	  const input_field = document.getElementById('id_username');
	  const emailAddress = input_field.value;
	  if( /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,6}$/.test(emailAddress) != true) {
	      // when invalid
	      event.preventDefault();
	      input_field.style.border = "thin solid red";
          // check existing warning field
	      if (! document.getElementById("myid_email_input_field")) {
		      // show warning text
		      let input_field_element = document.createElement("span");
		      input_field_element.setAttribute("id", "myid_email_input_field");
		      let input_field_content = document.createTextNode("This is not a valid email format.");
		      input_field_element.appendChild(input_field_content);
		      input_field_element.setAttribute("class", "px-3 bg-warning");
		      input_field.after(input_field_element);
	      }
	  }
      });
  })()
</script>
```

この方法はForm定義を変更した場合には動作しないので、いずれかの方法を選択することになると思います。

# さいごに

どんな手段を使っても、クライアント(Webブラウザ)側の検証処理は勝手に変更されたり、バイパスされたりする可能性があるので、サーバー側では、全ての入力を検証する必要があります。

あまり頑張り過ぎても良くないですが、Custom User Modelを使う場合には、ID入力欄の検証処理にも少しは気を配るべきかとは思います。