MSXFAQ MeetNow aktiv: Komm doch einfach dazu.

Token mit Zertifikat anfordern

Mein PowerShell-Script sollte sich von Entra ID ein OAUTH-Token abholen und dabei eine Anmeldung mit Zertifikat nutzen. Mit Modulen wie Get-MSALToken, Connect-MGGraph, Connect-ExchangeOnline gibt es die entsprechende Option sich mit einer AppID und ClientSecret oder auch ClientCertificate anzumelden. Ich wollte aber mal sehen, wie das manuell geht

Get-MSALToken und "Graph"

Wann immer wie mit Token arbeiten, sollten sie erst einmal nach entsprechenden Bibliotheken suchen. Wenn Sie nur eine AppID, eine TenantID und ein Client Secret haben, dann können Sie mit einem Invoke-Webrequest direkt Token als App anfordern.

param (
   $LoginUrl       = "https://login.Microsoftonline.com",
   $TenantID       = "604d9047-44e5-443a-ad8f-98abe5748b0a",   #msxfaqdev
   $ClientID       = "05b10bad-2cac-4268-93cd-cbb5e7dbc46d",   #msxfaqdev
   #$Scopes         = "api://ef59f0c2-12cc-43d7-aeee-d33eb3f03a8f/.default",
   $Scopes         = "https://graph.microsoft.com/.default",
   $ClientSicret   = "geheim"
)

$authresponse=Invoke-RestMethod `
           -body "client_id=$($ClientID)
                  &scope=$scopehttps%3A%2F%2Fgraph.Microsoft.com%2F.default
                  &client_secret=$($ClientSecret)
                  &grant_type=client_credentials
                  &api-version=1.0" `
           -uri "$($LoginUrl)/$($tenantID)/oauth2/v2.0/token"
 
$accesstoken= $authresponse.access_token

Natürlich haben Sie dann erst mal nur den String, den Sie so auch an die API übergeben können. Allerdings müssen Sie sich schon selbst um eine Decodierung kümmern, um das Token rechtzeitig neu anzufordern. Das funktioniert mit einem ClientSecret, also einem Kennwort, problemlos.

Sobald sie aber mit einem Client Zertifikat arbeiten wollen, müssen Sie nach meinem Verständnis einen JSON-Request erzeugen und mit dem Zertifikat digital signieren. Das zeige ich im folgenden Abschnitt. Ich weise aber auch darauf hin, dass es viel einfacher mit dem MSAL.PS-Modul möglich ist. Hier wird einfach nur der Thumbprint mit angegeben und das Modul kümmert sich um den Rest

Get-MSALToken `
   -TenantId "604d9047-44e5-443a-ad8f-98abe5748b0a" `
   -ClientId "05b10bad-2cac-4268-93cd-cbb5e7dbc46d" `
   -ClientCertificate $certtumbprint

Hinweis:
Wenn Sie diese Codes in einem Skript mit dem Taskplaner verwenden, dann muss das Dienstkonto oder "SYSTEM" die Berechtigungen auf den privaten Schlüssel bekommen. Das können Sie der Zertifikatsverwaltung machen oder Sie starten das Skript interaktiv als Administrator oder mit "High Priviledges".

Zertifikat ist nicht Kennwort

Wenn sich eine Applikation mit einem Zertifikat anmeldet, dann erstellen Sie in der Regel in der Umgebung ein "Self Signed Cert", welches möglichst nicht exportierbar ist und gerne auch mit der VM oder einen TPM-Chip verbunden ist. Der öffentliche Schlüssel wird als CER-Datei dann im Azure-Portal zur Applikation "hochgeladen.

Hinweis:
Gerade die KI liefert hier sehr oft Code-Beispiele, die einfach nur die gleiche CER-Datei in den Request als BASE64-codiertes "Kennwort" übertragen. Das kann natürlich nicht funktionieren

Bei einer Anmeldung mittels Zertifikat wird, vereinfach ausgedrückt, über die Daten der Anforderung über TenantID, ClientID, Scope etc. ein Hashwert erstellt und dieser mit dem privaten Schlüssel verschlüsselt. Entra ID nutzt dann den öffentlichen Schlüssel aus der mitgelieferten ClientID des Tenants, um diese verschlüsselte Information wieder lesbar zu machen und mit dem Hashwert zu vergleichen. Nur der Inhaber des Zertifikats mit dem privaten Schlüssel kann diese Signatur korrekt aufbringen.

Das bedeutet natürlich auch, dass ein selbst geschriebener PowerShell-Code sich auch um die Signierung kümmern muss. Das Skript ist absichtlich "kurz" gehalten und hat keine Fehlerbehandlung, damit es kurz und lesbar bleibt. In einem produktiven Betrieb sollten Sie schon Fehlerabfragen und Diagnose-Funktionen ergänzen.

Die ganzen "Geheimnisse" sind nicht kritisch. Die TenantID zu einem Tenant ist eh öffentlich und die ClientID ist quasi nur der Username. Selbst der Thumbprint ist nur ein Verweis auf das lokale Zertifikat. Ohne den privaten Schlüssel, der hier im Code nicht sichtbar ist, können Sie diese Rechte nicht ausnutzen. Insofern sind Zertifikat sehr gut zur Authentifizierung geeignet, weil im Code damit keine "Kennworte" landen.

Wenn Sie noch kein passendes Zertifikat haben, dann können Sie dies sehr einfach wie folgt erstellen:

# Erstellen eines SelfSigned Cert mit 99 Jahren 
$selfcert= New-SelfSignedCertificate `
   -DNSName "MSXFAQ.AppmitCert" `
   -CertStoreLocation Cert:\CurrentUser\My `
   -KeyExportPolicy NonExportable `
   -NotAfter (Get-Date).AddYears(99)

# Export des öffentlichen Teils als CER-Datei
Export-Certificate `
   -Cert $selfcert `
   -Type CER `
   -FilePath .\MSXFAQ.AppmitCert.cer

Die CER-Datei laden Sie als "Client Certificate" in die Entra ID Application und den Thumbprint brauchen Sie, genau wie die TenantID und AppID im folgenden Skript:

Param (
   [string]$TenantID       = "604d9047-44e5-443a-ad8f-98abe5748b0a",
   [string]$ClientID       = "a1f5b977-41f0-44d0-acad-48f43a8410b6",
   [string]$Scopes         = "api://fac5439f0c2-12cc-ef31-affe-d335427a8f/.default",
   [string]$CertThumbprint = "9FAAF5C5AD2A5661CDE7BDEA46FDD36BB92FF4E3"
)

Write-Host "Das Zertifikat laden und aktuelle Zeit bestimmen"
$Cert = Get-Item "Cert:\CurrentUser\My\$CertThumbprint"
$now = [DateTimeOffset]::UtcNow.ToUnixTimeSeconds()

Write-Host "JSON-Struktur aufbauen"
$header = @{
    alg = "RS256"
    typ = "JWT"
    x5t = [System.Convert]::ToBase64String($cert.GetCertHash())
}
$payload = @{
    iss = $clientId
    sub = $clientId
    aud = "https://login.microsoftonline.com/$TenantID/oauth2/token"
    jti = [Guid]::NewGuid().ToString()
    nbf = $now
    exp = $now + 600
}

Write-Host "Header und Payload in komprimiertes JSON umsetzten"
$headerJson  = ($header | ConvertTo-Json -Compress)
$payloadJson = ($payload | ConvertTo-Json -Compress)

Write-Host "Header und Payload BASE64 codieren und zusammensetzen"
$jwtUnsigned = "{0}.{1}" -f `
    [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($headerJson)).TrimEnd("="),
    [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($payloadJson)).TrimEnd("=")

Write-Host "Signatur über den BASE64-String erstellen"
$signature = $cert.PrivateKey.SignData(
    [Text.Encoding]::UTF8.GetBytes($jwtUnsigned),
    [Security.Cryptography.HashAlgorithmName]::SHA256,
    [Security.Cryptography.RSASignaturePadding]::Pkcs1
)

Write-Host "Signatur anhängen"
$jwtSigned = "$jwtUnsigned." + [Convert]::ToBase64String($signature).TrimEnd("=")

Write-Host "Payload für den Request erstellen"
$body = @{
   grant_type = "client_credentials"
   client_id = $clientId
   scope = $Scopes
   client_assertion_type = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"
   client_assertion = $jwtSigned
}

Write-Host "Request an Entra ID stellen"
$uri = "https://login.microsoftonline.com/$($TenantID)/oauth2/token?api-version=1.0"
Write-Host " OAUTH URL: $($uri)"
$res = Invoke-WebRequest `
          -Method Post  `
          -Uri $uri  `
          -Body $body

Write-Host "Ausgabe des Token"
$res

Die Variable "$Res" enthält die Ausgabe des HTLM-Request:

Interessant ist hier der "Content", der eine JSON-Struktur mit dem Bearer Token darstellt.

Ich habe daher den Aufruf auf "Invoke-RestMethod" umgestellt, so dass ich die JSON-Rückgabe gleich als PowerShell-Objekt habe:

Das Access-Token ist wieder die BASE64-codiert JSON-Struktur mit allen Informationen, welches sie einfach parsen können. Details finden Sie dazu auf Bearer Decoding und ConvertFrom-Bearertoken. Diese Token können Sie nun bei der weiteren Authentifizierung an den gewünschten Diensten nutzen.

Hinweis:
Der Inhalt von "expires_in" gibt die Gültigkeitsdauer basierend auf dem Abrufzeitpunkt an. Die Zahl wird nicht herunter gezählt o.ä. Ihre Software sollte daher das "exp"-Feld des decodierten Tokens nutzen oder sich selbst merken, wann das Token abgerufen wurde und damit ungültig wird. Es ist nicht effektiv, in sehr engen Abständen einfach auf Verdacht immer wieder ein neues Token abzufordern. Dann ist es vielleicht besser, später einen HTTP-Fehler abzufangen, wenn man ein abgelaufenes Token weiter sendet. Ideal wäre natürlich ein Hintergrundprozess, der sich eigenständig um die Verlängerung bzw. Erneuerung der verschiedenen Tokens (Identity, Access, Refresh) kümmert.

Weitere Links