Next.jsのSSRでJWTをHeaderに埋め込む方法


概要

Next.jsではCSR, SSR, SSG, ISRといった色々なレンダリング方法を使い分けることが可能です。

SSGやISRはビルド時にhtmlが生成されるためパフォーマンスが最も高く、Next.jsの開発元であるVercelも推奨しています。

ただビルド時だとユーザーのリクエストを受け取れないため、例えばログイン状態でAPIを叩いてレンダリングさせたい場合などはSSGでなくSSRを使ってPre-renderingするのがいいかと思います。

今回はSSRで実際どのように認証情報(JWT)を乗せてリクエストを送るのか、Apollo Clientとaxiosの2パターンを紹介しようと思います

参考
SSG と SSR で理解する Next.js のページレンダリング

前提

JWTをサーバー側に渡すにはcookieを使います
今回はnookiesというライブラリを使います
ログイン処理にて session という名前でトークンを保存しておきます

page/login.tsx
const emailLogin = async () => {
    const token = await user?.getIdToken();

    const options = {
      maxAage: 60 * 60,
      secure: true,
      path: '/',
    }

    if (token) setCookie({ res }, 'session', token, options);    
};

Apollo Client

page/index.tsx
export default function Home({
  postData
}) {
  /* 省略 */
}

export const getServerSideProps: GetServerSideProps = async (context: GetServerSidePropsContext) => {
    const cookies = nookies.get(context);
    const accessToken = cookies.session;
    const apolloClient = initializeApollo(null, context); //apolloClient初期化

    // トークンがない場合ログイン画面にリダイレクト
    if (!accessToken) {
      return {
        redirect: {
          permanent: false,
          destination: '/login',
        },

        props: {} as never,
      };
    }

    const { data: postData } = await apolloClient.query({
      query: GetPostDataDocument,
    });

    return {
      props: {
        postData
      },
    };
}
apolloClient.ts
let apolloClient: ApolloClient<NormalizedCacheObject>;

function createApolloClient(ctx: { req: any }) {
  const httpLink = createHttpLink({
    uri: 'https://api.service.com/',
  });

  const authLink = setContext((req, { headers }) => {
    const accessToken = nookies.get(ctx).session; // JWT取り出し
    return {
      headers: {
        ...headers,
        someoneToken: accessToken || '', // Headerに埋め込み
      },
    };
  });

  return new ApolloClient({
    ssrMode: typeof window === 'undefined',
    link: authLink.concat(httpLink),
    cache: new InMemoryCache(),
    credentials: 'include',
  });
}

export function initializeApollo(initialState = null, ctx) {
  const _apolloClient = apolloClient ?? createApolloClient(ctx); //このctxにcookie情報が含まれる

  if (initialState) {
    _apolloClient.cache.restore(initialState);
  }

  return _apolloClient;
}

ポイント

getServerSideProps は、SSRのときにサーバーサイドでのみ実行されるAPIとなります。

そちらで受け取る context パラメータには Node HTTPServer の req、 res などが内包しており、クライアント側の cookie 情報を取得できます。

なので context を apollo Client の初期化時に引数で渡して、Header に格納するという流れになります。

Axios

page/index.tsx

/* クライアント側はApollo Clientと同様 */

export const getServerSideProps: GetServerSideProps = async (context: GetServerSidePropsContext) => {
  const cookies = context.req.headers.cookie; /* session=AAAAAAAAAA... */

  // トークンがない場合ログイン画面にリダイレクト
  if (!cookies) {
    return {
      redirect: {
        destination: "/login",
        permanent: false,
      },
    };
  }

  const token = cookies.replace('session=', ''); // JWTのみを取り出す

  const res = await api.get('/posts', {
    headers: {
      "Authorization": `Bearer ${token}`
    }
  })
  const postData = res.data;

  return {
    props: {
      postData,
    }
  };
};
api.ts
/* Axios初期化 */

const api = Axios.create({
  baseURL: 'https://api.service.com/',
  headers: {
    Accept: "application/json",
    "Content-Type": "application/json",
  },
});

ポイント

ApolloClientと同様の流れでcontext → cookie → session(JWT) を取り出します。

今回はAPI呼び出し時にheadersを定義しているのでわかりやすいかと思います。

あとがき

自分が詰まった箇所だったのでメモがてらまとめてみました。
参考になりましたら幸いです。