Go APIでのAuth0 JWT認証ハンズオン

こちらは [GAOGAO Advent Calendar 2021 ことしもGAOGAOまつりです ](https://qiita.com/advent-calendar/2021/gaogao) の2日目の記事です。昨日の記事は [ますみんさん](https://qiita.com/mass-min) の [リバースプロキシをDocker Compose環境で実現する](https://zenn.dev/masumi_sugae/articles/84c40d20c82cdd) でした。

こんにちは、[GAOGAO](https://gaogao.asia) の案件に携わらさせていただいている [こうりん](https://github.com/Kourin1996) と申します。 よろしくお願いいたします。

この記事では、GoのAPIでAuth0のJWT認証をする方法をハンズオン形式で説明します。今回実装するコードは以下のリポジトリに公開します。

https://github.com/Kourin1996/go-auth0-example

## 実行環境
maxOS Big Sur version 11.6
Node.js: v16.13.0
npm: 8.1.0
go: 1.16.3

# Auth0とは

[Auth0](https://auth0.com) は **IDaaS** (Identity as a Service) に分類されるもので、クラウドでユーザー認証基盤を提供しているサービスです。ユーザーID/パスワード認証だけでなく Google や Facebook などのソーシャルアカウントを使ったサインイン/サインアップやMFA (Multi-Factor Authentication)の設定も可能です。Auth0では多くのプログラミング言語向けSDKを提供しており、Webだけでなくスマートフォンアプリでも使用できます。

## Auth0を使ったユーザー認証とAPIアクセスのフロー

WebアプリでAuth0を使う場合、以下のような関係になります。

![Untitled Diagram.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/915170/140f317d-fee9-aa84-ceb7-dc1fc99cadbd.png)

ブラウザ側でAuth0にアクセスしログインします。この時、結果としてブラウザへJWT(JSON Web Token)が渡されます。そして、このJWTをブラウザがAPIを呼ぶ時にトークンとして渡します。APIではJWTを検証を行い正しいユーザーからのアクセスであることを確認するとともに、リクエストがあったユーザーを識別します。

## JWTとは

[RFC7519](https://datatracker.ietf.org/doc/html/rfc7519) で標準化されたトークンの仕様です。JWTの中身は以下のように、ヘッダ・ペイロード・署名の3つがピリオド区切りの構成になっています。

```
<ヘッダ>
.
<ペイロード>
.
<署名>
```

**ヘッダ** はJWT作成・検証で必要なトークンタイプや署名アルゴリズムの種類などを格納します。この値をBase64エンコードしてJWTの一番目に配置します。

```json
{
  "typ": "JWT",
  "alg": "RS256"
}
```

**ペイロード** は具体的な認証情報を格納します。ヘッダと同様Base64エンコードしたものがJWTに配置されます。

Auth0では以下のような情報が含まれています。

* iss: トークンの発行者
* azp: クライアントID (Auth0で使用しているサービスの識別子)
* sub: Auth0でのユーザーの識別子
* aud: トークンを使用する対象 (APIなど)
* gty: トークンを取得した方法
* iat: トークンの発行日時
* exp: トークンの失効日時
* scope: 利用できるユーザーのリソースのリスト
* permissions: ユーザーが属するロールが持つ許可のリスト

```json
{
  "iss": "https://example.us.auth0.com",
  "azp": "xxxxxxxxx",
  "sub": "auth0|....",
  "aud": [
    "http://localhost:8080",
    "https://example.us.auth0.com"
  ],
  "gty": "password",
  "iat": 1637954372,
  "exp": 1638040772,
  "scope": "openid profile email",
  "permissions": [
    "sys:admin"
  ]
}
```

**署名** は`<Base64エンコードしたヘッダ>,<Base64エンコードしたペイロード>`を署名アルゴリズムで署名したものです。

JWTを検証する際は、JWTに含まれるヘッダとペイロードを元にダイジェストを作成します。そして、署名を復号した結果に含まれるダイジェストと一致するか確認して、JWTが改ざんされていないかチェックします。

# Auth0の初期設定

まず、Auth0を使用するための初期設定を行います。[Auth0.com](https://auth0.com/) へアクセスし、サインインをすると以下のようなコンソール画面に移動します。

![スクリーンショット 2021-12-02 1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/915170/5f28aff1-53cc-dbf6-8443-492d9d204bce.png)

左のメニューから **Applications** > **APIs** を選択します。

![スクリーンショット 2021-12-02 2 (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/915170/9055724f-2f9d-b9d6-e6df-0a38099191c3.png)

**APIs** で右上の **Create API** を押すと、次のようなモーダルが表示されます。

![スクリーンショット 2021-12-02 16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/915170/5f8987ce-680d-ad1a-f896-e4c4e7182cb7.png)

**Name** は好きな名前を設定します。**Identifier** はAPIの識別子として使われますが、通常はAPIのURLを指定します。 **Signing Algorithm** では **RS256**, **HS256** のどちらかを選択できます。RS256 が公開鍵/秘密鍵を使った署名方法、HS256 が共通鍵を使った署名方法です。特に問題が無ければ、RS256で良いと思います。

**Create** を押して作成が完了すると、以下のようなAPI設定画面へ遷移します。

![スクリーンショット 2021-12-02 16 (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/915170/b655d493-c653-dd37-5d56-df1dd4cf0387.png)

次にフロントエンドからAuth0へアクセスするためにApplicationを作成します。同様に左メニューから **Applications** > **Applications** を選択します。

![スクリーンショット 2021-12-02 2 (2).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/915170/9a366639-2d13-403f-cd8d-f8e18e58f817.png)

APIに対するApplicationが作られていますが、**Create Application** を押して新しく作成します。**Name** には任意の名前を入力してください。**Application Type** はフロントエンドにReactを使うので、**Single Page Web Application** を選択します。

![スクリーンショット 2021-12-02 2 (3).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/915170/928772a2-7bcf-0d0b-275d-4fa36fb217bf.png)


Applicationの作成が完了すると設定画面に遷移します。**Settings**タブをクリックします。**Domain** と **ClientID** は後ほどフロントエンドで使用するのでメモしておきます。

![スクリーンショット 2021-12-02 2 (4).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/915170/87e917b8-1ce4-5353-91a0-9c31e868983c.png)


Settingsで **Allowed Callback URLs**, **Allowed Logout URLs**, **Allowed Web Origins**, **Allowed Origins (CORS)** にフロントエンドのURLを設定します。とりあえず、ローカルで動かすのでそれぞれ `http://localhost:3000` を設定します。

![スクリーンショット 2021-12-02 2 (5).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/915170/d2935331-3695-542d-05c4-617a859f3f6e.png)

![スクリーンショット 2021-12-02 2 (6).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/915170/187d616f-677f-13d6-51c0-c743958f9549.png)


**Save Changes**を押して作成すると、同様に設定画面へ遷移します。ひとまずAuth0のコンソールで必要な作業は以上です。

# APIのコード実装

## プロジェクト構築

GoでAPIのコードを書いていきます。API用のプロジェクトのディレクトリを作り、`go mod init`でGoのプロジェクトをセットアップします。(GOPATHの外で`go mod init`をする場合は、プロジェクトのルートパスを指定する必要があります)

```shell
$ mkdir server
# server以下に移動する
$ cd server

# Goプロジェクトの初期化
$ go mod init
# GOPATHの外で実行する場合 (例)
$ go mod init github.com/Kourin1996/go-auth0-example/server
```

プロジェクトの構成は以下のようにします。`handlers`以下にAPIのハンドラ、`middlewares`以下にAuth0のJWT検証部分をそれぞれ実装します。

```
server
├── handlers
│   └── v1
│       ├── v1.go (/v1へのリクエストのハンドラ定義)
│       └── users
│           └── me (/v1/users/meへのリクエストのハンドラ定義)
│               └── me.go
├── middlewares
│   └── auth0 (Auth0向けの処理を定義)
├── main.go (エントリポイント)
└── go.mod
```

まず、APIの動作確認のためにシンプルなエンドポイントを実装します。`server/main.go` と `server/handlers/v1/v1.go` を作成し、それぞれ以下を記述します。

```go
// server/main.go
// APIのエントリポイント

package main

import (
	"fmt"
	"log"
	"net/http"

	v1 "github.com/Kourin1996/go-auth0-example/server/handlers/v1"
)

const (
	port = 8000
)

func main() {
	mux := http.NewServeMux()
	// /v1へのリクエストが来た場合のハンドラを追加
	mux.HandleFunc("/v1", v1.HandleIndex)

	addr := fmt.Sprintf(":%d", port)

	// localhost:8000 でサーバーを立ち上げる
	log.Printf("Listening on %s", addr)
	if err := http.ListenAndServe(addr, mux); err != nil {
		log.Fatal(err)
	}
}
```

```go
// server/handlers/v1/v1.go
// /v1向けのハンドラ

package v1

import (
	"net/http"
)

func HandleIndex(w http.ResponseWriter, r *http.Request) {
	// "Hello!" とだけ返します
	w.Write([]byte("Hello!"))
}
```

ファイルを保存し`go run main.go`でサーバーを起動します。

```bash
$ go run main.go
2021/12/02 12:47:27 Listening on :8000
```

別のシェルからcurlを使ってAPIにリクエストを投げると、正しくメッセージが返ってくることを確認できます。

```
$ curl http://localhost:8000/v1
Hello!
```

## JWT認証用の処理を追加

APIでAuth0向けのJWT認証を行うには大きく分けて3つのステップがあります

1. 署名を検証するための公開鍵をAPI起動時に取得する
2. auth0/go-jwt-middlewareを初期化する
2. 認証が必要なリクエストが来た場合にJWTの検証をする

それぞれ順番に実装していきます

### 公開鍵の取得

まず公開鍵を取得します。JWTは秘密鍵を使って署名を生成しているので、検証には対になる公開鍵が必要となります。これは`https://<DOMAIN>/.well-known/jwks.json`から取得できます。ドメインはAuth0のApplicationの設定画面などに記載されています。

![スクリーンショット 2021-12-02 13.07.57 (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/915170/8670cff8-6e7e-409f-3397-509026a85d0e.png)

ドメインを元に公開鍵を取得する関数を実装します。`server/middlewares/auth0/key.go`を作成し以下を記述します。取得するデータは、JWKS (JSON Web Key Set)というJSONに公開鍵などが入ってる形式なので、取得したJSONをGoの構造体にマッピングします。

```go
// server/middlewares/auth0/key.go

package auth0

import (
	"encoding/json"
	"fmt"
	"net/http"
	"net/url"
)

// JKWS向けの構造体定義
type JSONWebKeys struct {
	Kty string   `json:"kty"`
	Kid string   `json:"kid"`
	Use string   `json:"use"`
	N   string   `json:"n"`
	E   string   `json:"e"`
	X5c []string `json:"x5c"`
}

type JWKS struct {
	Keys []JSONWebKeys `json:"keys"`
}

func FetchJWKS(auth0Domain string) (*JWKS, error) {
	// ドメインを指定して公開鍵が入ったJWKSを取得する
	resp, err := http.Get(fmt.Sprintf("https://%s/.well-known/jwks.json", auth0Domain))
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	// 取得したJSONデータを構造体にマッピングする
	jwks := &JWKS{}
	err = json.NewDecoder(resp.Body).Decode(jwks)

	return jwks, err
}
```

### auth0/go-jwt-middlewareの初期化

auth0/go-jwt-middlewareはAuth0向けのJWTを検証するためのパッケージです。まず、パッケージを`go get`で追加します。

```bash
$ go get github.com/auth0/go-jwt-middleware@v1.0.1
```

次に`server/middlewares/auth0/auth0.go`を作成し、以下を記述します。

```go
// server/middlewares/auth0/auth0.go

package auth0

import (
	"errors"
	"fmt"
	"net/http"

	jwtmiddleware "github.com/auth0/go-jwt-middleware"
	"github.com/form3tech-oss/jwt-go"
)

func NewMiddleware(domain, clientID string, jwks *JWKS) (*jwtmiddleware.JWTMiddleware, error) {
	return jwtmiddleware.New(jwtmiddleware.Options{
		ValidationKeyGetter: newValidationKeyGetter(domain, clientID, jwks),
		// JWTで使われている署名アルゴリズムを指定する
		SigningMethod: jwt.SigningMethodRS256,
		ErrorHandler:  func(w http.ResponseWriter, r *http.Request, err string) {},
	}), nil
}

func newValidationKeyGetter(domain, clientID string, jwks *JWKS) func(*jwt.Token) (interface{}, error) {
	return func(token *jwt.Token) (interface{}, error) {
		claims, ok := token.Claims.(jwt.MapClaims)
		if !ok {
			return token, errors.New("invalid claims type")
		}

		// azpフィールドを見て、適切なClientIDのJWTかチェックする
		azp, ok := claims["azp"].(string)
		if !ok {
			return nil, errors.New("authorized parties are required")
		}
		if azp != clientID {
			return nil, errors.New("invalid authorized parties")
		}

		// issフィールドを見て、正しいトークン発行者か確認する
		iss := fmt.Sprintf("https://%s/", domain)
		ok = token.Claims.(jwt.MapClaims).VerifyIssuer(iss, true)
		if !checkIss {
			return nil, errors.New("invalid issuer")
		}

		// JWTの検証に必要な鍵を生成する
		cert, err := getPemCert(jwks, token)
		if err != nil {
			return nil, err
		}

		return jwt.ParseRSAPublicKeyFromPEM([]byte(cert))
	}
}

// JWKSからJWTで使われているキーをPEM形式で返す
func getPemCert(jwks *JWKS, token *jwt.Token) (string, error) {
	cert := ""

	for k := range jwks.Keys {
		if token.Header["kid"] == jwks.Keys[k].Kid {
			cert = "-----BEGIN CERTIFICATE-----\n" + jwks.Keys[k].X5c[0] + "\n-----END CERTIFICATE-----"
		}
	}

	if cert == "" {
		return "", errors.New("unable to find appropriate key")
	}

	return cert, nil
}
```

### Middlewareを作成

任意のハンドラでJWT認証を行えるように、Middlewareの形で関数を作成します。`server/middlewares/auth0/middleware.go`を作成し以下を記述します。

```go
// server/middlewares/auth0/middleware.go

package auth0

import (
	"context"
	"net/http"

	jwtmiddleware "github.com/auth0/go-jwt-middleware"
	"github.com/form3tech-oss/jwt-go"
)

// jwtmiddleware.JWTMiddlewareをContextに格納するためのキー
type JWTMiddlewareKey struct{}

// JWTをContextに保存するためのキー
type JWTKey struct{}

// jwtmiddleware.JWTMiddlewareをリクエストのContextに格納するためのMiddleware
func WithJWTMiddleware(m *jwtmiddleware.JWTMiddleware) func(http.Handler) http.Handler {
	return func(next http.Handler) http.Handler {
		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			// リクエストのContextにJWTMiddlewareを格納する
			ctx := context.WithValue(r.Context(), JWTMiddlewareKey{}, m)
			// 新しいContextを入れて次の処理に渡す
			next.ServeHTTP(w, r.WithContext(ctx))
		})
	}
}

// JWT検証を行うためのmiddleware
func UseJWT(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		// ContextからJWTMiddlewareを取得
		jwtm := r.Context().Value(JWTMiddlewareKey{}).(*jwtmiddleware.JWTMiddleware)
		// リクエスト中のJWTを検証
		if err := jwtm.CheckJWT(w, r); err != nil {
			http.Error(w, err.Error(), http.StatusUnauthorized)
			return
		}
		// JWT検証後に、Contextのjwtm.Options.UserPropertyからパース済みのトークンを取得する
		if val := r.Context().Value(jwtm.Options.UserProperty); val != nil {
			token, ok := val.(*jwt.Token)
			if ok {
				// リクエストのContextにJWTを保存する
				ctx := context.WithValue(r.Context(), JWTKey{}, token)
				// 新しいContextを入れて次の処理に渡す
				next.ServeHTTP(w, r.WithContext(ctx))
				return
			}
		}
		next.ServeHTTP(w, r)
	})
}

// Contextに埋め込まれたJWTを取得する
func GetJWT(ctx context.Context) *jwt.Token {
	rawJWT, ok := ctx.Value(JWTKey{}).(*jwt.Token)
	if !ok {
		return nil
	}
	return rawJWT
}
```

Middlewareとはリクエストのハンドラを呼ぶ前後に任意の処理を差し込む機能です。ここでは2つのMiddlewareを実装しています。

1. 2番のMiddlewareがJWTMiddlewareを参照できるようにContext中に埋め込む
2. リクエストに対応したハンドラの処理が呼ばれる前に、リクエスト中に含まれるJWTを検証する

1番の`WithJWTMiddleware`では、`JWTMiddleware`を受け取り新しいMiddlewareを作成します。このMiddlewareでは、リクエストのContextに`JWTMiddleware`を埋め込み、そのContextとともに次の処理へ渡します。

2番の`UseJWT`では、Contextの中にある`JWTMiddleware`を使ってリクエスト中のJWTを検証します。もし検証に失敗したらエラーを書き込んで返します。エラーの場合は`next.ServeHTTP(w, r)`が呼ばれないため、後続の処理は呼ばれずハンドラの処理は実行されません。JWTの検証に成功した場合は、パースしたJWTをContextに埋め込んで次の処理へ渡します。

`GetJWT`はContextにある`JWT`を取得するヘルパー関数です。他のMiddlewareやハンドラでJWTを参照したい場合に使用します。

### JWT認証が必要なエンドポイントの実装

JWT認証に必要なコードは実装できたので、実際にJWT認証が必要なエンドポイントを実装していきます。`/v1/users/me`にアクセスした時にJWT認証をするようにします。

まず、`server/handlers/v1/users/me/me.go`を作成します。ここでは`/v1/users/me`へのリクエストに対するハンドラを実装し、JWTに応じてユーザーの情報を返します。

このハンドラは事前にJWT認証をしているため、Contextに検証済みのJWTが含まれています。JWTの`Claims`にはペイロード情報が入っており、たとえば`sub`はAuth0におけるユーザーの識別子が入っています。この`sub`に紐づくユーザーを返します。ここではインメモリのデータを参照していますが、実際のAPIではDBにクエリを投げて`sub`が一致するユーザーレコードを取得するなどします。

そして、取得したユーザーをレスポンスとして返します。

```go
// server/handlers/v1/users/me/me.go

package me

import (
	"encoding/json"
	"fmt"
	"net/http"

	"github.com/Kourin1996/go-auth0-example/server/middlewares/auth0"
	"github.com/form3tech-oss/jwt-go"
)

type User struct {
	Name string `json:"name"`
	Age  int    `json:"age"`
}

var (
	subToUsers = map[string]User{
		"auth0|61a8178b21127500715968e2": {
			Name: "kourin",
			Age:  15,
		},
	}
)

// subを元にUserを取得する関数
// 実際のAPIではDBなどに照会し、subに紐づくUserを取得するなどをする
func getUser(sub string) *User {
	user, ok := subToUsers[sub]
	if !ok {
		return nil
	}
	return &user
}

// /v1/users/me のハンドラ
func HandleIndex(w http.ResponseWriter, r *http.Request) {
	token := auth0.GetJWT(r.Context())
	fmt.Printf("jwt %+v\n", token)

	// token.Claimsをjwt.MapClaimsへ変換
	claims := token.Claims.(jwt.MapClaims)
	// claimsの中にペイロードの情報が入っている
	sub := claims["sub"].(string)

	// userを取得する
	user := getUser(sub)
	if user == nil {
		http.Error(w, "user not found", http.StatusNotFound)
		return
	}

	// レスポンスを返す
	res, err := json.Marshal(user)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	w.Write(res)
}
```

## 初期化とMiddlewareの設定

```go
// server/main.go

package main

import (
	"fmt"
	"log"
	"net/http"

	v1 "github.com/Kourin1996/go-auth0-example/server/handlers/v1"
	"github.com/Kourin1996/go-auth0-example/server/handlers/v1/users/me"
	"github.com/Kourin1996/go-auth0-example/server/middlewares/auth0"
	"github.com/rs/cors"
)

const (
	port     = 8000
	domain   = "<AUTH0_DOMAIN>"
	clientID = "<AUTH0_CLIENT_ID>"
)

func main() {
	// 公開鍵を取得する
	jwks, err := auth0.FetchJWKS(domain)
	if err != nil {
		log.Fatal(err)
	}
	// domain, clientID, 公開鍵を元にJWTMiddlewareを作成する
	jwtMiddleware, err := auth0.NewMiddleware(domain, clientID, jwks)
	if err != nil {
		log.Fatal(err)
	}

	mux := http.NewServeMux()
	// /v1へのリクエストの場合のハンドラを登録
	mux.HandleFunc("/v1", v1.HandleIndex)
	// /v1/users/meへのリクエストの場合のハンドラを登録
	// auth0.UseJWTでラップし、ハンドラを呼ぶ前にJWT認証を行う
	mux.Handle("/v1/users/me", auth0.UseJWT(http.HandlerFunc(me.HandleIndex)))

	// フロントエンドからアクセスできるようにCORSの設定をする
	c := cors.New(cors.Options{
		AllowedOrigins:   []string{"http://localhost:3000"},
		AllowedHeaders:   []string{"Authorization", "Content-Type"},
		AllowCredentials: true,
		Debug:            true,
	})

	// リクエスト前にJWTMiddlewareをContextに埋め込むためのMiddlewareを追加
	wrappedMux := auth0.WithJWTMiddleware(jwtMiddleware)(mux)
	wrappedMux = c.Handler(wrappedMux)

	addr := fmt.Sprintf(":%d", port)
	log.Printf("Listening on %s", addr)
	if err := http.ListenAndServe(addr, wrappedMux); err != nil {
		log.Fatal(err)
	}
}
```

これで一通りのAPIのコードは実装が完了しました。`go run main.go`でAPIを起動して、サーバーが立ち上がったら完了です。

```bash
$ go run main.go
2021/12/02 14:50:50 Listening on :8000
```

## フロントエンドサンプルアプリ作成

### ログイン部分実装

最後にフロントエンドからアクセスするための簡単なReactアプリを作成します。今回はCreate React Appを使い、Reactアプリの雛形を作成します。プロジェクト構築が完了した後、Auth0認証に必要な `@auth0/auth0-react` を追加でインストールします。

```shell
# client以下にReactのプロジェクトを作成する
$ npx create-react-app client
# プロジェクトの中に移動する
$ cd client
# Auth0認証に必要なパッケージをインストールする
$ npm install --save　@auth0/auth0-react
```

まず、`src/index.js`を開き、`Auth0Provider`を`App`コンポーネントの上にラップします。この時, `domain`, `audience`, `clientId`, `redirectUri`を渡します。`audience`にはAPIのIdentifierを指定します。

```jsx
// src/index.js
...
import App from "./App";
import { Auth0Provider } from "@auth0/auth0-react";

ReactDOM.render(
  <React.StrictMode>
    <Auth0Provider
      domain="<DOMAIN>"
      audience="<API_IDENTIFIER>"
      clientId="<CLIENT_ID>"
      redirectUri="http://localhost:3000"
    >
      <App />
    </Auth0Provider>
  </React.StrictMode>,
  document.getElementById("root")
);
...
```

次に, `src/App.js`を開き、ここにログインボタンを設置します。ボタンが押されたら、`useAuth0`フックの中にある`loginWithRedirect`関数を呼びます。

```jsx
// src/App.js
// 分かりやすい用にオリジナルのコードは全て消してあります

function App() {
  const { loginWithRedirect, isAuthenticated } = useAuth0();

  const onClickLogin = () => {
    loginWithRedirect();
  };

  return (
    <div className="App">
      <div
        style={{ display: "flex", justifyContent: "center", marginTop: "64px" }}
      >
        <button onClick={onClickLogin} disabled={isAuthenticated}>
          {isAuthenticated ? "ログイン済み" : "ログイン"}
        </button>
      </div>
    </div>
  );
}

export default App;
```

`npm run start`で開発サーバーを立ち上げて、ブラウザからアクセスします。

```bash
$ npm run start
```

ブラウザからアクセスしログインボタンを押すと、Auth0のログインページにリダイレクトをします。

![スクリーンショット 2021-12-02 15.07.40.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/915170/59fe4201-2d32-3d12-9849-cdf3227d1d64.png)

サインインまたはサインアップをすると、元のページに戻ります。そして、`useAuth0()`フックに含まれる `isAuthenticated` が `true` になりログイン済みであることを取得できます。

![スクリーンショット 2021-12-02 15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/915170/563bb25e-4a85-5b6e-101a-dddfe672f6ca.png)

### API呼び出し部分実装

API呼び出し部分を実装します。まず、JWTを取得する必要があります。`useAuth0()`フックの中に`getAccessTokenSilently`という関数があり、これを呼ぶとJWTを取得できます。そして、APIを呼ぶ際にJWTをヘッダーの`Authorization`にセットします。

```jsx
// client/src/App.js

import { useState, useEffect } from "react";
import "./App.css";
import { useAuth0 } from "@auth0/auth0-react";

const API_URL = "http://localhost:8000";

// Auth0のJWTを取得するフック
const useAuth0Token = () => {
  const { isAuthenticated, user, getAccessTokenSilently } = useAuth0();
  const [accessToken, setAccessToken] = useState(null);

  useEffect(() => {
    const fetchToken = async () => {
      // JWTを取得して状態に保存する
      setAccessToken(await getAccessTokenSilently());
    };

    // ログイン済みの場合のみJWTを取得する
    if (isAuthenticated) {
      fetchToken();
    }
  }, [isAuthenticated, user?.sub]);

  return accessToken;
};

function App() {
  const { loginWithRedirect, isAuthenticated } = useAuth0();
  const token = useAuth0Token();
  // ユーザー情報を保持する状態
  const [me, setMe] = useState(null);
  // APIコールのエラーを保持する状態
  const [error, setError] = useState(null);

  const onClickLogin = () => {
    loginWithRedirect();
  };

  const onClickCall = async () => {
    try {
      // APIを呼ぶ
      const res = await fetch(`${API_URL}/v1/users/me`, {
        method: "GET",
        mode: "cors",
        headers: {
          // JWTをAuthorizationヘッダにセットする
          Authorization: `Bearer ${token}`,
          "Content-Type": "application/json",
        },
      });
      if (!res.ok) {
        throw new Error(res.statusText);
      }
      const me = await res.json();
      setError(null);
      setMe(me);
    } catch (error) {
      console.log("error", error);
      setError(error);
    }
  };

  return (
    <div className="App">
      <div
        style={{ display: "flex", justifyContent: "center", marginTop: "64px" }}
      >
        <button onClick={onClickLogin} disabled={isAuthenticated}>
          {isAuthenticated ? "ログイン済み" : "ログイン"}
        </button>
      </div>
      <div
        style={{ display: "flex", justifyContent: "center", marginTop: "64px" }}
      >
        <div
          style={{
            display: "flex",
            alignItems: "center",
            marginRight: "32px",
          }}
        >
          <button onClick={onClickCall}>ユーザー情報を取得</button>
        </div>
        <div style={{ width: "300px" }}>
          <p>ユーザー: {JSON.stringify(me)}</p>
          <p>エラー: {error ? error.toString() : ""}</p>
        </div>
      </div>
    </div>
  );
}

export default App;
```

|ログインしていない時の呼び出し|ログイン済みの場合の呼び出し|
|---|---|
|![スクリーンショット 2021-12-02 15.58.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/915170/c4b68ccb-7133-ffe7-6e5e-64a07fdf3aac.png)|![スクリーンショット 2021-12-02 15.59.01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/915170/55fa096e-ece9-afca-21a7-54f529a1a394.png)|

# まとめ

この記事ではSPA+Go APIの構成で、ユーザーのJWTをAPIから認証する方法について紹介しました。[auth0/go-jwt-middleware](https://github.com/auth0/go-jwt-middleware)のレポジトリを覗いたらver2.0 betaが上がってたので、時間があれば試してみます。

# 参考記事

- [Auth0の機能を調べてみた](https://cloudpack.media/54755)
- [Auth0 Go SDK Quickstarts](https://auth0.com/docs/quickstart/webapp/golang/01-login)
- [RFC7519](https://datatracker.ietf.org/doc/html/rfc7519)
- [Introduction to JSON Web Tokens](https://jwt.io/introduction)
- [Making and Using HTTP Middleware](https://www.alexedwards.net/blog/making-and-using-middleware)
- [auth0/go-jwt-middleware](https://github.com/auth0/go-jwt-middleware)