Graph und Kennworte

Mit Microsoft Graph können Sie auch Kennwort für Anwender setzen und die aktiven Anmeldeoptionen auslesen. Allerdings kommt dazu ein anderer URL-Pfad und Anmeldeweg zum Einsatz als ich auf Graph und Benutzer beschrieben habe.

Auf der eigenen Seite Graph Token finden sie Details zum Thema AppRegistrierung, Berechtigungen und Token-Generierung. Ich habe hier dennoch den Ablauf komplett beschrieben.

Berechtigungen

Dass Zugriff per Graph mit einem Token erfolgen und Sie für eigene Skripte ein App definieren müssen, sollte ihnen schon klar sein. Was anderes ist es nur, wenn die App von einem Dienstleister kommt und er diese in seinem Tenant definiert und freigegeben hat.

In meinem Beispiel gehe ich auf https://portal.azure.com und führe folgende Schritte durch:

  • Anlegen einer generischen App
  • Anlegen eines App Secret
    Am einfachsten ist ein Kennwort, welches Azure für Sie bereitstellt.
  • Berechtigungen zuweisen
    Die App muss für Microsoft Graph die Berechtigungen bekommen

Die Daten wie AppID, AppSecret etc. muss ich mir natürlich für mein Skript merken.

Eine eigenständige "App" kann diese Funktion aktuell nicht ausführen. Es muss immer ein "Benutzer" mit entsprechenden Anmeldedaten und Berechtigungen sein.

Es geht hier nur um das Setzen eines neuen Kennworts. Die bestehenden Anmeldemethoden können Sie auch als App lesen

Delegate Ja, App nein

Als ich diese Seite im Juni 2021 aktualisiert habe, konnten Sie irrtümlich annehmen, das auch eine Application Permission möglich sei. Bei der Zuweisung von Berechtigungen im Azure Portal können Sie bei Microsoft Graph auch bei den "Application permissions" das Recht "UserAuthenticationMethod.ReadWrite.All" auswählen.

Anscheinend blendet Microsoft keine Berechtigungen aus, die in dem Fall nicht wirksam sind. Ein Blick in die Dokumentation zeigt aber deutlich, dass diese Funktion nicht über eine "App" alleine zu erreichen ist.


https://docs.microsoft.com/en-us/graph/api/passwordauthenticationmethod-resetpassword?view=graph-rest-beta&tabs=http

Der Versuch dennoch mit einer Application Permission das Kennwort eines Benutzers zurück zu setzen scheiter mit folgenden Fehler

{"error":{"code":"badRequest",
       "message":"{\"error\":{\"code\":\"BadRequest\",
                \"message\":\"Upn from claims with value null is not a valid upn.\",
                \"innerError\":{\"request-id\":\"xxxx"}}}",...

Token anfordern

Wir können nicht den einfachen Flow einer Application mit AppID/AppSecret oder AppID/AppZertifikat nutzen, sondern müssen einen anderen Prozess aussuchen. Microsoft bietet dazu drei Optionen an:

  1. Authorization Code
    https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow
    Der Weg wird gerne genutzt, wenn z.B. ein Teams Telefon oder ein Skript sich anmelden muss aber der Anwender kein Kennwort eingeben kann oder soll. Er bekomme eine URL mit einem Token, über die er dann dem Telefon oder Skript ein Token zukommen lassen kann
  2. Resource Owner Password Credentials (ROPC)
    https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth-ropc
  3. Implicit Grant
    https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-implicit-grant-flow
    Auf der Seite steht aber auch "With the plans for third party cookies to be removed from browsers, the implicit grant flow is no longer a suitable authentication method."

Ich habe mich für den Weg über ROPC entschieden, da ich hier auch Zugangsdaten übermitteln kann.

$LoginUrl="https://login.microsoftonline.com"  # Graph API URLs
$ResourceUrl="https://graph.microsoft.com"     # Ressource API URLs
$AppID="12345678-1234-1234-1234-1234567890ab"
$Appkey="xxxxxx" 
$TenantName="msxfaq.onmicrosoft.com"           # Tenant name

$username="pwdresetter@carius.onmicrosoft.com"   # keine Sorge. den Benutzer gibt es nicht 
$password="SuperGeheimesKennwort"    
 
$authresponse=invoke-restmethod `
                 -method POST `
                 -ContentType "application/x-www-form-urlencoded" `
                 -body "client_id=$($AppID)
                       &scope=https%3A%2F%2Fgraph.microsoft.com%2F.default
                       &client_secret=$($Appkey)
                       &username=$($username)
                       &password=$($password)
                       &grant_type=password" `
                 -uri "$($LoginUrl)/$($tenantName)/oauth2/v2.0/token"

Offen: Wie gehe ich mit Benutzern und Conditional Access um? Denkbar sind AppKennworte oder das "Dienstkonto" darf generell nur die App nutzen und damit jegliche interaktive Anmeldung unterbunden. Angreifer müssten dann schon die App (Skript/EXE) und die Zugangdaten haben

Password Methoden lesen

Zuerst prüfe ich durch ein "READ", ob der Zugriff mit dem Token schon funktioniert.

$passwordmethodes = Invoke-RestMethod `
   -Method GET `
   -Headers @{"Authorization" = "Bearer $($accesstoken)"} `
   -ContentType "application/json" `
   -URI "https://graph.microsoft.com/beta/users/clouduser1%40msxfaq.net/authentication/passwordMethods"

Die Antwort ist im Graph Explorer gut zu sehen:

Dieser Benutzer hat wohl noch nie ein Kennwort bekommen. Ganz glauben kann ich das natürlich nicht, denn auch andere Benutzer. Übrigens können Sie per Graph auch ganz einfach alle Authentifizierungsmethoden eines Anwenders ermitteln:

$passwordmethodes = Invoke-RestMethod `
   -Method GET `
   -Headers @{"Authorization" = "Bearer $($accesstoken)"} `
   -ContentType "application/json" `
   -URI "https://graph.microsoft.com/beta/users/clouduser1%40msxfaq.net/authentication/Methods"

So sieht das bei meinem Benutzer aus, der neben dem klassischen Kennwort auch die Microsoft Authenticator App auf einem IPhone11 nutzen kann

Kennwort setzen

Die Beschreibung zum Setzen eines Kennwort " passwordAuthenticationMethod: resetPassword" (https://docs.microsoft.com/en-us/graph/api/passwordauthenticationmethod-resetpassword?view=graph-rest-beta&tabs=http) beschreibt eine URL, in der zweimal eine {id} vorkommt.

POST https://graph.microsoft.com/beta/users/{id | userPrincipalName}/authentication/passwordMethods/{id}/resetPassword
Content-type: application/json

{
  "newPassword": "newPassword-value",
}

Die erste ID ist dabei die BenutzerID aber die zweite ID ist die Authenticationmethod zu dem Benutzer.

Die GUID der ID ist bei jedem Anwender unterschiedlich!

Wenn ich es daher richtig verstanden habe, muss ich erst mit einem "https://graph.microsoft.com/beta/users/clouduser1%40msxfaq.net/authentication/passwordMethods" mir die ID holen, um dann das Kennwort zu setzen. Das sieht dann so aus:

$passwordmethods = Invoke-RestMethod `
   -Method GET `
   -Headers @{"Authorization" = "Bearer $($accesstoken)"} `
   -ContentType "application/json" `
   -URI "https://graph.microsoft.com/beta/users/clouduser1%40msxfaq.net/authentication/passwordMethods"

Invoke-RestMethod `
   -Method POST `
   -Headers @{"Authorization" = "Bearer $($accesstoken)"} `
   -ContentType "application/json" `
   -URI "https://graph.microsoft.com/beta/users/clouduser1%40msxfaq.net/authentication/passwordMethods/$($passwordmethods.value.id)/resetPassword" `
   -body "{""newPassword"": ""Super4geheim!""}"

Eigentlich sollte der Aufruf eine URL zurückliefern, über den der Kennwort Update Flow ermittelbar ist. Es gibt aber sehr wohl Fehlermeldungen

Situation Meldung

Benutzer nicht gefunden

"error":{"code":"resourceNotFound",
      "message":"The specified user could not be found.",
   "innerError":{"xxxx

Kennwort zu schwach

Achtung: Ich habe auch mal "Password" als neues Kennwort versucht. Es wurde nicht gesetzt aber der Invoke-Webrequest hat auch keine Fehlermeldung generiert.

ADSync Konto mit Password Hash Sync (PHS)

Keine Meldung. Laut Anleitung wird das Kennwort gesetzt und wenn "Password Writeback" aktiviert ist, zum lokalen AD repliziert

Falsche ID

{"error":{"code":"methodNotAllowed",
       "message":"The method is not supported for this URL.",
    "innerError":{"xxxx

Kontrolle

Leider habe ich bei keiner der erfolgreichen Änderungen eine deutliche Erfolgsmeldung bekommen. Ein einfacher 200OK reicht mir aber nicht, wenn die oben aufgeführten Fehler auch einen 200 OK liefern. Auch eine erneute Abfrage der passwordMethods liefert keine weiteren Informationen.

$passwordmethods = Invoke-RestMethod `
   -Method GET `
   -Headers @{"Authorization" = "Bearer $($accesstoken)"} `
   -ContentType "application/json" `
   -URI "https://graph.microsoft.com/beta/users/clouduser1%40msxfaq.net/authentication/passwordMethods"


$passwordmethodes.value | fl

id : 28c10230-6103-485e-b985-444c60001490
password :
creationDateTime :
createdDateTime :

Ich habe hier kein CreationDateTime-Feld bekommen.

Anzeige beim Benutzer

Wenn das Kennwort durch den Administrator geändert wurde, muss er es natürlich dem Benutzer mitteilen. Bei der ersten Anmeldung wird der Benutzer gezwungen, ein neues Kennwort anzugeben.

Sicherheit

Zuerst hatte ich gehofft, dass ich mit einer App alleine die Funktion "Kennwort Reset" bereitstellen könnte. Aber das ist nicht möglich. Es muss immer ein ServiceAccount mit entsprechend privilegierten Berechtigungen genutzt werden. Allerdings können sie mit Conditional Access auch beide Konten miteinander verknüpfen:

Das Dienstkonto darf keine App außer ihre selbst geschriebene App nutzen. Ein Angreifer müsste dann sowohl die Zugangsdaten des Dienstkontos wissen als auch die App-Credentials aus der App haben, um damit etwas anstellen zu können.

Damit ist quasi ein vier Augenprinzip möglich. Der Entwickler der App kann damit alleine sich keine Hintertüren schaffen oder Konten übernehmen. Die Administratoren, die die Zugangsdaten es Dienstkontos wissen, müssen auch über die App verfügen. Wenn Sie noch sicherer sein wollen, dann könnte die Logik als "Azure Function" in der Cloud implementiert werden und die Zugangsdaten im "Azure Key Vault" hinterlegt werden.

Kennwort über Azure Az PowerShell

 

# Installiere Module
Install-Module -Name Az -Scope CurrentUser -Repository PSGallery -Force 



Get-AzADUser -UserPrincipalName clouduser@msxfaq | Update-AzADUser -Password $NewPW.Password -ForceChangePasswordNextLogin:$false

UserPrincipalName : test1@gsa.lspb.de
ObjectType        : User
UsageLocation     : DE
GivenName         : Test
Surname           : User
AccountEnabled    : True
MailNickname      : test1
Mail              : test1@gsa.lspb.de
DisplayName       : Test User
Id                : 21b54c02-3293-488d-a727-b347db4d7ab1
Type              : Member
 

 

Kennwort über AzureAD PowerShell

Hinweis: Von von diesem Modul genutzte API ist zum Sommer 2022 abgekündigt. Ich weiß nicht, ob das Modul angepasst wird oder sie besser die "Azure Az PowerShell" nutzen

Wenn ich ein Kennwort per Graph setze, dann bedeutet dies aber auch, dass der Anwender bei der nächsten Anmeldung zur Änderung aufgefordert wird, Wenn ich ein Kennwort einfach nur setzen will, dann geht das aktuell (Stand Juni 2021) nur per Azure AD PowerShell und "Set-ADUserPassword".

# Optional Modul erst installieren
Install-Module AzureAD 

# Login to Azure AD PowerShell With Admin Account
Connect-AzureAD

# ObjectID zum Benutzer erhalten
$user = Get-AzureADUser -Searchstring Testuser

Set-AzureADUserPassword `
   -ObjectId $user.ObjectId `
   -ForceChangePasswordNextLogin:$false

Interessant ist natürlich der Blick hinter die Kulissen, was hier passiert. Das "Set-AzureADUserPassword"-Commandlet startet genau einen einzelnen HTTP-Request.

Das PowerShell-Modul nutzt eine REST-API, die aber nicht unter "https://graph.microsoft.com" sondern "https://graph.windows.com" wartet. Auch der UserAgent ist interessant

 Allerdings ist diese API bereits "Deprecated" und wird Mitte 2022 dann auch entfallen:


Quelle: https://docs.microsoft.com/en-us/graph/migrate-azure-ad-graph-planning-checklist

Insofern sollten Sie vielleicht nicht allzu viel Zeit investieren, um diese API direkt anzusprechen sondern direkt bei Microsoft Graph nach einer entsprechenden Schnittstelle schauen.

Zwischenstand

Schade, dass eine einfache App keine Kennworte setzen darf. Aber auf der anderen Seite kann ich es auch wieder verstehen, dass so ein sensibler Prozess an erweiterten Berechtigungen hängt. Auf der anderen Seite fände ich eine "verschlossene signierte" App für ein automatisches Provisioning aber immer noch besser als ein Skript, in dem sich ein Benutzerkonto anmeldet um dann mit der App das Kennwort zu ändern.

Auch sonst bin ich etwas irritiert, dass beim Setzen eines schwachen Kennworts einfach ein 200OK kommt aber kein Fehler und die Rückgabe bei der Auflistung von Anmeldedaten kein CreatedDate liefert. Für die Auswertung, wie alt ein Kennwort ist oder wann die letzte Anmeldung durch den Anwender erfolgt ist, müssen weitere APIs herhalten, die ich aber noch nicht verwendet habe.

Weitere Links