Better Authを理解する
はじめに
Webで動的なサイトを作るとき,多くの場合で認証は欠かせません.
PrAhaで作成しているWebアプリでも,その大半に何らかのログイン機構が組み込まれています.
これらのログイン機構について,多くの場合ではフルスクラッチでの実装ではなく,ライブラリによる実装を選択すると思います.
今回は,それらのライブラリの1つとして,TypeScriptの認証・認可フレームワークである,Better Authについて紹介したいと思います.
ただし,導入や基本的な利用方法については,公式ドキュメントに譲り,この記事では,Better Authの全体を俯瞰して理解することを目指します.
バージョンはv1.4.17です.
「ログインする」とは何か?
一口にログインするとは言っても,具体的には何を行うことで「ログインした」と言えるのでしょうか?
この記事では,「サーバーが何らかの形で検証可能な秘密情報を,クライアントが保存する」ことを「ログインする」という言葉の説明としたいと思います.
例えば,Basic認証では,一度ユーザーID,パスワードの検証を終えると,ブラウザにユーザーIDとパスワードが保存され,リクエストごとにAuthorizationヘッダーに設定して送付することになります.つまり,「クライアントにユーザーIDとパスワードを保存する」ことが,「ログインする」ことに相当します.
一般的なログインフレームワークでは,ユーザーがIDとパスワードを送付すると,サーバーはそれを検証し,成功した場合,何らかのセッションを発行します.
このセッションをCookieに保存してそれを送付したり,もしくは認証成功時に取得したアクセストークンをAuthorizationヘッダーに含めて送付することで,サーバーは以前に認証したユーザーであることを確認します.
つまり,「クライアント(Cookie)にセッションを保存する」ことが「ログインする」ことに相当しています.
Better Authでも,デフォルトでは何らかの認証で生成した秘密情報をCookieに保存し,「ログイン」を実現しています.
Better Authのコア機能を理解する
最も核となるのは,betterAuthメソッドで作成できるインスタンスです.以下のように作成できます.
import { betterAuth } from "better-auth";
export const auth = betterAuth({});
認証用エンドポイントを用意する
このauthは,認証に用いる様々なHTTPエンドポイントを提供します.
authを直接利用したり,Better Authの提供するアダプターを用いることで,様々なフレームワークで認証用のエンドポイントをマウントできます.
// Hono
const app = new Hono();
app.on(["POST", "GET"], "/api/auth/*", (c) => {
return auth.handler(c.req.raw);
});
serve(app);
// Next.js
import { toNextJsHandler } from "better-auth/next-js";
export const { GET, POST } = toNextJsHandler(auth);
Honoの例では,特にHono用のアダプターを利用することなくマウントしています.
Honoとauth.handlerが共にWeb標準であるfetchベースのインターフェースを採用することで,特別なアダプターを必要とせず連携することができています.
マウントされる認証用エンドポイント
auth.handlerによってマウントされるエンドポイントは多岐に渡り,例えば以下のようなものがあります.
/sign-in/email/sign-in/oauth2/get-session/account-info/get-access-token- etc...
これらのエンドポイントに対して,適切なパラメータとともにGET/POSTリクエストを送信することで,ログインや現在のログインセッション,アカウント情報の取得,外部APIへアクセスするためのアクセストークンの取得などが行えます.
ただし,これらのエンドポイントのうち,利用できるエンドポイントは,選択した認証方法に関連したものに限られるという点に注意する必要があります.
とにかく,ここまでで理解する点としては,「betterAuthで生成するインスタンスを用いて,認証用エンドポイントを用意できる」という点です.
Better Authの最もコアな機能としてこれがあり,ここにオプショナルな実装を加えて,実用の認証機構を構成します.
エンドポイントを利用する
実際の認証方法の設定の前に,これらのエンドポイントの利用方法について説明します.
クライアントサイドから利用する
まず基本として,Better AuthによってauthClientが提供されています.
これはブラウザから利用するためのクライアントになっています.
import { createAuthClient } from "better-auth/vanilla"
import { genericOAuthClient } from "better-auth/client/plugins"
export const authClient = createAuthClient({})
createAuthClientは"better-auth/react"や"better-auth/vue"などからインポートすることもでき,各フレームワークに適したAPIが追加で提供されています.
authClientでは,マウントされたエンドポイントを,それがHTTPエンドポイントであることを意識することなく利用することができます.
await authClient.signIn.email({
email: "sample@example.com",
password: "password123",
});
await authClient.getSession();
await authClient.accountInfo();
サーバーサイドで利用する
前述のauthClientは,内部的にHTTPエンドポイントにアクセスしており,謂わばfetchのラッパーと呼べるものです.
この時,authClientはクライアントの持つCookieを自動的に送付し,その情報を利用して様々な処理を行います.
従って,サーバーサイドでauthClientを呼び出しても,クライアントの持つCookieを送付できないため,そのままでは利用できません.
しかし,Next.jsのサーバーサイドやHonoなど,Better Authのエンドポイントを提供しつつ,同じサーバーサイドで認証結果を利用したい場面は数多くあります.
このような時に利用するのがbetterAuthによって作成されるauthに含まれるauth.apiからアクセスできるメソッド群です.
const headersList = c.req.raw.headers; // Hono
const headersList = await headers(); // Next.js
await auth.api.getSession({ headers: headersList });
await auth.api.accountInfo({ headers: headersList });
// etc...
authClientでは暗黙的に渡されていたCookie(headers)ですが,auth.apiではフレームワーク固有の方法で取得したリクエストヘッダーを利用します.
また,auth.apiはauthClientと異なり,ネットワークアクセスが発生しません.
一見すると同じauth.handlerによってマウントされたHTTPエンドポイントのラッパーなのですが,auth.apiでは内部的にこれを呼び出すという違いがあります.
「認証方法」を選択する
一口にログインと言っても,パスワード,Googleログイン,その他のソーシャルログインなど,その方法は多岐に亘ります.
Better Authは標準的なEmail and Passwordをはじめとした,様々な認証方法にデフォルトで対応しています.
Better Authでは,これらの認証方法はproviderと呼ばれています.
Googleログイン(ソーシャルログイン)
Email & Passwordが基本の認証方法だと思うのですが,より構成が簡単なGoogleログインについて説明します.
なお,その他のソーシャルログイン(Apple,Microsoft, LinkedInなど)もほぼ同様の手順で進めることができます.
コード上の設定
まずは,Google Cloud ConsoleからOAuth Client IDを取得します.
これは,Google側にソーシャルログインをするアプリを登録する,という理解で問題ないと思います.
具体的な手順は公式ドキュメント(Get your Google credentials)を参照してください.
次に,Google認証を行うためのproviderを設定します.
前述のbetterAuthによるインスタンス作成の際に,以下のようなオプションを渡します.
export const auth = betterAuth({
baseURL: process.env.BETTER_AUTH_URL, // エンドポイントをマウントするホスト
socialProviders: {
google: {
// Google Cloud Consoleで取得したシークレットを設定する
clientId: process.env.GOOGLE_CLIENT_ID as string,
clientSecret: process.env.GOOGLE_CLIENT_SECRET as string,
},
},
});
この設定により,auth.api,authClientで以下のようなメソッドを利用できるようになります.
await auth.api.getAccessToken({
body: {
providerId: "google",
},
headers: headersList,
});
await authClient.signIn.social({
provider: "google",
});
具体的なログイン画面などの実装はドキュメントを参照してください.
ここで重要なのは,providerを追加することで,対応する認証方法がauth.api,authClientで利用できるようになるということです
Email & Password
こちらもドキュメントに従い,以下のように導入することができます.
export const auth = betterAuth({
emailAndPassword: {
enabled: true,
},
});
一般的なユースケースだけあって,簡便に導入できるようになっています.
auth.api,authClientからも,Email認証に対応するメソッドが利用できるようになります.
ここで,signUpを実行してみます.
await auth.api.signUpEmail({
body: {
name: "Example User",
email: "sample@example.com",
password: "password1234",
},
});
await authClient.signUp.email({
name: "Example User",
email: "sample@example.com",
password: "password1234",
});
実際に上記のメソッドを呼び出してみると,以下のようなレスポンスが返却されます.
{
"token": "***",
"user": {
"name": "Example User",
"email": "sample@example.com",
"emailVerified": false,
"createdAt": "***",
"updatedAt": "***",
"id": "***"
}
}
ユーザーが保存されたようなレスポンスが返却されますが,実際にはユーザーは永続化されていません.
実際にユーザーを永続化するためには,databaseの設定を行う必要があります.
import { betterAuth } from "better-auth";
import { Pool } from "pg";
export const auth = betterAuth({
emailAndPassword: {
enabled: true,
},
// postgresqlの例
database: new Pool({
connectionString: "postgres://user:password@localhost:5432/database",
}),
});
databaseの設定がない場合,Better Authはインメモリにユーザーを保存します.
その他のprovider
代表的なソーシャルログインは,前述のGoogleログインのように,socialProvidersとして設定できます.
また,OIDC(OAuth2.0)についてもGeneric OAuthプラグインで対応できるため,様々なIdPに対応しています.
まとめ
まとめると,Better Authは以下のような理解ができます.
- 認証用のエンドポイントを提供する
- エンドポイントにアクセスするインターフェースを提供する
- providerとして抽象化された,様々な認証方法に対応する
- 永続化のために,databaseを設定することができる
ポイントとしては,認証方法と永続化が独立しており,例えばソーシャルログインのような,「その場限りでユーザーの情報を得られれば良い」というような場面では,必ずしも永続化を実装しなくてもいい,という点が挙げられます.
これによって,どのような認証方法を採用しても,違和感のないインターフェースで実装できると感じています.
また,認証方法を自由に選択でき,プラグインで任意に拡張することもできるため,非常に柔軟な認証を実現できます.
認証ライブラリの中でも,特に優れたライブラリだと感じました.
Discussion