OAuth2.0｜Auth0に入門する React + Ktor

クライアント（React）でAuth0からアクセストークンを取得し、そのアクセストークンをAuthorizationヘッダーに付与して保護されたリソースサーバー（Ktor）にリクエストし、Ktorでアクセストークンを検証する仕組みを作成します。

著者が開発中に遭遇したエラーを再現しながら進めていきます。OAuth2.0はまだまだ勉強中の身ですので、間違いなどありましたら、ご指摘いただけると幸いです。

サーバーサイドフレームワークにはKtorを使用しています。（kotlinサーバーサイド開発が個人的なトレンドです。）

Ktorの需要があまり高くないかもしれませんが、Auth0上での設定やアプリケーションコードの本質的な処理内容はフレームワークに依存せず同じです。

フレームワークに依存する細かいアプリケーションコードは他の記事を参照して頂ければと思います。

## Auth0のアカウント作成

![スクリーンショット 2025-01-02 16.09.10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2520459/9367e9ab-47ef-1b65-cbd0-09eebec79718.png)

- Account Type: ```Other```
- I need advanced settings: ```Checked```

![スクリーンショット 2025-01-02 16.10.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2520459/7b39c0da-3478-3876-8e1f-b92a51cc0388.png)

## Auth0でテナント作成

- Tenant Domain: ```良い感じの名前```
- Region: ```Japan```

![スクリーンショット 2025-01-02 16.12.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2520459/c25d3b45-1fcc-a962-1da3-382445b54b2f.png)

## Auth0でSample Appを作成

Tenantを作成すると、Auth0の素晴らしいオンボーディングが始まるので、これを体験していきます。

Auh0のダッシュボードでポチポチとクリックするだけで、OAuth2.0が組み込まれたReactを起動できます。

### Step 1

プラットフォームのタイプを選択します。ReactなのでSPAです。

- Select a platform for your app: ```Single-Page App```
- Select the technology: ```React```

![スクリーンショット 2025-01-02 16.13.54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2520459/b5ca7d11-1ae4-5158-b9af-f56059660c16.png)

### Step 2

ログイン/サインアップページのUIをカスタマイズできます。

何も設定を変えずにContinueを押しても大丈夫です。（著者は Social Connections に GitHub を追加する変更だけ行ないました。）

![スクリーンショット 2025-01-02 16.15.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2520459/4c65d413-b8f7-c828-7ee9-00f699d0a64b.png)

![スクリーンショット 2025-01-02 16.15.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2520459/09bc5782-6820-83ea-050d-ced8ebe2a3d9.png)

### Step 3

Try Loginを押すと、カスタマイズしたUIで、ログイン/サインアップページのプレビューを閲覧できます。

![スクリーンショット 2025-01-02 16.18.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2520459/39ebe918-e4e4-5987-5ed4-b99362fa710a.png)

![スクリーンショット 2025-01-02 16.17.40.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2520459/8276be45-e733-db89-a304-ebe6b4817ae2.png)

### Step 4

DOWNLOAD SAMPLE APPボタンを押すと、Reactをダウンロードできます。

画面右に記載の設定は後ほど行います。

![スクリーンショット 2025-01-02 16.18.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2520459/502b74f8-73cc-c86d-21d8-70b38c4256cc.png)

![スクリーンショット 2025-01-02 16.18.59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2520459/f65e554c-3b95-29d9-9839-d1e9307e1fa0.png)

## Auth0でApplicationとAPIを作成

ApplicationはAuth0上で**クライアントアプリケーション（Reactにあたる）を管理するコンポーネント**、APIはAuth0上で**リソースサーバー（Ktorにあたる）を管理するコンポーネント**です。

### Application作成

Applications > Applicationsタブを開きます。

```Application```は```Default App```が既に作成されているので、これを使用します。

![スクリーンショット 2025-01-02 16.20.33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2520459/e29fe8ff-d7e6-85fa-1ac5-360774fb2b26.png)

### API作成

Applications > APIs > Create API から作成します。

![スクリーンショット 2025-01-02 16.32.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2520459/e9503c47-b97c-a487-2c13-85faadcd6448.png)

- Name: ```良い感じの名前```
- Identifier: ```一意の識別子（URI）```
    - **アクセスを許可するリソースサーバーをTenant内で識別するための値**です
- JSON Web Token (JWT) Profile: ```Auth0```
- JSON Web Token (JWT) Signing Algorithm: ```RS256```

![スクリーンショット 2025-01-02 16.33.37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2520459/6c4f3bee-d5ba-5548-0400-3c05557da878.png)

これでAPIの作成も完了です。

![スクリーンショット 2025-01-02 16.34.16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2520459/5532a83c-24b5-8879-3000-7b5b2c5ceb48.png)

## Reactの起動

ダウンロードしたReactを ```npm install && npm start``` で起動します。

![スクリーンショット 2025-01-02 16.31.22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2520459/3e7e0daf-e8ea-f7d9-9491-1e2ef22ed246.png)

### Callback URL mismatch

ログインボタンを押すと、先ほどプレビューで閲覧したログインページを表示して欲しいところですが、エラーページが表示されてしまいます。

![スクリーンショット 2025-01-02 21.02.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2520459/a28078f1-3aa9-6336-9216-474d6195c44a.png)

エラーページには以下のように記載されています。

```
Callback URL mismatch.
The provided redirect_uri is not in the list of allowed callback URLs.
Please go to the Application Settings page and make sure you are sending a valid callback url from your application.
```

「Auth0で許可されているRedirect URI（Callback URL）」と「クライアント（React）が指定したRedirect URI（Callback URL）」が一致していない、とのことです。

Redirect URIはAuth0での認証結果を返却するURIを指します。

Redirect URIは、Auth0でもクライアント（React）でも何も設定していないので、当然と言えば当然です。オンボーディングの最後の画面の右に記載されていた内容が、この設定に関する内容です。

### Monitoring > Logs でエラー調査

今回のエラーは、エラー原因が画面に表示されていましたが、エラーが起きた際は、Auth0のダッシュボードから原因を調査することが可能です。

![スクリーンショット 2025-01-02 21.26.12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2520459/dbcd6d08-b366-4a1a-a7dd-ca18592c6b4d.png)

先ほどと同じ主旨の内容が記載されています。

```
Callback URL mismatch. http://localhost:3000 is not in the list of allowed callback URLs
```

![スクリーンショット 2025-01-02 21.30.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2520459/59f12b29-88be-1c85-0df1-0885ea4d0ac2.png)

## Auth0 Application と Reactの設定変更

クライアント（React）でAuth0からアクセストークンを取得する仕組みを作成します。

### Auth0 Applicationの設定変更

Default Appの設定を変更します。

![スクリーンショット 2025-01-02 16.20.33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2520459/e29fe8ff-d7e6-85fa-1ac5-360774fb2b26.png)

Settings > Application URIs の以下の３つの入力欄に```http://localhost:3000```と入力します。

- ```Allowed Callback URLs```
- ```Allowed Logout URLs```
- ```Allowed Web Origins```

:::note warn
localhostなので、httpsではなく、httpです。
:::

![スクリーンショット 2025-01-02 16.22.26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2520459/3298d967-a258-a21c-7dca-0fe938658b2b.png)

### Reactの設定変更

```auth_confg.json```の値を変更します。

```json
{
  "domain": "**********",
  "clientId": "**********",
  "audience": "**********"
}
```

domainとclientIdはAuth0のapplicationに記載されています。

![スクリーンショット 2025-01-02 16.28.13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2520459/b1495938-65f8-4fa0-d1d1-2613c7a49b62.png)

audienceはAPIのIdentifierです。

![スクリーンショット 2025-01-02 16.34.16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2520459/5532a83c-24b5-8879-3000-7b5b2c5ceb48.png)

## Application authentication methods

この段階でログインボタンを押すと、ログインページが表示されます。

![スクリーンショット 2025-01-02 16.17.40.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2520459/8276be45-e733-db89-a304-ebe6b4817ae2.png)

サインアップ画面に移動し、ユーザー登録も成功しますが、アクセストークンの取得に失敗し、ホーム画面にリダイレクトされません。

エラーのlogを見ても、```Failed Exchange```とか```feacft```とかの記載だけで、良く分かりません。

![スクリーンショット 2025-01-02 23.16.40.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2520459/f0dea570-d219-aee8-2563-edcb5879f932.png)

この問題は修正されるかもしれませんが、最初に作成されるDefault AppのアプリケーションタイプがSPAであるにもかかわらず、Application authentication methodsがClient Secret (Post)に設定されています。

SPAの場合、ここはNoneであるべきなので、設定を変更します。

![スクリーンショット 2025-01-02 16.24.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2520459/e0ea02bc-56b3-0207-0a3d-49ae03cb74a6.png)

もう一度、localhost:3000にアクセスしてログインすると、アクセストークンの取得に成功し、ホーム画面にリダイレクトされます。

![スクリーンショット 2025-01-02 16.38.20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2520459/2cc8e0c4-2e63-10b6-3865-fdfebab188e4.png)

## ユーザーの確認

User Management > Users から実際に作成されたユーザーを確認することができます。

![下書き一覧 (2)のコピー.tiff](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2520459/4c35d3d9-93d6-3e1d-b113-0900e8c89ab6.tiff)

## Ktorでトークンの検証を行う

クライアント（React）でAuth0からアクセストークンを取得できました。

リソースサーバー（Ktor）は保護されているサーバーです。誰でもリソースにアクセスできるわけではありません。

アクセスを許可されている（認可されている）ことを証明するために、クライアントがリクエスト時に付与するものがアクセストークンです。

なので、リソースサーバーでは、リクエストを受けた時に、このアクセストークンが正しいものであるか検証する必要があります。

### ReactからKtorにリクエストする

Reactの```Extarnal API``` > ```Ping API``` ボタンを押すと、```RESULT``` にJsonが表示されます。しかし、これはモックなので、Ktorのレスポンスを表示します。

![スクリーンショット 2025-01-04 9.57.42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2520459/5d28bda4-2716-33ee-f834-6ab8181dde80.png)

まず、Ktorに簡単なAPIを１つ作成しておきます。

エンドポイント：```http://localhost:8080/health```
レスポンス：```{"msg":"OK!"}```　※ モックと同じJosn構造です。

```kotlin:Application.kt
fun Application.module() {

    routing {
        get("health") {
            call.respondText("""{"msg":"OK!"}""", ContentType.Application.Json)
        }
    }
}
```

Reactの```ExtarnalAPI.js```の```callApi```関数でKtorのエンドポイントを指定します。

```diff_javascript:ExtarnalAPI.js
const callApi = async () => {
     .....
-    const response = await fetch(`${apiOrigin}/api/external`, {
+    const response = await fetch("http://localhost:8080/health", {
         headers: {
           Authorization: `Bearer ${token}`,
         },
     });
     .....
}
```

もう一度、```Ping API``` ボタンを押すと、```{"msg":"OK!"}```というJsonがKtorから返却されました。

※ もし新しいJsonが画面に反映されない場合、画面をリロードしてから、```Ping API``` ボタンを押してみてください。

![スクリーンショット 2025-01-04 10.08.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2520459/13e605fa-5c1d-41b5-4479-bc1512ff9f2e.png)

### Ktorでトークン検証の仕組みを作る

Ktorの```Application.kt```の```Application.module```関数内に以下のコードを記述します。
```issuer```、```audience```、```myRealm```の```${...}```は置き換えてください。

```kotlin:Application.kt
import com.auth0.jwk.JwkProviderBuilder
import io.ktor.server.application.*
import io.ktor.server.auth.*
import io.ktor.server.auth.jwt.*

fun Application.module() {

    val issuer = "https://${Auth0で作成したTenantの名称}.jp.auth0.com/"
    val audience = "${Auth0で作成したAPIのIdentifier}"
    val myRealm = "${このリソースサーバーを示す名称}"

    val jwkProvider = JwkProviderBuilder(issuer).build()

    install(Authentication) {
        jwt("auth0") {
            realm = myRealm
            verifier(jwkProvider, issuer)
            validate { credential ->
                val containsAudience = credential.payload.audience.contains(audience)
                if (containsAudience) JWTPrincipal(credential.payload) else null
            }
        }
    }
}
```

エンドポイントを保護するには以下のようにコードを追記します。


```diff_kotlin:Application.kt
fun Application.module() {

    routing {
+        authenticate("auth0") {
             get("health") {
                 call.respondText("""{"msg":"OK!"}""", ContentType.Application.Json)
             }
+        }
    }
}
```

ブラウザから```http://localhost:8080/health```にリクエストすると、```401 Unauthorized```が返却され、エンドポイントが保護されているのが確認できます。

![スクリーンショット 2025-01-04 9.54.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2520459/39a81eca-8aa6-7c48-d2af-13aad085d1ac.png)

しかし、同じエンドポイントにReactからリクエストすると、正常にレスポンスが返却されます。

これは、Reactからリクエストするとき、Authorizationヘッダーにアクセストークンが付与されているからです。Ktorでトークンの検証が行われ、認可されているユーザーと判断されました。

![下書き一覧のコピー.tiff](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2520459/859daced-c976-46fc-f46c-3cbbe954bc3a.tiff)

試しに、トークンに適当な文字列を追加してみると、Ktorからは```401 Unauthorized```が返却されます。

```diff_javascript:ExtarnalAPI.js
const callApi = async () => {
     .....
     const response = await fetch("http://localhost:8080/health", {
         headers: {
-          Authorization: `Bearer ${token}`,
+          Authorization: `Bearer ${token}abc`,
         },
     });
     .....
}
```

![下書き一覧 (1)のコピー.tiff](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2520459/32fa3f7d-6d54-7438-1b60-26f4bd5e8d3a.tiff)

## 参考

https://auth0.com/blog/adding-auth0-authorization-to-a-ktor-http-api/

https://dev.classmethod.jp/articles/auth0-golang-tutorial/

https://dev.classmethod.jp/articles/auth0-react-tutorial/