iMind Developers Blog

iMind開発者ブログ

Flaskでのパスワードの再設定ページの実装

概要

ユーザーがパスワードを忘れた場合に、入力されたメールアドレス宛にパスワード更新用のURLを送り、パスワードの再設定を行えるような画面をFlaskで作る場合のサンプル。

サンプルコードではDB操作とメール送信については取り扱わずにスタブで済ませています。

バージョン情報

  • Flask==1.1.1
  • Flask-WTF==0.14.2
  • itsdangerous==1.1.0

参考ページ

http://www.patricksoftwareblog.com/password_reset_via_email_link/

サンプルコードのフロー

  • フォームにメールアドレスを入力してsubmit
  • メールアドレスと時刻から作ったトークンをパラメータに持つURLがメール送信される
  • URLからパスワードの再設定ページを開いて、パスワードを設定する

tokenの生成

URLに渡すtokenはitsdangerousのURLSafeTimedSerializerを利用する。

from itsdangerous.url_safe import URLSafeTimedSerializer

def create_token(user_id, secret_key, salt):
    ''' user_idからtokenを生成
    '''
    serializer = URLSafeTimedSerializer(secret_key)
    return serializer.dumps(user_id, salt=salt)

def load_token(token, secret_key, salt):
    ''' tokenからuser_idとtimeを取得
    '''
    serializer = URLSafeTimedSerializer(secret_key)
    return serializer.loads(token, salt=salt, return_timestamp=True)

下記のように呼び出せる。

secret_key = 'jugemjugemnoponpokorinnoponn'
salt = 'shio'

token = create_token('hogehoge', secret_key, salt)
    #=> ('ImhvZ2Vob2dlIg.Xl_Egw.Z_F3qYCxdu9W_awkz6by0Uxg0oo',)

user_id, time = load_token(token, secret_key, salt)
    #=> ('hogehoge', datetime.datetime(2020, 3, 4, 15, 20, 31)

上記例では、serializer.loadsの箇所でreturn_timestampを指定して戻り値にtimestampを含めている。

max_ageを指定することで、有効期限切れのtokenであれば例外が出るようにすることもできる。

max_ageの単位は秒。

def load_token(token, secret_key, salt):
    ''' tokenからuser_idとtimeを取得
    '''
    serializer = URLSafeTimedSerializer(secret_key)
    return serializer.loads(token, salt=salt, max_age=60)

実行結果

SignatureExpired: Signature age 907 > 60 seconds

今回の処理ではmax_ageを指定した方が向いてそうなので、以後のサンプルでは後者を利用する。

また、トークンに適当な文字列を指定すると、BadSignature例外が発生する。

user_id = load_token('間違ったトークン', secret_key, salt)

    #=> BadSignature: No b'.' found in value

フォルダ構成

下記のような構成でファイルを作成する。

.
├── app.py
└── templates
    ├── mail.html
    └── new_pwd.html

templates/mail.htmlがメールアドレスの入力画面。

templates/new_pwd.htmlが新規パスワード入力画面。

templates/mail.htmlの記述

メールアドレスを入力するフォーム画面。

flashメッセージやvalidate時のエラーメッセージについても実装している。

<form method="POST">
  {% for message in get_flashed_messages() %}
    <p>{{ message }}</p>
  {% endfor %}

  {{ form.csrf_token }}

  {% for error in form.mail.errors %}
    <p style="color:red">{{ error }}</p>
  {% endfor %}
  {{ form.mail.label }}
  {{ form.mail(placeholder="メールアドレス", size=20, id="mail") }}

  <button>送信</button>
</form>

こんなシンプルな画面を記述した。

f:id:mwsoft:20200308020303p:plain

app.pyの記述(mail側)

DB参照やメール送信については割愛して、画面にパスワード変更用のURLを表示する。

import flask
import flask_wtf
import wtforms
from wtforms import validators
from itsdangerous.url_safe import URLSafeTimedSerializer

app = flask.Flask(__name__)
app.secret_key = 'nanika secret key wo ireru'
SALT = 'shio'

@app.route('/mail', methods=['GET', 'POST'])
def index():
    ''' メールアドレスを入力してもらって
        パスワード変更画面のURLを通知する
    '''
    form = AddressForm()

    # ポストの場合のメール送信とメッセージ表示
    if flask.request.method == 'POST' and form.validate_on_submit():
        # TODO DBを参照して存在するメールアドレスか確認(割愛)

        # 先ほど書いたcreate_tokenメソッドでtokenを生成
        token = create_token(form.mail.data, app.secret_key, SALT)
        url = flask.url_for('new_pwd', token=token, _external=True)

        # TODO メール送信(割愛)

        # メール送信を割愛してURLをページ表示
        flask.flash('メール送ったよ : %s' % url)

    # ページ表示
    return flask.render_template(
            'mail.html',
            form=form)

@app.route('/new_pwd', methods=['GET', 'POST'])
def new_pwd():
    ''' 新規パスワード設定
    '''
    # TODO 後述 (url_forが動くようにスタブだけ先に記述)

class AddressForm(flask_wtf.FlaskForm):
    mail = wtforms.StringField('mail', [
        validators.Email(message='メールアドレスの形式が間違っています'),
        validators.InputRequired(message='メールアドレスを入力してください')])

flask runで実行。

validationエラー時。

f:id:mwsoft:20200308020338p:plain

実行成功時。

f:id:mwsoft:20200308020445p:plain

templates/new_pwd.htmlの実装

パスワード入力フォーム(確認用付き)に入力して送信できるページを用意する。

<form method="POST">
  {% for message in get_flashed_messages() %}
    <p>{{ message }}</p>
  {% endfor %}

  {{ form.csrf_token }}


  <p>{{ mail_address }}</p>

  <p>
    {{ form.token() }}
  </p>
  <p>
    {% for error in form.new_pwd1.errors %}
      <p style="color:red">{{ error }}</p>
    {% endfor %}
  </p>
  <p>
    {{ form.new_pwd1.label }}
    {{ form.new_pwd1(placeholder="パスワード", size=20) }}
  </p>
  <p>
    {{ form.new_pwd2.label }}
    {{ form.new_pwd2(placeholder="パスワード(確認用)", size=20) }}
  </p>

  <button>送信</button>
</form>

app.pyの記述(new_pwd側)

新規パスワード設定画面の実装。

tokenからメールアドレスを取得するload_tokenメソッドは下記の実装とする。有効時間は10分。

def load_token(token, secret_key, salt, max_age=600):
    ''' tokenからメールアドレスを取得
    '''
    serializer = URLSafeTimedSerializer(secret_key)
    return serializer.loads(token, salt=salt, max_age=max_age)

app.pyへの記述。

@app.route('/new_pwd', methods=['GET', 'POST'])
def new_pwd():
    ''' 新規パスワード設定
    '''
    if flask.request.method == 'GET':

        # アドレスの取得
        try:
            token = flask.request.args.get('token')
            mail_address = load_token(token, app.secret_key, SALT)
        except Exception as e:
            return flask.abort(400)

        # ページ表示
        form = NewPwdForm(token=token)
        return flask.render_template(
                'new_pwd.html',
                form=form,
                mail_address=mail_address)
    else:
        form = NewPwdForm() 
        # アドレスの取得
        try:
            mail_address = load_token(form.token.data, app.secret_key, SALT)
        except Exception as e:
            return flask.abort(400)

        if form.validate_on_submit():
            # TODO ここにパスワードのudpate処理

            flask.flash('パスワードを更新しました')

        return flask.render_template(
                'new_pwd.html',
                form=form,
                mail_address=mail_address)

class NewPwdForm(flask_wtf.FlaskForm):
    token = wtforms.HiddenField('token', [
        validators.InputRequired()] )
    new_pwd1 = wtforms.PasswordField('パスワード', [
        validators.InputRequired(),
        validators.EqualTo('new_pwd2')] )
    new_pwd2 = wtforms.PasswordField('パスワード(確認用)', [
        validators.InputRequired()] )

これでメールアドレス入力ページで取得したURLを叩くと、下記のような画面でパスワード入力ができる。

f:id:mwsoft:20200308185747p:plain

不正なtokenが入力された場合は400 Bad Request。

f:id:mwsoft:20200308190409p:plain

改定履歴

Author: Masato Watanabe, Date: 2020-03-08, 記事投稿