Azure AD連携を使ってVue.js（Vue3）のAzure OpenAIチャットアプリのアクセス権を制御する

# 概要
Azure AD連携を使ってVue.js（Vue3）のAzure OpenAIチャットアプリのアクセス権を制御する

## why?
例によって（？）社内なんでも屋としてAzure OpenAIの検証をやってます。
とりあえず動かすのであれば、APIキーハードコーディングで必要十分なのですが、他のメンバーとかに公開するにあたっては、AD連携とか仕込むのが定石と思います。
色々情報かき集めながら実装したのですが、技術セットの食い合わせ的なやつで辻褄合わせに結構詰まったので書き残し
最初Key-Vault？とか調べてたんですが、もっとだいぶシンプルでした。

## 構成
- Node.js
- Vue.js (Vue3 + TypeScript + Composition API）
- Azure OpenAI

# Azure上での手順
## Azure OpenAI
申し込んで許可されるのを待ちましょう。ちゃんとフォームを埋めれば5営業日あればAcceptされるという風のうわさ。モデルはgpt-35-turboあたりデプロイしておけばok
エンドポイントだけメモっておく。
OpenAIリソース自体にも何かしらの権限が必要かと。

## アプリの登録
なんかすげーわかりにくいメニューだと思うのは私だけでしょうか。
まあとりあえずここ
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/570991/723c760f-2fc8-b1af-d3ed-c2693e37c272.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/570991/e47d4963-20e2-4231-0371-1867772bd224.png)
ここは適当でOK。
作れたらアプリケーション（クライアント）IDとディレクトリ（テナント）IDをメモ。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/570991/5f25cfde-8f09-f980-c33f-19f2dda2f72d.png)


## APIのアクセス許可
OpenAIはCognitiveService属なので、APIの許可を追加します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/570991/0aa0de65-5594-997a-7aa9-ec6fb8cc3295.png)

# Vue.jsでの実装
## モジュールの追加
```
npm install @azure/msal-browser
```
または
```
yarn add @azure/msal-browser
```
## 設定ファイル作成
ここにアプリケーションの登録でメモったIDを入れます。
redirectUriはとりあえずローカル。
ちゃんと公開するなら書き換えるとか動的に取得するとかまぁてけとーに。

```  Typescript:src\config\authConfig.ts
import type { Configuration } from "@azure/msal-browser";

export const authconfig: Configuration = {
  auth: {
    authority:"https://login.microsoftonline.com/{ディレクトリ（テナント）ID}",
    clientId: "{アプリケーション（クライアント）ID}",
    redirectUri: "http://localhost:3000",
  },
  cache: {
    cacheLocation: "sessionStorage",
    storeAuthStateInCookie: false,
  },
};
```
## 認証用プラグイン
参考サイトからほぼほぼそのまま頂きました。
scopes: ["https://cognitiveservices.azure.com/.default"]
が肝です。（めいびー）
あと、Composition API前提なので、app.provideを追加しました。
グローバル変数に突っ込む感じですね。

``` TypeScript:src\plugins\authPlugin.ts
import { App } from "vue";
import { Configuration, PublicClientApplication } from "@azure/msal-browser";

let auth: PublicClientApplication;
let accessToken = "";

export async function init(config: Configuration) {
  auth = new PublicClientApplication(config);
  await auth.handleRedirectPromise();
}

export type AuthPluginType = {
  login(): Promise<string | undefined>;
  logout(): Promise<void>;
  accessToken(): string;
};

export const AuthPlugin = {
  install(app: App, config: Configuration) {
    app.config.globalProperties.$auth = {
      async login() {
        if (auth.getAllAccounts().length > 0) {
          auth.setActiveAccount(auth.getAllAccounts()[0]);
          const result = await auth.acquireTokenSilent({
            scopes: ["https://cognitiveservices.azure.com/.default"],
            redirectUri: config.auth.redirectUri,
          });
          accessToken = result.accessToken;
          return accessToken;
        } else {
          await auth.acquireTokenRedirect({
            redirectStartPage: location.href,
            scopes: ["https://cognitiveservices.azure.com/.default"],
            redirectUri: config.auth.redirectUri,
          });
        }
      },
      async logout() {
        await auth.logoutRedirect();
      },
      accessToken() {
        return accessToken;
      },
    };
    app.provide("auth", app.config.globalProperties.$auth);
  },
};
```

## main.tsでの読み込み
app.mount("#app");の直前にAD認証初期化の処理を入れればOK

``` TypeScript:src\main.ts
import App from "./App.vue";
import { AuthPlugin, init } from "@/plugins";
import { authconfig } from "./config/authConfig";

// Composables
import { createApp } from "vue";

const app = createApp(App);

// AD認証の初期化
await init(authconfig);
app.use(AuthPlugin, authconfig);

app.mount("#app");
```
（追記）
コンパイラに「Topでawaitすんじゃねーよ！！」って怒られた時の修正版
べっつにー？最新のChromeだけでいいしー？って時はガン無視でよい？
``` TypeScript:src\main.ts
// AD認証の初期化
init(authconfig).then(() => {
  app.use(AuthPlugin, authconfig);

  app.mount("#app");
});
```


## 認証したい画面 ログイン
inject でauthオブジェクトを持ってきて、ログイン呼びます。
未ログインであればMSの認証画面に遷移し、戻ってきます。
Microsoft Authenticator連携とかも働くので、かなりイイかんじ！

ログイン済であれば、すぐに元のページにリダイレクトされます。


トークンはstoreに入れてもいいし、画面で持っていてもいいと思います。
アプリの規模次第ですね。
``` TypeScript:src\pages\ChatSpace.vue
<script lang="ts" setup>
import { reactive, onMounted, inject } from "vue";
import { getChatRes } from "@/api_call";
import type { AuthPluginType } from "@/plugins";

const auth = inject("auth") as AuthPluginType;
onMounted(async () => {
  await auth.login();
  console.log(auth.accessToken());
});
</scrpit>
``` 
## APIの呼び出し
Authorizationヘッダでトークンを渡します。
Bearer frefixつけろよ！って公式リファレンスに書いてありました。

``` TypeScript:src\api_call\azureapi.ts
export async function getChatRes(
  input: Array<Record<string, string>>,
  token: String
): Promise<String | void> {
    const req = JSON.stringify({
        model: "gpt-3.5-turbo",
        messages: input
    });
    try {
        const api_endpoint = "{Azure OpenAI Endopint}";
        const completion = await fetch(api_endpoint, {
          method: "POST",
          headers: {
            "Content-Type": "application/json",
            Authorization: "Bearer " + token,
          },
          body: req,
        });
        const json_res = await res.json();
        const res_text = json_res["choices"][0]["message"]["content"];
        return res_text;
    } catch (error) {
        console.error(error);
        return void 1;
    }

}
``` 
## やっと会えたね！
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/570991/fef6c2c8-6529-867b-45b2-d8b9db8c0b5b.png)


# 引き続き
出力をオリジナルの標準出力っぽくするのとかやりたいので調査中。


最近はGithub copilotも使ったりしていて、補完が爆速なのはよいのですが、
まだフレームワークと組み合わせたり、実装方針（Composition API or Option API）に寄り添ってはくれない、ということでまあやっぱりあっちこっちから情報をかき集めることになりますね。。
VSCodeにBingChatがネイティブに組み込まれたりすると幸せになれるのかもなぁ。


# 参照サイト
- [MSAL\.jsを使ってウェブフロントエンドだけでAzureAD認証する \| フューチャー技術ブログ](https://future-architect.github.io/articles/20221118a/)
- [既存のVue\.jsプラグインがVue 3で使えない場合の対応 \- Qiita](https://qiita.com/laineus/items/2b00aca6aa64a455e556)
- [Microsoft Authentication Library \(MSAL\) での認証フローのサポート \- Microsoft Entra \| Microsoft Learn](https://learn.microsoft.com/ja-jp/azure/active-directory/develop/msal-authentication-flows)
- [Azure OpenAI Service の REST API リファレンス \- Azure OpenAI \| Microsoft Learn](https://learn.microsoft.com/ja-jp/azure/cognitive-services/openai/reference)
- [マネージド ID を使用して Azure OpenAI Service を構成する方法 \- Azure OpenAI \| Microsoft Learn](https://learn.microsoft.com/ja-jp/azure/cognitive-services/openai/how-to/managed-identity)