たかぎとねこの忘備録

プログラミングに関する忘備録を自分用に残しときます。マサカリ怖い。

Remix AuthにおけるStrategyのsuccessメソッドを呼び出してもセッションデータが保存されない問題を解決する

Remix Authとは

Remixにサーバーサイドでの認証を組み込みたいときに便利なのがremix-authというパッケージです。 このパッケージはPassport.jsからとても強い影響を受けていて、ストラテジーベースの認証を提供しています。

github.com

カスタムストラテジーを自分で実装してみた

そしてremix-authではカスタムなストラテジーを自分で実装できます。なので勉強がてらremix-auth-twitterを参考にTwitter認証用のストラテジー車輪の再発明してみました。 特徴としては内部でtwitter-api-v2パッケージを採用することでユニークさ(?)を出しています。

github.com

github.com

問題が発生

しかしその過程で、authenticateのさいごでthis.successを呼び出しても、リダイレクト先のloaderisAuthenticatedの返り値がnullになってしまう問題に直面しました。

この場合、authenticateの内部では認証が成功しているにもかかわらず、failureRedirectに指定したパスに遷移してしまいます。

最初、authenticateの実装方法に間違っているのかと思い、他のコミュニティのStrategyの実装を確認してみました。 しかし、authenticateの内部でcommitSessionを直接呼び出している様子はありませんでした。

そしてsuccessメソッドの実装を確認してみるとわかるのですが、内部でcommitSessionを呼び出していることが確認できます。なので手動で呼び出す必要はないのです。

// remix-auth/build/strategy.js
class Strategy {
  ...
  async success(user, request, sessionStorage, options) {
      // if a successRedirect is not set, we return the user
      if (!options.successRedirect)
          return user;
      let session = await sessionStorage.getSession(request.headers.get("Cookie"));
      // if we do have a successRedirect, we redirect to it and set the user
      // in the session sessionKey
      session.set(options.sessionKey, user);
      session.set(options.sessionStrategyKey || "strategy", this.name);
      throw server_runtime_1.redirect(options.successRedirect, {
          headers: { "Set-Cookie": await sessionStorage.commitSession(session) },
      });
    }
}

次にcallback.tsxloaderを確認してみました。 内部で呼び出しているauthenticator.authenticateに対して、successRedirectを指定せずに返り値のUser型の値を取り出してみます。 その値に対して手動でcommitSessionを呼び出しSet-Cookieを設定することで、上記の問題の解決を図りました。

export const loader: LoaderFunction = async ({ request }) => {
  const user = await authenticator.authenticate("twitter", request, {});
  if (user) {
    const session = await getSession(request);
    session.set("user", user.id);
    return redirect("/authenticated", {
      headers: {
        "Set-Cookie": await commitSession(session),
      },
    });
  } else {
        return redirect("/")
    }
};

・・・が、うまくいきません。。。

UTF-8問題の可能性が浮上

色々ネットを漁っていたら、Remixのリポジトリに、FIX utf8 text content cookie problemというプルリクエストを見つけました。

github.com

要約すると、cookieモジュールの内部で使用されているatob関数とbtoa関数がUTF-8をサポートしていないため、UTF-8を含んだ値を含むクッキーを保存し、そのクッキーを読み込もうとすると正しい結果を得ることができないとのことです。

なので、verifyコールバック関数内でフィルターされたUser型のデータを返すとき、Twitterのユーザー名やプロフィール等は日本語が含まれる可能性が高いので、これを含めないようにします。

代わりにid_strscreen_nameなどの内容のみをセッションデータに保存するようにすることで上記の問題を回避できました。

// app/services/auth.server.ts
...
import { sessionStorage } from "./session.server";
...

export type User = {
  id: number;
  screenName: string;
};

export let authenticator = new Authenticator<User>(sessionStorage);

...

authenticator.use(
  new TwitterStrategy(
    {
      ...
    },
    async ({ accessToken, accessSecret, profile, context }) => {
      return {
        id: profile.id,
        screenName: profile.screen_name,
      };
    }
  ),
  "twitter"
);

まとめ

セッションデータにはあくまでユーザーを識別できる最小の値のみを含むようにして、loaderでそれを取り出し、使用しているデータベースからその識別子を元にユーザーデータを取り出すようにすることを心がけるように設計すると良いと思います。