kevinhakanson.com

Exploring the Microsoft Graph API from Azure Cloud Shell

April 22, 2020 #azure #cli #odata #cloudshell

When I’m first exploring a new API, it is comfortable to use a GUI based API explorer tool. For the Microsoft Graph API, this is the Graph Explorer and a tool I enjoy working with. Try it out with this direct link which will query https://graph.microsoft.com/v1.0/me/ and get a response similar to:

{
    "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#users/$entity",
    "businessPhones": [],
    "displayName": "Kevin Hakanson",
    "givenName": "Kevin",
    "jobTitle": null,
    "mail": "kevin.hakanson@example.com",
    "mobilePhone": null,
    "officeLocation": null,
    "preferredLanguage": null,
    "surname": "Hakanson",
    "userPrincipalName": "kevin.hakanson@example.com",
    "id": "11112222-3333-4444-5555-666677778888"
}

Other times I want to explore an API using the command line, since it is easier for me to document my research in markdown than using browser screenshots. I opened up Azure Cloud Shell Azure Cloud Shell

…and gave curl a try:

kevin@Azure:~$ curl https://graph.microsoft.com/v1.0/me/
{
  "error": {
    "code": "InvalidAuthenticationToken",
    "message": "Access token is empty.",
    "innerError": {
      "request-id": "613cf582-fd4f-4c28-8504-063e78d5334e",
      "date": "2020-05-27T14:01:22"
    }
  }
}

This error makes sense, because without an access token, how would it know who /me/ was? Using browser DevTools to look at the outgoing HTTP request from Graph Explorer, I see it includes an Authorization header with a Bearer token.

GET /v1.0/me/ HTTP/1.1
Host: graph.microsoft.com
SdkVersion: GraphExplorer/4.0, graph-js/2.0.0 (featureUsage=6)
Authorization: Bearer [redacted]

Graph API Access Token

How can I get a token without having to cut/paste from Graph Explorer? Luckily, one can acquire an access token in Cloud Shell by using the $MSI_ENDPOINT. I pipe the output through jq for formatting and use —silent to not have the progress meter get in the way.

kevin@Azure:~$ curl $MSI_ENDPOINT -d "resource"="https://graph.microsoft.com/" --header "metadata: true" --silent | jq
{
  "access_token": "[redacted]",
  "refresh_token": "",
  "expires_in": "3851",
  "expires_on": "1587590674",
  "not_before": "1587586823",
  "resource": "https://graph.microsoft.com/",
  "token_type": "Bearer"
}

That worked great for inspection, but now to save it in the $GRAPH_JWT environment variable for later use. I’ll also generate a link to the jwt.ms website to decode the token and inspect the claims.

kevin@Azure:~$ export GRAPH_JWT=`curl $MSI_ENDPOINT --data "resource=https://graph.microsoft.com/" --header "metadata: true" --silent | jq .access_token -r`

kevin@Azure:~$ echo "https://jwt.ms/?#access_token=$GRAPH_JWT"

Go to "https://jwt.ms/?#access_token=..."

Since I am submitting the request to /me/, you would expect the data values to match. However, the JWT claims of “name”, “oid”, and “upn” don’t exact the keys of “displayName”, “id”, and “userPrincipalName from the Graph API JSON. Here is a subset of the JWT claims:

  "aud": "https://graph.microsoft.com/",

  "app_displayname": "AzurePortal Console App",

  "name": "Kevin Hakanson",
  "oid": "11112222-3333-4444-5555-666677778888",
  "upn": "kevin.hakanson@example.com",

Let’s run that curl request using the $GRAPH_JWT as the Bearer token in our Authorization header:

kevin@Azure:~$ curl https://graph.microsoft.com/v1.0/me/ --header "Authorization: Bearer $GRAPH_JWT" --silent | jq
{
  "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#users/$entity",
  "businessPhones": [],
  "displayName": "Kevin Hakanson",
  "givenName": "Kevin",
  "jobTitle": null,
  "mail": "kevin.hakanson@example.com",
  "mobilePhone": null,
  "officeLocation": null,
  "preferredLanguage": null,
  "surname": "Hakanson",
  "userPrincipalName": "kevin.hakanson@example.com",
  "id": "11112222-3333-4444-5555-666677778888"
}

There’s a lot of null value fields in that response I want to ignore, so let’s add an OData $select to the URL. In the examples above that included $MSI_ENDPOINT or $GRAPH_JWT, the $ indicated an environment variable, but the $select below is a literal string so we need to escape the $ as \$ for it to work correctly.

kevin@Azure:~$ curl "https://graph.microsoft.com/v1.0/me/?\$select=id,userPrincipalName,displayName" --header "Authorization: Bearer $GRAPH_JWT" --silent | jq
{
  "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#users(id,userPrincipalName,displayName)/$entity",
  "id": "11112222-3333-4444-5555-666677778888",
  "userPrincipalName": "kevin.hakanson@example.com",
  "displayName": "Kevin Hakanson"
}

The Azure CLI includes an az rest command that invokes a request and automatically authenticates using the logged in credentials. Therefore, there is no need to pass the $GRAPH_JWT ourselves.

kevin@Azure:~$ az rest --url "https://graph.microsoft.com/v1.0/me/?\$select=id,userPrincipalName,displayName"
{
  "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#users(id,userPrincipalName,displayName)/$entity",
  "displayName": "Kevin Hakanson",
  "id": "11112222-3333-4444-5555-666677778888",
  "userPrincipalName": "kevin.hakanson@example.com"
}

The result is the same, but the JSON fields are in a different order. Let’s redirect the output to a file and compare. It looks like az rest is not preserving the original order and sorting them instead.

kevin@Azure:~$ az rest --url "https://graph.microsoft.com/v1.0/me/?\$select=id,userPrincipalName,displayName" --output-file me.json

kevin@Azure:~$ cat me.json | jq
{
  "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#users(id,userPrincipalName,displayName)/$entity",
  "id": "11112222-3333-4444-5555-666677778888",
  "userPrincipalName": "kevin.hakanson@example.com",
  "displayName": "Kevin Hakanson"
}

Note: Each of these Graph API URLs return the same JSON response:

The same user data can be fetched using /directoryObjects but this response includes an @odata.type field with the #microsoft.graph.user value.

kevin@Azure:~$ az rest --url "https://graph.microsoft.com/v1.0/directoryObjects/11112222-3333-4444-5555-666677778888?\$select=id,userPrincipalName,displayName"
{
  "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#directoryObjects(id,userPrincipalName,displayName)/$entity",
  "@odata.type": "#microsoft.graph.user",
  "displayName": "Kevin Hakanson",
  "id": "11112222-3333-4444-5555-666677778888",
  "userPrincipalName": "kevin.hakanson@example.com"
}

Yet another way to get this data is calling /users with using a $filter of my id, but that response is a value[] of users.

kevin@Azure:~$ az rest --url "https://graph.microsoft.com/v1.0/users?\$filter=id eq '11112222-3333-4444-5555-666677778888'&\$select=id,userPrincipalName,displayName"
{
  "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#users(id,userPrincipalName,displayName)",
  "value": [
    {
      "displayName": "Kevin Hakanson",
      "id": "11112222-3333-4444-5555-666677778888",
      "userPrincipalName": "kevin.hakanson@example.com"
    }
  ]
}

I bet you are sick of /me/ by now - I know I just want to curl up and rest after all this exploration.


© 2020 Kevin Hakanson (built with Gatsby)