フューチャー技術ブログ

MSAL.jsで開発時は認証スキップしたい

MSAL.jsはとても便利なライブラリです。前に書いたエントリーで説明しましたが、AzureAD側の設定は必要ですが、コードへの組み込みもすぐです。コールバックを受けるバックエンドサーバーの用意も不要で、フロントエンドだけで認証が完結します。

ですが、開発時にAzureADがない場合もありますし、開発者全員が開発で使うAzureADにユーザー登録されていないかもしれません。また、権限ごとにいろんなユーザーを用意してテストできるようにしたいとかのニーズもあると思います。また、E2Eテストで毎回認証をすると遅いとか、コールバックを受けるコードがGitHub Actionsではうまく動かず実AzureAD認証を組み込むのが難しいとか、認証をスキップしたいニーズもいろいろあるため、開発時にはMSAL.jsをスキップできるようにしてみます。

設定の外だし

前回はハードコードしましたが、AzureADの接続情報などは.envで設定を流し込むべきですので、別ファイルに切り出します。各フレームワークごとに、ブラウザに環境変数を公開するには、キーの名前のルールがあります。Vue.jsであればVUE_APP_を前につけますし、Vite.jsだとVITE_をつけますし、Next.jsだとNEXT_PUBLIC_ですね。これらの設定はサーバーではなくてフロントエンド側なので、それらのルールに従った名前にします。Vue.jsだったら次の通り。

.env
VUE_APP_AZURE_DUMMY_USER=dummy-user@example.com
VUE_APP_AZURE_ISSUER=https://login.microsoftonline.com/${テナントID},
VUE_APP_AZURE_APP_ID=${アプリケーションID}

これらの設定を使うようにします。コールバックのURLは現在実行中のホストの/callbackを向くように動的にパスを作っています。このパスをAzureAD側の設定にも入れる想定です。

authConfig.ts
import type { Configuration } from "@azure/msal-browser";

export const config: Configuration = {
auth: {
authority: process.env.VUE_APP_AZURE_ISSUER,
clientId: process.env.VUE_APP_AZURE_APP_ID,
redirectUri: `${location.protocol}//${location.host}/callback`
},
cache: {
cacheLocation: "localStorage",
storeAuthStateInCookie: false,
}
};

テストユーザー対応

MSAL.jsを使うと、AzureADで認証してJWTトークンを作って返してくれます。それをそのままサーバーにも渡し、サーバー側でIDを取り出して使います。開発用モードを作るとして大幅なif分岐などは作りたくはないですよね?

  • ダミーのJWTは作り、IDが分かるようにする
  • ただしAzureADの証明書での署名はできないので、署名の確認はサーバーではあきらめる

AzureADのトークンを使う場合

今回は開発用のテストユーザーを環境変数から設定できるようにします。

.env.development
VUE_APP_AZURE_DUMMY_USER=dummy-user@example.com

ブラウザ上でダミーのJWTを作るためにjoseパッケージを使います。これはブラウザで使えますが、npmパッケージのほとんどはNode.jsの機能を使っていてブラウザで使えないものが多かったです。

$ npm install jose

前回と違うところを主にサンプルとして提示しています。

ログインではダミーユーザーがあるかどうかで条件判断を行い、ダミーユーザーがいたらJWTを作って返しています。内容はだいたいAzureADが作っているものに似せるようにはしています(完全ではない)。

AzureADのトークンはsubではUUIDのようなコードが入っています。おそらくサーバー側でログインしたユーザーのIDをもとに権限管理をしたりするのであれば、preferred_usernameに入っているメールアドレスを使うことになるんじゃないかと思います。AzureAD側の設定でIDトークンに入れるクレームを増やして、emailクレームを足したりもできるようです。

authPlugin
// 追加
import { UnsecuredJWT } from 'jose'

// 前回はaccessTokenだったがidTokenに変更
let idToken = "";

// 前回のloginメソッドの修正
async login () {
if (process.env.VUE_APP_AZURE_DUMMY_USER) { // ダミーユーザーモード
const jwt = await new UnsecuredJWT({
idp: 'https://sts.windows.net/....',
name: 'Dummy User(ダミー ユーザー)',
preferred_username: process.env.VUE_APP_AZURE_DUMMY_USER,
sub: btoa(process.env.VUE_APP_AZURE_DUMMY_USER), // ナチュラルキーっぽくする
ver: '2.0'
})
.setIssuer(`${process.env.VUE_APP_AZURE_ISSUER}/v2.0`)
.setAudience(process.env.VUE_APP_AZURE_APP_ID)
.setIssuedAt()
.setExpirationTime('1h')
.setNotBefore(Date.now() / 1000)
.encode()
idToken = jwt
} else { // 本番モード
if (_auth.getAllAccounts().length > 0) {
_auth.setActiveAccount(_auth.getAllAccounts()[0])
const result = await _auth.acquireTokenSilent({
scopes: [`${process.env.VUE_APP_AZURE_APP_ID}/.default`],
redirectUri: config.auth.redirectUri
})
idToken = result.idToken
return idToken
} else {
return _auth.acquireTokenRedirect({
redirectStartPage: location.href,
scopes: [`${process.env.VUE_APP_AZURE_APP_ID}/.default`],
redirectUri: config.auth.redirectUri
})
}
}
},
async logout() {
if (!process.env.AZURE_DUMMY_USER) {
return _auth.logoutRedirect({
postLogoutRedirectUri: `${location.protocol}//${location.host}/`
})
}
}

これで、AzureADがあるふりをしてそれっぽいIDトークンを作って返すコードができました。

サーバー側ではリクエストを受けるときにこのトークンを受けることになります。サーバー側も環境変数で少し動作をコントロールして、テストモードの時には署名の検証は行わない必要がありますが、expiration time(exp)、not before(nbf)、aud、issといったクレームを使った検証は可能です。

まとめ

ログインが必要なサービスで、開発時にログイン回りをどう処理すればいいのか、というのはいつも悩むポイントです。いろんなログイン方式が使えるサーバーであればID/パスワードでログインする機構を別に作ったり、本番同等の認証サーバーを立てて、テストユーザーを入れるなどもあるでしょう。ですが、外部システムへの依存があると結合テストやCIがやりにくくなったりもしますし、処理時間も伸びてしまいます。あと、せっかくMSAL.jsを使えば認証の組み込みが簡単なのに、認証回り以外にたくさんのif文が入るのもうれしくありません。

今回はテスト用にAzureADのログインをバイパスしダミーのJWTを作るという方向で実装しました。比較的影響範囲をログイン回りに閉じ込めつつ実装できたんじゃないかな、と思います。