🔐

Next.js (Auth.js) と Backend API による OAuth2 認証の実装例

2024/04/18に公開

🎯 目的

Next.js (Auth.js) と Backend API 構成によるアプリにおける OAuth2 認証の実装例をまとめて、再現できるようにする

💡 前提

今日の Web サービスでは、Frontend と Backend で切り離されたシステム構成にしていることが多いように思われます。例えば、Frontend には、昨今人気の Next.js を採用し、Backend API には、PHP の Laravel や Python の FastAPI、Ruby の Ruby on Rails などを採用しているなどといった構成です。私も個人開発では Frontend に Next.js を採用し、 Backend API には FastAPI を採用し、開発をしています。

そんな中、 Next.js の認証ライブラリ Auth.js (NextAuth.js v5) を利用した、Next.js と Backend API による OAuth 認証の実装がわからなかったのでこの記事に調べた内容をまとめます。

システムの前提としては以下の通りです。

💡 Auth.js による OAuth 認証のフロー

Auth.js で OAuth 認証を行う際のフローは以下のページにまとまっています。しかし、このページでは Frontend × Backend API 構成のシステムにおける認証フローがありません。。。

https://authjs.dev/concepts/oauth

🛠️ 設計

Next.js (Auth.js) と Backend API による OAuth 認証フローとして、以下の記事を参考にしました。

https://qiita.com/iwsh/items/a22a7cb4ea2f65437dba#実現したい処理の流れ

上記の記事では、 OAuth プロバイダーである GitHub から発行された一時コードを Frontend から Backend API に送信し、最終的に Backend API が発行したアクセストークンを Frontend に返却しています。(ここでいうアクセストークンは、Backend API が独自に発行したアクセストークンのことであり、GitHub のアクセストークンではありません。)

本記事でも、Frontend から一時コードを Backend API に送信し、Backend API がアクセストークンを生成し、返却する。以後 Frontend は返却されたアクセストークンをヘッダーに指定して、 Backend API に送信するようにします。

💻 実装

💻 GitHub CLIENT_ID / CLIENT_SECRET を発行する

https://authjs.dev/getting-started/authentication/oauth

CLIENT_IDCLIENT_SECRET を発行する : Settings > Developer settings > OAuth AppsRegister a new applicationボタンを押し、以下のように入力します。

💻 Next.js (Auth.js) の改修

auth.config.ts
export default {
    providers: [
        Github({
            clientId: process.env.GITHUB_CLIENT_ID,
            clientSecret: process.env.GITHUB_CLIENT_SECRET,
            token: `${process.env.NEXT_PUBLIC_API_BASE_URL}/auth/github/token`,  // 一時コードを送信する Backend API のパスを指定する
            userinfo: {
                async request({
                    tokens
                }: {
                    tokens: {access_token: string; refresh_token: string; token_type: 'bearer'}  // Backend API から返却されたアクセストークン
                }) {
                    // Backend API で返却されたアクセストークンをもとにユーザー情報を取得する
                    const me = await fetch(`${process.env.NEXT_PUBLIC_API_BASE_URL}/users/me`, {
                        headers: { 'Authorization': `bearer ${tokens.access_token}` }
                    }).then((res) => res.json());
                    return {
                        email: me.email_address,
                        accessToken: tokens.access_token
                    };
                }
            },
            profile(profile) {
                return {
                    email: profile.email,
                    accessToken: profile.accessToken
                }
            }
        }),
    ],
    // ...
}
backend_api.py
import json
from typing import Annotated

import requests
from fastapi import APIRouter, Depends, Form


class AccessTokenRequest:
    def __init__(
        self,
        code: Annotated[str, Form()],
        redirect_uri: Annotated[str, Form()],
        code_verifier: Annotated[str, Form()],
        grant_type: Annotated[str, Form()]
    ):
        self.code = code
        self.redirect_uri = redirect_uri
        self.code_verifier = code_verifier
        self.grant_type = grant_type


class GitHubResource:
    router = APIRouter(prefix='/auth/github', tags=['Auth'])
    GITHUB_API = 'https://api.github.com'

    def __init__(self, client_id: str, client_secret: str):
        self.__client_id = client_id
        self.__client_secret = client_secret
        self.router.add_api_route("/token", self.token, methods=["POST"], response_model=Token)

    def token(self, request: AccessTokenRequest = Depends()) -> dict:
        headers = {'Authorization': f"Bearer {self.__access_token_from(request.code)}"}
        user = requests.get(f'{self.GITHUB_API}/user', headers=headers).json()
        emails = requests.get(f'{self.GITHUB_API}/user/emails', headers=headers).json()

        # ここでユーザー認証を行う。
        # - ex) 該当メールアドレスを保持するユーザーがあれば、GitHubアカウントを紐づけて認証完了とする
        # - ex) 該当メールアドレスを保持するユーザーアカウントが存在しなければ、ユーザー登録を行い、認証完了とする
        return {'access_token': '...', 'refresh_token': '...', 'token_type': 'bearer'}

    def __access_token_from(self, code: str) -> str:
        access_token = requests.post(
            'https://github.com/login/oauth/access_token',
            headers={'Accept': 'application/json'},
            params={'client_id': self.__client_id, 'client_secret': self.__client_secret, 'code': code}
        ).json()
        return access_token['access_token']

📝 今後の調査

今回のような Next.js と Backend API による OAuth 認証方法について調べている中で、以下の Issue を見つけました。この Issue では、 REST API 用の Adapter を実装して、Frontend × Backend の認証を実現しているので、今後この方法についても調査したいと思います。

https://github.com/nextauthjs/next-auth/issues/7538

Discussion