0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Last updated at Posted at 2023-06-21

概要

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
image.png
ここは適当でOK。
作れたらアプリケーション(クライアント)IDとディレクトリ(テナント)IDをメモ。
image.png

APIのアクセス許可

OpenAIはCognitiveService属なので、APIの許可を追加します。
image.png

Vue.jsでの実装

モジュールの追加

npm install @azure/msal-browser

または

yarn add @azure/msal-browser

設定ファイル作成

ここにアプリケーションの登録でメモったIDを入れます。
redirectUriはとりあえずローカル。
ちゃんと公開するなら書き換えるとか動的に取得するとかまぁてけとーに。

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を追加しました。
グローバル変数に突っ込む感じですね。

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

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だけでいいしー?って時はガン無視でよい?

src\main.ts
// AD認証の初期化
init(authconfig).then(() => {
  app.use(AuthPlugin, authconfig);

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

認証したい画面 ログイン

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

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

トークンはstoreに入れてもいいし、画面で持っていてもいいと思います。
アプリの規模次第ですね。

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つけろよ!って公式リファレンスに書いてありました。

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

引き続き

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

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

参照サイト

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?