Microsoft Authentication Library (MSAL)

JavaScriptTypeScriptMicrosoftAzure ADOAuthOpenID Connect認証ライブラリシングルサインオン

認証ライブラリ

Microsoft Authentication Library (MSAL)

概要

Microsoft Authentication Library (MSAL) は、Microsoft Azure Active Directory および Microsoft Personal Account を使用してユーザーを認証し、セキュリティ トークンを取得するためのライブラリです。

詳細

MSAL(Microsoft Authentication Library)は、Microsoft identity platform(旧 Azure AD v2.0)の一部として開発された認証ライブラリです。OAuth 2.0とOpenID Connectの業界標準プロトコルを使用して、保護されたAPIを呼び出すためのセキュリティトークンを取得できます。

複数のプラットフォームと言語をサポートしており、JavaScript/TypeScript、.NET、Java、Python、Android、iOS/macOS向けの各バージョンが提供されています。ブラウザベースのアプリケーション(SPA)、Node.jsアプリケーション、デスクトップアプリケーション、モバイルアプリケーションまで幅広いアプリケーションタイプに対応しています。

JavaScript版では、MSAL Browser(ブラウザ用)、MSAL Node(Node.js用)、MSAL Angular(Angular統合)、MSAL React(React統合)として提供されています。認証フローとしては、認証コードフロー(PKCE付き)、暗黙的フロー、デバイスコードフロー、クライアント認証情報フロー、On-Behalf-Ofフローなど、様々な認証シナリオをサポートしています。

トークン管理機能では、アクセストークンとリフレッシュトークンの自動管理、サイレントトークン更新、トークンキャッシング機能を提供しています。Azure AD B2C、マルチテナント認証、条件付きアクセス、MFA(多要素認証)にも対応しており、エンタープライズ環境での幅広いセキュリティ要件を満たします。

AngularやReactなどの人気フレームワークとの統合ライブラリも提供されており、ルートガード、HTTPインターセプター、認証状態管理などの機能を簡単に組み込むことができます。Microsoft Graph APIやその他のAzureサービスとのシームレスな統合が可能で、enterprise-gradeのアプリケーション開発に最適化されています。

メリット・デメリット

メリット

  • Microsoft エコシステム統合: Azure AD、Microsoft 365、Dynamics 365との完全統合
  • 複数プラットフォーム対応: Web、モバイル、デスクトップアプリに対応
  • 豊富な認証フロー: 様々な認証シナリオとセキュリティ要件に対応
  • 自動トークン管理: アクセストークンとリフレッシュトークンの自動更新
  • エンタープライズ機能: 条件付きアクセス、MFA、B2Cサポート
  • フレームワーク統合: Angular、Reactとの専用統合ライブラリ
  • セキュリティ標準準拠: OAuth 2.0、OpenID Connect準拠
  • 包括的ドキュメント: Microsoft公式の詳細なドキュメントとサンプル

デメリット

  • Microsoft依存: Microsoft以外のIDプロバイダーは限定的サポート
  • 学習コストが高い: Azure ADとMicrosoft identity platformの理解が必要
  • 設定の複雑さ: エンタープライズ環境での初期設定が複雑
  • バンドルサイズ: 他の軽量認証ライブラリと比較してサイズが大きい
  • ベンダーロックイン: Microsoft エコシステムへの依存度が高い
  • 過剰機能: シンプルな認証ニーズには機能が多すぎる場合がある
  • アップデート頻度: Microsoft のポリシー変更に伴うアップデートが必要

主要リンク

書き方の例

基本設定(ブラウザ用)

import { PublicClientApplication } from "@azure/msal-browser";

const msalConfig = {
    auth: {
        clientId: "your_client_id_here",
        authority: "https://login.microsoftonline.com/your_tenant_id",
        redirectUri: "http://localhost:3000",
        postLogoutRedirectUri: "http://localhost:3000",
    },
    cache: {
        cacheLocation: "localStorage",
        storeAuthStateInCookie: false,
    },
    system: {
        loggerOptions: {
            loggerCallback: (level, message, containsPii) => {
                console.log(message);
            },
            logLevel: "Info",
            piiLoggingEnabled: false,
        },
    },
};

const msalInstance = new PublicClientApplication(msalConfig);

ポップアップ認証フロー

const loginRequest = {
    scopes: ["openid", "profile", "User.Read"],
    prompt: "select_account"
};

async function signInPopup() {
    try {
        const loginResponse = await msalInstance.loginPopup(loginRequest);
        console.log("Login successful:", loginResponse);
        
        const currentAccounts = msalInstance.getAllAccounts();
        if (currentAccounts.length > 0) {
            const account = currentAccounts[0];
            console.log("Signed in user:", account.username);
        }
    } catch (error) {
        console.error("Login failed:", error);
    }
}

// トークン取得(サイレント → ポップアップフォールバック)
async function getTokenPopup(account) {
    const tokenRequest = {
        scopes: ["User.Read"],
        account: account
    };

    try {
        // まずサイレント取得を試行
        const response = await msalInstance.acquireTokenSilent(tokenRequest);
        return response.accessToken;
    } catch (error) {
        // サイレント失敗時はポップアップで取得
        console.log("Silent token acquisition failed, falling back to popup");
        const response = await msalInstance.acquireTokenPopup(tokenRequest);
        return response.accessToken;
    }
}

リダイレクト認証フロー

// リダイレクトハンドラーの設定
msalInstance.handleRedirectPromise().then((tokenResponse) => {
    if (tokenResponse !== null) {
        // ログイン成功
        const account = tokenResponse.account;
        const accessToken = tokenResponse.accessToken;
        console.log("Redirect login successful:", account.username);
    } else {
        // 既存セッションの確認
        const currentAccounts = msalInstance.getAllAccounts();
        if (currentAccounts.length > 0) {
            const account = currentAccounts[0];
            console.log("User already signed in:", account.username);
        }
    }
}).catch((error) => {
    console.error("Error handling redirect:", error);
});

function signInRedirect() {
    const loginRequest = {
        scopes: ["openid", "profile", "User.Read"],
        redirectUri: "http://localhost:3000/redirect"
    };
    
    msalInstance.loginRedirect(loginRequest);
}

async function getTokenRedirect(account) {
    const tokenRequest = {
        scopes: ["User.Read"],
        account: account
    };

    try {
        return await msalInstance.acquireTokenSilent(tokenRequest);
    } catch (error) {
        console.log("Silent token acquisition failed, using redirect");
        return msalInstance.acquireTokenRedirect(tokenRequest);
    }
}

Node.js でのサーバーサイド実装

const msal = require('@azure/msal-node');

const config = {
    auth: {
        clientId: "your_client_id_here",
        authority: "https://login.microsoftonline.com/your_tenant_id",
        clientSecret: "your_client_secret_here",
    },
    system: {
        loggerOptions: {
            loggerCallback(loglevel, message, containsPii) {
                console.log(message);
            },
            piiLoggingEnabled: false,
            logLevel: msal.LogLevel.Verbose,
        }
    }
};

const cca = new msal.ConfidentialClientApplication(config);

// 認証コードフロー(Step 1: 認証URL生成)
async function getAuthUrl() {
    const authCodeUrlParameters = {
        scopes: ["user.read"],
        redirectUri: "http://localhost:3000/redirect",
    };

    try {
        const response = await cca.getAuthCodeUrl(authCodeUrlParameters);
        return response;
    } catch (error) {
        console.error("Error generating auth URL:", error);
    }
}

// 認証コードフロー(Step 2: トークン取得)
async function acquireTokenByCode(authCode) {
    const tokenRequest = {
        code: authCode,
        redirectUri: "http://localhost:3000/redirect",
        scopes: ["user.read"],
    };

    try {
        const response = await cca.acquireTokenByCode(tokenRequest);
        return response;
    } catch (error) {
        console.error("Error acquiring token:", error);
    }
}

Angular統合

// app.module.ts
import { NgModule } from '@angular/core';
import { 
    MsalModule, 
    MsalInterceptor, 
    MSAL_INSTANCE,
    MSAL_GUARD_CONFIG,
    MSAL_INTERCEPTOR_CONFIG
} from '@azure/msal-angular';
import { PublicClientApplication } from '@azure/msal-browser';

export function MSALInstanceFactory() {
    return new PublicClientApplication({
        auth: {
            clientId: 'your_client_id',
            authority: 'https://login.microsoftonline.com/your_tenant_id',
            redirectUri: '/',
        },
        cache: {
            cacheLocation: 'localStorage',
        },
    });
}

@NgModule({
    imports: [
        MsalModule
    ],
    providers: [
        {
            provide: MSAL_INSTANCE,
            useFactory: MSALInstanceFactory,
        },
        {
            provide: MSAL_GUARD_CONFIG,
            useValue: {
                interactionType: 'redirect',
                authRequest: {
                    scopes: ['user.read'],
                },
            },
        },
        {
            provide: MSAL_INTERCEPTOR_CONFIG,
            useValue: {
                interactionType: 'redirect',
                protectedResourceMap: new Map([
                    ['https://graph.microsoft.com/v1.0/me', ['user.read']],
                ]),
            },
        },
    ],
})
export class AppModule { }
// auth.service.ts
import { Injectable } from '@angular/core';
import { MsalService } from '@azure/msal-angular';

@Injectable({
    providedIn: 'root',
})
export class AuthService {
    constructor(private msalService: MsalService) {}

    login() {
        this.msalService.loginPopup({
            scopes: ['user.read'],
        });
    }

    logout() {
        this.msalService.logout();
    }

    isLoggedIn(): boolean {
        return this.msalService.instance.getAllAccounts().length > 0;
    }

    async getAccessToken(): Promise<string> {
        const accounts = this.msalService.instance.getAllAccounts();
        
        if (accounts.length > 0) {
            const request = {
                scopes: ['user.read'],
                account: accounts[0],
            };

            try {
                const response = await this.msalService.acquireTokenSilent(request).toPromise();
                return response.accessToken;
            } catch (error) {
                const response = await this.msalService.acquireTokenPopup(request).toPromise();
                return response.accessToken;
            }
        }
        
        throw new Error('No account found');
    }
}

Microsoft Graph API呼び出し

async function callMicrosoftGraph(accessToken) {
    const headers = new Headers();
    headers.append("Authorization", `Bearer ${accessToken}`);

    const options = {
        method: "GET",
        headers: headers
    };

    try {
        const response = await fetch("https://graph.microsoft.com/v1.0/me", options);
        const data = await response.json();
        return data;
    } catch (error) {
        console.error("Error calling Microsoft Graph:", error);
    }
}

// 使用例
async function getUserProfile() {
    const accounts = msalInstance.getAllAccounts();
    
    if (accounts.length > 0) {
        const tokenResponse = await getTokenPopup(accounts[0]);
        const userProfile = await callMicrosoftGraph(tokenResponse);
        console.log("User profile:", userProfile);
    }
}

デバイスコードフロー

// デバイスコードフロー(Node.js)
const pca = new msal.PublicClientApplication(config);

const deviceCodeRequest = {
    deviceCodeCallback: (response) => {
        console.log("デバイスコード:", response.userCode);
        console.log("認証URL:", response.verificationUri);
        console.log("メッセージ:", response.message);
    },
    scopes: ["user.read"],
};

try {
    const response = await pca.acquireTokenByDeviceCode(deviceCodeRequest);
    console.log("トークン取得成功:", response.accessToken);
} catch (error) {
    console.error("デバイスコードフロー失敗:", error);
}

サインアウト処理

async function signOut() {
    const logoutRequest = {
        account: msalInstance.getActiveAccount(),
        postLogoutRedirectUri: "http://localhost:3000",
    };

    try {
        await msalInstance.logoutPopup(logoutRequest);
        console.log("Logout successful");
    } catch (error) {
        console.error("Logout failed:", error);
    }
}

// またはリダイレクトでのサインアウト
function signOutRedirect() {
    const logoutRequest = {
        account: msalInstance.getActiveAccount(),
        postLogoutRedirectUri: "http://localhost:3000",
    };

    msalInstance.logoutRedirect(logoutRequest);
}