Closed9
RemixとAuth0で認証
![Plat](https://res.cloudinary.com/zenn/image/fetch/s--2UUCGfAo--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/941e33db28.jpeg)
参考
![Plat](https://res.cloudinary.com/zenn/image/fetch/s--2UUCGfAo--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/941e33db28.jpeg)
remix-authの導入
npm install remix-auth remix-auth-auth0
![Plat](https://res.cloudinary.com/zenn/image/fetch/s--2UUCGfAo--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/941e33db28.jpeg)
Remixプロジェクトの作成
npx create-remix@latest your-project-name
サーバーはVercelを選んだが、これは別にどこでもいいと思う(多分)
![Plat](https://res.cloudinary.com/zenn/image/fetch/s--2UUCGfAo--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/941e33db28.jpeg)
環境変数のセット
- dotenvを導入
npm install dotenv
-
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,
})
}
-
.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
![Plat](https://res.cloudinary.com/zenn/image/fetch/s--2UUCGfAo--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/941e33db28.jpeg)
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>
)
}
![Plat](https://res.cloudinary.com/zenn/image/fetch/s--2UUCGfAo--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/941e33db28.jpeg)
modelの作成
app/models/user.ts
を作成
app/models/user.ts
export interface User {
email: string
}
export async function login(email: string): Promise<User> {
return { email }
}
![Plat](https://res.cloudinary.com/zenn/image/fetch/s--2UUCGfAo--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/941e33db28.jpeg)
authのセットアップ
app/services
1. app/services
フォルダを作成し、app/services/auth.server.ts
とapp/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
app/routes/auth
2. 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),
},
})
}
app/routes/index.tsx
3. 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>
)
}
![Plat](https://res.cloudinary.com/zenn/image/fetch/s--2UUCGfAo--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/941e33db28.jpeg)
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 Origins
とAllowed Origins (CORS)
は、ローカルで動かすときはあんま気にしなくていい気がします。独自ドメインを使うときに気を付けておくといいと思います。
![Plat](https://res.cloudinary.com/zenn/image/fetch/s--2UUCGfAo--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/941e33db28.jpeg)
実行
npm run dev
を実行して http://localhost:3000 にアクセスすると、こんな感じになります。
(※Chakra UIを使って見た目を若干変えていますが、内容は同じだと思います)
Login
ボタンを押すと、
このような画面になれば成功です。
サインイン、サインアップのどちらもちゃんとできるはずです。
サインインすると、https://localhost:3000 に戻され、
こんな感じでログインに使ったメアドが表示されます。
このスクラップは2022/06/14にクローズされました