【Auth0 × React × Express API】Auth0 After Quickstarts

こんにちは [Rui](https://qiita.com/engineer-ruirui) です

本記事はQiitaエンジニアフェスタ2021のAuth0テーマ記事になります。
今回は Auth0 の After Quickstarts として、React App から Express API を呼び出すサンプルアプリを作成しようと思います。

## 【前提条件】React Quickstarts
任意のテナントでReactアプリを作成してください。
もしくは以下のドキュメントを参考にハンズオンします。
https://auth0.com/docs/quickstart/spa/react

今回の接続情報例は以下の通りです。

``` json:auth_config.json
{
  "domain": "<自身のdomain>.jp.auth0.com",
  "clientId": "<自身のclientId>",
  "audience": "YOUR_API_IDENTIFIER"
}
```

メールアドレスアカウント or Googleアカウントで、ログインできる状態であること。
![ScreenShot 2021-07-11 20.59.52.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/639403/d33cf674-30df-2550-7308-679c3d618e21.png)

### 備考
この時のアクセストークンは JWT ではなく Opaque トークンとなっています。
![ScreenShot 2021-07-11 21.28.19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/639403/bacff24c-e5ef-62f3-da18-20f6cfd3088f.png)



## 【前提条件】Node.js(Express) API Quickstarts
任意のテナントで、Node.js(Express) APIを作成してください。
もしくは以下のドキュメントを参考にハンズオンします。
https://auth0.com/docs/quickstart/backend/nodejs/01-authorization

今回の接続情報は以下の通りです。

```env:.env
AUTH0_AUDIENCE=http://localhost:3010
AUTH0_DOMAIN=<自身のdomain>.jp.auth0.com
```
http://localhost:3010/api/public/ をcallできる状態であること。
![ScreenShot 2021-07-11 21.00.06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/639403/4e51243b-084e-4fd8-224b-ae149fd552d7.png)


## 1. Express API のプライベートエンドポイントにアクセスする

初めに Express API のプライベートエンドポイント http://localhost:3010/api/private へのアクセスを試みます。

### 1-1. Express 側の仕様確認

まず、Express 側のエンドポイントがどのように保護されているか見てみます。
実装を確認すると checkJwt という処理が実装されています。

```Javascript:[Express]server.js
app.get('/api/private', checkJwt, function(req, res) {
  res.json({
    message: 'Hello from a private endpoint! You need to be authenticated to see this.'
  });
});
```

checkJwt の中身を確認すると、以下の項目を検証しています。

- 署名
- audience（認証者）
- issuer（トークン発行者）

ですので、これらの claim を正しく含む JWT をリクエストに付与すればアクセス可能となります。

```Javascript:[Express]server.js
const checkJwt = jwt({
  // Auth0 の wellknown エンドポイントから公開鍵を取得
  secret: jwksRsa.expressJwtSecret({
    cache: true,
    rateLimit: true,
    jwksRequestsPerMinute: 5,
    jwksUri: `https://${process.env.AUTH0_DOMAIN}/.well-known/jwks.json`
  }),

  // audience と issuer を検証
  audience: process.env.AUTH0_AUDIENCE,
  issuer: [`https://${process.env.AUTH0_DOMAIN}/`],
  algorithms: ['RS256']
});
```

つまり、アクセストークンの条件は以下の通りです。
現状のアクセストークンは Opaque 形式であるため、これが JWT 形式である必要があります。

#### アクセストークンの条件
- JWT 形式であること
- 署名されていること
- audience（認証者） claim を含むこと
- issuer（トークン発行者） claim を含むこと


### 1-2. React APP 接続情報を更新

続いて React APP 側に API サーバーの接続情報を設定します。

まず、auth_config.json を修正します。
audience に APIサーバーの audience の値（.env の AUTH0_AUDIENCE の値） を設定します。
ここでは、http://localhost:3010 となる。
※ audience の値は Auth0 の任意のテナント内で一意にアプリケーションを識別できる値です

``` json:auth_config.json
{
  "domain": "<自身のdomain>.jp.auth0.com",
  "clientId": "<自身のclientId>",
  "audience": "http://localhost:3010"
}
```

次に、ExternalApi.js を修正します。
記載された通信先ドメインとパス変更してください。

```Javascript:ExternalApi.js
export const ExternalApiComponent = () => {
  // 変更点：http://localhost:3010 に設定
  const { apiOrigin = "http://localhost:3010", audience } = getConfig();
　　　　// 省略

  const callApi = async () => {
    try {
      const token = await getAccessTokenSilently();

      // 変更点：パスを /api/private に変更
      const response = await fetch(`${apiOrigin}/api/private`, {
        headers: {
          Authorization: `Bearer ${token}`,
        },
      });
　　　　// 省略
```


### 1-3. アクセストークンが JWT であることを確認

ここで、再度ログインすると、アクセストークンが JWT 形式になっているかと思います。
※Auth0の仕様で audience を設定すると JWT になるようです。 

![ScreenShot 2021-07-11 21.37.12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/639403/5ca26394-aea2-38f9-5af8-fb39a5eb5926.png)

アクセストークンをデコードすると、以下の claim が含まれていることが確認できます。
これで準備はできました。

![ScreenShot 2021-07-11 21.39.06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/639403/f959a9fe-d32c-94e7-d5ac-c19388565fbe.png)

![ScreenShot 2021-07-11 21.41.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/639403/35441fb8-c6a3-1853-66e1-db1f1a0a11a8.png)


### 1-4. /api/private にアクセス

API にアクセスしてみましょう。
http://localhost:3000/external-api にアクセスし、Ping APIボタンを押下して API を call します。
正常終了であれば画面にレスポンスが表示されます。

![ScreenShot 2021-07-11 21.59.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/639403/ef1fabfb-e269-90fb-482c-8abcf018f121.png)

通信を確認すると、リクエストの Authorization ヘッダーにアクセストークンが設定されていることがわかります。

![ScreenShot 2021-07-12 8.46.40.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/639403/83b15031-18aa-09fd-fa24-aaad095b4235.png)

## 2. Express API の Scope 管理されたプライベートエンドポイントにアクセスする

続いて Express API の Scope 管理されたプライベートエンドポイント http://localhost:3010/api/private-scoped へのアクセスを試みます。

### 2-1. Express 側の仕様確認

次に Scope 管理されたエンドポイント /api/private-scoped にアクセスします。
Express 側の実装を確認すると checkJwt の他に checkScopes という処理が実装されています。

```Javascript:ExternalApi.js
app.get('/api/private-scoped', checkJwt, checkScopes, function(req, res) {
  res.json({
    message: 'Hello from a private endpoint! You need to be authenticated and have a scope of read:messages to see this.'
  });
});
```

checkScopes の実装を確認すると、アクセストークンの scope claim に「read:messages」という値を含んでいるかチェックしています。
ここでは、auth0が提供する[express-jwt-authz](https://github.com/auth0/express-jwt-authz)というライブラリが利用されています。
※ライブラリの内容を確認すると、scope 管理専用のライブラリのようです。

```Javascript:ExternalApi.js
const checkScopes = jwtAuthz(['read:messages']);
```

以上を踏まえると、アクセストークンの条件は以下の通りです。

#### アクセストークンの条件
- JWT 形式であること
- 署名されていること
- audience（認証者） claim を含むこと
- issuer（トークン発行者） claim を含むこと
- scope claim に read:messages を含むこと


### 2-2. API に Scope 設定

現状のアクセストークンの scope claim に「read:messages」という値は含まれていませんので、含めるよう設定する必要があります。


Auth0 側のAPIにScopeを追加します。
APIs > Permissions > Permission(Scope)に read:messages を設定（Descriptionは適宜設定）

これで read:messages スコープを扱うことができるようになります。

![ScreenShot 2021-07-12 13.05.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/639403/31243ea9-d360-a334-1521-7ae484359b85.png)

### 2-3. React APP に Scope 設定を追加

認可リダイレクト時に scope パラメータが含まれるよう React APP 側を修正します。
まず、auth_config.json に scope を追加します。

``` json:auth_config.json
{
  "domain": "<自身のdomain>.jp.auth0.com",
  "clientId": "<自身のclientId>",
  "audience": "http://localhost:3010",
  "scope": "read:messages"
}
```
次に、config.js にも設定を追加します。

```Javascript:config.js
import configJson from "./auth_config.json";

export function getConfig() {
  const audience =
    configJson.audience && configJson.audience !== "YOUR_API_IDENTIFIER"
      ? configJson.audience
      : null;

  return {
    domain: configJson.domain,
    clientId: configJson.clientId,
    ...(audience ? { audience } : null),
    // 以下追加
    scope: configJson.scope
  };
}
```
最後に index.js を修正します。

```Javascript:index.js
const providerConfig = {
  domain: config.domain,
  clientId: config.clientId,
  ...(config.audience ? { audience: config.audience } : null),
　　　　// 以下追加
  scope: config.scope,
  redirectUri: window.location.origin,
  onRedirectCallback,
};
```

### 2-4. React APP の APIパスを変更

/api/private-scoped にアクセスするよう変更する。

```Javascript:ExternalApi.js
export const ExternalApiComponent = () => {
　　　　// 省略
  const callApi = async () => {
    try {
      const token = await getAccessTokenSilently();

      // 変更点：パスを /api/private-scoped に変更
      const response = await fetch(`${apiOrigin}/api/private-scoped`, {
        headers: {
          Authorization: `Bearer ${token}`,
        },
      });
　　　　// 省略
```

### 2-5. アクセストークンを確認

ここで再度ログインしてみます。
認可エンドポイントへのリダイレクト時の scope パラメータに「read:messages」が追加されていることがわかります。
画面上では、Messages を共有することに対して同意を求める文言が表示されていることが確認できます。

![ScreenShot 2021-07-13 12.31.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/639403/d771fa91-4fa8-bebb-2aad-d592a37c6a7f.png)

ログイン後に取得したアクセストークンをデコードしてみましょう。
scope パラメータに「read:messages」が追加されていることがわかります。
![ScreenShot 2021-07-13 12.32.05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/639403/828000eb-aed7-3b42-3679-4132eeefc0b2.png)

### 2-6. private-scoped にアクセス

これで準備はできましたので、Scope 管理されたプライベートエンドポイントにアクセスしてみましょう。
正常終了すれば画面にレスポンスが表示されます。

![ScreenShot 2021-07-13 12.33.25.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/639403/87b65e58-91a5-0858-9240-6b9bb29c5ed8.png)