はじめに
証明書を利用してクライアント認証を行いたいケースに遭遇しました。
実装を進める中でいろいろとわかったことを備忘録としてまとめたいと思います。
環境
- Windows:
11 21H2
- PowerShell:
7.4.5
事前準備
STEP 1 : 自己証明書の作成
こちらの記事に従って、自己証明書を作成していきます。
.cert と .pfx の両方をエクスポートします。
# 必要なパラメータを設定
# ここでは、環境変数に設定した値を読み込んでいます。
$certname = $env:CERT_NAME # 証明書の名前
$certPass = ConvertTo-SecureString -String $env:CERT_PASSWORD -Force -AsPlainText # 証明書に設定するパスワード
$outputPath = $env:OUTPUT_PATH #証明書の出力フォルダパス
# 自己証明書の生成
$cert = New-SelfSignedCertificate -Subject "CN=$certname" -CertStoreLocation "Cert:\CurrentUser\My" -KeyExportPolicy Exportable -KeySpec Signature -KeyLength 2048 -KeyAlgorithm RSA -HashAlgorithm SHA256 -NotAfter (Get-Date).AddYears(1)
# 証明書を .cer 形式でエクスポート
Export-Certificate -Cert $cert -FilePath "$outputPath/$certname.cer"
# 証明書を .pfx 形式で秘密鍵を含んだ状態でエクスポート
# アプリケーションが別のコンピューターやクラウド (Azure Automation など) から実行される場合は、秘密鍵も必要
Export-PfxCertificate -Cert $cert -FilePath "$outputPath/$certname.pfx" -Password $certPass
実行結果
ターミナルには以下のように表示され、指定したパスに証明書がエクスポートされます。
Mode LastWriteTime Length Name
---- ------------- ------ ----
----- 2024/10/30 20:55 784 cert-auth-test.cer
----- 2024/10/30 20:55 2612 cert-auth-test.pfx
STEP 2 : Entra ID に証明書を登録する
Entra ID には、.cer
形式の証明書と登録します。
アプリケーション画面から STEP: 1 で作成した .cer
をアップロードしてください。
STEP 3 : アクセス許可の設定をする
実行したい API や Azure リソースに対するアクセス許可を設定します。
今回は、アプリケーションに対して User.ReadBasic.All
の権限を付与しました。
STEP 4 : JWT を生成する
証明書を利用してクライアント認証をする場合、JWTが必要になります。
JWT にどんなパラメータが必要かは、こちらのドキュメントに例が記載されています。
JWT については下記の記事がわかりやすかったです。(ありがとうございます!)
ドキュメントや ChatGPT の力も少し借りて、スクリプトを作成していましたが、ここは結構苦しみました。。
ヘッダーの拇印のところですが、ドキュメントには下記のように記載されています。
なので、Base64url エンコード SHA-256
拇印 を作成していましたが、何回やっても下記のエラーが出ていました。(抜粋)
"error_description": "AADSTS700027: The certificate with identifier used to sign the
| client assertion is not registered on application. [Reason - The key was not found., Thumbprint of key used by client:
で調べていると、下記の記事を見つけました。
これらには、SHA-256 ではなく、SHA-1 と書いてあります。。。
てことで、 Base64Url エンコード SHA-1
拇印を作成して試すと、うまくいきました。
最終的な JWT を生成するスクリプトは下記の通りです。
function Generate-JWTFromCert {
# 仮引数
param (
[Parameter(Mandatory=$true)]
[System.Security.Cryptography.X509Certificates.X509Certificate2]$Cert, # 証明書
[Parameter(Mandatory=$true)]
[string]$TenantId, # テナントID
[Parameter(Mandatory=$true)]
[string]$ClientId, # アプリケーション (クライアント) ID
[Parameter(Mandatory=$false)]
[int] $Span = 3600 # 有効期間(秒)
)
# Base64url エンコード SHA-1 拇印を生成
$sha1 = $Cert.GetCertHash([System.Security.Cryptography.HashAlgorithmName]::SHA1) # SHA-256 ではなく、SHA-1
$base64Thumbprint = [Convert]::ToBase64String($sha1)
$base64UrlThumbprint = $base64Thumbprint -replace '\+', '-' -replace '/', '_' -replace '=', ''
# ヘッダーの作成
$headers = @{
"alg" = "RS256"
"typ" = "JWT"
"x5t" = $base64Thumbprint
}
# JWT の有効期間を設定(エポック)
$nbf = [Math]::Floor((Get-Date -UFormat "%s"))
$exp = $nbf + $Span
# ペイロードの作成
$payload = @{
"aud" = "https://login.microsoftonline.com/$TenantId/oauth2/v2.0/token"
"iss" = $ClientId
"sub" = $ClientId
"jti" = [guid]::NewGuid().ToString()
"nbf" = $nbf
"exp" = $exp
}
# ヘッダーとペイロードをJson化
$headersJson = $headers | ConvertTo-Json
# Write-Output $headersJson
$payloadJson = $payload | ConvertTo-Json
# Write-Output $payloadJson
# ヘッダーとペイロードをBase64URLエンコード
$headersBase64 = [System.Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($headersJson)) -replace '\+', '-' -replace '/', '_' -replace '=', ''
$payloadBase64 = [System.Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($payloadJson)) -replace '\+', '-' -replace '/', '_' -replace '=', ''
# JWT の前半部分
$unsignedToken = "$headersBase64.$payloadBase64"
# 秘密鍵で署名生成
$privateKey = $Cert.PrivateKey
$signatureBytes = $privateKey.SignData([System.Text.Encoding]::UTF8.GetBytes($unsignedToken), [System.Security.Cryptography.HashAlgorithmName]::SHA256, [System.Security.Cryptography.RSASignaturePadding]::Pkcs1)
$signatureBase64 = [System.Convert]::ToBase64String($signatureBytes) -replace '\+', '-' -replace '/', '_' -replace '=', ''
# JWTの形成
$jwt = "$unsignedToken.$signatureBase64"
return $jwt
}
STEP 5 : アクセストークンを取得する
API 実行時に必要となる、アクセストークンを取得するためのスクリプトを作成します。
こちらの「2 番目のケース:証明書を使ったアクセス トークン要求」を参考にスクリプトを作成しました。
client_assertion
には、JWT をセットします。
function Get-AccessToken {
# 仮引数
param (
[Parameter(Mandatory=$true)]
[string]$TenantId, # テナントID
[Parameter(Mandatory=$true)]
[string]$ClientId, # アプリケーション (クライアント) ID
[Parameter(Mandatory=$true)]
[string]$Scope, # アプリケーションID URI
[Parameter(Mandatory=$true)]
[string]$Jwt # JSON Web トークン
)
# ヘッダー
$headers = @{
"Content-Type" = "application/x-www-form-urlencoded"
}
# リクエストBody
$body = @{
scope = "$Scope/.default"
client_id = $ClientId
client_assertion_type = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"
client_assertion = $Jwt
grant_type = "client_credentials"
}
$uri = "https://login.microsoftonline.com:443//$TenantId/oauth2/v2.0/token"
# リクエスト送信
$response = Invoke-WebRequest -Method "POST" -Headers $headers -Uri $uri -Body $body
# アクセストークン
$accessToken = ($response.Content | ConvertFrom-Json).access_token
return $accessToken
}
以上で事前準備は完了です。
実際に試してみる
下記スクリプトを実行し、アクセストークンが取得、API の実行ができるか確認します。
# STEP: 3, STEP: 4 の関数定義は割愛
# 必要なパラメータ設定
# ここでは、環境変数に設定した値を読み込んでいます。
$tenantId = $env:TENANT_ID # テナント ID
$clientId = $env:CLIENT_ID # アプリケーション (クライアント) ID
$scope = $env:SCOPE # 今回は、https://graph.microsoft.com/
$pfxPath = $env:PFX_PATH # 証明書のパス(.pfx)
$pfxPassword = ConvertTo-SecureString -String $env:azure -Force -AsPlainText # 証明書のパスワード
# 証明書(.pfx)を読み込む
Write-Host "Load Certification..."
$cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2($pfxPath, $pfxPassword)
# JWT 生成
Write-Host "Generate JWT..."
$jwt = Generate-JWTFromCert -Cert $cert -TenantId $tenantId -ClientId $clientId
Write-Output "JTW: $jwt`n"
# アクセストークン取得
Write-Host "Get access token..."
$accessToken = Get-AccessToken -TenantId $tenantId -ClientId $clientId -Scope $scope -Jwt $jwt
Write-Host "Access token: $accessToken`n"
# API 実行 (ユーザーリストの取得)
Write-Host "Execute API..."
$headers = @{"Authorization" = "Bearer $accessToken"}
$response = Invoke-RestMethod -Method "GET" -Headers $headers -Uri "https://graph.microsoft.com/v1.0/users"
Write-Host $response
実行結果
アクセストークン取得も問題なく、API も実行できることが確認できます。
Load Certification...
Generate JWT...
JTW: ew0KICAieDV0IjogIjJVdnA5L1...ddvcwhc3CCc5h3wQ2b3N6GChhzYmmCN2D-LkZw
Get access token...
Access token: eyJ0eXAiOiJKV1QiL...V_fpg6HAV_V5ZssX8YFGzpqtXIEk0fPpq8_nsg
Execute API...
@{@odata.context=https://graph.microsoft.com/v1.0/$metadata#users; value=System.Object[]}
さいごに
今回は Entra ID からクライアント証明書を利用して、アクセストークンを取得する方法をまとめました。
自分でもいい勉強になったかなぁと思います。
公式ドキュメントが間違っているとは思いませんでした…
とまあそんな感じで、誰かの役に立てば幸いです。
参考