Graph und Benutzer

Dass Sie mit Microsoft Graph nicht nur Exchange und Teams-Daten bearbeiten können sondern auch AzureAD-Objekte manipulieren können, zeige ich auf dieser Seite. Ich gehe auch auf die Besonderheiten der Rufnummern ein, für deren Pflege sie andere Pfade nutzen müssen. Für die UNIX-Nutzer habe ich neben meinen PowerShell-Befehlen auch CURL beschrieben.

Es gibt auch ein Microsoft.Graph PowerShell SDK, welche z.B. den Befehl Get-MGUser/Update-MGUser enthält.

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

Achtung: In Verbindung mit ADSync/AADConnect können Sie natürlich nur die Felder schreiben, die nicht durch ADSync blockiert werden

App registrieren

Mein Skript arbeitet nicht als "Benutzer" und schon gar nicht als "GlobalAdmin" sondern werde ich eine "App" im Azure AD registrieren und mit den erforderlichen Berechtigungen versehen. Das geht per Browser im AzureAD und habe ich z.B. auf Get-O365Usage schon beschrieben.

Die Anlage per PowerShell konnte ich bislang nur für die ersten Schritte erreichen

# Modul installieren, wenn noch nicht da, ggfls. update-module
Install-Module AzureAD
Update-Module AzureAD

# Unter PowerShell 7 muss es im compatibilitymode eingebunden werden. 
# ansonsten kann der Fehler Could not load type 'System.Security.Cryptography.SHA256Cng' from assembly 'System.Core  die Anmeldung verhindern 
Import-Module AzureAD -UseWindowsPowerShell 

# Anmelden mit einem AdminUser, ggfls mit einer TenantID
Connect-AzureAD

# Anlegen einer neuen App
$app = New-AzureADApplication -DisplayName MSXFAQGraphUser

Write-host "AppID: $($App.appid)"

# Anlegen von AppCredentials
$appkey = New-AzureADApplicationPasswordCredential `
   -ObjectId $app.ObjectId `
   -CustomKeyIdentifier "Access Key" `
   -EndDate (get-date).AddYears(1)

Write-host "Apppassword: $($appkey.value)"

# Hinweis: Die Rückgabe ist anscheinend Base64 codiert aber eine Umwandlung in ein nutzbares Kennwort ist mir auch noch nicht gelungen
# $apppassword = [convert]::FromBase64String($appkey.value)
# [system.text.encoding]::default.getString($apppassword)

Die nächsten Schritte habe ich noch nicht per PowerShell umsetzen können und müssen daher per https://portal.azure.com von einem Administrator manuell durchgeführt werden

  • ggfls. App Credential anlegen
  • App berechtigen
    Für die nächsten Schritte brauchen wir:
  • Admin Consent erteilen
    Als Admin stimmen Sie den Rechten der App zu.

Was genau im Hintergrund noch alles vom Portal angelegt wird, habe ich noch nicht weiter analysiert. Insbesondere ob zur App noch ein AzureADServicePrincipal angelegt wird, z.B. mit:

New-AzureADServicePrincipal -AppId $App.appid

Und auch die Erteilung des Consent ist nicht mal eben gemacht.

az ad app permission admin-consent --id $App.appid

Für die weitere Nutzung muss ich mir die Werte natürlich speichern, z.B. als Variablen:

$LoginUrl="https://login.microsoftonline.com"  # Graph API URLs
$ResourceUrl="https://graph.microsoft.com"     # Ressource API URLs
$AppID="12345678-1234-1234-1234-123456789abc"  # App ID aus dem Azure Portal
$Appkey="xxxxxxxxx"                            # App Key 
$TenantName="msxfaq.onmicrosoft.com"           # Tenant name

In einem Shell-Skript sieht es ähnlich aus:

LoginUrl="https://login.microsoftonline.com"  # Graph API URLs
ResourceUrl="https://graph.microsoft.com"     # Ressource API URLs
AppID="12345678-1234-1234-1234-123456789abc"  # App ID aus dem Azure Portal
Appkey="xxxxxxxxx"                            # App Key 
TenantName="msxfaq.onmicrosoft.com"           # Tenant name

Access-Token holen

Der nächste Schritt ist dann ,dass ich mir als App ein Zugriffstoken besorgen:

$authresponse=invoke-restmethod `
           -body "client_id=$($AppID)
                  &scope=https%3A%2F%2Fgraph.microsoft.com%2F.default
                  &client_secret=$($Appkey)
                  &grant_type=client_credentials
                  &api-version=1.0" `
           -uri "$($LoginUrl)/$($tenantName)/oauth2/v2.0/token"
 
$accesstoken= $authresponse.access_token

In einem Shell-Skript ist es mit CURL nicht viel anders (Umbruch zur besseren Lesbarkeit)

TOKEN=`curl -d "client_id=${AppID}
                &scope=https%3A%2F%2Fgraph.microsoft.com%2F.default
                &client_secret=${Appkey}
                &grant_type=client_credentials
                &api-version=1.0" 
            "${LoginUrl}/${TenantName}/oauth2/v2.0/token"`

Zur Überprüfung können Sie das Token auch einfach, z.B. auf jwt.io eingeben und decodieren lassen. Prüfen Sie hier, ob Sie wirklich alle erforderlichen Berechtigungen haben

Feld lesen

Um zu sehen, ob ich korrekt damit Graph nutzen kann, sollte ich einfach die Properties eines Benutzers lesen. z.B. so:

Invoke-RestMethod `
   -Method GET `
   -Headers @{"Authorization" = "Bearer $($accesstoken)"} `
   -URI "https://graph.microsoft.com/v1.0/users/user%40msxfaq.de"

Die Ausgabe liefert die Default Felder:

@odata.context    : https://graph.microsoft.com/v1.0/$metadata#users/$entity
businessPhones    : {12345}
displayName       : user
givenName         : user-givenname
jobTitle          : user-jobtitle
mail              : user@msxfaq.de
mobilePhone       :
officeLocation    :
preferredLanguage :
surname           : user-Surname
userPrincipalName : user@msxfaq.de
id                : a74255aa-0327-221b-8ab2-f128eff44557

Mit CURL ist es vergleichbar:

curl -i 
   -X GET 
   -H "Accept-Charset: utf-8" 
   -H "Authorization: Bearer $TOKEN" 
    https://graph.microsoft.com/v1.0/users/user%40msxfaq.de

HTTP/1.1 200 OK
Date: Thu, 17 Jun 2021 11:35:28 GMT
Content-Type: application/json;odata.metadata=minimal;odata.streaming=true;IEEE754Compatible=false;charset=utf-8
Cache-Control: no-cache
Transfer-Encoding: chunked
Strict-Transport-Security: max-age=31536000
request-id: cc64718a-2325-48f7-9217-39dcd7c3a71a
client-request-id: cc64718a-2325-48f7-9217-39dcd7c3a71a
x-ms-ags-diagnostic: {"ServerInfo":{"DataCenter":"West Europe","Slice":"E","Ring":"5","ScaleUnit":"003","RoleInstance":"AM1PEPF0000807C"}}
x-ms-resource-unit: 1
OData-Version: 4.0

{
   "@odata.context":"https://graph.microsoft.com/v1.0/$metadata#users/$entity",
   "businessPhones":["052511322792"],
   "displayName":"User1",
   "givenName":"Alfred","jobTitle":"demo2",
   "mail":"user@msxfaq.de",
   "mobilePhone":null,
   "officeLocation":null,
   "preferredLanguage":null,
   "surname":"User",
   "userPrincipalName":"user@msxfaq.de",
   "id":"a74255aa-0327-221b-8ab2-f128eff44557"}

Natürlich müssen Sie bei CURL die Ausgabe noch parsen, z.B. mit JQ. PowerShell liefert mehr mehr als "nur" ein Textstring.

Feld ändern

Der nächste Schritt ist das Beschreiben eines Feldes:

Invoke-RestMethod `
               -Method PATCH `
               -Headers @{"Authorization" = "Bearer $($accesstoken)"} `
               -ContentType "application/json" `
               -URI "https://graph.microsoft.com/v1.0/users/user%40msxfaq.de" `
 -body "{""Jobtitle"": ""User-Jobtitle""}"

Auch Felder, die noch nicht vorhanden sind, werden mit einem "PATCH" aktualisiert. Wenn Sie stattdessen ein PUT versuchen, bekommen Sie folgenden Fehler:

{"error":{"code":"Request_BadRequest",
       "message":"Specified HTTP method is not allowed for the request target.",
    "innerError":{"date":"2021-06-17T09:13:53","request-id":"4a411f04-28a3-4508-8dd7-40fe5e47f1c8","client-request-id":"4a411f04-28a3-4508-8dd7-40fe5e47f1c8"}}}

Auch hier noch mal die CURL-Version in einem Shellskript.

### jsonfile
#{
#  "Jobtitle" : "User-Jobtitle"
#}

curl 
   -X PATCH 
   -H "Content-Type: application/json" 
   -H "Authorization: Bearer $TOKEN" 
   -data "{ "Jobtitle" : "User-Jobtitle" }" 
   "https://graph.microsoft.com/v1.0/users/user%40msxfaq.de.de"

Im Prinzip ist es nicht leichter oder schwerer als PowerShell. Die weiteren Beispiele mache ich ohne CURL.

Fehler durch ADSync

Beachten Sie, dass sie auch per Graph keine Felder ändern können, die durch ADSync aus dem lokalen AD gefüllt werden. Hier ein Versuch den Displayname eines per ADSync gepflegten Kontos zu verwalten:

Invoke-RestMethod `
   -Method PATCH `
   -Headers @{"Authorization" = "Bearer $($accesstoken)"} `
   -ContentType "application/json" `
   -URI "https://graph.microsoft.com/v1.0/users/user%40msxfaq.de" `
   -body "{""displyname"": ""ADUser2b""}"

Die Fehlermeldung ist aussagekräftig:

{"error": {"code": "Request_BadRequest",
        "message": "Unable to update the specified properties for On-Premises mastered Directory Sync objects or objects currently undergoing migration.",
     "innerError": {xxx  }}}

Falscher Feldname

Ich habe mich auch bei der Angabe eines Feld einmal vertan, um den Fehler zu ermitteln.

{"error": {"code": "Request_BadRequest",
        "message": "One or more property values specified are invalid.",
     "innerError": {xxxx}}}

Allerdings verrät Graph nicht, welches Feld das Problem ist.

Sonderfall "mobile" etc

Etwas kniffliger wird es mit dem Beschreiben der Felder "businessPhones", "mobilePhone" und "otherMails". Diese Inhalte haben hinsichtlich einer "Multi Faktor Auth" eine besondere Bedeutung und sollten daher nicht so einfach veränderbar sein. Mein erster Versuch schlug daher fehl

Invoke-RestMethod `
   -Method PATCH `
   -Headers @{"Authorization" = "Bearer $($accesstoken)"} `
   -ContentType "application/json" `
   -URI "https://graph.microsoft.com/v1.0/users/user%40msxfaq.de" `
   -body "{""mobilePhone"": ""12345""}"

Ds hat aber mit folgender Fehlermeldung nicht geklappt:

{"error":{"code":"Authorization_RequestDenied",
       "message":"Insufficient privileges to complete the operation.",
    "innerError":{xxx}}}

Das Feld war noch nicht belegt, so dass ich vielleicht ein PUT statt ein POST brauche?

Invoke-RestMethod `
   -Method PUT  `
   -Headers @{"Authorization" = "Bearer $($accesstoken)"} `
   -ContentType "application/json" `
   -URI "https://graph.microsoft.com/v1.0/users/user%40msxfaq.de" `
   -body "{""mobilePhone"": ""12345""}"

Es gab aber einen anderen Fehler:

{"error":{"code":"Request_BadRequest",
       "message":"Specified HTTP method is not allowed for the request target.",
    "innerError":{xxx}}}

Beides geht nicht aber ich habe definitiv die Berechtigungen. Eine kurze Recherche liefert dann aber.

For an app with delegated permissions to read access reviews of a group or app, the signed-in user must be a member of one of the following administrator roles: Global Administrator, Security Administrator, Security Reader or User Administrator. For an app with delegated permissions to write access reviews of a group or app, the signed-in user must be a member of one of the following administrator roles: Global Administrator or User Administrator.
Quelle: https://docs.microsoft.com/en-us/graph/permissions-reference#remarks-5

Aber auch dieses Recht hatte ich eigentlich und da es sich bei mir um keinen "RiskyUser" handelt, brauche ich auch nicht "IdentityRiskyUser.ReadWrite.All". Ich habe dann per Graph Explorer (https://developer.microsoft.com/en-us/graph/graph-explorer) mit als Administrator angemeldet und den gleichen PATCH erfolgreich ausführen können.

Im Prinzip kann ich das Feld also schon setzen, wenn ich die erforderlichen Berechtigungen habe.

phoneauthenticationmethod

Das Setzen der Rufnummern beim Benutzer ist nicht automatisch identisch mit der erweiterten Authentifizierung per SMS oder Anruf. Diese Properties werden nicht direkt beim Benutzer hinterlegt sondern nutzen eine eigene URL mit eigenen Berechtigungen. Gerade weil die Rufnummern auch für die Authentifizierung genutzt werden können, gibt es einen eigenen Aufruf mit einem eigenen Recht.

In dem Artikel gibt es noch den Hinweis auf eine verlängerte URL für die drei Rufnummern:

  • b6332ec1-7057-4abe-9331-3d72feddfe41 to update the alternateMobile phoneType.
  • e37fc753-ff3b-4958-9484-eaa9425c82bc to update the office phoneType.
  • 3179e48a-750b-4051-897c-87b9720928f7 to update the mobile phoneType.

Entsprechend habe ich den HTTP-Request erweitert.

Invoke-RestMethod `
   -Method PUT `
   -Headers @{"Authorization" = "Bearer $($accesstoken)"} `
   -ContentType "application/json" `
   -URI "https://graph.microsoft.com/beta/users/user%40msxfaq.de/authentication/phoneMethods/3179e48a-750b-4051-897c-87b9720928f7" `
   -body "{""phoneNumber"": ""12345"",""phoneType"": ""mobile"",}"

Diesmal bekam ich eine weitere Fehlermeldung, die aber Hoffnung machte:

{"error":{"code":"invalidPhoneNumber",
       "message":"The provided phone number did not start with a plus (\"+\") character",
    "innerError":{xxxx}}

Das backen prüft, ob die Telefonnummer zumindest formal richtig ist. Mit einer korrekt formatierten Rufnummer ist der Aufruf erfolgreich gewesen.

Invoke-RestMethod `
   -Method PUT `
   -Headers @{"Authorization" = "Bearer $($accesstoken)"} `
   -ContentType "application/json" `
   -URI "https://graph.microsoft.com/beta/users/user%40msxfaq.de/authentication/phoneMethods/3179e48a-750b-4051-897c-87b9720928f7" `
   -body "{""phoneType"": ""mobile"",""phoneNumber"": ""+495152304613""}"

@odata.context : https://graph.microsoft.com/beta/$metadata#users('user%40msxfaq.de')/authentication/phoneMethods/$en
                 tity
id             : 3179e48a-750b-4051-897c-87b9720928f7
phoneNumber    : +49 5251304613
phoneType      : mobile
smsSignInState : notAllowedByPolicy

Der Blogbeitrag und insbesondere Codes in den Kommentaren zeigt aber, dass es ohne GUID gehen sollte.

Beim nachfolgenden Abruf der Daten ist dann auch das Feld "Mobile" gefüllt.

Invoke-RestMethod `
   -Method POST `
   -Headers @{"Authorization" = "Bearer $($token)"} `
   -ContentType "application/json" `
   -URI "https://graph.microsoft.com/beta/users/user%40msxfaq.de/authentication/phoneMethods/3179e48a-750b-4051-897c-87b9720928f7" `
   -body "{""phoneType"": ""mobile"",""phoneNumber"": ""+495152304613""}"

Wenn Sie statt dem PUT ein PATCH verwenden, weil Sie z.B. eine bestehende Nummer ändern wollen, dann kommt eine andere Fehlermeldungp>

{"error":{"code":"UnknownError",
       "message":"{\"Message\":\"No HTTP resource was found that matches the request URI 
                      'https://mface.windowsazure.com/odata/users('user%40msxfaq.de')/authentication/
                        phoneMethods('3179e48a-750b-4051-897c-87b9720928f7')'.\",
                       \"MessageDetail\":\"No type was found that matches the controller named 'users'.\"}",
    "innerError":{xxxx}}}

Interessanterweise löst der Name mface.windowsazure.com sogar auf und ist per HTTPS erreichbar.

C:\>nslookup mface.windowsazure.com

Nicht autorisierende Antwort:
Name:    www.tm.a.prd.aadg.akadns.net
Addresses:  20.190.160.73
          20.190.160.69
          20.190.160.8
          20.190.160.4
          20.190.160.136
          20.190.160.67
          20.190.160.134
          20.190.160.132
Aliases:  mface.windowsazure.com
          a.privatelink.msidentity.com
          prda.aadg.msidentity.com

Password

Siehe eigene Seite Graph und Password

Weitere Links