Auth0で脆弱なWebアプリケーションを作る~設計編~

## はじめに

今から1つ大きな脆弱なポイントがあるアプリケーション(フィクション)の設計を紹介します。どこに問題があるか考えながら読んでみいただけると楽しめるかもしれません。

## 事業会社のCIOとベンダー企業のある話。

**注 : この話はフィクションです**

CIO: 「ウチのECサイトにAuth0を導入しようと思う」

ベンダー: 「Auth0ですか、最近Oktaに買収されたIDaaSですよね」

CIO: 「そーそー、これを機にセキュリティリスクを転嫁してしまった方がええんやと思う。ということで認証システムのリプレースよろしく」

ベンダー: 「はい!!!」

数ヶ月後

ベンダー: できました！

CIO: 「おー、じゃあテキトーにシーケンス図でも見せて」

ベンダー: 「はい、ではまずログインのところから。現在SPAで構築されているWebアプリケーションを認可コードフロー+PKCEで認証しトークンを取得します。今までの認証方式からOAuth2.0に準拠した安全な認証方式に作り替えます。」

※ Webアプリケーションはフロントエンドorクライアントと読み替えていただいても大丈夫です。

### Auth0のログイン

```mermaid

sequenceDiagram
    participant User as ユーザー
    participant WebApp as Webアプリケーション
    participant Auth0 as Auth0認証サーバー

    User->>WebApp: Webアプリケーションにアクセス
    activate WebApp
    WebApp->>WebApp: code_verifier を生成 <br> code_challenge_method(S256 or plain) を用いて code_challenge を生成し保存
    WebApp->>Auth0: code_challenge と code_challenge_method を送信
    deactivate WebApp
    activate Auth0
    Auth0->>User: ユーザー認証の要求と画面表示
    User->>Auth0: メールアドレス,pwを入力し送信する
    Auth0 ->> Auth0: Auth0のアカウント作成
    Auth0->>User: 認可コードを返す
    deactivate Auth0
    activate User
    User->>WebApp: 認可コードと共にWebアプリケーションにリダイレクト
    activate WebApp
    WebApp->>Auth0: 認可コードとcode_verifierを送信
    deactivate WebApp
    activate Auth0
    Auth0 ->> Auth0: 認可コードの確認<br> code_verifierをcode_challenge_methodを用いて変換 <br> 以前送られた code_challenge と同値になることを確認する
    Auth0->>WebApp: トークンを返す
    deactivate Auth0
    activate WebApp
    WebApp->>User: ログイン成功通知
    deactivate WebApp

```

CIO: 「モダンでええやないか！ちなみにこれって前から使ってたユーザーがいるだろう？その認証情報との紐付けはどうするつもりだ？」

ベンダー: 「はい、そちらは大丈夫です。Auth0にログインしてもらったのちに以前から登録されていたメールアドレスと照合し、そこにワンタイムパスワードを送信することで所持情報による本人確認を実施。Auth0のuser_idとアプリケーションで管理しているidの紐付けを実施します。」

### 旧認証方式との統合

上記のログイン成功後より

```mermaid

sequenceDiagram
    participant User as ユーザー
    participant WebApp as Webアプリケーション
    participant Auth0 as Auth0
    participant API as API
    participant DB as DB
    participant Mailer as メーラー
    Auth0 ->> WebApp: トークン
    WebApp ->> API: ヘッダー: アクセストークン /user
    activate API
    API ->> API: 署名の検証. アクセストークン内のuser_idを確認。
    API ->> WebApp: ユーザー情報なし
    deactivate API
    WebApp ->> API: ヘッダー: IDトークン /signup
    API ->> API: IDトークン署名の検証<br>デコードしメールアドレスの抽出
    API ->> DB: メールアドレスの照会
    DB ->> API: 既存ユーザーの登録メールアドレスを取得
    API ->> Mailer: ワンタイムパスワード
    Mailer ->> User: ワンタイムパスワード受信
    User ->> WebApp: ワンタイムパスワード入力
    WebApp ->> API: ワンタイムパスワード送信
    API ->> API: ワンタイムパスワードの照会によるメールアドレスの所持情報の確認
    API ->> DB: Auth0のIDトークン内にあるメールアドレスと<br>同一なidとAuth0のuser_idを紐付け
    　 
```

CIO: 「ほお、なかなか厳重そうだの。その後の操作はどうなる？」

ベンダー: 「まず、ログイン以降のAPIの認可はAuth0のアクセストークンを用いて行います。また、基本的にはテーブル構造は変更していないので既存のアプリケーションで管理しているユーザーidを使って注文情報のCRUD操作を行えるようになっています。」

### アプリケーションAPIの利用

```mermaid

sequenceDiagram
    participant User as ユーザー
    participant WebApp as Webアプリケーション
    participant API as API
    participant DB as DB
    
    WebApp ->> API: /user Auth0のアクセストークン
    API ->> API: アクセストークンの署名の検証
    API ->> DB: Auth0とアプリケーションの<br>紐づきテーブルからidを取得
    DB ->> API: アプリケーションで管理している<br>ユーザーid
    API ->> WebApp: idを含むユーザー情報の返却
    WebApp ->> User: 表示
    User ->> WebApp: 注文操作
    WebApp ->> API: /order<br>ヘッダー:アクセストークン<br>ボディ:ユーザーid,注文情報
    API ->> API: アクセストークンの署名の検証
    API ->> DB: CRUD操作
    
```

CIO: 「全体として良さそうだね。あとはいい感じによろしく」

## この設計について

いかがだったでしょうか？この設計に大きな脆弱なポイント見つけれたでしょうか？
全体として割としっかりセキュリティについて考慮されている雰囲気はありつつ何点かツッコミポイントを用意してあります。無論これから話すことはあくまで一意見なのでそれ以外にも軽微な問題点はあると思いますが一点明らかにマズいだろうというのが存在しているので今回はそれ以外の問題点については目をつぶっていこうと思います。

### Auth0ログインのレビュー

アーキテクチャを再掲しておりますが、ここの設計は大きな問題点はないです。OAuth2.0に準拠した認可コードフロー+PKCEの構成になっておりユーザー認証に関しては概ね問題ない構成になっています。
強いていうなら stateを絡ませてCSRFの対策はできた方が良かったですね。
ただ、正直この辺に関しては最近は便利な[クライアント用のSDK](https://auth0.com/docs/quickstarts#spa)をAuth0が出してくれているのでそれを利用すればその辺はほとんど心配いらないです。


ちなみにこの辺のフローをクライアント側を自前で作りたい場合も[Auth0がAPI](https://auth0.com/docs/get-started/authentication-and-authorization-flow/add-login-using-the-authorization-code-flow-with-pkce#authorization-url-example)を提供してくれているので可能です。

```mermaid

sequenceDiagram
    participant User as ユーザー
    participant WebApp as Webアプリケーション
    participant Auth0 as Auth0認証サーバー

    User->>WebApp: Webアプリケーションにアクセス
    activate WebApp
    WebApp->>WebApp: code_verifier を生成 <br> code_challenge_method(S256 or plain) を用いて code_challenge を生成し保存
    WebApp->>Auth0: code_challenge と code_challenge_method を送信
    deactivate WebApp
    activate Auth0
    Auth0->>User: ユーザー認証の要求と画面表示
    User->>Auth0: メールアドレス,pwを入力し送信する
    Auth0->>User: 認可コードを返す
    deactivate Auth0
    activate User
    User->>WebApp: 認可コードと共にWebアプリケーションにリダイレクト
    activate WebApp
    WebApp->>Auth0: 認可コードとcode_verifierを送信
    deactivate WebApp
    activate Auth0
    Auth0 ->> Auth0: 認可コードの確認<br> code_verifierをcode_challenge_methodを用いて変換 <br> 以前送られた code_challenge と同値になることを確認する
    Auth0->>WebApp: トークンを返す
    deactivate Auth0
    activate WebApp
    WebApp->>User: ログイン成功通知
    deactivate WebApp
```


### 旧認証方式との統合のレビュー

旧認証方式との統合も大きな問題はないと思います。Auth0のIDトークンはemailでログインした場合メールアドレスを含んでいるのでIDトークンをデコードすればペイロード部分からメールアドレスを取得できます。また、その際にjwtの署名の検証もすることでそのトークンの改ざんを検知できます。また、そのメールアドレスにワンタイムパスワードを送信することでメールアドレスの所持情報を確認することができるので、他人のメールアドレスとpwでAuth0のアカウントを新規作成して既存のアプリケーションのユーザー情報と紐付けられるという不正を防止することもできます。


#### 悪意ある第3者が不正に利用しようとした場合の図


```mermaid

sequenceDiagram
    participant User as 悪意あるユーザー
    participant WebApp as Webアプリケーション
    participant Auth0 as Auth0
    participant API as API
    participant DB as DB
    participant Mailer as メーラー
    participant RealUser as emailの所持者
    User ->> Auth0: 他人のemailとpwで新規登録
    Auth0 ->> WebApp: トークン
    WebApp ->> API: ヘッダー: IDトークン /signup
    API ->> API: IDトークン署名の検証<br>デコードしメールアドレスの抽出
    API ->> DB: メールアドレスの照会
    DB ->> API: 既存ユーザーの登録メールアドレスを取得
    API ->> Mailer: ワンタイムパスワード
    Mailer ->> RealUser: ワンタイムパスワード
    User ->> User: Auth0アカウントを作成したものの<br>他人のユーザー情報も何も取得できず攻撃失敗
```

ちなみにAuth0アカウントで新規アカウント作成の時にメールを送って、メールアドレスの所持情報が確認できないとアカウントを作成させないようにすることも可能です。(多分そっちの方がいい)

また、この辺の統合に関しては [Auth0 Actions](https://auth0.com/docs/customize/actions) を活用するといい感じにできますがそれは別の機会にでも気が向いたら紹介します。

### アプリケーションAPIの利用のレビュー

問題はここです。察しの良い方は認識の通りですが認可が適切にできていないというのが問題でした。
Auth0でログインし、既存のアプリケーションと統合するところまでは問題はありませんでした。ただ、その後トークンとユーザーidをクライアントから渡すというのが問題です。
具体的には以下のケースが考えられます。


```mermaid

sequenceDiagram
    participant User as 悪意あるユーザー<br>id:1
    participant WebApp as Webアプリケーション
    participant Auth0 as Auth0
    participant API as API
    participant DB as DB
    
    User ->> Auth0: ログイン実施<br>Auth0のemailとpw
    Auth0 ->> WebApp: トークン
    WebApp ->> API: /user Auth0のアクセストークン
    API ->> API: アクセストークンの署名の検証
    API ->> DB: Auth0とアプリケーションの<br>紐づきテーブルからidを取得
    DB ->> API: アプリケーションで管理している<br>ユーザーid:1
    alt 攻撃のフロー
        User ->> WebApp: トークンをブラウザから抽出
        WebApp ->> User: アクセストークン
        User ->> API: ヘッダー: 自身のアクセストークン <br> ボディ: id:2 (なりすます対象のユーザーid。※どうidを入手するかは省略)<br>個人情報の閲覧や勝手な注文など悪意のある操作内容
        API ->> API: アクセストークンの署名の検証<br>トークンは改竄していないので問題なし
        API ->> DB: id:2 のデータを参照・更新可能
    end
    
```

このケースの場合、トークンは攻撃者であるid:1のもの。しかし、APIでユーザーのidとして認識しているはリクエストボディに含まれるidです。なのでこのidを変えてしまいさえすれば誰にでも成りすませてしまうのです。

## 何が悪かったか

ジャンル分けで問題点を整理していこうと思います。

### 権限管理

今回、Auth0のアクセストークンを使ってAPIの認可をしていました。しかし、このケースでは「トークンさえあればどのユーザーの情報でも操作ができる」という **誰でもAdmin** 状態になってしまっていました。

### マイグレーション

「普通こんな実装するわけないだろ」と思うエンジニアもいると思いますが確かに普通はそうです。ただ、今回は旧ECサイトから認証をAuth0にマイグレーションするという課題が発生しました。今後、Auth0がメジャーになっていくにあたってどんどんこういった要件は増えていくかとも思います。その時に、**Auth0に認証すれば適切に認可ができる状態**というのを構築する必要があります。権限管理で挙げた課題であるidをリクエストボディに含めないようにしたとしたら、アクセストークン一つで「このユーザーが誰なのか？」という識別をできるようにしなくてはいけません。
余談ですが、認可とは認証が済んでいることが前提で成り立つ仕組みです。なので認可の時には誰が認証したんだっけってわかるような最低限の情報が必要なわけなので、トークンのペイロードの中にidかそれに相当する個人の識別子が最低限含まれているわけです。

![スクリーンショット 2023-07-16 18.24.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/274030/c3dd6132-18d3-7372-6d8b-13c5ae3297c3.png)

[jwt.ioより](https://jwt.io/)


### 組織的なところ

「Auth0を導入すればセキュリティはバッチリで責任を転嫁できる」みたいな考えも間違ってはいないと思います。確かに、id,pwを自前のDBで保管する必要はなくなるのでリスクは減ります。ただ、Auth0は顧客のすべての認証認可の責任を担保するサービスではないです。現状サイトがあるのであれば、認証や認可に関してどのような課題があり、それをクリアするにはどのような機能があれば解決できるか？といった要件を整理した上でAuth0を選ぶべきだったでしょう。

これを助言できるシステムも事業も良くわかっている人材が大事なのかもですね

## 問題の解決策

組織的な問題は置いておいて、Auth0を利用しどのようにAPIで適切な認可ができるようになるか？というのを主観に2つの解決策の設計例を提示しようと思います。

そして双方に共通することとして肝としては「どのようにAuth0で発行されるuser_idとアプリケーションで管理していたユーザーidを結びつけるか？」ということになります。

### アプリケーション管理のDBにあるAuth0との紐付きテーブルを活用する

現状のマイグレーションの方式であればDBのテーブルにAuth0のuser_idとアプリケーションで管理されている既存のユーザーidの紐付きテーブルが存在しています。
つまり、Auth0のアクセストークンがリクエストされる度にトークンの署名の検証をし、そのテーブルを参照してuser_idからアプリケーションで利用しているidをAPI側で用意してやればクライアント側のなりすましを防ぐことができます。

```mermaid

sequenceDiagram
    participant User as ユーザー
    participant WebApp as Webアプリケーション
    participant API as API
    participant DB as DB
    
    User ->> WebApp: 注文操作
    WebApp ->> API: /order<br>ヘッダー:アクセストークン<br>ボディ:注文情報
    API ->> API: 署名の検証,jwtのデコード
    API ->> DB: AUth0のuser_id
    DB ->> API:アプリケーションの<br>ユーザーid
    API ->> DB: CRUD操作

```

#### メリット

- WebアプリケーションとAPIの改修だけで対応できる
- 紐付きの管理がしやすい

#### デメリット

- 認可を判断するためにDBの紐付き情報を確認する必要があるので、そのテーブルにアクセスできるAPIしか認可できなくなる。
- クエリの結合条件を変更する必要がある

### Auth0にアプリケーションのユーザーidを持たせる

これはAuth0のアクセストークンのペイロード内に既存のアプリケーションのユーザーidを持たせてしまうという案です。[Actions](https://auth0.com/docs/customize/actions) という機能と [ManagementAPI](https://auth0.com/docs/manage-users/user-accounts/manage-users-using-the-management-api) を活用すれば可能です。

Auth0にemialとpwでユーザーに新規登録してもらい、emailの所持情報が確認できたらAuth0の対象ユーザーのapp_metadataという領域にアプリケーションのユーザーidを保存します。
その後Actionsの設定を追加することでアクセストークンの中にユーザーidが渡されてくるようになるのでそれ以降はトークンの検証だけでAPIの認可ができるようになります。

```mermaid

sequenceDiagram
    participant User as ユーザー
    participant WebApp as Webアプリケーション
    participant Auth0 as Auth0
    participant API as API
    participant DB as DB
    participant Mailer as メーラー
    
    User ->> WebApp: ログイン操作
    WebApp ->> Auth0: ログイン操作(一部省略)
　　 Auth0 ->> User: ログイン画面の表示
    User ->> Auth0: メールアドレスとpwを入力し新規登録
    Auth0 ->> Mailer: 入力したメールアドレスにワンタイムパスワード送信
    Mailer ->> User: ワンタイムパスワードをメールで送信
    User ->> Auth0: ワンタイムパスワード入力してメールアドレスの所持情報確認
    Auth0 ->> API: ActionsでAPIをコール<br>emailアドレス照会
    API ->> DB: emialアドレスからユーザーidを照会
    DB ->> API:ユーザーid
    API->>Auth0: ユーザーid
    Auth0 ->> Auth0: ユーザーidをapp_metadataに格納してアカウント作成
    Auth0 ->> WebApp: リダイレクト&トークン<br>一部フロー省略
    WebApp ->> User: ログイン成功通知
    User ->> WebApp: 操作
    WebApp ->> API: ヘッダー:アクセストークン<br>ボディ:操作内容
    API ->> API: 署名の検証,jwtのデコード<br>ユーザーidの抽出
    API ->> DB: ユーザーidをもとにDBのデータ操作

```

#### メリット

- 認可を判断するためにDBの紐付き情報を確認する必要がないので、そのテーブルにアクセスできないAPIでも認可ができるようになる。
- Auth0でコンソール内での[Actions](https://auth0.com/docs/customize/actions)の実装や設定の変更が必要になる。


#### デメリット

- 紐付きの管理がしにくい
- 認可の仕組みを変更するので

#### 余談

「この設計方法って普通思いつくよね？」て思ったエンジニアの方もいるかもしれませんが確かにそうです。ただ現場で大いに起こりうる過ちだとも思っています。なぜならIFの大きな変更は不安でリスクも大きいからです。「リクエストボディにパラメータ増やす」とかであれば全然簡単ですが、「ヘッダーにトークンを付与させてその署名の検証をして中からidを抽出してテーブルと突合させる」となるとその実装だけで大変ですし、認可の仕組みが大きく変わることで自動テストの修正が発生する可能性がある、ほぼすべてのクエリも修正する必要がある。そういった実装による障害発生のリスク回避をしようと結果セキュリティリスクを発生させ未来に負債を残すなんて話は聞いたことが、、、あるかもしれません。


## Auth0 は認証認可の銀の弾丸ではない

Auth0とは認証と認可をアプリケーションに簡単に実装できるようにするためのサービスで、俗にIDaaSと呼ばれるサービスの一種です。自前でそれら全ての認証・認可の仕組みを構築する頃に伴う時間やコスト、リスクを避けることができます。

> Auth0 is a flexible, drop-in solution to add authentication and authorization services to your applications. Your team and organization can avoid the cost, time, and risk that come with building your own solution to authenticate and authorize users.

[Auth0 Overview より](https://auth0.com/docs/get-started/auth0-overview)

つまり、あくまでシステム構築の便利なツールなので

⭕️ Auth0を使って認証・認可の仕組みをいい感じに作ることができる

❌ Auth0を使えばいい感じに認証・認可の仕組みが出来上がる

というわけです。


## 最後に

Auth0は優れたIDaaSのサービスです。ただ、AWSやfirebaseといったサービスと一緒でどう扱うかで本当に良くも悪くもなります。使えば良いというものではありません。
だからこそ正しく使える方法を考える機会を作っていきたいと思い今回の記事を作成しました。

また、今後も Auth0 をいじって色んなナレッジを貯めていきたいと思いますので設計や実装で相談などある方いたらご連絡ください。