RemixにAuth0認証を組み込んでみた

ReactベースのフレームワークRemixにAuth0の認証機能を組み込んでみました。
2021.12.28

はじめに

こんにちは、CX事業本部MAD事業部の森茂です。

Webアプリケーションをつくるにあたって多くの場合に必要なってくるのが認証の機能です。とはいえ認証の仕組みを1から整えるのはとても大変です。今回はIDaaSとして多く利用されているAuth0を利用した認証の仕組みをRemixに組み込んでみました。

Remixのインストール

RemixアプリケーションはCLIからテンプレートを作成できるのでCLIを利用してプロジェクトを作成します。今回は基本的な構成のRemix App Serverで構築します。

$ npx create-remix@latest

Remixのインストールや初期設定については下記記事も参照ください。

Auth0の組み込み

RemixでAuth0を利用した認証を行いたい場合、SSRを利用するためAuth0から配布されている公式ライブラリはそのまま利用できず若干手間がかかっていましたが、Remixのコントリビューターでもある@sergiodxa 氏のremix-authライブラリを利用することで手軽に環境が用意できます。また今回はAuth0用のStrategyテンプレートのライブラリも一緒にインストールします。

sergiodxa/remix-auth: Simple Authentication for Remix

$ npm install remix-auth remix-auth-auth0

今回は実際に表示を行うURLは/loginのみ。認証前にはログインページにしかアクセスできず、認証後にトップページへアクセスができるというページ構成を想定して作成していきます。

URL ページ種別 備考
/ トップページ 認証あり
/login ログインページ 認証なし
/auth0 Auth0へのリダイレクト用 POSTのみ受け取る
/callback Auth0から認証後にリダイレクトされるURL なし
/logout ログアウト用 POSTのみ受け取る

Auth0の設定を反映

事前にAuth0のアカウントやアプリケーションの用意が必要となります。アプリケーションの設定画面より下記情報をあらかじめ取得し環境変数として諒するために.envファイルとしてプロジェクトルートへ用意しておいてください。

新規にAuth0の環境を用意される場合はは下記記事が参考になると思います。

.env

AUTH0_RETURN_TO_URL=http://localhost:3000
AUTH0_CALLBACK_URL=http://localhost:3000/callback
AUTH0_CLIENT_ID=clientID
AUTH0_CLIENT_SECRET=clientSecret
AUTH0_DOMAIN=******.jp.auth0.com
AUTH0_LOGOUT_URL=https://******.jp.auth0.com/v2/logout
SECRETS=foobar

あわせてアプリケーション内から環境変数を利用するためにdotenvライブラリのインストールと環境変数読み込み用のファイルを用意します。

$ npm install dotenv

app/constants/index.ts

import { config } from 'dotenv';

config();

export const AUTH0_RETURN_TO_URL = process.env.AUTH0_RETURN_TO_URL!;
export const AUTH0_CALLBACK_URL = process.env.AUTH0_CALLBACK_URL!;
export const AUTH0_CLIENT_ID = process.env.AUTH0_CLIENT_ID!;
export const AUTH0_CLIENT_SECRET = process.env.AUTH0_CLIENT_SECRET!;
export const AUTH0_DOMAIN = process.env.AUTH0_DOMAIN!;
export const AUTH0_LOGOUT_URL = process.env.AUTH0_LOGOUT_URL!;
export const SECRETS = process.env.SECRETS!;

Auth0認証用ロジック

Auth0への認証を行うためのサーバー側のロジックを用意します。Remixでは*.server.tsとすることでサーバーサイドでのみ動作するスクリプトを明示的に記載することができます。

import { Authenticator } from 'remix-auth';
import { Auth0Strategy } from 'remix-auth-auth0';
import { createCookieSessionStorage } from 'remix';
import {
  AUTH0_CALLBACK_URL,
  AUTH0_CLIENT_ID,
  AUTH0_CLIENT_SECRET,
  AUTH0_DOMAIN,
  SECRETS,
} from '~/constants';

export type User = {
  email: string;
};

// ユーザーのEmailアドレスを受け取り返すだけのサンプル関数
export async function login(email: string): Promise<User> {
  return { email };
}

const sessionStorage = createCookieSessionStorage({
  cookie: {
    name: '_remix_session',
    sameSite: 'lax',
    path: '/',
    httpOnly: true,
    secrets: [SECRETS],
    secure: process.env.NODE_ENV === 'production',
  },
});

export const authenticator = new Authenticator(sessionStorage);

const auth0Strategy = new Auth0Strategy(
  {
    callbackURL: AUTH0_CALLBACK_URL,
    clientID: AUTH0_CLIENT_ID,
    clientSecret: AUTH0_CLIENT_SECRET,
    domain: AUTH0_DOMAIN,
  },
  async ({ profile }) => {
    // profileにAuth0のプロフィール情報が返ってきます
    console.log(profile);
    //
    // 返ってきた情報を利用してDBへ書き込むなどの処理
    // 加工するなどの処理を入れる
    //
    // 今回はユーザーのEmailアドレスを返す関数を返すのみ
    return await login(profile.emails[0].value);
  },
);

authenticator.use(auth0Strategy);

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

ログイン画面の作成

ログイン、ログアウトの一連の動作を行うためのファイルを作成します。

  • app/routes/index.tsx
  • app/routes/login.tsx
  • app/routes/logout.tsx
  • app/routes/auth0.tsx
  • app/routes/callback.tsx

未ログインのユーザーが/にアクセスすると/loginにリダイレクトされ、ログインボタンを謳歌すると/auth0にPOSTを送り、そのままAuth0のログイン認証画面へリダイレクト。認証処理後に/callbackに戻り、問題なく認証されていれば/にリダイレクトされるという流れになります。

ログアウト時はPOSTを/logoutを送ることでセッション情報がクリアされ/loginページへ再びリダイレクトされます。

app/routes/index.tsx

import type { LoaderFunction } from 'remix';
import { authenticator, User } from '~/utils/auth.server';
import { useLoaderData } from 'remix';

export const loader: LoaderFunction = async ({ request }) => {
  const user = await authenticator.isAuthenticated(request, {
    failureRedirect: '/login',
  });
  return { user };
};

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

  return (
    <div>
      {data.user && (
        <>
          <form action="logout" method="post">
            <button>Logout</button>
          </form>
          <h1>{data.user.email}</h1>
        </>
      )}
      <h1>Welcome to Remix</h1>
    </div>
  );
}

app/routes/login.tsx

app/routes/login.tsx

export default function Login() {
  return (
    <form action="auth0" method="post">
      <button>Login with Auth0</button>
    </form>
  );
}

app/routes/logout.tsx

app/routes/logout.tsx

import { ActionFunction, redirect } from 'remix';
import { destroySession, getSession } from '~/utils/auth.server';
import {
  AUTH0_CLIENT_ID,
  AUTH0_LOGOUT_URL,
  AUTH0_RETURN_TO_URL,
} from '~/constants';

export let action: ActionFunction = async ({ request }) => {
  const session = await getSession(request.headers.get('Cookie'));
  const logoutURL = new URL(AUTH0_LOGOUT_URL);
  logoutURL.searchParams.set('client_id', AUTH0_CLIENT_ID);
  logoutURL.searchParams.set('returnTo', AUTH0_RETURN_TO_URL);
  return redirect(logoutURL.toString(), {
    headers: {
      'Set-Cookie': await destroySession(session),
    },
  });
};

app/routes/auth0.tsx

app/routes/auth0.tsx

import type { ActionFunction, LoaderFunction } from 'remix';
import { redirect } from 'remix';

import { authenticator } from '~/utils/auth.server';

export const loader: LoaderFunction = () => redirect('/login');

export const action: ActionFunction = ({ request }) => {
  return authenticator.authenticate('auth0', request);
};

app/routes/callback.tsx

app/routes/callback.tsx

import type { ActionFunction, LoaderFunction } from 'remix';

import { authenticator } from '~/utils/auth.server';

export const loader: LoaderFunction = ({ request }) => {
  return authenticator.authenticate('auth0', request, {
    successRedirect: '/',
    failureRedirect: '/login',
  });
};

ひととおりファイルができあがったところで開発サーバーを起動し、ブラウザで/loginへアクセスしてみましょう。

ログインボタンを押下するとAuth0のログインページへ飛び、認証後に/へ戻ってくるはずです。認証がうまくいっている場合はページ内にログインユーザーのメールアドレスが表示されます。

なお、認証が必要なページで下記LoaderFunctionを利用することで認証情報の取得や、状態によるリダイレクト処理を行うことができます。

app/routes/index.tsx

...

export const loader: LoaderFunction = async ({ request }) => {
  const user = await authenticator.isAuthenticated(request, {
    failureRedirect: '/login',
  });
  return { user };
};

...

さいごに

RemixでSSRを利用したAuth0の認証機能をサクッと組み込んでみました。remix-authライブラリを利用することで手間をかなり軽減することができました。Strategyテンプレートを利用することでOAuth2を利用した各種認証方式にも対応できるのでAuth0だけでなく他の認証サービスとの連携にも活躍しそうです。