💻

Azure AD (Microsoft Entra ID) アプリケーションのクライアント資格情報について

2023/08/16に公開

はじめに

証明書まわりでわからなくなったのでメモ。

機密クライアント アプリケーションの認証方法

Azure AD (Microsoft Entra ID) アプリケーションには大きく 2 種類あります。

  • 機密クライアント アプリケーション: サーバー上で実行されるアプリケーション。Web アプリ、Web API アプリ、デーモン アプリなど。
  • パブリック クライアント アプリケーション: クライアント上で上で実行されるアプリケーション。デスクトップ アプリ、モバイル アプリ、SPA アプリなど。

https://learn.microsoft.com/ja-jp/azure/active-directory/develop/msal-client-applications?WT.mc_id=M365-MVP-5002941

機密クライアント アプリケーション で認証フローを実施する場合はクライアント資格情報を提供する必要があります。資格情報としては クライアント シークレット または クライアント証明書 を使用することができます。クライアント シークレット のほうが認証フローが簡単なためよく使われがちですが、リクエストに クライアント シークレット を含めて送る必要があるため、セキュリティ上の安全性は低くなります。一方、クライアント証明書 の場合は、証明書そのものではなく、証明書を使って署名された クライアント アサーション を送るため、セキュリティ上の安全性が高いです。複数のアプリケーションを使用する場合も、同じ証明書を使うことができるため、管理上のメリットが発生する場合があります。いずれの場合も有効期限には注意する必要があります。

項目 難易度 安全性 アプリ間の共有
クライアント シークレット できない
クライアント証明書 できる

SharePoint のアプリ専用のアクセス許可は クライアント シークレット をサポートしていないため、クライアント証明書 を使用しなければならない場合もあります。

https://learn.microsoft.com/ja-jp/sharepoint/dev/solution-guidance/security-apponly-azuread?WT.mc_id=M365-MVP-5002941

証明書の種類

証明書にもいろいろな種類があり非常にややこしいです。ざっくりと説明すると以下のような感じになります。拡張子は慣習なので .cer.der だったり .key.pem だったりするようです。あと証明書署名リクエストを示す .csr もありますが認証フローでは使わないので今回は除外します。それぞれの形式は OpenSSL を使って変換することができます。

OS 拡張子 鍵の種類 データ形式
Windows .cer 公開鍵 バイナリ形式 (DER)
.pfx 秘密鍵 バイナリ形式 (PKCS #12)
Linux .crt 公開鍵 テキスト形式 (PEM)
.key 秘密鍵 テキスト形式 (PEM)

認証フローとしてはどちらの形式を使ってもできるのですが、MSAL (Microsoft Authentication Library) を使用する場合、実装によってはどちらかの形式しか対応していないことがあります。たとえば MSAL.NET の場合は X509Certificate2 クラスを使って証明書をインポートできますが、.NET 5 以前のバージョンでは PEM 形式には対応していません。

証明書については、一般的な SSL 通信では認証局 (CA) から発行された証明書を使用する必要がありますが、Azure AD (Microsoft Entra ID) アプリケーションの場合は自己署名証明書でも問題ありません。ただしマイクロソフトとしてはセキュリティを強化するため証明機関から証明書を購入することを推奨しています。

自己署名証明書は、既定では信頼されないため、管理が困難になる可能性があります。 また、強力でない可能性のある旧式のハッシュや暗号スイートが使用される場合もあります。 セキュリティを強化するには、よく知られている証明機関によって署名された証明書を購入してください。

https://learn.microsoft.com/ja-jp/azure/active-directory/develop/howto-create-self-signed-certificate?WT.mc_id=M365-MVP-5002941

以下は自己署名証明書を使用する前提で進めていきます。

実装方法 (Windows の場合)

証明書の作成

Windows の場合は Windows PowerShell を使って自己署名証明書を作成することができます。作成した証明書は証明書ストアに格納されます。

$cert = New-SelfSignedCertificate -DnsName "{{domain-name}}" -NotBefore "{{start-date}}" -NotAfter "{{end-date}}" -CertStoreLocation "cert:\CurrentUser\My"

作成した証明書の公開鍵 (.cer ファイル) をエクスポートします。

Export-Certificate -Cert $cert -FilePath "{{file-name}}.cer"

作成した証明書の秘密鍵 (.pfx ファイル) をエクスポートします。秘密鍵のエクスポートにはパスワードが必要な点について注意してください。これは秘密鍵を取り出すときに使用されるもので、実際の秘密鍵の内容とは関係ありません。

Export-PfxCertificate -Cert $cert -FilePath "{{file-name}}.pfx" -Password (ConvertTo-SecureString -String "{{password}}" -AsPlainText -Force)

証明書のアップロード

作成した公開鍵 (.cer ファイル) を Azure AD (Microsoft Entra ID) アプリケーションにアップロードします。

コードの記述

クライアント資格情報フロー (Client Credentials Flow) を使用して実際にアクセスできることを確認します。

はじめに MSAL.NET のパッケージを追加します。

dotnet add package Microsoft.Identity.Client

コードは以下のようになります。

using Microsoft.Identity.Client;
using System.Security.Cryptography.X509Certificates;

var tenantId = "{{tenant-id}}";
var clientId = "{{client-id}}";
var pfxPath = "{{file-name}}.pfx";
var pfxPassword = "{{password}}";

var msalApp = ConfidentialClientApplicationBuilder
    .Create(clientId)
    .WithAuthority($"https://login.microsoftonline.com/{tenantId}")
    .WithCertificate(new X509Certificate2(pfxPath, pfxPassword))
    .Build();
var msalResult = await msalApp
    .AcquireTokenForClient(new[] { "https://graph.microsoft.com/.default" })
    .ExecuteAsync();

Console.WriteLine(msalResult.AccessToken);

実装方法 (Linux の場合)

証明書の作成

Linux の場合は OpenSSL を使って自己署名証明書を作成することができます。

秘密鍵 (.key ファイル) を作成します。

openssl genrsa -out "{{file-name}}.key" 2048

公開鍵 (.crt ファイル) を作成します。

openssl req -x509 -nodes -new -keyout "{{file-name}}.key" -out "{{file-name}}.crt" -days 365 -subj "/CN={{domain-name}}"

証明書のアップロード

作成した公開鍵 (.crt ファイル) を Azure AD (Microsoft Entra ID) アプリケーションにアップロードします。

コードの記述

クライアント資格情報フロー (Client Credentials Flow) を使用して実際にアクセスできることを確認します。

はじめに MSAL.NET のパッケージを追加します。

dotnet add package Microsoft.Identity.Client

コードは以下のようになります。

using Microsoft.Identity.Client;
using System.Security.Cryptography.X509Certificates;

var tenantId = "{{tenant-id}}";
var clientId = "{{client-id}}";
var crtPath = "{{file-name}}.crt";
var keyPath = "{{file-name}}.key";

var msalApp = ConfidentialClientApplicationBuilder
    .Create(clientId)
    .WithAuthority($"https://login.microsoftonline.com/{tenantId}")
    .WithCertificate(X509Certificate2.CreateFromPemFile(crtPath, keyPath))
    .Build();
var msalResult = await msalApp
    .AcquireTokenForClient(new[] { "https://graph.microsoft.com/.default" })
    .ExecuteAsync();

Console.WriteLine(msalResult.AccessToken);

実装方法 (低レイヤー)

上記の CreateFromPemFile メソッドは .NET 5.0 以上をサポートしているため、.NET Standard の場合は使用することができません。その場合は クライアント証明書 ではなく クライアント アサーション を作成する必要があります。

クライアント アサーション の作成方法については以下にドキュメントがあります。

https://learn.microsoft.com/ja-jp/azure/active-directory/develop/msal-net-client-assertions?WT.mc_id=M365-MVP-5002941

実際に .key ファイルを使って認証するコードは以下のようになります。拇印は Azure AD (Microsoft Entra ID) アプリケーションに公開鍵 (.crt ファイル) を登録したときに取得することができます。

using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using Microsoft.Identity.Client;

var tenantId = "{{tenant-id}}";
var clientId = "{{client-id}}";
var keyPath = "{{file-name}}.key";
var thumbprint = "{{thumbprint}}";

// 秘密鍵のインポート
var keyFile = File.ReadAllText(keyPath)
    .Replace("-----BEGIN PRIVATE KEY-----", "")
    .Replace("-----END PRIVATE KEY-----", "")
    .Replace(Environment.NewLine, "");
var rsa = RSA.Create();
rsa.ImportPkcs8PrivateKey(Convert.FromBase64String(keyFile), out var _);

// ヘッダーの作成
var header = new Dictionary<string, string>()
{
    { "alg", "RS256" },
    { "typ", "JWT" },
    { "x5t",  Base64UrlEncode(Convert.FromHexString(thumbprint)) },
};

// クレームの作成
var claims = new Dictionary<string, object>()
{
    { "aud", $"https://login.microsoftonline.com/{tenantId}/oauth2/v2.0/token" },
    { "exp", DateTimeOffset.UtcNow.AddMinutes(10).ToUnixTimeSeconds() },
    { "iss", clientId },
    { "jti", Guid.NewGuid().ToString() },
    { "nbf", DateTimeOffset.UtcNow.ToUnixTimeSeconds() },
    { "sub", clientId }
};

// アサーションの作成
var headerBytes = JsonSerializer.SerializeToUtf8Bytes(header);
var claimsBytes = JsonSerializer.SerializeToUtf8Bytes(claims);
var token = Base64UrlEncode(headerBytes) + "." + Base64UrlEncode(claimsBytes);
var signature = Base64UrlEncode(rsa.SignData(Encoding.UTF8.GetBytes(token), HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1));
var assertion = string.Concat(token, ".", signature);

var msalApp = ConfidentialClientApplicationBuilder
    .Create(clientId)
    .WithAuthority($"https://login.microsoftonline.com/{tenantId}")
    .WithClientAssertion(assertion)
    .Build();
var msalResult = await msalApp
    .AcquireTokenForClient(new[] { "https://graph.microsoft.com/.default" })
    .ExecuteAsync();

Console.WriteLine(msalResult.AccessToken);

static string Base64UrlEncode(byte[] bytes)
{
    const char Base64PadCharacter = '=';
    const char Base64Character62 = '+';
    const char Base64Character63 = '/';
    const char Base64UrlCharacter62 = '-';
    const char Base64UrlCharacter63 = '_';
    return Convert.ToBase64String(bytes)
        .Split(Base64PadCharacter)[0]
        .Replace(Base64Character62, Base64UrlCharacter62)
        .Replace(Base64Character63, Base64UrlCharacter63);
}

おわりに

.NET Framework の場合は ImportPkcs8PrivateKey がないのでさらにしんどいことになるのですが、どうしてもやりたければ BouncyCastle を使うことになるようです。基本的には Windows であればおとなしく .pfx ファイルを使うことをおすすめします。

Discussion