Rails×Next.js×Auth0で認証機能をサックと開発する

## はじめに

今回はRailsのAPIモードとNext.jsを利用したSPA構成に、Auth0で認証機能をサクッと開発していきます。

Auth0は導入が簡単かつチュートリアルやドキュメントが豊富なので、初めての人でも簡単に認証機能を実装することができます。

直近で自分が行った新規開発案件でもAuth0が採用されることが多かったです。

なおAuth0についての詳しい解説は本記事では割愛するので気になった方は、こちらを確認してみてください。

https://dev.classmethod.jp/articles/auth0-architecture/

またAuth0とFirebase Authenticationといった他の認証機能との比較記事はこちらで確認してみてください。

https://qiita.com/yutaro50/items/55455fe34385852d5ac0

本記事で解説する内容はAuth0の公式ドキュメントに沿って解説していきます。

https://auth0.com/docs/quickstart/spa/react/01-login

https://auth0.com/blog/building-secure-apis-with-rails-6-and-auth0/

## この記事の対象者

- Next.js×Auth0で認証機能を開発したい
- RailsのAPIモードでAuth0の認証機能を開発したい
- Rails×Next.jsで認証機能(新規登録・ログイン・ログアウト)を開発したい
- 手を動かしながらAuth0を理解したい
- SPAで認証機能を持つアプリを開発したい


## フロント側(Next.js側)の開発

まずはフロント側でのAuth0の実装をしていきます。

### Next.jsプロジェクトの環境構築

nodeやyarnの環境構築されているものとして進めていきます。

またフロント側はauth0-front、バックエンド側はauth0-backという名前でプロジェクトを作成します。(これは任意です)

さっそく下記のコマンドでNext.js×TypeScriptの環境を作成します。

```
npx create-next-app auth0-front --typescript
```

終わったらdev環境を立ち上げます

```
npm run dev   
```
こちらが表示されていたら、Next.js×TypeScriptのプロジェクトの環境構築は完了です。

![スクリーンショット 2022-07-11 18.27.40.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2695521/934a39b9-d177-4c85-89c4-8d7cf135f809.jpeg)

### Auth0でのプロジェクト作成

次にAuth0側での設定を進めていきます。

こちらは公式ドキュメントを元に解説を進めていきます。

https://auth0.com/docs/quickstart/spa/react/01-login

まず初めに[Auth0の公式サイト](https://auth0.com/jp)へアクセスしGoogleログイン等でAuth0へのアカウント作成を行います。

今回は個人で開発として進めるので```Personal```を選択します。

![スクリーンショット 2022-07-11 18.31.00.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2695521/5f2dd312-5bb4-e471-c61e-d2d17a9b2e66.jpeg)

次に「+Create Application」からAuth0を利用する新規にプロジェクトを作成します。

![スクリーンショット 2022-07-11 18.32.27.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2695521/54012a88-ea1d-3114-6672-f147a07134fd.jpeg)

開発するアプリ名を入れて、**Single Page Web Applications**を今回は選択します。

![スクリーンショット 2022-07-11 18.33.21.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2695521/cbdb8c85-f5aa-e630-8e3d-f61623f5a5a8.jpeg)

次にsettingより「**Domain**」「**Client ID**」を確認しておきます。

こちらは後でNext.jsの環境変数として設定します。

![auth0-app.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2695521/95224fa0-d47d-753a-2068-08929a24367e.png)

#### コールバック用のURLを設定する

setting画面で下にスクロールすると「Application URIs」という項目が出てくるので該当の箇所にURLを入力していきます。

はじめにAuth0での認証完了後にユーザーのリダイレクト先を設定します。

なおフロント側のポート番号は9000、バックエンド側は3000で設定します。(設定は後で行います)


```
http://localhost:9000/login
```

![スクリーンショット 2022-07-14 9.58.04.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2695521/40c93cd4-e3b1-bded-11fd-eb605799a81e.jpeg)

このように設定することでフロント側でAuth0経由でのログイン処理完了後に```http://localhost:9000/login```のページへ遷移することができます。

※ ログイン後なので```http://localhost:9000/home```等で設定した方が見やすかったかもしれないです。(追記)

実際の遷移先ページの実装はこの後やります。

#### ログアウト用のURLを設定

こちらも同様に以下のURLを設定します。

```
http://localhost:9000/
```

### Allowed Web OriginsとAllowed Origins (CORS)

最後にAllowed Web OriginsとAllowed Origins (CORS)の2つを設定します。

こちらも今までと同様に以下のURLを入れます。

```
http://localhost:9000/
```

![スクリーンショット 2022-07-14 10.02.13.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2695521/ed89d164-c977-63a8-8e0d-e45cc8dbf969.jpeg)

以上でAuth0側での設定は完了です。

### Next.jsでのAuth0の準備

次にNext.jsと今準備をしたAuth0の連携を進めていきます。

#### SDKをインストール

はじめにAuth0 React SDKをインストールするために以下のコマンドを実行します。

```
npm install @auth0/auth0-react
```

#### Auth0Providerコンポーネントを設定する

Auth0 React SDKではReact Contextを利用することで認証状態を管理することができます。

先程インストールした```@auth0/auth0-react```から```Auth0Provider```を```import```してNext.jsアプリをラップします。

またその際に先程Auth0のsetting画面で確認した「**domain**」「**client_id**」を環境変数に定義しておきます。(後述)

```process.env["NEXT_PUBLIC_AUTH0_AUDIENCE"]```の値はRails側の環境構築完了後に```.env```に記載するので一旦空の値になっていても大丈夫です。

```js:pages/_app.tsx
import "../styles/globals.css";
import { Auth0Provider } from "@auth0/auth0-react";

function MyApp({ Component, pageProps }) {
  //ログイン後のリダイレクト先を指定
  const redirectUri = `${process.env["NEXT_PUBLIC_BASE_URL"]}/login`;
  return (
    <Auth0Provider
      domain={process.env["NEXT_PUBLIC_AUTH0_DOMAIN"]!}
      clientId={process.env["NEXT_PUBLIC_AUTH0_CLIENT_ID"]!}
      audience={process.env["NEXT_PUBLIC_AUTH0_AUDIENCE"]!}
      redirectUri={redirectUri}
    >
      <Component {...pageProps} />
    </Auth0Provider>
  );
}

export default MyApp;
```

環境変数は下記のように設定をします。

```xxxx```には先程Auth0のsetting画面で確認した値を入れてください。


```js:.env
NEXT_PUBLIC_BASE_URL='http://localhost:9000'
NEXT_APP_REST_URL="http://localhost:3000/api/v1"
NEXT_PUBLIC_AUTH0_DOMAIN='xxxxxxxxxxxxxxxxx'
NEXT_PUBLIC_AUTH0_CLIENT_ID='xxxxxxxxxxxxxxxxx'
```

#### ポート番号を変更する

最後にNext.js側のポート番号を9000に変更します。

``package.json``ファイルのdevを以下の変更することでポート番号を9000することができます。

```js:package.json
  "scripts": {
    "dev": "next dev -p 9000",
    "build": "next build",
  },
```

実際に確認します。

```js
npm run dev
```

ポート番号が9000のローカル環境が立ち上がることが確認できるかと思います。

以上でNext.jsアプリでAuth0を使う準備が完了しました。

### ログイン機能を作成する

#### ログイン後に遷移するページを作成する

```js:pages/login.tsx
import { useAuth0 } from "@auth0/auth0-react";
import { NextPage } from "next";
import React from "react";

const LoginPage: NextPage = () => {
  const { isAuthenticated } = useAuth0();

  return (
    <div>
      <h2>ログイン状態</h2>
      {isAuthenticated ? <p>ログイン中です</p> : <p>ログアウトしています</p>}
    </div>
  );
};

export default LoginPage;
```

```useAuth0```の```isAuthenticated```でログイン状態なのかを判定してくれます。

現状はログインをしていないので```localhost:9000/login```にアクセスした時に、「ログアウトしています」と表示されることが確認できます。

#### Homeページにログイン機能を追加する

次にログイン機能を実装します。

```js:pages/index.tsx
import { useAuth0 } from "@auth0/auth0-react";

export default function Home() {
  const { loginWithRedirect } = useAuth0();

  return (
    <div>
      <button onClick={() => loginWithRedirect()}>ログイン</button>
    </div>
  );
}
```

```js
npm run dev
```

これによって、画面にログインボタンが表示されていることが確認できます。

![スクリーンショット 2022-07-12 14.23.20.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2695521/38bfe2ad-cd3d-468e-adb1-2c53bc5cd5ec.jpeg)

実際にボタンをクリックすると以下のように　Auth0側のログイン画面に遷移することが確認できます。

![スクリーンショット 2022-07-12 14.24.14.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2695521/3a3e73b1-c681-66e0-4d7c-da429c3be67c.jpeg)


Googleアカウントでログインすると以下のような画面が表示されるのでAcceptをすればログイン(新規登録)が完了します。

![スクリーンショット 2022-07-12 14.25.04.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2695521/39b86035-8a0b-4e87-8e3c-45599a97471b.jpeg)

完了後は先程作成したlogin画面に遷移していることを確認できるかと思います。

![スクリーンショット 2022-07-12 14.36.34.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2695521/678ff9bc-7b47-27d6-cd21-5b5e189bc400.jpeg)


#### Homeページにログアウトボタンを追加する

先程作成した```pages/login.tsx```においてログイン中だった場合のみログアウトボタンを表示させログアウト処理が行えるように書き換えます。

```js:pages/login.tsx
import { useAuth0 } from "@auth0/auth0-react";
import { NextPage } from "next";
import React from "react";

const LoginPage: NextPage = () => {
  const { isAuthenticated, logout } = useAuth0();

  return (
    <div>
      <h2>ログイン状態</h2>
      {isAuthenticated ? (
        <>
          <p>ログイン中です</p>
          <button onClick={() => logout({ returnTo: window.location.origin })}>
            ログアウト
          </button>
        </>
      ) : (
        <p>ログアウトしています</p>
      )}
    </div>
  );
};

export default LoginPage;
```

実際にログアウトボタンをクリックすると文言が変わっていることが確認できます。

![スクリーンショット 2022-07-12 15.07.34.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2695521/c6942f49-3ede-ffff-e4a6-c44531cb1ae1.jpeg)

以上でフロント側での新規登録、ログイン、ログアウトの実装が完了しました。

次にRailsのAPIモード側のAuth0の設定をしていきます。

## バックエンド側(Rails側)の開発

バックエンド側もフロントと同様にAuth0での設定をした上でRailsアプリ側で実装を進めていきます。

こちらのドキュメントの流れで解説をしていきます。

https://auth0.com/blog/building-secure-apis-with-rails-6-and-auth0/

### Auth0側の設定

まずはじめにApplications > APIsからcreate APIをクリックします。

![スクリーンショット 2022-07-12 15.23.45.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2695521/eeadd985-9ddd-a5de-278c-e9695c78e23c.jpeg)

該当項目を入力しcreateをクリックします。

![スクリーンショット 2022-07-12 15.25.50.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2695521/676a9cdb-e8aa-e829-df56-4ec99e80ce1c.jpeg)

ドメイン情報を取得するためTest > urlからドメイン情報を確認します。

urlの.comまでがドメイン情報になっています。

![スクリーンショット 2022-07-12 15.27.45.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2695521/1da2669a-8354-1c28-db6d-36609a96671a.jpeg)

これでauth0側の設定は完了なのでRailsアプリの設定をしていきます。

### APIモードでRailsアプリを環境構築

auth0-backというディレクトリ名でアプリを開発していきます。

```js
rails new auth0-back --api
```

以下をgemファイルに追加して```bundle install```をします。

```ruby:Gemfile
gem 'dotenv-rails'
gem 'jwt'
gem 'rack-cors'
```

- ```dotenv-rails```は環境変数を設定するためのgem
- ```JsonWebToken```を利用して```Access Token```の検証をするので```jwt```を使う
- ```rack-cors```は```cors```の設定を行うために使います

CORSについてはこちらを確認してみてください。

https://developer.mozilla.org/ja/docs/Web/HTTP/CORS

### 環境変数を定義する

先程のAuth0側で確認した```domain```と```identifier```をこちらに格納します。

```js:.env
AUTH0_DOMAIN="xxxxxxxxx"
AUTH0_IDENTIFIER="xxxxxxxxx"
```

```identifier```に関してはAuth0の管理画面から確認できます。


![スクリーンショット 2022-07-12 15.44.50.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2695521/109701eb-5032-5e9f-2262-1735b28798cd.jpeg)

また```identifier```の値はフロント側の```.env```にも後で記載します。

### JWTクラスを作成する

こちらはドキュメントの記述をそのまま流用します。

https://auth0.com/blog/building-secure-apis-with-rails-6-and-auth0/

処理内容は公式ドキュメントに書いてある通り、リクエストの```Authorization```のヘッダーから取得した```Access Token```をデコードする```JsonWebToken```クラスを作成しています。

これでAuth0テナントの公開鍵を取得し、トークンの検証をすることができる。

```ruby:app/lib/json_web_token.rb
# lib/json_web_token.rb

# frozen_string_literal: true
require 'net/http'
require 'uri'

class JsonWebToken
  def self.verify(token)
    JWT.decode(token, nil,
               true, # Verify the signature of this token
               algorithms: 'RS256',
               iss: 'https://dev-k4qwtrhp.jp.auth0.com/',
               verify_iss: true,
               aud: Rails.application.secrets.auth0_api_audience,
               verify_aud: true) do |header|
      jwks_hash[header['kid']]
    end
  end

  def self.jwks_hash
    jwks_raw = Net::HTTP.get URI("https://dev-k4qwtrhp.jp.auth0.com/.well-known/jwks.json")
    jwks_keys = Array(JSON.parse(jwks_raw)['keys'])
    Hash[
      jwks_keys
      .map do |k|
        [
          k['kid'],
          OpenSSL::X509::Certificate.new(
            Base64.decode64(k['x5c'].first)
          ).public_key
        ]
      end
    ]
  end
end
```

### 認証用のクラスを作成する

認証のロジックを抽出することでJWTの真正性を検証する処理とHTTPリクエストを認証する処理を分離している。

```ruby:app/services/authorization_service.rb
class AuthorizationService
  def initialize(headers = {})
    @headers = headers
  end

  def current_user
    @auth_payload, @auth_header = verify_token
    @user = User.from_token_payload(@auth_payload)
  end

  private

  def http_token
    @headers['Authorization'].split(' ').last if @headers['Authorization'].present?
  end

  def verify_token
    JsonWebToken.verify(http_token)
  end
end
```

- ```AuthorizationService```
    - ```Authorization HTTP Header```に含まれるアクセストークンを取得しJWTに渡して検証する
- ```verify_token```
    - ```json_web_token.rb```を実行してTokenを渡す
- ```current_user```
    - ユーザーモデルメソットを実行し、ユーザー情報を返す

### SecuredControllerを作成する

APIエンドポイントのセキュリティー確保を行う```secured_controlle```を作成する。

こちらに関しても同様にAuth0のドキュメントを参考に記述をします。

https://auth0.com/blog/building-secure-apis-with-rails-6-and-auth0/#Securing-an-API-Endpoint

```ruby:app/controllers/secured_controller.rb
class SecuredController < ApplicationController
  before_action :authorize_request

  private

  def authorize_request
    authorize_request = AuthorizationService.new(request.headers)
    @current_user = authorize_request.current_user
  rescue JWT::VerificationError, JWT::DecodeError
    render json: { errors: ['Not Authenticated'] }, status: :unauthorized
  end
end
```

```SecuredController```クラスha```ApplicationController```を継承しており、全てのコントローラー実行前に```authorize_request```を実行します。

```authorize_request```ではtokneを解析し、ユーザー認証ができなかった場合はエラーメッセージを返すようなメソットです。

最後に```Parameter Wrapping```を無効にするために以下のような記述をします。

```ruby:config/initializers/wrap_parameters.rb
wrap_parameters format: []
```

以上でRails側でのAuth0の準備は完了しました。

###  モデルを作成する

次にAuth0を利用してユーザーモデルを作成します。

今回はブログ投稿型のアプリを開発するというて程で、Auth0の認証機能を使っていきます。

実際に行う内容は下記の内容です

- ブログの新規作成は認証済みユーザーだけができる
- ブログの閲覧は認証していないユーザーもできる

#### ユーザーモデルの作成

まずは以下のコマンドでユーザーモデルを作成します。

```ruby
rails g model User
```

マイグレーションファイルを以下のように書きます。

```ruby:20220712073809_create_users.rb
class CreateUsers < ActiveRecord::Migration[6.1]
  def change
    create_table :users do |t|
      t.string :sub, null: false
      t.timestamps
    end
  end
end
```

subはユーザーidが入ってきます。

またユーザーとブログ記事は```1:N```の関係でリレーションが結ばれているので、ユーザーモデルを下記のように書き換えます。

```ruby:app/models/user.rb
class User < ApplicationRecord
  has_many :posts, dependent: :destroy

  def self.from_token_payload(payload)
    find_by(sub: payload['sub']) || create!(sub: payload['sub'])
  end
end
```

```self.from_token_payload(payload)```では```token```情報を参照して対象の```user```が存在する場合は```user```情報を返し、存在しない場合は新規作成の処理を行いまう。

#### ブログ記事のモデルを作成する

次にブログ記事のモデルを作成していきます。

```
rails g model Post user:references title:string caption:text
```

マイグレーションを実行します。
```
rails db:migrate
```

#### ブログ記事のコントローラーを作成

- ブログの取得(```index,show```)は認証なしで実行できる
- ブログの新規作成は認証がないと実行できない
- ブログの削除も認証がないと実行ができない

```authorize_request```の処理は```app/controllers/secured_controller.rb```に記述済み。

```ruby:app/controllers/api/v1/posts_controller.rb
class Api::V1::PostsController < SecuredController
  skip_before_action :authorize_request, only: [:index,:show]

  def index
    posts = Post.all
    render json: posts
  end

  def show
    post = Post.find(params[:id])
    render json: post
  end

  def create
    # ユーザー認証
    post = @current_user.posts.build(post_params)

    if post.save
      render json: post
    else
      render json: post.errors, status: :unprocessable_entity
    end
  end

  def destroy
    post = Post.find(params[:id])
    post.delete
  end

  private

  def post_params
    params.permit(:title,:caption)
  end
end
```

```skip_before_action```でindexとshowメソットのみ認証処理をスキップしている。

#### ルーターの設定

```ruby:config/routes.rb
Rails.application.routes.draw do
  namespace :api do
    namespace :v1 do
      resources :posts
    end
  end
end
```

#### Postmanを利用してAPIコールを試す

Postmanの準備に関しては割愛します。まだの方はこちらを参考に準備をしてください。

https://yu-report.com/entry/postman/

#### トークンを入れずに新規作成(POST)を試す

はじめにトークンを入れずにPostmanでブログ記事の新規作成を試します。

Railsのサーバーを立ち上げます。

```
rails s
```

- POSTを選択肢して```http://127.0.0.1:3000/api/v1/```を入れます
- Paramsを選択肢```title```と```caption```に値を入れます
- その上でSendボタンを押します
- トークンを入れていないので結果として"Not Authenticated"が返ってきます

![スクリーンショット 2022-07-14 15.03.53.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2695521/ceca727b-52dd-555a-6f45-61c8d5e4bbe1.jpeg)

以下のファイルの```authorize_request```の処理が実行されていることが確認できます。

```Tokne```を付与していないので```error```が処理が走っています。

```ruby:app/controllers/secured_controller.rb
class SecuredController < ApplicationController
  before_action :authorize_request

  private

  def authorize_request
    authorize_request = AuthorizationService.new(request.headers)
    @current_user = authorize_request.current_user
  rescue JWT::VerificationError, JWT::DecodeError
    render json: { errors: ['Not Authenticated'] }, status: :unauthorized
  end
end
```

#### トークンを入れて新規作成(POST)を試す

次にトークンを入れて新規作成のメソットを実行してみます。

まずAuth0の管理画面を開きTokenを取得します。管理画面のTestタブの下記の部分よりTokenを取得します。

長いのでコピーする際は注意してください。

![スクリーンショット 2022-07-12 17.09.43.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2695521/b803a680-c80d-b4fe-ffc2-06893795b377.jpeg)

次にpostman側で取得したTokenを格納していきます。

Authorizationのタブを選択肢Typeを```Bearer Token```に設定します。

右側のTokneの中に先ほどAuth0から取得した値を貼り付けます。

![スクリーンショット 2022-07-14 15.32.31.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2695521/1870cea7-ca2a-e6c2-1d14-4dcadc3fbe80.jpeg)

これで準備は完了なので先程と同様にparamに値をいれPOSTを叩いてみます。

![スクリーンショット 2022-07-14 15.39.21.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2695521/4fb085a4-8295-faa3-6bcf-5c73f225f23d.jpeg)

データが実際に登録されて、レスポンスが返ってきてることが確認できます。

#### 取得(GET)メソットを試す

次にGETメソットを実行して、先ほど新規作成した記事が実際に登録されているかを確認します。

![スクリーンショット 2022-07-14 15.41.55.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2695521/8715a6b4-5023-1a34-015f-fe8b2a34f62f.jpeg)

Tokenを空にしてもGETメソットはレスポンスは返ってきます。

#### 削除(DLETE)メソットを試す

idが2のデータの記事を削除する処理を行います。

こちらはTokneの値を入れないと認証エラーになります。

![スクリーンショット 2022-07-14 15.56.09.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2695521/0373101e-1fb0-dee5-72f5-f7c5f2a411a6.jpeg)

GETメソットを実行するとidが2のデータが削除されていることが確認できます。

![スクリーンショット 2022-07-14 15.56.59.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2695521/3e63e03a-4309-560d-123b-27fcf25784bc.jpeg)

以上でRails側のCRUDの確認が完了しました。

## フロントとバックエンドの連携

最後にRailsとNext.jsの連携を進めていきます。

概要としては以下の内容を行います。

- フロント側でログイン処理をする
- ログイン後に付与されるTokneをフロント側のstateで管理する
- Token情報を付与しフロント側からリクエストを送る

### Rails側の設定

フロント側からのアクセスを許可するためにcorsの設定をおこないます。

```ruby:config/initializers/cors.rb
Rails.application.config.middleware.insert_before 0, Rack::Cors do
  allow do
    origins 'http://localhost:9000'

    resource '*',
            headers: :any,
            methods: %i[get post put patch delete options head]
  end
end
```

### Next.js側の設定

#### 環境変数の設定

先ほどRailsの.envに記載した下記の内容をNext.jsの環境変数にも書いて対応させます。

```ruby:.env
AUTH0_IDENTIFIER=xxxxxxx
```

Next.js側の追記事項

```js
NEXT_PUBLIC_BASE_URL='http://localhost:9000'
NEXT_PUBLIC_AUTH0_DOMAIN='xxxxxx'
NEXT_PUBLIC_AUTH0_CLIENT_ID='xxxxxx'
NEXT_APP_REST_URL="http://localhost:3000/api/v1"
NEXT_PUBLIC_AUTH0_AUDIENCE="xxxxx" // ここにAUTH0_IDENTIFIEの値を入れる
```

_appで環境変数を読み取る

```js:pages/_app.tsx
import "../styles/globals.css";
import { Auth0Provider } from "@auth0/auth0-react";

function MyApp({ Component, pageProps }) {
  //ログイン後のリダイレクト先を指定
  const redirectUri = `${process.env["NEXT_PUBLIC_BASE_URL"]}/login`;
  return (
    <Auth0Provider
      domain={process.env["NEXT_PUBLIC_AUTH0_DOMAIN"]!}
      clientId={process.env["NEXT_PUBLIC_AUTH0_CLIENT_ID"]!}
      audience={process.env["NEXT_PUBLIC_AUTH0_AUDIENCE"]!} // 追記
      redirectUri={redirectUri}
    >
        <Component {...pageProps} />
    </Auth0Provider>
  );
}
export default MyApp;
```

これでNext.js側でログイン後に取得できるToKenを利用して、Rails側のアプリでも認証をすることができるようになりました。

#### aixosのインストール

API通信は```axios```を利用して行うのでインストールをする

```
npm install axios --save
```

#### Recoilの導入

今回はグローバルState(Recoil)でAuth0から取得できるTokenを管理します。

フロント側は以下の流れで処理を行います。

- Auth0経由でログイン処理
- ログイン後にリダイレクトするページでTokenを取得しRecoilへ格納
- ブログ投稿ページへ遷移しRecoilからTokenを取得
- 取得したTokenを使ってRails側に新規投稿のリクエストを送る

なおRecoilについての詳しい解説はここでは割愛するので、気になった方はこちらの記事を参考にしてみてください。

https://zenn.dev/yuki_yuki/articles/c3bac6943f70b6

さっそくRecoilをインストールしていきます。

```
npm install recoil     
```

``` RecoilRoot```でアプリをラップする。

```js:pages/_app.tsx
import "../styles/globals.css";
import { Auth0Provider } from "@auth0/auth0-react";
import { RecoilRoot } from "recoil";

function MyApp({ Component, pageProps }) {
  //ログイン後のリダイレクト先を指定
  const redirectUri = `${process.env["NEXT_PUBLIC_BASE_URL"]}/login`;
  return (
    <Auth0Provider
      domain={process.env["NEXT_PUBLIC_AUTH0_DOMAIN"]!}
      clientId={process.env["NEXT_PUBLIC_AUTH0_CLIENT_ID"]!}
      audience={process.env["NEXT_PUBLIC_AUTH0_AUDIENCE"]!}
      redirectUri={redirectUri}
    >
      <RecoilRoot>
        <Component {...pageProps} />
      </RecoilRoot>
    </Auth0Provider>
  );
}

export default MyApp;
```

次にRecoilで使う```atoms```の設定を行います。

```recoil/atoms/tokenState.ts
import { atom } from "recoil";

const tokenState = atom({
  key: "tokenState",
  default: "",
});

export default tokenState;
```

#### ログイン後にトークンを取得しRecoilに格納する

Auth0側でログイン完了後にリダイレクトされるページでTokneを取得しRecoilに格納します。

```js:pages/login.tsx
import { useAuth0 } from "@auth0/auth0-react";
import { NextPage } from "next";
import { useRouter } from "next/router";
import React, { useEffect } from "react";
// recoil
import { useSetRecoilState } from "recoil";
import tokenState from "../recoil/atoms/tokenState";

const LoginPage: NextPage = () => {
  const router = useRouter();
  const { isAuthenticated, logout, getAccessTokenSilently } = useAuth0();
  const setToken = useSetRecoilState(tokenState);

  // ログイン完了後にトークンを取得しRecoilへ格納
  useEffect(() => {
    const getToken = async () => {
      try {
        const accessToken = await getAccessTokenSilently({});
        setToken(accessToken);
      } catch (e) {
        console.log(e.message);
      }
    };
    getToken();
  }, []);

  return (
    <div>
      <h2>ログイン状態</h2>
      {isAuthenticated ? (
        <>
          <p>ログイン中です</p>
          <button onClick={() => logout({ returnTo: window.location.origin })}>
            ログアウト
          </button>
          <button
            onClick={() => {
              router.push("/blog");
            }}
          >
            記事投稿ページへ
          </button>
        </>
      ) : (
        <p>ログアウトしています</p>
      )}
    </div>
  );
};

export default LoginPage;
```

- ```useAuth0()```の```getAccessTokenSilently```を利用してTokenを取得する
- ```useSetRecoilState(tokenState)```でRecoilのTokenを更新する
- ```useEffect```内でレンダリング時に処理が走るようにする
- また```useRouter()```でblogページへ遷移する導線を作成

#### ブログページでReocilからTokneを取得する

遷移したblogページでRecoilからTokneを取得できるか確認します。

```js:pages/blog.tsx
import { NextPage } from "next";
import React from "react";
// recoil
import { useRecoilValue } from "recoil";
import tokenState from "../recoil/atoms/tokenState";

const BlogPage: NextPage = () => {
  const token = useRecoilValue(tokenState); // RecoilのTokneを取得する

  console.log(token);

  return <div>ブログ投稿</div>;
};

export default BlogPage;
```

ブラウザを確認するとconsoleでトークンが表示されていることが確認できるかと思います。

#### GETメソットでブログデータを取得する

まずはTokneを付与せずに実行できるGETメソットを使ってブログ記事が取得できるかを試してみます。

```js:pages/blog.tsx
import axios from "axios";
import { NextPage } from "next";

import React, { useEffect } from "react";
// recoil
import { useRecoilValue } from "recoil";
import tokenState from "../recoil/atoms/tokenState";

const BlogPage: NextPage = () => {
  const token = useRecoilValue(tokenState);
  useEffect(() => {
    const getPosts = async () => {
      const res = await axios.get("http://localhost:3000/api/v1/posts");
      console.log(res);
    };
    getPosts();
  });

  return <div>ブログ投稿</div>;
};

export default BlogPage;
```

ブラウザで確認するとデータが取得できていることが確認できます。

![スクリーンショット 2022-07-14 22.41.26.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2695521/25bd3a00-1727-8120-485c-a01c88eff5ba.jpeg)

#### Tokneを付与してPOSTメソットを実行する

次はReocilのTokneをリクエストヘッダーに付与し記事を新規作成します。

まずはタイトルと説明を入力するフォームとstateを追加します。

確認ようなのでスタイルは特に当てていないです。

```js:pages/blog.tsx
import { useAuth0 } from "@auth0/auth0-react";
import axios from "axios";
import { NextPage } from "next";

import React, { useEffect, useState } from "react";
// recoil
import { useRecoilValue } from "recoil";
import tokenState from "../recoil/atoms/tokenState";

const BlogPage: NextPage = () => {
  const token = useRecoilValue(tokenState);

  const [title, setTitle] = useState<string>("");
  const [caption, setCaption] = useState<string>("");

  const onChange = (
    e: React.ChangeEvent<HTMLInputElement>,
    setState: React.Dispatch<React.SetStateAction<string>>
  ) => {
    setState(e.target.value);
  };

  return (
    <div>
      <label htmlFor="">タイトル</label>
      <input
        type="text"
        value={title}
        onChange={(e) => {
          onChange(e, setTitle);
        }}
      />
      <br />
      <label htmlFor="">本文</label>
      <input
        type="text"
        value={caption}
        onChange={(e) => {
          onChange(e, setCaption);
        }}
      />
      <br />
      <button>新規投稿</button>
    </div>
  );
};

export default BlogPage;
```

次に入力されたデータをパラメタと送信するための新規登録用のAPIコールメソットを作成します。

```js:pages/blog.tsx
import { useAuth0 } from "@auth0/auth0-react";
import axios from "axios";
import { NextPage } from "next";

import React, { useEffect, useState } from "react";
// recoil
import { useRecoilValue } from "recoil";
import tokenState from "../recoil/atoms/tokenState";

const BlogPage: NextPage = () => {
  const token = useRecoilValue(tokenState);

  const [title, setTitle] = useState<string>("");
  const [caption, setCaption] = useState<string>("");

  const onClick = () => {
    const params = {
      title: title,
      caption: caption,
    };
    console.log(token);

    axios
      .post("http://127.0.0.1:3000/api/v1/posts", params, {
        headers: {
          Authorization: `Bearer ${token}`,
        },
      })
      .then((res) => {
        console.log(res);
      })
      .catch((error) => {
        console.log(error);
      });
  };

  const onChange = (
    e: React.ChangeEvent<HTMLInputElement>,
    setState: React.Dispatch<React.SetStateAction<string>>
  ) => {
    setState(e.target.value);
  };

  return (
    <div>
      <label htmlFor="">タイトル</label>
      <input
        type="text"
        value={title}
        onChange={(e) => {
          onChange(e, setTitle);
        }}
      />
      <br />
      <label htmlFor="">本文</label>
      <input
        type="text"
        value={caption}
        onChange={(e) => {
          onChange(e, setCaption);
        }}
      />
      <br />
      <button onClick={onClick}>新規投稿</button>
    </div>
  );
};

export default BlogPage;
```

- axiosの```headers```の中にRecoilに保存されていたTokenを入れる
- ```params```にtitleとcaptionの値を入れる
- 登録後の成功を確認するために```res```を```console```で表示させる

実際に値を入れて新規登録ボタンを押してみます

![スクリーンショット 2022-07-15 9.27.50.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2695521/6d172989-981d-5e83-96ff-0b595bdd8313.jpeg)

consoleを確認すると成功のレスポンスが確認できます。

![スクリーンショット 2022-07-15 9.29.35.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2695521/9c809aa2-07c2-e390-3cce-7a5d0c2f6a0a.jpeg)

Tokneをの値を空にして同様の処理を行うと```Not Authenticated```のレスポンスが返ってきます。


## 最後に

いかがだったでしょうか。

今回はAuth0を利用してNext.js×RailsのSPA構成で認証機能の実装を行いました。

Auth0はドキュメントも豊富かつチュートリアルもあるのでサクッと導入することができるので、ぜひ今後の認証機能周りの開発の際に使っていただければと思います。


他にも記事を出しているので、併せて読んでいただけると嬉しいです。

https://qiita.com/SMAC/items/f3268b311e11d5b821c0

https://qiita.com/SMAC/items/3b14e2e8f89a33c0f959

https://qiita.com/SMAC/items/e50c981e0518141be28f