この記事は、SolidJSアドベントカレンダー 2024 2 日目の記事です。
はじめに
SolidStartの認証機能の実装の記事がなかったので書いてみました。
ライブラリ
mediakit を使います。
@auth/solid-start でもいいのですが、@auth/solid-startはsolidstartがv0.2の時のAPIを参照してexampleなどが書かれてるので今回はsolidstartのv1に対応してるmediakitを使っていきます。
インストール
auth.jsを内部で使ってるので@auth/coreも必要です。
pnpm add @solid-mediakit/auth @auth/core
実装
AUTH0_ISSUER=<issuer>
AUTH0_CLIENT_ID=<client_id>
AUTH0_CLIENT_SECRET=<client_secret>
VITE_AUTH_PATH=/api/auth
AUTH_URL=http://localhost:3000
AUTH_SECRET=<secret> # https://authjs.dev/guides/environment-variables#auth-secret を見て生成してください
AUTH0_ISSUER
, AUTH0_CLIENT_ID
, AUTH0_CLIENT_SECRET
はauth0の以下の箇所から取得してください。
AUTH0
から始まる3つは私が命名しただけなので、自由に変えてもらって大丈夫です。他3つはライブラリから参照されてるので名前を変更しないでください。
続いてauth.jsを使うときのお約束のauthOptionを作成します。
import Auth0 from "@auth/core/providers/auth0";
import type { SolidAuthConfig } from "@solid-mediakit/auth";
import crypto from "node:crypto";
if (process.env.NODE_ENV === "development") {
// authjs内部でcryptoを使っているが、なぜか「cryptoがない」と怒られるためglobalに代入する
global.crypto = crypto as never;
}
declare module "@auth/core/jwt" {
interface JWT {
accessToken: string;
}
}
declare module "@auth/core/types" {
interface Session {
accessToken: string;
}
}
const auth0 = Auth0({
issuer: process.env.AUTH0_ISSUER,
clientId: process.env.AUTH0_CLIENT_ID,
clientSecret: process.env.AUTH0_CLIENT_SECRET,
});
export const authOption: SolidAuthConfig = {
basePath: process.env.VITE_AUTH_PATH,
providers: [auth0],
callbacks: {
async jwt({ token, account }) {
if (account) {
token.accessToken = account?.access_token ?? "";
}
return token;
},
async session({ session, token }) {
session.accessToken = token.accessToken;
return session;
},
},
};
auth.jsをssrで使うときのお約束のapi routesを作成します。
import { authOption } from "~/lib/auth/option";
import { SolidAuth, type SolidAuthConfig } from "@solid-mediakit/auth";
const handlers = SolidAuth(authOption);
export const GET = handlers.GET;
export const POST = handlers.POST;
セッションを読み込みを一元管理するためにSesssionProviderをつけます。
import { MetaProvider, Title } from "@solidjs/meta";
import { Route, Router } from "@solidjs/router";
import { FileRoutes } from "@solidjs/start/router";
import { Suspense } from "solid-js";
import "./app.css";
import { SessionProvider } from "@solid-mediakit/auth/client";
export default function App() {
return (
<Router
preload={false}
root={(props) => (
<MetaProvider>
<Title>SolidStart - Basic</Title>
<Suspense>
<SessionProvider>{props.children}</SessionProvider>
</Suspense>
</MetaProvider>
)}
>
<FileRoutes />
</Router>
);
}
設定完了したのでログインします。
import { useAuth } from "@solid-mediakit/auth/client";
import { onMount } from "solid-js";
export default function Page() {
const auth = useAuth();
onMount(() => {
auth.signIn("auth0", { callbackUrl: window.location.origin }); // http://localhost:3000にリダイレクトさせる
});
return <div>Loading...</div>;
}
これでとりあえず使えます。
実際にはToken Rotationの処理も実装することにはなると思うのですが、auth.jsを使った他のフレームワークでのサンプルが探せばあると思うのでそちらを参照してください。
tips
ログアウト
ログイン同様に実装できます。
import { useAuth } from "@solid-mediakit/auth/client";
import { onMount } from "solid-js";
export default function Page() {
const auth = useAuth();
onMount(() => {
auth.signIn("auth0", { callbackUrl: window.location.origin });
});
return <div>Loading...</div>;
}
ページ保護
mediakitにも protected$があるのですが、propsがセッションだけに固定されるので自作したほうがいいと思います。
補足:Next.jsのlayout.tsxで認証チェックすると情報漏洩するかもが気になったので確認してみましたが、認証通るまで子コンポーネントがレンダリングされず子コンポーネントのfetchが走らないため大丈夫でした。(fetch結果を必要としないdomはclientに渡ってたので注意は必要そう)
import { getSession } from "@solid-mediakit/auth/client";
import { createAsync, query, redirect } from "@solidjs/router";
import { type Component, Show } from "solid-js";
import { getRequestEvent } from "solid-js/web";
const getUser = query(async () => {
"use server";
const event = getRequestEvent();
const session = await getSession(event);
if (!session) {
throw redirect("/");
}
return session;
}, "media-user");
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
export function protectedRoute<T extends Record<string, any>>(
Comp: Component<T>
) {
return (props: T) => {
const session = createAsync(() => getUser());
return <Show when={session()}>{(_) => <Comp {...props} />}</Show>;
};
}
import type { RouteSectionProps } from "@solidjs/router";
import { protectedRoute } from "~/components/protectedRoute";
export default protectedRoute<RouteSectionProps>(function PrivateLayout(props) {
return (
<div>
<div>private</div>
{props.children}
</div>
);
});
セッションのpreload
サーバーサイドでセッションを取得できるようmiddlewareを作成します。
これがないとクライアント側での取得になります。
solidstartではmiddlewareを読み込ませるには設定をする必要がありますが、それについては公式ドキュメントを参照してください。
import { authMiddleware } from "@solid-mediakit/auth";
import { createMiddleware } from "@solidjs/start/middleware";
import { authOption } from "./lib/auth/option";
import { redirect } from "@solidjs/router";
export default createMiddleware({
onRequest: [
authMiddleware(true, authOption),
],
});