Skip to content
Marius Kehl edited this page Mar 28, 2023 · 3 revisions

Abfrage von Mitarbeiterdaten aus dem Azure Active Directory

Marius Kehl

code

Generelles

Die Abfrage geschieht über die öffentliche Graph-API-REST-Schnittstelle von Microsoft, die per HTTPS angesprochen wird. Dazu ist eine Authentifizierung gegenüber Microsoft nötig. Das Modul ist in Delphi/Pascal geschrieben.

Sprache, Module

Zur Verwendung kamen Delphi-Systemeigene Pakete wie beispielsweise der HTTP-Client. Des Weiteren habe ich einen HTTP-Server-Socket aus einem Paket namens INDY (Internet Direct) für das OAuth verfahren genutzt. Ein Ziel meinerseits war es, den Code sprachlich gesehen möglichst modular und leicht erweiterbar, zeitgleich aber nicht die Effizienz aus den Augen zu verlieren.

Authentifizierung

Die Authentifizierung funktioniert mit einem “OAuth 2.0 authorization code grant flow”. Das bedeutet, dass der Client eine microsoft-login-page öffnet, über die der User sich einloggen kann.

Beispiel einer URL zur Login-page:

https://login.microsoftonline.com/{tenant}/oauth2/v2.0/authorize
?client_id=11111111-1111-1111-1111-111111111111
&response_type=code
&redirect_uri=http%3A%2F%2Flocalhost%2Fmyapp%2F
&response_mode=query
&scope=offline_access%20user.read%20mail.read
&state=12345
Parameter Description
{tenant} Dies muss mit der Tenant-ID ersetzt werden.
client_id Die Client-ID, die im der "app" im registration portal zugewisen wurde.
response_type Muss code sein, um den authorization code flow zu nutzen (was wir machen wollen)
redirect_uri Die/eine der redirect-uri/-s, die man im registration portal gesetzt hat.
scope Dieser Query-Parameter ist eine Space( )-seperated Liste aus allen scopes die die "App" benötigt.
response_mode Kann query oder form_post sein. Legt die methode fest, die genutzt werden soll um den code an den client zu übermitteln. In meinem fall habe ich immer query benutzt. (query=GET-req mit query parametern, form_post=POST-req mit form-data als payload)
state Ein frei wählbarer string, solle definitiv randomness beinhalten, um vor gewissen attacken zu schützen.

Mehr dazu: hier

Dann leitet (redirect) microsoft den browser an eine "redirect" uri. Diese Redirect Uri kann von Systemadministartoren im azure portal selbständig festgelegt werden. In meinem fall habe ich eine redirect url auf http://localhost:8080/<some other stuff :) > festgelegt - mehr dazu später. Im redirect (bzw. der redirect URI), der von microsoft geschickt wird, befindet sich ein authorisation code. Dieser kann später zur weiteren Autorisierung genutzt werden.

Beispiel einer redirect-URI:

GET https://localhost/myapp/?
code=M0ab92efe-b6fd-df08-87dc-2c6500a7f84d
&state=12345
Parameter Description
code Der code der für die weitere auth verwendet wird
state Sollte der gleiche wert sein, der oben gesetzt wurde. Wenn nicht, dann ist man vermutlich einer Attacke zum Opfer gefallen. Also Lohnt es sich, diesen Parameter zu testen :)

Redirect_URI - warum nur http?

Für den Redirect ist in meinem Fall kein TLS nötig, da der Redirect auf localhost zeigt, also vom weg zwischen Browser und der Anwendung (also dem HTTP-Socket) niemand ist, der die Auth-daten abhören könnte. (zumindest sollte das der Fall sein. Nur ein Virus, der lokal installiert ist, könnte den code-flow abhören.) Wenn man jetzt auf eine Web-App übergeht, Der Redirect also auf einen Server außerhalb des internal-loopback des Endgeräts zeigt, dann sollte der Redirect IMMER mit TLS abgesichert sein.

Einen Token bekommen

Nun haben wir den code. Wir stellen einen Request an Mircosoft:

POST /{tenant}/oauth2/v2.0/token HTTP/1.1
Host: https://login.microsoftonline.com
Content-Type: application/x-www-form-urlencoded

client_id=11111111-1111-1111-1111-111111111111
&scope=user.read%20mail.read
&code=OAAABAAAAiL9Kn2Z27UubvWFPbm0gLWQJVzCTE9UkP3pSx1aXxUjq3n8b2JRLk4OxVXr...
&redirect_uri=http%3A%2F%2Flocalhost%2Fmyapp%2F
&grant_type=authorization_code
Parameter Description
tenant Siehe oben.
client_id Siehe oben.
grant_type Muss authorization_code für den authorization code flow sein.
scope Siehe oben.
code Den code den wir eben bekommen haben
redirect_uri Siehe oben. Es gibt keinen weiteren Redirect, dient nur zur weiteren Absicherung :)

Dieser Request gibt uns folgende Response-Payload:

{
    "token_type": "Bearer",
    "scope": "user.read%20Fmail.read",
    "expires_in": 3600,
    "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6Ik5HVEZ2ZEstZnl0aEV1Q...",
    "refresh_token": "AwABAAAAvPM1KaPlrEqdFSBzjqfTGAMxZGUTdM0t4B4..."
}
Parameter Description
token_type Zitat: Indicates the token type value. The only type that Azure AD supports is Bearer.
scope Nochmal der Scope. Sollte der gleiche sein wie oben.
expires_in Die Dauer, die der access_token gültig ist. (in Sekunden)
access_token Dieser Token wird benutzt, um Requests an die Graph-API zu stellen.
refresh_token Dieser Token hat typischerweise eine Gültigkeit von 90 Tagen. Mit ihm kann ein neuer acces_token sowie ein neuer refresh_token angefordert werden. Mehr dazu später.

Userdaten Abfragen

Die Userdaten werden mit einem GET-Request abgefragt:

GET /v1.0/{tenant}/users HTTP/1.1
Host: https://graph.microsoft.com
Accept: application/json
ConsistencyLevel: eventual

$filter=onPremisesSamAccountName eq ''{SysLogin}''
&$select=businessPhones,displayName,faxNumber,givenName,mail,mobilePhone,surname
&$count=true
Parameter Description
tenant Wieder die TenantID (in dem Fall von der MAC).
SysLogin Der Login-Name, der im LMPS hinterlegt ist.

OData

Die Parameter $filter und $select sind nach dem OData Standart definiert. OData ist, in eigenen worten "so etwas wie SQL für rest-Schnittstellen".

$filter

Mit diesem Parameter wird aus einer Ressourcen-Collection, in dem Fall eine Liste an Usern nach einem oder mehreren Feldern/Parametern gefiltert. Mehr dazu: hier und hier

$select

Dieser Parameter nimmt Komma(,)-Seperierte Feld-/Parameternamen entgegen. Dies bestimmt, welche daten die Graph-Schnittstelle bei diesem Request zurückgibt.

Response:

{
  "displayName": "Marius Kehl",
  "businessPhones": ["+49123459876"],
  "givenName": "Marius",
  "surname": "Kehl",
  "mail": "[email protected]",
  "mobilePhone": "+49123098456",
  "faxNumber": "+49123456789"
}

Die Felder sollten an sich selbsterklärend sein, Dokumentation zu den einzelnen Feldern befindet sich: hier.

Diese Daten parse ich dann und packe sie in ein für das LMPS lesbares Format (xml).

HTTP-Error Handling

Grundsätzlich ist das Prinzip recht einfach. Es gibt fünf Bereiche, in die alle HTTP-Status-Codes eingeteilt werden können. Vier davon liste ich hier auf:

Bereich Description
200 - 299 Request war erfolgreich, ohne Fehler.
300 - 399 Besondere Ereignisse wie Redirects, etc.
400 - 499 Der Client, also der Sender des Requests hat etwas "Falsch gemacht". Authorisierung nicht gültig/vergessen, eine Ressource angefragt die es nicht gibt, etc.
500 - 599 Der Host hat ein Problem/Fehler. InternalServerError, BadGateway, ServiceUnavailable, etc.

Mehr dazu

Nur wenn ein Response einen Statuscode außerhalb der 200-range hat, dann wird versucht eine Fehlermeldung zu parsen, anschließend wird eine Callback Funktion aufgerufen, die beim initialisieren des Auth-Objekts festgelegt werden kann.

Einen Refresh-Token benutzen

Mit einem Refresh-Token kann ein neues Set aus Acces- und Refresh-Tokens angefragt werden. Ein Request dazu sieht Folgendermaßen aus:

POST /{tenant}/oauth2/v2.0/token HTTP/1.1
Host: https://login.microsoftonline.com
Content-Type: application/x-www-form-urlencoded

client_id=11111111-1111-1111-1111-111111111111
&scope=user.read%20mail.read
&refresh_token=OAAABAAAAiL9Kn2Z27UubvWFPbm0gLWQJVzCTE9UkP3pSx1aXxUjq...
&grant_type=refresh_token
Parameter Description
tenant Siehe oben.
client_id Siehe oben.
grant_type Muss refresh_token sein.
refresh_token Der Refresh-Token, den wir oben bekommen haben.

Der Response ist Identisch zum Response von Einen Token bekommen.