初心者でもServerless Next.js+Auth0でログイン機能をもったserverlessなサイトが簡単に作れた話(1)

何でも見てやろう』(小田実)の精神で、自分でもコードを書いてみる編集者・長尾です。
KODANSHAtech LLC.でゼネラルマネージャーやってます。
エンジニアのみなさん大募集中なので、メディアやコンテンツ開発に興味のある方はぜひ〜。
実はAuth0 Ambassadorもやってます。

さて、今回はとっても便利なServerless Next.js1に、とっても便利なIdaaS、Auth0を組み合わせて、簡単便利にサーバレスな会員制サイトを作れそうということで、実験してみた一連の流れをご紹介してみます2

少し書いてみたら結構ボリューミーだったので、以下のように3回に分けようと思います。

  1. セッティングとユーザーのログイン状態を取り回せる基本的なページの構築
  2. SSR時にユーザー情報を必要とすることで、そもそも非ログインユーザーには表示できないページを作る方法
  3. 外部のAPIにauth tokenを送って検証させる方法

元ネタは以下になります。
とくにauth0/nextjs-auth0のexampleをなぞる部分が多くなっています。
- https://github.com/auth0/nextjs-auth0
- https://github.com/danielcondemarin/serverless-next.js

やってみた記事なので、変なところがあったら、ぜひご指摘ください…。

前提

serverless/Auth0など利用するリソースはこちら(折りたたんでいます)

このあたりまでは、すでに利用されている方向けです。
Go Serverless!

も使っていきます。
そしてもちろん、IdaaSとしては、我らがAuth0です。

serverlessでもいろいろなクラウドサービス(SaaS)が利用できますが、本記事ではAWSを利用する形で考えていきます。

Serverless Next.js

serverless-nextjs-logo.gif

ServerlessBlog: https://www.serverless.com/blog/serverless-nextjs/
GitHub: https://github.com/danielcondemarin/serverless-next.js

クラウドリソースをベースに、サーバを必要とせず、サービスを提供できるServerless Framework。
AWSコンソールをポチポチやらなくても、 serverless.ymlに必要な記述をすれば、 CloudFormationによってリソースが立ち上がってくれる、とっても便利なframeworkですよね。

そんなserverlessの世界をさらに豊かにするために作られたのが、 serverless component
ごくかいつまんでいうと、serverlessで構築されるサービスを、 Componentという塊として定義し、再利用可能にしたブロックのようなものです。

参考: https://www.serverless.com/blog/what-are-serverless-components-how-use/

Serverless Next.jsは、簡単設定でNext.jsのserverless modeが立ち上がるというすぐれものです。

筆者は初学者なので、充実した解説は他の方にお願いするとして、大きな特徴だと感じたのは、 Serverless Next.jsがLambda@Edgeでホスティングされるという点です。そのため、

  • pages のServerSideRendering(SSR)がLambda@Edge上で行われる。
  • APIも同じくLambda@Edgeで取り回され、実行される。

これはつまり、 CloudFrontのEdgeですべてが行われるということで、もはや特定のregionに依存しない形でNext.jsを利用したサービスを提供できることを意味します(この点は、後述する設定の仕方に少し影響しています)。

余談ですが、KODANSHAtech LLC.のサイトもServerless Next.jsで作ってみたサイトです。

Auth0

"Identity is Complex. Deal with it."

いわゆるログイン機能、あるいは認証認可の機能を提供するサービスといえば、 CognitoFirebase Authenticationなど、さまざまな選択肢があります。

そんな中、認証認可基盤であることに完全に特化することで、APIを通じた「疎」な世界観にマッチしたIdaaSとして注目されているのが、Auth0です。

どんなサービスにも組み込みやすく、ソーシャルログインやOIDC、SAMLへの対応も簡単、しかもドキュメンテーションが非常に充実していて、何をどう取り回せばセキュアなID管理ができるのか、すぐに調べられるのも大きな特徴でしょう。

この記事の末尾で触れたいと思いますが、Next.jsをserverlessで利用する場面では、やはりAuth0が「疎」なIdaaSを志向していることが、強みを発揮すると思います。

いろいろ書きたくなるのですが、「やってみた」パートに早く移るため、詳細は下記のリンクを示すことで代えさせていただきます!

Getting Started

今回は、Serverless Next.jsにAuth0 Next.jsの公式exampleを組み合わせて、ログイン機能を実現し、ユーザー情報を表示させるところまでを試してみます。

まずは、Serverless Next.jsの準備からです。

Preparing serverless-next.js

terminal
mkdir my-project
cd my-project
npm init -y
npm install --save-dev serverless-next.js
touch .env
touch serverless.yml

serverless.ymlには、よくServerless Frameworkで書くようにproviderとかresoucesといったことを列記する必要はなく、下記の記述だけで事足ります。

serverless.yml
myNextApplication:
  component: serverless-next.js

実際の利用の場面では、サービスを独自のドメインで公開することになると思います。
その場合、Route53を利用して設定するドメインの指定をここに記述します。

serverless.yml
myNextApplication:
  component: serverless-next.js
  inputs:
    domain: "example.com"

サブドメインを利用する場合は、次のようになります。

serverless.yml
myNextApplication:
  component: serverless-next.js
  inputs:
    domain: ["sub","example.com"]

ここで注意が必要なのは、ドメインのCertificateについてです。
利用したいドメインについての証明書は、deployの前に取得しておく必要があります。
しかし、上述したように、Serverless Next.jsはLambda@Edgeでホスティングされるため、

もはや特定のregionに依存しない形でNext.jsを利用したサービスを提供できる

ものになっています。
そのため、利用するドメインに対するcertificateは、Lambda@Edgeが利用可能なus-east-1で取得しておく必要があります。
AWSのCertificate Managerで証明書を取得するときは、リージョンに注意してください。

.envには、AWSのcredentialsを入れておきます。適宜、ご利用のものに入れ替えてください。

.env
AWS_ACCESS_KEY_ID=accesskey
AWS_SECRET_ACCESS_KEY=accesssecret

これはserverless-next.jsのために必要だという設定ではありませんが、あとで利用するために、package.jsonscriptsは以下のようにしておきます。

package.json
{
...
 "scripts": {
    "dev": "next",
    "build": "next build",
    "start": "next start -p $PORT",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
...
}

Preparing Auth0 Application

アカウントの作成などは、前述した他の記事をご参照ください。
ここでは、Auth0に新しいtenantを作り、Applicationsのタブを開きます。

Domain/ClientID/ClientSecretの取得方法や設定など。画像が多いので折りたたんでいます。Click Here to Unfold!

スクリーンショット 2020-05-16 18.37.24.png

+CREATE APPLICATIONで、Regular Web Applicationsを選択し、新しいApplicationを作ります。

スクリーンショット 2020-05-16 18.42.10.png

作ったApplicationのSettingsタブで、以下の情報が確認できるので、これをメモしておきます。

  • Domain
  • Client ID
  • Client Secret

スクリーンショット 2020-05-16 18.44.01.png

また、Settingsの最下部に、Show Advanced Settingsがありますので、ここを開いて、OAuthのタブを確認しておきましょう。
JsonWebToken Signiture AlgorithmRS256OIDC Conformantはonになっている必要があります。

スクリーンショット 2020-05-16 18.47.25.png

次に、ローカルでNext.jsのアプリを立ち上げて試す場合に、Auth0が機能するように、Application URLsのセクションに、次のように記入しておきます。

スクリーンショット 2020-05-16 18.54.13.png

Preparing Next.js Application with Auth0

次に、Next.jsとAuth0でアプリケーションを作っていきます。

terminal
npm install --save next react react-dom
touch next.config.js

next.config.jsには、以下の記述を入れます。

next.config.js
module.exports = {
  target: "serverless"
};

今回は、Next.jsで簡単にAuth0が使えるauth0/nextjs-auth0を利用していきます。

https://github.com/auth0/nextjs-auth0

terminal
npm install --save @auth0/nextjs-auth0 dotenv isomorphic-unfetch
mkdir lib
touch lib/auth0.js
touch lib/auth0-config.js

ちょっとディレクトリの命名に迷うのですが、今回はlibの中にauth0.jsを作ります(ちなみに公式GitHubではREADMEの解説でutil、exampleで使われている実際のディレクトリはlib)。
auth0.jsは以下のように記述します。
公式のままだと、場合によって書き直して使わないといけないので、少し変更していますが、お好みにあわせてどうぞ。

auth0.js
import { initAuth0 } from '@auth0/nextjs-auth0';
import config from './auth0-config';

const auth0 = (opt) => {
  opt = opt || {};
  let params = {
    domain: config.AUTH0_DOMAIN,
    clientId: config.AUTH0_CLIENT_ID,
    clientSecret: config.AUTH0_CLIENT_SECRET,
    scope: opt.scope || config.AUTH0_SCOPE,
    redirectUri: opt.redirectUri || config.REDIRECT_URI,
    postLogoutRedirectUri: opt.postLogoutRedirectUri || config.POST_LOGOUT_REDIRECT_URI,
    session: {
      // The secret used to encrypt the cookie.
      cookieSecret: config.SESSION_COOKIE_SECRET,
      // The cookie lifetime (expiration) in seconds. Set to 8 hours by default.
      cookieLifetime: opt.session && opt.session.cookieLifetime ? opt.session.cookieLifetime : config.SESSION_COOKIE_LIFETIME,
      // (Optional) The cookie domain this should run on. Leave it blank to restrict it to your domain.
      // cookieDomain: config.SESSION_COOKIE_DOMAIN, //今回は使わないでおきます。
      // (Optional) SameSite configuration for the session cookie. Defaults to 'lax', but can be changed to 'strict' or 'none'. Set it to false if you want to disable the SameSite setting.
      cookieSameSite: 'lax',
      // (Optional) Store the id_token in the session. Defaults to false.
      storeIdToken: opt.session && opt.session.storeIdToken ? opt.session.storeIdToken : false,
      // (Optional) Store the access_token in the session. Defaults to false.
      storeAccessToken: opt.session && opt.session.storeAccessToken ? opt.session.storeAccessToken : false,
      // (Optional) Store the refresh_token in the session. Defaults to false.
      storeRefreshToken: opt.session && opt.session.storeRefreshToken ? opt.session.storeRefreshToken : false
    },
    oidcClient: {
      // (Optional) Configure the timeout in milliseconds for HTTP requests to Auth0.
      httpTimeout: opt.oidcClient && opt.oidcClient.httpTimeout ? opt.oidcClient.httpTimeout : 2500,
      // (Optional) Configure the clock tolerance in milliseconds, if the time on your server is running behind.
      clockTolerance: opt.oidcClient && opt.oidcClient.clockTolerance ? opt.oidcClient.clockTolerance : 10000
    }
  };
  if(opt.aud){
    params['audience'] = config.AUDIENCE
  }
  return initAuth0(params);
};

export default auth0;

auth0-config.jsは、公式exampleにあわせ、次のようにしています。

auth0-config.js
if (typeof window === 'undefined') {
   /**
    * サーバーサイドで使われるセッティング
    */
   module.exports = {
     AUTH0_CLIENT_ID: process.env.AUTH0_CLIENT_ID,
     AUTH0_CLIENT_SECRET: process.env.AUTH0_CLIENT_SECRET,
     AUTH0_SCOPE: process.env.AUTH0_SCOPE,
     AUTH0_DOMAIN: process.env.AUTH0_DOMAIN,
     REDIRECT_URI: process.env.REDIRECT_URI,
     POST_LOGOUT_REDIRECT_URI: process.env.POST_LOGOUT_REDIRECT_URI,
     SESSION_COOKIE_SECRET: process.env.SESSION_COOKIE_SECRET,
     SESSION_COOKIE_LIFETIME: process.env.SESSION_COOKIE_LIFETIME,
     //SESSION_COOKIE_DOMAIN: process.env.SESSION_COOKIE_DOMAIN //指定したい場合。以下略。
     AUDIENCE: process.env.AUDIENCE
   };
} else {
   /**
    * クライアントサイドに露出するセッティング
    */
   module.exports = {
     AUTH0_CLIENT_ID: process.env.AUTH0_CLIENT_ID,
     AUTH0_SCOPE: process.env.AUTH0_SCOPE,
     AUTH0_DOMAIN: process.env.AUTH0_DOMAIN,
     REDIRECT_URI: process.env.REDIRECT_URI,
     POST_LOGOUT_REDIRECT_URI: process.env.POST_LOGOUT_REDIRECT_URI
   };
}

次に、next.config.jsenvで情報を読み込みます。

next.config.js
const dotenv = require('dotenv');

dotenv.config();

module.exports = {
  target: "serverless",
  env: {
    AUTH0_DOMAIN: process.env.AUTH0_DOMAIN,
    AUTH0_CLIENT_ID: process.env.AUTH0_CLIENT_ID,
    AUTH0_CLIENT_SECRET: process.env.AUTH0_CLIENT_SECRET,
    AUTH0_SCOPE: 'openid profile',
    REDIRECT_URI: process.env.REDIRECT_URI || 'http://localhost:3000/api/callback',
    POST_LOGOUT_REDIRECT_URI: process.env.POST_LOGOUT_REDIRECT_URI || 'http://localhost:3000/',
    SESSION_COOKIE_SECRET: process.env.SESSION_COOKIE_SECRET,
    SESSION_COOKIE_LIFETIME: 7200, // 2 hours
    AUDIENCE: process.env.AUDIENCE
  }
};

最後に各種の情報を.envに追記します。
先ほど、Auth0のコンソールをみながらメモした情報です。

.env
AWS_ACCESS_KEY_ID=accesskey
AWS_SECRET_ACCESS_KEY=accesssecret

AUTH0_DOMAIN=yourdomain.auth0.com
AUTH0_CLIENT_ID=************
AUTH0_CLIENT_SECRET=**************
REDIRECT_URI=*************
POST_LOGOUT_REDIRECT_URI=*********
SESSION_COOKIE_SECRET=************ //40文字以上のランダム文字列
AUDIENCE=******  //これはのちにAPIの保護で使うもので、今回はあまり関係ありません

テストの段階では、localhost:3000で動かすため、REDIRECT_URIPOST_LOGOUT_REDIRECT_URI.envから削除しておいてください。

Preparing APIs

次に、ログインやログアウトを取り回すAPIをpages/apiに作っていきます。

terminal
mkdir pages
mkdir pages/api
touch pages/api/{login.js,logout.js,callback.js,me.js}

lib/auth0の作りを公式とは少し変えていますので、それにあわせて変更しています。
Auth0のライブラリが細かいところをマネージしてくれるので、ひとつひとつは非常にシンプルですね。

login.js
import auth0 from '../../lib/auth0';

export default async function login(req, res) {
  try {
    await auth0().handleLogin(req, res);
  } catch (error) {
    console.error(error);
    res.status(error.status || 500).end(error.message);
  }
}
logout.js
import auth0 from '../../lib/auth0';

export default async function logout(req, res) {
  try {
    await auth0().handleLogout(req, res);
  } catch (error) {
    console.error(error);
    res.status(error.status || 500).end(error.message);
  }
}
callback.js
import auth0 from '../../lib/auth0';

export default async function callback(req, res) {
  try {
    await auth0().handleCallback(req, res);
  } catch (error) {
    console.error(error);
    res.status(error.status || 500).end(error.message);
  }
}
me.js
import auth0 from '../../lib/auth0';

export default async function me(req, res) {
  try {
    await auth0().handleProfile(req, res);
  } catch (error) {
    console.error(error);
    res.status(error.status || 500).end(error.message);
  }
}

callback.jsは、ログイン時のcallbackにあたります。
上の設定時に、Auth0で、

としたのは、このためです。
/api/loginを経て、Auth0の認証画面から戻ってきた際、callbackにはクエリストリングとしてsession情報(やstate。今回はsessionしかありません)が渡されてきます。
handleCallbackは、このクエリストリングの情報を取得し、検証後に暗号化されたsession cookieを保存する役割を果たしています。

Handling User State

componentsを作り始める前に、もうひと手間!
ユーザーのログイン状態を取り回すuser.jslibに作っておきます。
個人的には、contextsやhocsに切り分けて書かれているほうが読みやすいような気がしたんですが、それはまた別の機会に挑戦してみます。

terminal
touch lib/user.js
user.js
import React from 'react';
import fetch from 'isomorphic-unfetch';

// グローバルにユーザーを保存することで、ページ遷移時に再度APIを呼んで読み込むことを回避。
let userState;

const User = React.createContext({ user: null, loading: false });

export const fetchUser = async () => {
  if (userState !== undefined) {
    return userState;
  }

// 先ほど作った'api/me'を呼んで、ログインしているユーザーの情報を取得。
  const res = await fetch('/api/me');
  userState = res.ok ? await res.json() : null;
  return userState;
};

export const UserProvider = ({ value, children }) => {
  const { user } = value;

  // SSR時にユーザーがfetchされていれば、userStateに追加。これにより、再度fetchする必要がなくなる。
  React.useEffect(() => {
    if (!userState && user) {
      userState = user;
    }
  }, []);

  return <User.Provider value={value}>{children}</User.Provider>;
};

export const useUser = () => React.useContext(User);

export const useFetchUser = () => {
  const [data, setUser] = React.useState({
    user: userState || null,
    loading: userState === undefined
  });

  React.useEffect(() => {
    if (userState !== undefined) {
      return;
    }

    let isMounted = true;

    fetchUser().then((user) => {
      // componentがまだマウントされているときだけユーザーをセット。
      if (isMounted) {
        setUser({ user, loading: false });
      }
    });

    return () => {
      isMounted = false;
    };
  }, [userState]);

  return data;
};

Creating Components

いよいよComponentの準備です。
ここでは、ヘッダーにログイン/ログアウトのボタンがある、一般的なレイアウトのページを作っていきます。

terminal
mkdir components
touch components/{header.jsx,layout.jsx}
header.jsx
import React from 'react';
import Link from 'next/link';

import { useUser } from '../lib/user'; //先ほどのuserからユーザーの状態をもらってくる。

const Header = () => {
  const { user, loading } = useUser();

  return (
    <header>
      <nav>
        <ul>
          <li>
            <Link href="/">
              <a>Home</a>
            </Link>
          </li>
          {!loading &&
            (user ? (
              <>
                <li>
                  <Link href="/profile">
                    <a>Profile</a>
                  </Link>
                </li>{' '}
                <li>
                  <a href="/api/logout">Logout</a>
                </li>
              </>
            ) : (
              <>
                <li>
                  <a href="/api/login">Login</a>
                </li>
              </>
            ))}
        </ul>
      </nav>

      <style jsx>{`
        header {
          padding: 0.2rem;
          color: #fff;
          background-color: #333;
        }
        nav {
          max-width: 42rem;
          margin: 1.5rem auto;
        }
        ul {
          display: flex;
          list-style: none;
          margin-left: 0;
          padding-left: 0;
        }
        li {
          margin-right: 1rem;
        }
        li:nth-child(1) {
          margin-right: auto;
        }
        a {
          color: #fff;
          text-decoration: none;
        }
        button {
          font-size: 1rem;
          color: #fff;
          cursor: pointer;
          border: none;
          background: none;
        }
      `}</style>
    </header>
  );
};

export default Header;

些細な点ですが、<style jsx>:nth-child(1)という書き方をしているところがあります。
これは、公式ではページのバリエーションがもう少し多かった(本当は3だった)ためなので、:first-childでもよいと思います。

layout.jsx
import React from 'react';
import Head from 'next/head';

import Header from './header';
import { UserProvider } from '../lib/user';

const Layout = ({ user, loading = false, children }) => (
  <UserProvider value={{ user, loading }}>
    <Head>
      <title>Next.js with Auth0</title>
    </Head>

    <Header />

    <main>
      <div className="container">{children}</div>
    </main>

    <style jsx>{`
      .container {
        max-width: 42rem;
        margin: 1.5rem auto;
      }
    `}</style>
    <style jsx global>{`
      body {
        margin: 0;
        color: #333;
        font-family: -apple-system, 'Segoe UI';
      }
    `}</style>
  </UserProvider>
);

export default Layout;

Creating Pages

いよいよpagesを作っていきます。

terminal
touch pages/{index.jsx,profile.jsx}
index.jsx
import React from 'react';

import Layout from '../components/layout';
import { useFetchUser } from '../lib/user';

export default function Home() {
  const { user, loading } = useFetchUser();

  return (
    <Layout user={user} loading={loading}>
      <h1>Next.js and Auth0 Example</h1>

      {loading && <p>Loading login info...</p>}

      {!loading && !user && (
        <>
          <h4>Try it!</h4>
          <p>
            To test the login click in <i>Login</i>
          </p>
        </>
      )}

      {user && (
        <>
          <h4>Welcome!</h4>
          <p>You successfully logged in!</p>
        </>
      )}
    </Layout>
  );
}
profile.jsx
import React from 'react';

import Layout from '../components/layout';
import { useFetchUser } from '../lib/user';

export default function Profile() {
  const { user, loading } = useFetchUser();

  return (
    <Layout user={user} loading={loading}>
      <h1>Profile</h1>

      {loading && <p>Loading profile...</p>}

      {!loading && user && (
        <>
          <p>Profile:</p>
          <pre>{JSON.stringify(user, null, 2)}</pre>
        </>
      )}
    </Layout>
  );
}

Test run!

まずはローカルでテストです。npm run devhttp://localhost:3000を確認してみます。

スクリーンショット 2020-05-17 13.53.15.png

こんな画面が出てきます。Loginをclickしてみます。

スクリーンショット 2020-05-17 13.54.45.png

Auth0が提供しているログインのウィジェットlockが表示されます。
lockもいろいろカスタマイズができて便利なのですが、今回の記事では触れません。

ログインを進めてみます。

スクリーンショット 2020-05-17 13.57.29.png

狙い通り、メッセージが切り替わり、headerのメニューがProfileLogoutに切り替わりました。
ここで試しにcookieをチェックしてみると、

スクリーンショット 2020-05-17 13.59.22.png

a0:sessionという名前で、セッションが保存されています。
また、指定通りSame-SiteLax、さらに標準でHttp-Onlyになっていることがわかります。

これは前述したように、login後のcallbackで/api/callbackに帰ってきた際に、handleCallbackによってクエリストリングから保存しなおされたものです。
今回の記事では触れませんが、APIの保護に利用するauth tokenなどを取り回す場合は、同様にhandleCallbackがcookieに置き直してくれる情報を利用していきます。

では、お楽しみのProfileページを見てみましょう。

スクリーンショット 2020-05-17 14.02.46.png

ここではGoogleを利用してログインしたので、OIDCで取れてくる情報がユーザー情報として表示されています。
subはユーザーのユニークなidにあたるものですが、Auth0の場合は{connection name}|{random id}という形式になっています。
connectionとはユーザープール名のようなものですが、socail loginの場合は上記のようにgoogle-oauth2などとid providerの種類が入ってきます。

また、ここでは、emailが含まれていないことに気づかれた方もいらっしゃるでしょう。
利用場面によっては、当然、「ユーザーのメールアドレスがほしい」ということもあると思います。

その場合は、.envに記述したscopeの部分にemailを追加し、openid profile emailとすると、メールアドレスが取れるようになります。

Deploy!

さて、最後にdeployの方法です。これはとんでもなく簡単です(注:まだやらないでください)。

terminal
npx serverless

終了! と言いたいところですが、上記したように、このままdeployしてもAuth0のSettingが済んでいません。

Auth0のコンソールを開き、上のAuth0の設定についてのセクションで、http://localhost:3000http://localhost:3000/api/callbackとしていた部分に、serverless.ymlで指定したドメインについても追記する必要があります。
あらためて画像を貼ると、console-->Applications-->Settings-->Application URLsのセクションです。

スクリーンショット 2020-05-16 18.54.13.png

また「テストの段階では削除しておいてください」と注記しておいた.envにも、修正が必要です。
REDIRECT_URIPOST_LOGOUT_REDIRECT_URIに、それぞれdeploy後の正しいURLを記述する必要があります。
REDIRECT_URIのほうが、https://your.domain.com/api/callbackの形になります。

ここまで確認できたら、上述のnpx serverlessを実行してみてください。
deployの速度が早いことも体感できると思います。
この高速deployを可能にしている大きな理由のひとつが、Serverless Next.jsがCloudFormationを利用していないことです。

Behind the Curtain

今回、nextjs-auth0を使ってみようと思ったのは、次のブログを読んだからでした。

https://auth0.com/blog/ultimate-guide-nextjs-authentication-auth0/

このブログでは、Next.jsでAuthenticationを実装する代表的なシナリオについて、それぞれの具体的な方法や長所・短所がまとめられています。

中でも興味をひかれたのが、 "Next.js Serverless Deployment Model" というセクションでした。
ここに、上でたびたび触れた、callbackでの挙動に関連した説明がありますので、はしょりながらですが、ざっと訳してみようと思います。

......Next.jsがその輝きを見せるのは、すべてのページやAPI Routeが、それぞれZEIT NowやAWS Lambdaのようなserverless functionとして実装される、serverless deployment modelのもとで利用される場面だ。

このモデルでは、(Express.jsのような)本格的なweb frameworkは存在しない。その代わり、ランタイムは( (req, res)=>{} という形で)リクエストとレスポンスのオブジェクトをやりとりする関数を実行することになる。そして、このことが我々がExpress.jsのような伝統的なweb frameworkや、Passport.jsのようにユーザーの認証を取り回したり、express-sessionsのようにsessionを作ったりする、できあいのパッケージを利用できない大きな理由になっている。

(中略)

......nextjs-auth0を利用すると、ユーザーはAuthorization Code Grantを利用してサインインすることになる。ユーザーはまず、必要なすべての認証認可ロジック(サインアップ、サインイン、MFA、<social loginなどの>許可など)を取り回すAuth0にリダイレクトされ、そののち、(サービス側の)アプリケーションにクエリストリングにAuthorizationCodeを含んだ状態でリダイレクトされて帰ってくる。

サーバサイド(というより、serverless function)は、このコードをid_token、またオプションとしてaccess_tokenrefresh_tokenと交換する。id_tokenが検証されたのち、セッションが作られ、暗号化されたcookieとして保存される。ページが(サーバサイドで)レンダリングされるか、API Routeが呼ばれるたびに、session cookieがserverless functionsに渡されることで、serverless functionsはセッションや関連するユーザー情報にアクセスすることができるようになる。

実のところ、今回、実験してみた範囲は、クライアントサイドのみでのuserの取り回しでした。
なので、たとえばProfileページのURLをログインしていない状態で直に叩くと、次のような画面になります。

スクリーンショット 2020-05-17 15.39.56.png

もちろん、不用意にuserの情報が露出するといったことはないわけですが、実際のサービスを構築する場面を想定すると、ユーザー情報が抜けた「枠」だけのページであっても、非ログイン状態のユーザーがアクセスできるのは、いささか不都合です。

しかし、上の翻訳にあるように、nextjs-auth0を利用する方法なら、サーバサイドでユーザー情報を利用することもできるわけです。

次回は、サーバサイドでuserを見ることで、非ログインユーザーによるURLへの直アクセスでログインを求める仕掛けを作る方法を書いてみたいと思います。


  1. ここでいう「Serverless Next.js」は、Next.jsのserverless modeについてではなく、それを利用したserverless componentとして提供されているserverless-next.jsを指します。 

  2. 本書き込みは、基本的に初学者の「やってみた話」です。productionレベルでのご検討、より正確な情報は各種公式ドキュメント等をご参照ください。 

iMissYu
2019年で創業110周年となるtraditionalな出版社で、エンジニアのみなさんの力をいただきながら、どうにか自社開発のチームを立ち上げました。いわゆる「編集者」ですが、おもしろいことができればと思っています。KODANSHAtech LLC.でGMもやってます。
https://kodansha.tech/ja
kodanshatech
現代ビジネス、FRIDAYデジタル、ブルーバックス、FRaU、ViVi、VOCEなど講談社のウェブメディアやデジタルコンテンツ開発を行っています。
https://kodansha.tech/ja
ユーザー登録して、Qiitaをもっと便利に使ってみませんか。
  1. あなたにマッチした記事をお届けします
    ユーザーやタグをフォローすることで、あなたが興味を持つ技術分野の情報をまとめてキャッチアップできます
  2. 便利な情報をあとで効率的に読み返せます
    気に入った記事を「ストック」することで、あとからすぐに検索できます
ユーザーは見つかりませんでした