Azure AD B2Cのチュートリアルをやったメモです。
Azure AD B2Cを一言で言うと、コンシューマ向けにも使える認証基盤、というところでしょうか。
同じようなサービスではFirebase Authentificationや、Auth0、oktaなどがあります。
Azure ADとAzure AD B2Cの違いですが、素のAzure ADの上にユーザー登録や認証手段(Twitterなどの外部ID)を追加したもの、となります。ではそもそもAzure ADって・・?
会社でOffice365などに契約すると、「xxxx.onmicrosoft.com」ドメインのアカウントをもらう場合があります。この「onmicrosoft.com」のアカウントを管理しているのがAzure ADになり、サブドメインの「xxxx」ごとに、別々のAzure ADテナントが存在することになります。
マイクロソフトのアカウントの種別として、組織アカウントと個人アカウントがありますが、組織アカウントはAzure ADが管理していて、個人アカウントはマイクロソフトアカウントとして管理されています。
Azure ADは、基本的には企業で使うアカウントになります。ユーザーに対して、企業内のリソース(Azureの各種サービスなど)の使用権限を与えます。ユーザーは外部から招待することもでき、他のAzure ADのユーザーや、個人のメールアドレスもマイクロソフトアカウントとして招待が可能です。
そしてAzure AD B2Cとなると、Azure ADの機能に加えて、ユーザーが招待を受けなくても自分でアカウントを作成し、ログインについても外部のIDプロバイダを使って認証できるようになります。
今回は、Azure AD B2Cをnode + expressを使ったWebサイトで使うチュートリアルをやってみましたが、はっきり言って手順は相当面倒くさいです。また、前述のようにAzure ADがベースとなっているので、それ関連の用語もたくさんでてきますので、情報量の多いFirebase Authentificationなどに比べると大変そうだなあというのが印象です。Firebaseの場合はUnityクライアントもありますしね。。
では順を追ってやっていきますが、まずはAzureのアカウントがない場合は作成してください(要クレカ)
Azure PortalでAzure AD B2Cを作成しますが、他のAzureのサービスと違い、請求単位であるサブスクリプションの設定を変更する必要があります。
リソース プロバイダーの登録
サブスクリプションのリソースプロバイダーから「Microsoft.AzureActiveDirectory」を探して、その行を選択した状態で「登録」ボタンをクリックします。
この概念はAzure ADと同じなのですが、ADに対して「アプリケーション」を登録します。アプリケーションはアプリケーションID(クライアントID)や、クライアントシークレットを保持しています。Azure ADというとMS独自のものと思われがちですが、認証のプロトコル的にはオープンなOAuth 2.0やOCIDが使われていますので、その中で必要なパラメータなどもアプリケーション単位で作成していきます。
ここからが、素のAzure ADではなくB2C独特の設定項目になります。
アプリケーションにアクセスしてきたユーザーに対して、ユーザー作成・サインインやパスワード変更、プロフィールの変更を行うフローを作成します。デフォルトのフローを使うとコーディングなしで上記の機能を提供できます。
手順通りに、デフォルトのフローで
・サインアップとサインインユーザーフロー
・セルフサービス パスワードリセット
・プロファイル編集ユーザーフロー
の3つを作成します。
いままでのAzure AD B2Cの設定項目が再度記載されているので再確認します。
ここで、アプリケーションのリダイレクトURLは、下記のように「http://localhost:3000/redirect」を設定しておいてください。これは、認証後に戻ってくるサイトを自由に設定できないようにするOAuth 2.0のセキュリティの仕組みになります。
サンプルをGitHubから取得し、.envファイルを編集することで、作成したAzure AD B2Cとの紐付けを行っていきます。
APP_CLIENT_IDは作成したアプリケーションのアプリケーションIDになります。
SESSION_SECRET=sessionSecretHere の部分は、Azureとは別の部分で使われる値なので変更しなくでも大丈夫です。
APP_CLIENT_SECRETは、作成したシークレットの値となりますが、これは作成時にしか見れないので注意です。
次からの3つは、先ほどB2Cで作成した各フローを設定します。
SIGN_UP_SIGN_IN_POLICY_AUTHORITY=https://<your-tenant-name>.b2clogin.com/<your-tenant-name>.onmicrosoft.com/<sign-in-sign-up-user-flow-name>
<your-tenant-name>の部分は、Azure AD B2Cを作成する時に決めた「xxxx.onmicrosoft.com」のサブドメイン部分を入れます。
<sign-in-sign-up-user-flow-name>は、フロー作成時に決めた、「B2C_1_」で始まるフローの名称となります。
ここまで入力して
% node index.js
で起動し、http://localhost:3000/ にアクセスし、Sign Inボタンを押すと、Azure AD B2Cが描画した画面でサインイン or ユーザー登録の画面が現れ、ユーザー登録後にログインできるようになります。
このページは、先ほどGitHubからcloneして使ったサンプルコードをイチから作成する流れになります。コードの解説も入っているので、仕組みを知りたい方は読んでみてください、
使われているnpmパッケージとしては、Webサーバであるexpressに、テンプレートエンジンのexpress-handlebars、そしてMSALライブラリの@azure/msal-nodeと、設定ファイルを読み込むためのdotenvとなります。
特にMSAL(Microsoft Authentication Library)のJavaScript実装である@azure/msal-nodeの使い方がポイントとなります。素のAzure ADを使う時にもこのライブラリを使用しますので、B2C用に何かを別途インストールする必要はありません。
認証を行うには、@azure/msal-nodeのクラス、ConfidentialClientApplicationをインスタンス化します。
const confidentialClientApplication = new msal.ConfidentialClientApplication(confidentialClientConfig);
このオブジェクトを使って、Azure AD B2Cに対して各種の操作を行う関数、getAuthCodeが定義されています。
サインインの際には引数のauthorityに対して、Azure AD B2Cで定義したサインインフローのURLを渡します。同様に、パスワード変更の際にはパスワード変更フロー、プロフィール変更の際にはプロフィール変更のフローのURLを渡します。
const getAuthCode = (authority, scopes, state, res) => { // prepare the request console.log("Fetching Authorization code") authCodeRequest.authority = authority; authCodeRequest.scopes = scopes; authCodeRequest.state = state; //Each time you fetch Authorization code, update the relevant authority in the tokenRequest configuration tokenRequest.authority = authority; // request an authorization code to exchange for a token return confidentialClientApplication.getAuthCodeUrl(authCodeRequest) .then((response) => { console.log("\nAuthCodeURL: \n" + response); //redirect to the auth code URL/send code to res.redirect(response); }) .catch((error) => { res.status(500).send(error); }); }
各フローでの処理の結果は、confidentialClientConfigに入っているリダイレクトURL(/redirect)に返ってきます。
redirectでは、Azure AD B2Cからのレスポンスの”state”を見ることによって、どのフローが処理されてきたのかを判別することができるので、それぞれの処理を記載していきます。
app.get('/redirect',(req, res)=>{ //determine the reason why the request was sent by checking the state if (req.query.state === APP_STATES.LOGIN) { //prepare the request for authentication tokenRequest.code = req.query.code; confidentialClientApplication.acquireTokenByCode(tokenRequest).then((response)=>{ req.session.sessionParams = {user: response.account, idToken: response.idToken}; console.log("\nAuthToken: \n" + JSON.stringify(response)); res.render('signin',{showSignInButton: false, givenName: response.account.idTokenClaims.given_name}); }).catch((error)=>{ console.log("\nErrorAtLogin: \n" + error); }); }else if (req.query.state === APP_STATES.PASSWORD_RESET) { // 略 }else if (req.query.state === APP_STATES.EDIT_PROFILE){ // 略 });