はじめに
Djangoを使っていて違和感を感じたのは、EメールアドレスをログインIDにすることはデフォルトの挙動ではないという点でした。どちらかを選択するという考えでもないようで、簡単に切り替える方法は提供されていません。
これを解決するために、強力な汎用機能である"Custom User Model"を利用する方法は様々なところでガイドされています。
問題は、そこからログインページで、ID、パスワードを入力する際に、IDが'@'文字を含まない場合でも処理が進んでしまうことでした。
Custom User Modelを利用している前提で、IDにドメイン名を含まない場合に、処理を進めないようにしてみたいと思います。
技術的な問題点
EメールをIDとして利用できるようになっても、inputタグ自体はusernameのまま変化していません。
usernameとしてはあらゆる文字列が想定されるため、基本的には何でも受け入れてしまいます。
ここで困る事は、ドメイン名(e.g. @example.com)を含まない場合でも、Loginボタンが押下できて、ページ遷移が発生し、無駄に処理(DBやLDAPサーバーとの通信)が発生します。
<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にはカスタマイズしたログインページを表示させたいので、次のような設定が入っています。
from .forms import RecaptchaLoginForm
...
path('accounts/login/',
LoginView.as_view(
form_class=RecaptchaLoginForm,
),
name='login',
),
...
このRecaptchaLoginFormクラスは次のようになっています。
...
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フィールドを上書きしてあげればうまく動きそうです。
元のコードを参考に、次のように変更しました。
...
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は次のように変更されました。
...
<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処理が停止します。
マウスオーバー時のテキストも変更されますので、これが一番効果的かもしれません。
課題
このTooltipは何も表示されないよりは良いですが、autofocusによってブラウザが入力候補を表示してしまうと視認できずにUXが低下します。
今回の方法はフレームワークの手順に従った適切な対応ですが、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入力欄の検証処理にも少しは気を配るべきかとは思います。