Closed9

RemixとAuth0で認証

remix-authの導入

npm install remix-auth remix-auth-auth0

Remixプロジェクトの作成

npx create-remix@latest your-project-name

サーバーはVercelを選んだが、これは別にどこでもいいと思う(多分)

環境変数のセット

  1. dotenvを導入
npm install dotenv
  1. app/entry.server.tsxを編集
    dotenvを読み込むようにする
app/entry.server.tsx
+ import "dotenv/config"
import ReactDOMServer from "react-dom/server"
import type { EntryContext } from "remix"
import { RemixServer } from "remix"

export default function handleRequest(
    request: Request,
    responseStatusCode: number,
    responseHeaders: Headers,
    remixContext: EntryContext
) {
    let markup = ReactDOMServer.renderToString(<RemixServer context={remixContext} url={request.url} />)

    responseHeaders.set("Content-Type", "text/html")

    return new Response("<!DOCTYPE html>" + markup, {
        status: responseStatusCode,
        headers: responseHeaders,
    })
}
  1. .envを作成
.env
AUTH0_CALLBACK_URL="http://localhost:3000/auth/callback" # callbackで使うurl
AUTH0_CLIENT_ID="hogehogehogehoge" # auth0のclient id
AUTH0_CLIENT_SECRET="hugahugahugahuga" # auth0のclient secret
AUTH0_DOMAIN="hogehoge.auth0.com" # auth0のドメイン
AUTH0_LOGOUT_URL="http://localhost:3000/auth/logout" # logoutで使うurl
AUTH0_RETURN_TO_URL="http://localhost:3000" # ログアウトした後にリダイレクトするurl

app/root.tsxを変更

app/root.tsx
import type { LinksFunction, LoaderFunction } from "remix"
import { Links, LiveReload, Meta, Outlet, Scripts, useCatch, useLoaderData } from "remix"
import stylesUrl from "./styles/global.css"

# スタイルの指定とかはお好みで
export let links: LinksFunction = () => {
    return [{ rel: "stylesheet", href: stylesUrl }]
}

export let loader: LoaderFunction = async () => {
    return { date: new Date() }
}

function Document({ children, title }: { children: React.ReactNode; title?: string }) {
    return (
        <html lang="en">
            <head>
                <meta charSet="utf-8" />
                <link rel="icon" href="/favicon.ico" type="image/ico" />
                {title ? <title>{title}</title> : null}
                <Meta />
                <Links />
            </head>
            <body>
                  {children}
                  <Scripts />
                  {process.env.NODE_ENV === "development" && <LiveReload />}
            </body>
        </html>
    )
}

export default function App() {
    let data = useLoaderData()

    return (
        <Document>
            <Outlet />
            <footer>
                <p>This page was rendered at {data.date.toLocaleString()}</p>
            </footer>
        </Document>
    )
}

export function CatchBoundary() {
    let caught = useCatch()

    switch (caught.status) {
        case 401:
        case 404:
            return (
                <Document title={`${caught.status} ${caught.statusText}`}>
                    <h1>
                        {caught.status} {caught.statusText}
                    </h1>
                </Document>
            )

        default:
            throw new Error(`Unexpected caught response with status: ${caught.status}`)
    }
}

export function ErrorBoundary({ error }: { error: Error }) {
    console.error(error)

    return (
        <Document title="Uh-oh!">
            <h1>App Error</h1>
            <pre>{error.message}</pre>
            <p>Replace this UI with what you want users to see when your app throws uncaught errors.</p>
        </Document>
    )
}

modelの作成

app/models/user.tsを作成

app/models/user.ts
export interface User {
    email: string
}

export async function login(email: string): Promise<User> {
    return { email }
}

authのセットアップ

1. app/services

app/servicesフォルダを作成し、app/services/auth.server.tsapp/services/session.server.tsを作成

app/services/auth.server.ts
import { Authenticator } from "remix-auth"
import { Auth0Strategy, Auth0ExtraParams, Auth0Profile } from "remix-auth-auth0"
import { login, User } from "~/models/user"
import { sessionStorage } from "~/services/session.server"

// Create an instance of the authenticator, pass a generic with what your
// strategies will return and will be stored in the session
export const authenticator = new Authenticator<User>(sessionStorage)

if (!process.env.AUTH0_CALLBACK_URL) {
    throw new Error("Missing AUTH0_CALLBACK_URL env")
}

if (!process.env.AUTH0_CLIENT_ID) {
    throw new Error("Missing AUTH0_CLIENT_ID env")
}

if (!process.env.AUTH0_CLIENT_SECRET) {
    throw new Error("Missing AUTH0_CLIENT_SECRET env")
}

if (!process.env.AUTH0_DOMAIN) {
    throw new Error("Missing AUTH0_DOMAIN env")
}

if (!process.env.AUTH0_LOGOUT_URL) {
    throw new Error("Missing AUTH0_LOGOUT_URL env")
}

authenticator.use(
    new Auth0Strategy(
        {
            callbackURL: process.env.AUTH0_CALLBACK_URL,
            clientID: process.env.AUTH0_CLIENT_ID,
            clientSecret: process.env.AUTH0_CLIENT_SECRET,
            domain: process.env.AUTH0_DOMAIN,
        },
        async ({ accessToken, refreshToken, extraParams, profile }) => {
            // Get the user data from your DB or API using the tokens and profile
            console.log(profile)
            return login(profile.emails[0].value)
        }
    ),
    "auth0"
)
app/services/session.server.ts
// app/services/session.server.ts
import { createCookieSessionStorage } from "remix"

// export the whole sessionStorage object
export let sessionStorage = createCookieSessionStorage({
    cookie: {
        name: "_session", // use any name you want here
        sameSite: "lax", // this helps with CSRF
        path: "/", // remember to add this so the cookie will work in all routes
        httpOnly: true, // for security reasons, make this cookie http only
        secrets: ["SeCreT"], // replace this with an actual secret (よくわからん。多分なんでもいい)
        secure: process.env.NODE_ENV === "production", // enable this in prod only
    },
})

// you can also export the methods individually for your own usage
export let { getSession, commitSession, destroySession } = sessionStorage

2. app/routes/auth

app/routes/authを作成し、

  • app/routes/auth/callback.tsx
  • app/routes/auth/login.tsx
  • app/routes/auth/logout.tsx
    を作成する
app/routes/auth/callback.tsx
import { LoaderFunction } from "remix"
import { authenticator } from "~/services/auth.server"

export let loader: LoaderFunction = async ({ request }) => {
    await authenticator.authenticate("auth0", request, {
        successRedirect: "/",
        failureRedirect: "/",
    })
}
app/routes/auth/login.tsx
import { ActionFunction } from "remix"
import { authenticator } from "~/services/auth.server"

export let action: ActionFunction = async ({ request }) => {
    await authenticator.authenticate("auth0", request)
}
app/routes/auth/logout.tsx
import { ActionFunction, redirect } from "remix"
import { destroySession, getSession } from "~/services/session.server"

export const action: ActionFunction = async ({ request }) => {
    if (!process.env.AUTH0_CLIENT_ID) {
        throw new Error("Missing AUTH0_CLIENT_ID env")
    }

    if (!process.env.AUTH0_DOMAIN) {
        throw new Error("Missing AUTH0_DOMAIN env")
    }

    if (!process.env.AUTH0_LOGOUT_URL) {
        throw new Error("Missing AUTH0_LOGOUT_URL env")
    }

    if (!process.env.AUTH0_RETURN_TO_URL) {
        throw new Error("Missing AUTH0_RETURN_TO_URL env")
    }

    const session = await getSession(request.headers.get("Cookie"))
    const logoutURL = new URL(`https://${process.env.AUTH0_DOMAIN}/v2/logout`)
    logoutURL.searchParams.set("client_id", process.env.AUTH0_CLIENT_ID)
    logoutURL.searchParams.set("returnTo", process.env.AUTH0_RETURN_TO_URL)
    return redirect(logoutURL.toString(), {
        headers: {
            "Set-Cookie": await destroySession(session),
        },
    })
}

3. app/routes/index.tsx

import { Link, LoaderFunction, MetaFunction, useLoaderData, Form } from "remix"
import { User } from "~/models/user"
import { authenticator } from "~/services/auth.server"

export const meta: MetaFunction = () => {
    return {
        title: "Remix Starter",
        description: "Welcome to remix!",
    }
}

export const loader: LoaderFunction = async ({ request }) => {
    const user = await authenticator.isAuthenticated(request)
    return { message: "this is awesome 😎", user }
}

export default function Index() {
    const data = useLoaderData<{ user: User; message: string }>()

    return (
        <div style={{ textAlign: "center", padding: 20 }}>
            <h2>Welcome to Remix!</h2>
            <p>
                <a href="https://docs.remix.run">
                    Check out the docs
                </a>{" "}
                to get started.
            </p>
            <p>Message from the loader: {data.message}</p>
            <p>
                <Link to="not-found">Link to 404 not found page.</Link> Clicking this link will land you in your root
                CatchBoundary component.
            </p>
            {!data.user && (
                <>
                    <div>Not logged in</div>
                    <form action="/auth/login" method="post">
                        <button type="submit">Login</button>
                    </form>
                </>
            )}
            {data.user && (
                <>
                    <div>User: {data.user.email}</Box>
                    <form action="/auth/logout" method="post">
                        <button>Logout</button>
                    </form>
                </>
            )}
        </div>
    )
}

Auth0の設定

.envの内容(再掲)

.env
AUTH0_CALLBACK_URL="http://localhost:3000/auth/callback" # callbackで使うurl
AUTH0_CLIENT_ID="hogehogehogehoge" # auth0のclient id
AUTH0_CLIENT_SECRET="hugahugahugahuga" # auth0のclient secret
AUTH0_DOMAIN="hogehoge.auth0.com" # auth0のドメイン
AUTH0_LOGOUT_URL="http://localhost:3000/auth/logout" # logoutで使うurl
AUTH0_RETURN_TO_URL="http://localhost:3000" # ログアウトした後にリダイレクトするurl

Basic Information

Domain→AUTH0_DOMAIN
Client ID→AUTH0_CLIENT_ID (xxx.xx.auth0.comみたいな感じになってる)
Client Secret→AUTH0_CLIENT_SECRET

Application Properties

多分SPAでいいと思う

Application URIs

この設定をちゃんとやらないとちゃんと認証ができない。
今回の通りに進める場合は、画像の通りに設定すれば問題ない。
Allowed Logout URLsで設定するURLはリダイレクト先のURL(AUTH0_RETURN_TO_URL)を入れることに注意してください。

Allowed Web OriginsAllowed Origins (CORS)は、ローカルで動かすときはあんま気にしなくていい気がします。独自ドメインを使うときに気を付けておくといいと思います。

実行

npm run dev

を実行して http://localhost:3000 にアクセスすると、こんな感じになります。
(※Chakra UIを使って見た目を若干変えていますが、内容は同じだと思います)

Loginボタンを押すと、

このような画面になれば成功です。
サインイン、サインアップのどちらもちゃんとできるはずです。

サインインすると、https://localhost:3000 に戻され、

こんな感じでログインに使ったメアドが表示されます。

このスクラップは6ヶ月前にクローズされました
ログインするとコメントできます