nikkie-ftnextの日記

イベントレポートや読書メモを発信

素振りの記:Auth0で認証するDjangoアプリを作る

はじめに

我々は百鬼夜行を行う!1 nikkieです

締切に追われる完璧主義者向けのWebアプリケーションフレームワークDjangoは、ユーザの認証機能もバッチリ備えています。
例えばDjango Girls Tutorialでは、少しのコード量でログインを実装しています。
https://tutorial-extensions.djangogirls.org/ja/authentication_authorization/#%E3%83%A6%E3%83%BC%E3%82%B6%E3%83%BC%E3%83%AD%E3%82%B0%E3%82%A4%E3%83%B3

Djangoの認証機能を使って全然よいのですが、ここをAuth0にお任せできるということを知り、素振りしました。

目次

Auth0

Auth0はWebアプリやモバイル、APIなどに対して認証・認可のサービスをクラウドで提供している、いわゆるIDaaS (Identity as a Service)ベンダーです。

フューチャー技術ブログに導入編がありました。

Djangoでの事例はこちら。
hokanさんの例です

DjangoでAuth0のQuickstart

以下のチュートリアルで素振りしていきます。

チュートリアルの完成コードはこちら
auth0-django-web-app/01-Login at master · auth0-samples/auth0-django-web-app · GitHub

Auth0で認証するDjangoアプリ

ユーザは http://127.0.0.1:3000/ にアクセス2

(画像はポート番号を間違えて撮影してしまっています)

「Login」は http://127.0.0.1:3000/accounts/login へのリンクになっています。
今回の実装では、「Login」をクリックした後、Auth0側に遷移します。
Auth0で認証した後は、Djangoアプリの http://127.0.0.1:3000/accounts/callback (callback URL) にリダイレクトされます。
その後、ユーザはDjangoアプリにログイン扱いとなり、ログインユーザ用の表示がされます
(Auth0から返ってきているトークンが表示される実装です。開発者向けチュートリアルのためと思います)

動作環境

Python 3.11.8

authlib
django
django-environ
requests

requestsは私たちのDjangoアプリではimportしないのですが、authlibが使っています

具体的なバージョンはこちらをどうぞ(Django 5.0で動いています)
https://github.com/ftnext/django-auth0-practice/blob/0947c946e96feb872e1dc32be4a9600996d347a1/requirements.txt

Auth0の設定

Auth0に開発者用アカウントを作ると「Default App」というApplicationが作成されていました。
今回の素振りではこれを使っています

(1) Djangoアプリの環境変数に設定する値たち
django-environで.envから読み取るように実装しました3

「Basic Information」の

  • Domain
  • Client ID
  • Client Secret

(2) Djangoアプリに関連する値を設定する箇所

「Application URIs」の

ほかの箇所はいじっていません

DjangoアプリでAuth0を使う実装

チュートリアルに沿ってビューに実装しています。
チュートリアルからアプリケーションの切り分け方を変えて、accountsアプリを導入しました。

authlibライブラリを使っていきます

docs.authlib.org

# accounts/views.py
from authlib.integrations.django_client import OAuth
from django.conf import settings

oauth = OAuth()
oauth.register(
    "auth0",
    client_id=settings.AUTH0_CLIENT_ID,
    client_secret=settings.AUTH0_CLIENT_SECRET,
    client_kwargs={"scope": "openid profile email"},
    server_metadata_url=f"https://{settings.AUTH0_DOMAIN}/.well-known/openid-configuration",
)
ログイン

https://docs.authlib.org/en/latest/client/django.html#routes-for-authorization

def login(request):
    return oauth.auth0.authorize_redirect(
        request, request.build_absolute_uri(reverse("accounts:callback"))
    )

oauthオブジェクトはAuth0を使うように設定されており、Djangoアプリへのログインではauthorize_redirectを呼び出しています。
これでAuth0側に遷移するという理解です

コールバック

Auth0で認証されたら、Djangoアプリ側のcallback URLにリダイレクトされます(ここでリダイレクト先は、「Allowed Callback URLs」になければならない)

def callback(request):
    token = oauth.auth0.authorize_access_token(request)
    request.session["user"] = token
    return redirect(request.build_absolute_uri(reverse("myapp:index")))

トークンを取得し、セッション(request.session4に保存しています5
その後、Djangoアプリのindexページにリダイレクトし、ログインしているのでユーザの情報が表示されます。

indexページ(ログインしたときの見え方)

myappアプリケーションに実装しました。

# myapp/views.py
def index(request):
    return render(
        request,
        "myapp/index.html",
        context={
            "session": request.session.get("user"),
            "pretty": json.dumps(request.session.get("user"), indent=4),
        },
    )

テンプレートでは{% if session %}と分岐しています

  • Auth0で認証されると、callback URLでrequest.session["user"]を設定するので、{% if session %}Trueの分岐へ。ログインユーザ向けの表示ができる
  • Auth0で認証する前やログアウトした後はrequest.sessionにuserというキーが設定されていないので、{% if session %}Falseの分岐へ。「Welcome Guest」表示
ログアウト
# accounts/views.py
def logout(request):
    request.session.clear()

    return redirect(
        f"https://{settings.AUTH0_DOMAIN}/v2/logout?"
        + urlencode(
            {
                "returnTo": request.build_absolute_uri(reverse("myapp:index")),
                "client_id": settings.AUTH0_CLIENT_ID,
            },
            quote_via=quote_plus,
        )
    )

セッションをクリアしています。
その後Auth0側にリダイレクトし、Auth0でもログアウトさせています。

ログイン状態でなければ表示しないページはどう作る?

Auth0を使った場合、@login_requiredに相当するビューはどのように実装するのでしょうか。

I wrote a small function that gets email from request.session.get(“user”), and then find the user.

@login_requireduser.is_authenticatedを見ています6が、この属性はAuth0で認証済みか否かを意味しません

indexページの実装も参考に、以下のように実装しました(ダッシュボード画面を追加)。
実務で使う場合はもっと突き詰めたいと考えています(Auth0のサンプルコードの中にあるかな?)

# myapp/views.py
def dashboard(request):
    if not request.session.get("user"):
        return redirect_to_login(
            request.build_absolute_uri(), reverse("accounts:login")
        )

    return render(request, "myapp/dashboard.html")

終わりに

Auth0で認証するDjangoアプリの素振りの記でした。
こんなことができるんですね〜

hokanさんの事例にあるのですが、認証をAuth0に切り出すことで「二要素認証やAD連携など機能強化」にもつなげられるそうです。
私の関わる開発で使い所があるかは分かりませんが、手札が増えました!