Dify の公開アプリ・埋め込みチャットを Amazon Cognito 認証でセキュアにする (IaC コード付き)

## TL;DR

* **課題:** Dify の公開アプリ・ウェブページへの埋め込みチャット機能は非常に強力だが、公開範囲については All or Nothing の公開設定しかできず、たとえば自社の社員のみにアプリを公開するような制御は実現できない
* **解決策:** 以下 GitHub リポジトリの CDK コードのように ALB + Cognito を利用することで、認証されたユーザにのみ利用させるような制御を実装することができる

https://github.com/mabuchs/dify-on-aws-cdk-with-cognito

※ デプロイの前提として、Route53 のホストゾーンが必要です (HTTPS リスナーが必要なため)。  
※ Dify 0.6.15 でのみ動作確認済みです。今後の Dify のアップデート次第で動かなくなりうることにご注意ください。

**動作イメージ**

<blockquote class="twitter-tweet" data-media-max-width="560"><p lang="ja" dir="ltr">Dify のアプリ埋め込み、UI のことを何も考えなくていい一方でユーザ認証なしのフルオープンしかできないのが利用のネックという認識なんですが、ALB / Lambda / Cognito こね回して、ちょっと不格好だけど認証強制ありの埋め込みができました　あとで何かしらの形でアレします <a href="https://t.co/BtDQy1NF7J">pic.twitter.com/BtDQy1NF7J</a></p>&mdash; mabuchs (@mabuchsss) <a href="https://twitter.com/mabuchsss/status/1819375376910754239?ref_src=twsrc%5Etfw">August 2, 2024</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

## はじめに

Difyは、ノーコード・ローコードで手軽に生成AIアプリを構築できるOSSです。ノーコード・ローコード系の生成 AI アプリ開発プラットフォームとしては最近頭一つ飛び抜けた注目を集めており、社内業務の改善で検証している事例を耳にするようになってきています。

### Dify の公開アプリ・サイト埋め込みのメリット

特に Dify でのアプリ開発で優れている点のひとつに、 Dify のアプリ公開・ウェブページ埋め込み機能があります。
Dify 上でアプリケーションを構築して公開すると、即座に利用できるチャットアプリの URL が生成でき、ユーザーはここにアクセスするだけでチャットアプリを利用することができます。

![DifyPubApp (1).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3790935/0bb4fe9d-bda0-1140-7308-901e3fb3bb66.gif)

また、Web ページに埋め込むための HTML / JS のスニペットも出力できるため、既存の Web ページにそのコードを追加するだけで簡単に既存 Web ページ上にチャットを埋め込むことができます。自前で UI 部分を作る必要がなく、 API 実行などを手動で実装する必要がありません。細かいフロントエンド実装スキル不要で、自社の業務にチャットを組み込むのを非常に手軽にしてくれる機能です。

![DifyEmbedApp (1).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3790935/d21de7b7-4e28-50da-ae37-c83a30f8aa37.gif)



### Dify の公開アプリ・サイト埋め込みにおける課題

公開アプリ・サイト埋め込みは非常に便利な機能ですが、実際の利用にあたって注意すべき課題があります。それは、**Dify 上では詳細なアクセス制限をかけられず、完全に公開 or 完全に非公開のどちらかに設定するほかない**ことです。公開状態だとアプリの URL を知ってしまえば誰でもアクセスできてしまうため、社内のユーザーだけにアプリケーションを公開したい、というセキュリティ要件があったときにこれを満たすことができません。

インフラ側の設定で IP アドレス制限をかける (例: AWS ではセキュリティグループ) ことなどは可能なのものの、在宅勤務などで社内 IP アドレス以外からのアクセスを許容するようなケースに対応することは難しいでしょう。

### 解決策: ALB + Cognito

これを解決するために、ALB + Amazon Cognito による認証を利用することができます。セルフサインアップを無効にすれば限定的なユーザーにのみ公開することもできますし、社内の IdP と SAML 連携を設定して社内のユーザーだけに利用を制限することもできます。また、Cognito の Lambda トリガーなどを利用すれば、メールアドレスのドメインを制限してセルフサインアップを有効にすることもできます (参考: [GenU での実装例](https://github.com/aws-samples/generative-ai-use-cases-jp/blob/main/packages/cdk/lib/construct/auth.ts#L92-L113))。

今回は、 tmokmss 氏の作成した [Dify on AWS CDK デプロイ版](https://github.com/tmokmss/dify-on-aws-cdk)に Cognito 認証オプションを追加し、Dify の利用に必要十分な認証をかけました。GitHub 上にソースコードを公開しています。

https://github.com/mabuchs/dify-on-aws-cdk-with-cognito/tree/main


### デプロイ方法

基本は元の tmokmss 氏の Dify on AWS CDK に則った手順により構築することができます。
https://note.com/yukkie1114/n/n0d9c5551569f

ただし、  `npx cdk deploy` コマンドを実行するまえに、 `bin/cdk.ts` 内の `new DifyOnAwsStack()` の引数に変更を加えます。

* `domainName` : 自身の AWS アカウントの Route 53 にホストしている Hosted Zone のドメイン名
* `hostedZoneId` : 同ドメインの Hosted Zone ID
* `subDomain` : dify に割り当てるサブドメイン名 (省略すると `dify` になる)
* `requireAuth` : これを true にすると、Dify への Cognito 認証を有効にできる

※ 注意 : 認証を有効にするには、 `domainName` ・ `hostedZoneId` ・ `requireAuth` のすべてが設定されている必要があります。

**設定例**

```diff_javascript
new DifyOnAwsStack(app, 'DifyOnAwsStackAlbAuth', {
  env: {
    region: 'ap-northeast-1',
    // You need to explicitly set AWS account ID when you look up an existing VPC.
    // account: '123456789012'
  },
  // Allow access from the Internet. Narrow this down if you want further security.
  allowedCidrs: ['0.0.0.0/0'],
  difyImageTag: '0.6.15',
+ domainName: 'example.com', // 自身のアカウントでホストしている Hosted Zone のドメイン名
+ hostedZoneId: 'ABCDEFGHIJKLMNOPQRSTU', // Hosted Zone の Id
+ subDomain: 'dify-auth', // Dify のサブドメイン名 (省略可能、省略すると 'dify' となる)
+ requireAuth: true, // Dify への Cognito 認証を必須にするかどうか
});
```

これらの設定をしてから `npx cdk deploy` を実行してください。成功すれば、 `https://${subDomain}.${domainName}/` が Dify の URL となり、 Cognito 認証を通してログインできるようになります。
なお、そのまま利用すると Cognito はセルフサインアップ可能な構成となっているため、 実際の利用時に求められるセキュリティ要件に合わせた設定 (例: セルフサインアップを無効化、SAML 連携など) を実施するようにしてください。

## 実装の詳細

ここからは、

* Dify の公開アプリ・埋め込みチャットがどのような挙動で動いているのか
* 公開アプリ・埋め込みチャットをセキュアに使うためにどう制御すればよいのか
* それを踏まえた実装内容

を説明していきます。

### Dify の公開アプリ・埋め込みチャットでの初期シーケンスの理解

Dify アプリを Web ページに埋め込んだ場合の、チャットウィンドウ初期表示までのシーケンスは以下のようになっています。なお、Dify のソースを追っている訳ではなく、Chrome の DevTool や README 内の nginx ルーティングの図などを元に整理したものであるため、正確性は担保しません。
また、Dify で公開したアプリ URL を利用する場合も、スタート地点が④になり、 ⑤ が `GET /chatbot/xxxxxx` から `GET /chat/xxxxxx` に変わるだけで、それ以降のシーケンスは同じとなります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3790935/c08dda21-4d7e-2eca-481b-a67a1dda8fd7.png)


今回は、未認証の場合は初期チャットメッセージで認証用 URL に誘導し、誘導先の URL で Cognito 認証を完了したらチャットが利用できるようにしたいと考えました。そのためには、ここに登場するリクエスト群が (本物のレスポンスであれダミーのレスポンスであれ) 成功で返却される必要があります。

### Dify へのリクエストの分類と、認証要否の対応方針

各パスのリソースについて、どのような情報が含まれており、どう対応すべきかを整理したのが以下の表です。

|#	|パス	|概要	|リクエスト先	|用途	|対応方針	|
|---	|---	|---	|---	|---	|---	|
|1	|/api/passport	|公開アプリ・埋め込みチャットで利用する匿名ユーザを一意に特定するための JWT トークンを生成・返却する	|api	|公開アプリ・埋め込みチャットに最初にアクセスする際に実行され、以降の API リクエスト時に Bearer トークンとして利用する	|**Cognito 認証不要でリクエストを通す** <br> 理由： <br> ・このリクエストが通らないと以降のアクションが実行できないこと<br>・適当なダミーデータを返してしまうと、以降の API 実行時にもこの JWT が利用されてしまい、正常に動作しない<br>・含まれている情報はアプリIDと匿名ユーザの UUID のみで、チャットアプリのタイトルやメッセージなどは含まれていない。かつ、この JWT トークンが取得できたとして、以降の API リクエストに認証を強制できれば情報が追加で取得できることはない	|
|2	|/embed.min.js<br>/_next/static/\*<br>/favicon.ico<br>/logo/logo-site.png	|静的ファイル群	|web	|公開アプリや埋め込みチャットの UI を構成する静的ファイル群	|****Cognito** 認証不要でリクエストを通す**<br>理由：<br>・これらの静的ファイルにアプリ固有情報は含まれないため	|
|3	|/chat/\*<br>/chatbot/\*	|公開アプリ・埋め込みチャットごとのエントリポイントとなる HTML が返却される	|web	|静的ファイルの呼び出しや初期実行される API 群 (#1, #4) のエントリポイント	|****Cognito** 認証不要でリクエストを通す**<br>理由：<br>・アプリID 以外のアプリ固有情報は含まれておらず、以降の API リクエストに認証を強制できれば情報が追加で取得できることはない	|
|4	|/api/meta<br>/api/parameters<br>/api/conversations<br>/api/site	|公開アプリ・埋め込みチャットのアプリに関する各種情報	|api	|公開アプリ・埋め込みチャットの初期表示時に呼び出され、この情報を元にモーダルウィンドウが生成される	|****Cognito** 未認証時はダミーデータを返却し、認証済であればリクエストを通す**<br>理由：<br>・これらの API のレスポンスにはアプリタイトルや過去のチャット履歴、初期メッセージ等が含まれるため、許可されたユーザ以外に出してはならない<br>・一方で、初期表示時にはこれらの API リクエストが成功し、初期表示に必要な一通りの値を得られる必要がある<br>→ 未認証時には、アプリ固有の値を含まない架空のダミーデータを返却し、未認証であることをユーザに示して認証用 URL に誘導する。<br>　 また、認証済みであれば、 API コンテナにリクエストを転送する。	|
|5	|/v1/*	|作成した Dify アプリを、 API で直接呼び出すための API 群	|api	|自作のアプリケーションのバックエンドとして Dify アプリを呼び出す際に利用	|****Cognito** 認証不要でリクエストを通す**<br>理由：<br>・Dify 自身の API キー機能を用いて認証することができるため、ALB / Cognito では認証スコープ外とする	|
|6	|/api/\*<br>/console/api/\*<br>/files/\*	|その他すべての API 群	|api	|Dify アプリの実行時に呼ばれる	|**Cognito** 認証を必須にする	|
|7	|/*	|その他すべてのリクエスト	|web	|Dify アプリの実行時に呼ばれる	|**Cognito** 認証を必須にする	|

※ 今回のメインのスコープは公開アプリと埋め込みチャットのため、 API 利用についてはもう少し検討が必要です。特に、API で Dify アプリを叩いて、その中で生成された /files/* 配下のファイルを参照したい場合が今回はカバーできていません (Cognito にリダイレクトされてしまうため)。現実的には、 API 利用時は専用の API キー的な文字列を生成し、 「/v1/* と /files/* のパスパターン、かつ当該 API キーを HTTP リクエストヘッダに含む場合は認証不要でリクエストを通す」といった形になるのかなと思います。

### アーキテクチャへの落とし込み

先ほどの表の「対応方針」で整理した結果、#5 以外は Cognito + ALB のリスナールールで対処できそうです。が、#4 に示した API 群については、ALB リスナールールで対応しきることができません (リスナーアクションに Cognito 認証を含めた場合、未認証の場合のアクションが「許可」「拒否」「認証にリダイレクト」の三択となるため)。そこで、Lambda を利用してここをハンドリングするアーキテクチャを取ることにしました。

![dify-arch.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3790935/2d850d95-cd85-c010-f178-197dbfd885a6.png)


＃4 以外で web / api のコンテナに直接リクエストを転送するケースでは、パスベースのリスナールールにより Cognito 認証要否を判定してリクエスト可否を決めます。 
#5 については、図に示したとおり、まずは Lambda 関数をターゲットに設定し、リクエストヘッダに ALB の Cognito 認証時のセッション cookie があるかどうかをチェックします。もしセッション cookie がない場合は未認証であるため、対応方針の通りにダミーのレスポンスを Lambda から返却します。これにより、未認証の場合はダミーレスポンスを元に公開アプリ/埋め込みチャットの画面を描画することができますし、ダミーの初期メッセージ中で Cognito の認証必須のパス※に誘導することができます。ユーザーはその誘導に則って Cognito 認証を済ませることで、 ALB の認証セッション cookie を得ることができるため、以降のリクエストはすべて認証済みの状態で実施できるようになります。 セッション cookie がある場合は再び ALB に転送し、そちらで認証状態の正式なチェックを通した後に api コンテナに転送する、という挙動にしています。 Lambda からのリクエストを受け付ける専用のリスナーを別途作成し、NATGW の IP アドレスから同リスナーへのリクエストのみを許可するセキュリティグループを設定することにより、この一連の流れをセキュアに実行することができます。
※ なお、認証必須のパスとして、「認証完了」という固定レスポンスを返すパス ( `/auth-result` ) を作成しています


これらの内容を ALB の設定と Lambda 関数に含めたものが今回の実装となります。詳細は以下のファイル群を参照ください。

https://github.com/mabuchs/dify-on-aws-cdk-with-cognito/blob/main/lib/constructs/alb.ts#L143-L303

https://github.com/mabuchs/dify-on-aws-cdk-with-cognito/blob/main/lib/constructs/lambda/lambda_proxy.ts


## おわりに

OSS 版 Dify は、手軽に利用できる一方でセキュリティの細かい制御が難しいです。が、ALB + Amazon Cognito + Lambda の組み合わせにより、認証されたユーザにのみ Dify で作成したアプリを開放できるようになります。
この記事が、皆様の生成 AI 活用の一助になれば幸いです。

※ 最大限検証・確認したうえで本記事を公開していますが、利用される際にはセキュリティに関して十分検証のうえ利用して下さい。