MS Graph with PowerShell

Microsoft Graph is an exceptionally easy to use web API. The hardest part (as is often the case) is dealing with proper authentication and authorization. But don't worry, we'll work through a complete example here, with a focus on client apps.

Using MS Graph

MS Graph APIs are a bunch of APIs that give you access to someone's data in their enterprise (if using a work/school account) or in their personal store (if using a Microsoft Account aka MSA).

The basic model is as follows:

Register your app

Registering your app is pretty straightforward for PowerShell. You can follow the .NET Core tutorial, including enabling public client flows (Device Code Flow).

If you're planning on using a different platform rather than PowerShell, note there may be some coupling between how you register your app and how you configure your application. This can happen in a few ways.

// snippet sketch
auto u = ::winrt::Windows::Security::Authentication::Web::
  WebAuthenticationBroker::GetCurrentApplicationCallbackUri();
std::wstring replyUri = L"ms-appx-web://Microsoft.AAD.BrokerPlugIn/" +
  u->Host();
OutputDebugStringW((L"Reply URI for config: " + replyUri).c_str());

Getting an access token

We're not going to use MSAL.NET or WAM or anything of the sort. Instead, we're going to use the device code flow, where the user gets a code they should input into a browser to authenticate.

You might have seen something like this when logging in to Netflix on a TV for example. Rather than typing a user name and password with a remote control, it's more convenient to use a device with better input, and the code is the thing that ties the login together across devices.

Note that I'm going to be reusing the functions to keep an encrypted cache of stuff.

# this should match your configuration - use your registered app client id and the scopes you selected
[string]$ClientId="0d4768f4-xxxx-xxxx-xxxx-15cbe3a20254";
[string]$Scopes="user.read openid profile offline_access";

[string]$AccessToken = $null
[string]$RefreshToken = $null

# this is rather clunky, but scopes for a small thing like this are likely quite stable
[string]$TokenCacheKey = $ClientId + ":" + $Scopes

$Scopes = [System.Uri]::EscapeDataString($Scopes)

if ($CacheObj[$TokenCacheKey]) {
  $AccessToken = $CacheObj[$TokenCacheKey]["a"]
  $RefreshToken = $CacheObj[$TokenCacheKey]["r"]
  $ExpiresIn = $CacheObj[$TokenCacheKey]["e"]
}

$Headers = @{
  "Content-Type" = "application/x-www-form-urlencoded"
}

# If there is no valid token, but there's a refresh token, use that - we'll do this later.
# ...

# If there is no valid token, ask for a device code.
if (-not $AccessToken) {
  # Ask for a device code
  [string]$sourceUrl = "https://login.microsoftonline.com/common/oauth2/v2.0/devicecode"
  $Body = "client_id=" + $ClientId + "&scope=" + $Scopes
  $ResultCode = Invoke-WebRequest -Uri $sourceUrl -Method "POST" -Headers $Headers -Body $Body -UseBasicParsing
  $CodeContent = ConvertFrom-Json $ResultCode.Content
  $DeviceCode = $CodeContent.device_code
  $DeviceCodeInterval = $CodeContent.interval
  
  # Tell the user to go authorize through any device/browser.
  Write-Host $CodeContent.message

  # check in a loop ...
  do {
    Start-Sleep -Seconds $DeviceCodeInterval
    $sourceUrl = "https://login.microsoftonline.com/common/oauth2/v2.0/token"
    $Body = "grant_type=urn:ietf:params:oauth:grant-type:device_code" +
      "&client_id=" + $ClientId +
      "&device_code=" + $DeviceCode
    $ResultTokenError = $null
    try {
      $ResultToken = Invoke-WebRequest -Uri $sourceUrl -Method "POST" -Headers $Headers -Body $Body -UseBasicParsing
      $TokenObj = ConvertFrom-Json $ResultToken.Content
      $AccessToken = $TokenObj.access_token
      $IdToken = $TokenObj.id_token
      $RefreshToken = $TokenObj.refresh_token
      $ExpiresIn = $TokenObj.expires_in
    }
    catch {
      Write-Host $_
      $ResponseObj = ConvertFrom-Json $_.ErrorDetails.Message
      if ($ResponseObj.error -eq "authorization_pending") {
        # still waiting, so don't spam the output
        # Write-Host $ResponseObj.error_description
      } elseif ($ResponseObj.error -eq "authorization_declined") {
        Write-Host $ResponseObj.error_description
        return
      } elseif ($ResponseObj.error -eq "bad_verification_code") {
        Write-Host $ResponseObj.error_description
        return
      } elseif ($ResponseObj.error -eq "expired_token") {
        Write-Host $ResponseObj.error_description
        return
      }
    }
  } while (-not $AccessToken)

  $CacheObj[$TokenCacheKey] = @{
    "a" = $AccessToken
    "i" = $IdToken
    "e" = ([DateTime]::UtcNow).AddSeconds($ExpiresIn)
    "r" = $RefreshToken
  }
  Serialize-ToEncryptedFile $CacheObj $CachePath
}

Making a graph call

Let's put that access token to use with the https://graph.microsoft.com/v1.0/me URL, described in the Get a user page.

$AuthHeaders = @{
  "Authorization" = "Bearer " + $AccessToken
}
$Uri="https://graph.microsoft.com/v1.0/me"

# get self user
$GraphResult = Invoke-WebRequest -Uri $Uri -UseBasicParsing -Headers $AuthHeaders
Write-Host ("Full response: " + $GraphResult.Content)

$GraphContent = ConvertFrom-Json $GraphResult.Content
Write-Host ("Hello, " + $GetMeContent.displayName)

More information on user APIs is available on the user resource type page.

Using a refresh token

If you looked carefully at the code above, the refresh token is provided in the response along with the access token. The general auth code flow doc describe how to refresh the access token with it.

Again, this is a simple web request, so we won't use any libraries but instead manage this directly for our little script. We would do this before starting a device code flow (because we already went through that before, and it'd be nice to not have the user have to go through it again), so this would come right after deserializing our cache.

...

# If there is no valid token, but there's a refresh token, use that.
if ((-not $AccessToken -or ($AccessToken -and $ExpiresIn -lt [DateTime]::UtcNow)) -and $RefreshToken) {
  $sourceUrl = "https://login.microsoftonline.com/common/oauth2/v2.0/token"
  $Body = "grant_type=refresh_token" +
    "&client_id=" + $ClientId +
    "&refresh_token=" + $RefreshToken +
    "&scope=" + $Scopes
  $ResultTokenError = $null
  try {
    $ResultToken = Invoke-WebRequest -Uri $sourceUrl -Method "POST" -Headers $Headers -Body $Body -UseBasicParsing
    $TokenObj = ConvertFrom-Json $ResultToken.Content
    $AccessToken = $TokenObj.access_token
    $IdToken = $TokenObj.id_token
    $RefreshToken = $TokenObj.refresh_token
    $ExpiresIn = $TokenObj.expires_in
    $CacheObj[$TokenCacheKey] = @{
      "a" = $AccessToken
      "i" = $IdToken
      "e" = ([DateTime]::UtcNow).AddSeconds($ExpiresIn)
      "r" = $RefreshToken
    }
    Serialize-ToEncryptedFile $CacheObj $CachePath
  }
  catch {
    $ResponseObj = ConvertFrom-Json $_.ErrorDetails.Message
    Write-Host "msg:" + $_.ErrorDetails.Message
    Write-Host "falling through"
    $AccessToken = $null
  }
}

# If there is no valid token, ask for a device code.
...

If you put all the bits together, you should have a nice script to run graph queries. At some point, this will likely all end up in GitHub, so you should be able to get the full sample there.

Happy querying!

Tags:  powershelltutorial

Home