SolidStartと認証(auth0)

この記事は、[SolidJSアドベントカレンダー 2024](https://qiita.com/advent-calendar/2024/solidjs) 2 日目の記事です。

## はじめに

SolidStartの認証機能の実装の記事がなかったので書いてみました。

## ライブラリ

[mediakit](https://mediakit-taupe.vercel.app/auth/install) を使います。

[@auth/solid-start](https://authjs.dev/reference/solid-start) でもいいのですが、@auth/solid-startはsolidstartがv0.2の時のAPIを参照してexampleなどが書かれてるので今回はsolidstartのv1に対応してるmediakitを使っていきます。

### インストール

auth.jsを内部で使ってるので@auth/coreも必要です。

```bash
pnpm add @solid-mediakit/auth @auth/core
```

## 実装

```env:.env
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の以下の箇所から取得してください。

![スクリーンショット 2024-11-30 225547.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3714420/1cb9ae87-d551-8257-925f-012610f35f37.png)


`AUTH0` から始まる3つは私が命名しただけなので、自由に変えてもらって大丈夫です。他３つはライブラリから参照されてるので名前を変更しないでください。

続いてauth.jsを使うときのお約束のauthOptionを作成します。

```tsx:src/lib/auth/option.ts
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を作成します。

```tsx:src/routes/api/auth/[...route].ts
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をつけます。

```tsx:src/app.tsx
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>
  );
}

```

設定完了したのでログインします。

```tsx:src/routes/index.tsx
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

### ログアウト

ログイン同様に実装できます。

```tsx
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$](https://mediakit-taupe.vercel.app/auth/protected)があるのですが、propsがセッションだけに固定されるので自作したほうがいいと思います。

補足：[Next.jsのlayout.tsxで認証チェックすると情報漏洩するかも](https://zenn.dev/moozaru/articles/0d6c4596425da9)が気になったので確認してみましたが、認証通るまで子コンポーネントがレンダリングされず子コンポーネントのfetchが走らないため大丈夫でした。(fetch結果を必要としないdomはclientに渡ってたので注意は必要そう)

```tsx:src/components/protectedRoute.tsx
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>;
  };
}

```

```tsx
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を読み込ませるには設定をする必要がありますが、それについては[公式ドキュメント](https://docs.solidjs.com/solid-start/advanced/middleware)を参照してください。

```tsx:src/middlewares.ts
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),
  ],
});

```