【Entra ID】証明書を利用してアクセストークンを取得する

# はじめに

証明書を利用してクライアント認証を行いたいケースに遭遇しました。
実装を進める中でいろいろとわかったことを備忘録としてまとめたいと思います。

# 環境

- Windows: `11 21H2`
- PowerShell: `7.4.5`

# 事前準備

## STEP 1 : 自己証明書の作成

[こちらの記事](https://learn.microsoft.com/ja-jp/entra/identity-platform/howto-create-self-signed-certificate#create-and-export-your-public-certificate)に従って、自己証明書を作成していきます。
.cert と .pfx の両方をエクスポートします。

```powershell
# 必要なパラメータを設定
# ここでは、環境変数に設定した値を読み込んでいます。
$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
```

### 実行結果

ターミナルには以下のように表示され、指定したパスに証明書がエクスポートされます。

```powershell
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` をアップロードしてください。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2513223/6af97ab3-0475-4a37-b042-86b711c52387.png)


## STEP 3 : アクセス許可の設定をする

実行したい API や Azure リソースに対するアクセス許可を設定します。

今回は、アプリケーションに対して `User.ReadBasic.All` の権限を付与しました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2513223/71975195-c57e-6b2e-ec96-c041cd98ac52.png)



## STEP 4 : JWT を生成する

証明書を利用してクライアント認証をする場合、JWTが必要になります。
JWT にどんなパラメータが必要かは、[こちらのドキュメント](https://learn.microsoft.com/ja-jp/entra/identity-platform/certificate-credentials)に例が記載されています。

JWT については下記の記事がわかりやすかったです。（ありがとうございます！）

https://qiita.com/asagohan2301/items/cef8bcb969fef9064a5c#3-jwt%E3%81%AE%E4%BD%9C%E3%82%8A%E6%96%B9

ドキュメントや ChatGPT の力も少し借りて、スクリプトを作成していましたが、ここは結構苦しみました。。

ヘッダーの拇印のところですが、ドキュメントには下記のように記載されています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2513223/488601ea-6bc8-c0dd-a236-6ceb669548b8.png)


なので、Base64url エンコード `SHA-256` 拇印 を作成していましたが、何回やっても下記のエラーが出ていました。（抜粋）

```powershell
"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: 
     
```

で調べていると、下記の記事を見つけました。

https://learn.microsoft.com/en-us/answers/questions/1183354/issue-with-doing-certificate-based-authentication?page=1

https://stackoverflow.com/questions/50657463/how-to-obtain-value-of-x5t-using-certificate-credentials-for-application-authe

これらには、SHA-256 ではなく、SHA-1 と書いてあります。。。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2513223/9982203b-b63f-b7db-62c8-b04c6296e04f.png)


てことで、 Base64Url エンコード `SHA-1` 拇印を作成して試すと、うまくいきました。

最終的な JWT を生成するスクリプトは下記の通りです。

```powershell
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 番目のケース:証明書を使ったアクセス トークン要求**](https://learn.microsoft.com/ja-jp/entra/identity-platform/v2-oauth2-client-creds-grant-flow#second-case-access-token-request-with-a-certificate)」を参考にスクリプトを作成しました。

`client_assertion` には、JWT をセットします。

```powershell
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 の実行ができるか確認します。

```powershell
# 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 も実行できることが確認できます。

```powershell
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 からクライアント証明書を利用して、アクセストークンを取得する方法をまとめました。
自分でもいい勉強になったかなぁと思います。
公式ドキュメントが間違っているとは思いませんでした…
とまあそんな感じで、誰かの役に立てば幸いです。

# 参考
https://learn.microsoft.com/ja-jp/entra/identity-platform/howto-create-self-signed-certificate#create-and-export-your-public-certificate

https://learn.microsoft.com/ja-jp/entra/identity-platform/certificate-credentials

https://qiita.com/asagohan2301/items/cef8bcb969fef9064a5c#3-jwt%E3%81%AE%E4%BD%9C%E3%82%8A%E6%96%B9

https://learn.microsoft.com/en-us/answers/questions/1183354/issue-with-doing-certificate-based-authentication?page=1

https://stackoverflow.com/questions/50657463/how-to-obtain-value-of-x5t-using-certificate-credentials-for-application-authe

https://learn.microsoft.com/ja-jp/entra/identity-platform/v2-oauth2-client-creds-grant-flow#second-case-access-token-request-with-a-certificate

https://qiita.com/UndMon/items/9b001b21ca258c91acdb