🐻‍❄️

Remix, Remix Authでredirect_toみたいなものを実現する

2024/10/20に公開

基本的にはドキュメントにとっかかりが書いてあるのでそちらも是非ご参照ください。
手っ取り早く実装方法を知りたい方は、実装するセクションをご覧ください。

前提条件

  • Remix & TypeScript
  • Remix Auth & remix-auth-google (GoogleStrategy)

今回の場合はremix-auth-googleを使用していますが、他のストラテジー, 認証方法でも適用可能です。

既存のコード

/app/routes/auth.google.tsx
import { ActionFunctionArgs, redirect } from "@remix-run/node";

import { authenticator } from "~/scripts/server/user.server";

export const loader = () => redirect("/");

export const action = ({ request }: ActionFunctionArgs) => {
    return authenticator.authenticate("google", request);
};
/app/routes/auth.google.callback.tsx
import { LoaderFunctionArgs } from "@remix-run/node";

import { authenticator } from "~/scripts/server/user.server";

export const loader = ({ request }: LoaderFunctionArgs) => {
    return authenticator.authenticate("google", request, {
        successRedirect: "/dashboard"
    });
};

極めて一般的な実装ですね。
ここで、/invite/himitsuを踏んでログイン or サインアップした場合、認証後に/himitsuにリダイレクトする実装を組んでいきたいと思います。
/invite/himitsuを踏んでないユーザーが/himitsuにアクセスできないようにする〜等は本記事の範囲外です。

ドキュメントを読む

上記に示してある通り、Remix Authのドキュメントにはとっかかりが書いてあります。

Custom redirect URL based on the user
Say we have /dashboard and /onboarding routes, and after the user authenticates, you need to check some value in their data to know if they are onboarded or not.
If we do not pass the successRedirect option to the authenticator.authenticate method, it will return the user data.
Note that we will need to store the user data in the session this way. To ensure we use the correct session key, the authenticator has a sessionKey property.
https://github.com/sergiodxa/remix-auth#custom-redirect-url-based-on-the-user

ざっくり翻訳すると

ユーザーに基づいたカスタムリダイレクトURL
/dashboardと/onboardingというルートがあるとします。ユーザー認証後、ユーザーデータの中の特定の値をチェックして、オンボーディングが完了しているかどうかを確認する必要があります。
authenticator.authenticateメソッドにsuccessRedirectオプションを渡さない場合、ユーザーデータが返されます。
この方法では、ユーザーデータをセッションに保存する必要があることに注意してください。正しいセッションキーを使用するために、authenticatorにはsessionKeyプロパティがあります。

ということなので、successRedirectを指定しなければ自動リダイレクトしないので自前実装を挟む余地があるということになります。

実装する

Sessionの拡張

userキーにはRemix Authで使用するユーザー情報を。
redirect_toキーには主に認証時に使うリダイレクト先の情報を。
それぞれ保存します。

/app/scripts/server/session.server.ts
import { createCookieSessionStorage } from "@remix-run/node";

import type { UserData } from "~/scripts/server/user.server";
// type UserData = { user_id: string };

export const sessionStorage = createCookieSessionStorage<{
    user: UserData, // 追加
    redirect_to: string // 追加
}>({
    cookie: {
        // ...省略
    }
});

export const { getSession, commitSession, destroySession } = sessionStorage;

Authenticatorの拡張

前述のSessionの拡張で指定した、ユーザー情報を格納するキー名をAuthenticator側にも指定しておきます。

/app/scripts/server/user.server.ts
// 前略

const authenticator = new Authenticator<UserData>(sessionStorage, {
    sessionKey: "user" // 追加
    // ☝️ session.server.ts で指定したUserDataのkey(`user`)と同じ値であること
});

// 後略

/invite/himitsuの実装

ここら辺はかなり要件次第なのでいい感じに実装してください。

/app/routes/invite.himitsu.tsx
import { LoaderFunctionArgs, redirect } from "@remix-run/node";

import { commitSession, getSession } from "~/scripts/server/session.server";
import { authenticator } from "~/scripts/server/user.server";

export const loader = async ({ request }: LoaderFunctionArgs) => {
    // ユーザー情報を取得
    const user = await authenticator.isAuthenticated(request);
    // 未ログインの場合
    if (!user) {
        // Sessionを取得
        const session = await getSession(request.headers.get("Cookie"));
        // Sessionの`redirect_to`にリダイレクト先をSET
        session.set("redirect_to", "/himitsu");
        
        // requestをclone
        const new_request = request.clone();
        // cloneしたrequest.headersにSessionを書き込み
        new_request.headers.set("Cookie", await commitSession(session));

        // cloneしたrequestともに認証プロセス開始
        return authenticator.authenticate("google", new_request);
    }

    // ログインしているユーザーが/invite/himitsuを踏んだ場合はトップページへリダイレクト
    // 要件次第で調整してくだ🦏
    return redirect("/");
};

/auth/google/callbackの拡張

ここも基本的にコード内に説明を書いているので追加の説明は不要だと思います。

強いて言えば、今回はSessionに入っている情報を信用する実装方針です。
ユーザーの入力値を直接redirect_toに保存するような仕様にしてしまうと、ユーザーが思わぬ間に攻撃サイトにいる。みたいな脆弱性になるのでリダイレクト前にバリデーションをかけてください。

/app/routes/auth.google.callback.tsx
import { LoaderFunctionArgs, redirect } from "@remix-run/node";

import { commitSession, getSession } from "~/scripts/server/session.server";
import { authenticator } from "~/scripts/server/user.server";

export const loader = async ({ request }: LoaderFunctionArgs) => {
    // successRedirectを削除することで、自動リダイレクトを防止しユーザー情報を取得
    const user = await authenticator.authenticate("google", request);

    // 認証結果をセッションに保存
    const session = await getSession(request.headers.get("Cookie"));
    session.set(authenticator.sessionKey as "user", user);

    // `redirect_to`をセッションから取得
    const redirect_to = session.get("redirect_to");
    session.unset("redirect_to"); // 取得したら削除

    // session内容をもとにheadersを作成
    const headers = new Headers({ "Set-Cookie": await commitSession(session) });

    // もしも、前述の`redirect_to`が存在すれば、headersと共にリダイレクト
    if (redirect_to)
        return redirect(redirect_to, { headers });

    // 存在しなければ規定の/dashboardへ、headersと共にリダイレクト
    return redirect("/dashboard", { headers });
};

まとめ

これを拡張すれば、/invite/${id}みたいなのも簡単に実装可能です。
参考になれば幸いです。

Discussion