FlaskのBasic認証HTTPBasicAuthで401ページをカスタマイズする
TL;DR
HTTPBasicAuth.error_handler()
の引数にwerkzeug.exceptions.Unauthorized.get_body()
の文字列を返すメソッドを渡す。
背景
flask_httpauth
のBasic認証HTTPBasicAuth
をFlask
に適用する際、デフォルトだとUnauthorized(認証失敗)のページは以下のように表示される。
他の404等のエラーページではもう少しリッチな表示になっている一方で、401の時のみただの1行テキストでかなり質素な表示となり味気ない。
対策
HTTPBasicAuth.error_handler()
で自前のエラーメッセージを返すように再設定する。
実装例
from flask import Flask
from flask_httpauth import HTTPBasicAuth
from werkzeug.exceptions import Forbidden, InternalServerError, Unauthorized
def main() -> None:
app: Flask = Flask(__name__)
auth: HTTPBasicAuth = HTTPBasicAuth(realm="My auth")
http_errors: dict[int, str] = {
code: error().get_body()
for code, error in {
401: Unauthorized,
403: Forbidden,
500: InternalServerError,
}.items()
}
def handle_error(status: int) -> tuple[str, int]:
return http_errors.get(status, http_errors[500]), status
auth.error_handler(handle_error)
@auth.get_password
def get_auth_password(username: str) -> str:
return "password" if username == "admin" else None
@app.get("/")
@auth.login_required
def index() -> str:
return "Hello world"
app.run(host="0.0.0.0", port=8888)
if __name__ == "__main__":
main()
認証エラーページ
説明
flask_httpauth.HTTPAuth.__init__()
において、エラーハンドラerror_handler()
に対して内部メソッドdefault_auth_error()
を渡している。
これにより、認証エラー時にはこのメソッドの戻り値が返されるようになるが、ここでデフォルトのUnauthorized Access
のテキストが返るようにしている。
class HTTPAuth(object):
def __init__(self, scheme=None, realm=None, header=None):
self.scheme = scheme
self.realm = realm or "Authentication Required"
self.header = header
self.get_password_callback = None
self.get_user_roles_callback = None
self.auth_error_callback = None
def default_get_password(username):
return None
def default_auth_error(status):
return "Unauthorized Access", status
self.get_password(default_get_password)
self.error_handler(default_auth_error)
一方で、Flask
の標準エラーページはwerkzeug.exceptions
以下のエラークラスを用いており、すべてException
を継承したHTTPException
クラスを継承している。
HTTPException.get_body()
で自身の持つステータスコードや説明を用いてHTMLの文字列を構築しており、ブラウザ上ではこの文字列がページとして表示されてるようになっている。
class HTTPException(Exception):
・・・
def get_body(
self,
environ: WSGIEnvironment | None = None,
scope: dict | None = None,
) -> str:
"""Get the HTML body."""
return (
"<!doctype html>\n"
"<html lang=en>\n"
f"<title>{self.code} {escape(self.name)}</title>\n"
f"<h1>{escape(self.name)}</h1>\n"
f"{self.get_description(environ)}\n"
)
Unauthorized
クラスはこれに対してデフォルトで以下のようなコードと説明が設定されるので、HTML文字列を構築した結果を取り出しておけば後はHTTPAuth.error_handler()
でデフォルトのメソッドと同様にこの文字列とステータスを返却すれば良い。
class Unauthorized(HTTPException):
・・・
code = 401
description = (
"The server could not verify that you are authorized to access"
" the URL requested. You either supplied the wrong credentials"
" (e.g. a bad password), or your browser doesn't understand"
" how to supply the credentials required."
)
ただし、API仕様を見ると403エラーも来たりなど必ずしも401となるわけではないようなので、例では複数のステータスが来る可能性を考慮して引数のstatus
を元にHTMLを切り替えるようにしている。
Discussion