1
/
5

Djangoで2段階認証を実装しました

こんにちは、日本システム技研の浦野です。

今回はDjangoで、2段階認証を実装する方法についてまとめてみました。

2段階認証とは

通常の(IDとパスワードを使用した)ログインに加えて、予め登録していた電話番号やメールアドレスに届く認証コードを使って、ログインする方法です。
こちらの方法ですと、より強固なセキュリティを確保できるため、GoogleやLINE等メジャーなサービスでもよく利用されています。

環境

言語: python(3.5.1) + Django(2.0)
DB: sqlite(デフォルト)

例のごとくlocalhostで試したので、webサーバに載せるときはまた変わってくるかもしれません。
また既にログインができるシステムの改造を前提としていますので、ログインをまだ実装していない場合は、以下のドキュメントを参考に実装してみてください。
http://docs.djangoproject.jp/en/latest/topics/auth.html

実装

まずは各アカウントに認証コードを発行するための設定を行います。
今回は以下のソースを参考に、ログイン後の画面にQRコードを表示させるようにしました。
https://github.com/shinsaka/googleauthenticator_demo

実装後の画面はこんな感じです。
「QRコード作成」ボタンを押すと、QRコードが表示されるようにしています。
(デモ用にQRコードは加工しています)

以下は画面からのQRコードリクエストに対して、JSON形式で返すDjangoメソッドです。
TwoAuthというのは、Userに対応したsecret_keyを保持するためのテーブルです。
認証コードの取得には、ユーザ毎に一意のID(今回はメールアドレス)と、secret_keyが必要ですので、secret_keyの扱いには注意してください。

from django.contrib.auth.decorators import login_required
from django.db import transaction
from django.http.response import JsonResponse

# https://github.com/shinsaka/googleauthenticator_demo/blob/master/demo/utils.py
from google2authtest.apps.utils import get_secret, get_image_b64, get_auth_url

from cms.models.two_auth import TwoAuth


@login_required()
@transaction.atomic
def display_qrcode(request):
user_id = request.user.email
secret = get_secret()

try:
two_auth = TwoAuth.objects.get(fk_user=request.user)
two_auth.secret_key = secret
except TwoAuth.DoesNotExist:
two_auth = TwoAuth(fk_user=request.user, secret_key=secret)

two_auth.save()

return JsonResponse({
'img': get_image_b64(get_auth_url(user_id, secret)),
})

QRコードが発行できたら、そこから生成される認証コードを読み取る必要があります。
今回私はGoogle認証システム(https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2&hl=ja)を使用しましたが、認証コードさえ読み取れれば、どのアプリを使っても大丈夫です。

認証コードが確保できたら、いよいよ肝のログイン画面を修正します。
今回は以下の流れとなるように実装しています。

1. IDとパスワードでログイン
2-A. secret_keyが存在しない(QRコードを発行していない)場合
そのままログイン
2-B. secret_keyが存在する(QRコードを発行している)場合
認証コードの入力を要求
3. 認証コードが一致していたらログイン

1. IDとパスワードでログイン
デフォルトのログイン機能は使わず、authenticate()でユーザの存在チェックのみを行うようにします。
ユーザが存在する場合は、secret_keyの有無で分岐させます。

2-A. secret_keyが存在しない(QRコードを発行していない)場合
login()でそのままログインさせます。

2-B. secret_keyが存在する(QRコードを発行している)場合
ID/パスワードに追加で、認証コードの入力を要求します。
login()を少し改造し、以下のように認証コードの整合性もチェックするようにします。

def login(request, template_name='registration/login.html',
          redirect_field_name=REDIRECT_FIELD_NAME,
          authentication_form=AuthenticationForm,
          extra_context=None, redirect_authenticated_user=False):
    if request.method == 'POST':
        username = request.POST['username']
        password = request.POST['password']

        # ここから認証コードチェック
        user = authenticate(username=username, password=password)
        secret = TwoAuth.get_secret_key(user)
        if secret:
            auth_token = request.POST['auth_token']
            is_valid = otp.valid_totp(token=int(auth_token), secret=secret)
        else:
            is_valid = True
        if not is_valid:
            form = AuthenticationForm(request.POST)
            return render(request, template_name, context={'form': form})
        # 認証コードチェックここまで

    warnings.warn(
        'The login() view is superseded by the class-based LoginView().',
        RemovedInDjango21Warning, stacklevel=2
    )
    return auth_views.LoginView.as_view(
        template_name=template_name,
        redirect_field_name=redirect_field_name,
        form_class=authentication_form,
        extra_context=extra_context,
        redirect_authenticated_user=redirect_authenticated_user,
    )(request)

以上で2段階認証実装完了です。v(`・ω・´)v
サンプルソースはbitbucketにアップしてありますので、気になった方は見てみてください。
(https://bitbucket.org/Tohru_jsl/google2authtest)
割とざっくり実装しましたが、もうちょっといじれば既存のシステムに実装できる形となると思います。

こんな感じで今後も技術ネタをつらつら書いて行こうと思うので、今後ともよろしくお願いします。

株式会社日本システム技研では一緒に働く仲間を募集しています
9 いいね!
9 いいね!
今週のランキング
株式会社日本システム技研からお誘い
この話題に共感したら、メンバーと話してみませんか?