Inovking a RESTful API with PowerShell

In this article we will take a look at invoking a RESTful API with PowerShell. We are going to use the Invoke-RestMethod cmdlet and learn about its parameters. Maybe it's useful to someone to get going quickly.

The Problem

There was a bug in the ODataAuthorization library, and instead of using Postman I thought it would be nice to learn a little PowerShell and execute requests using a Script.

So basically, I want to make a HTTP Post Request to obtain a Json Web Token (JWT) from an Auth Endpoint and execute some HTTP GET requests to OData-enabled endpoints.

The Solution

PowerShell comes with the built-in Invoke-RestMethod cmdlet to call RESTful APIs:

The idea for each request is to basically define the parameters for Invoke-RestMethod like this:

$authRequestParameters = @{
    Method = "POST"
    Uri = "some-url"
    Body = ($someBodyForPost | ConvertTo-Json) 
    ContentType = "application/json"
    # More Parameters here ...
}

The request to a given $AuthUrl is going to return a JSON object, where the token property contains the bearer token, that we'll need to send in the Authorization header.

$AuthEmail = "..."
$AuthPassword = "..."
# More variables ...

$authRequestBody = @{
    Email = $AuthEmail
    Password = $AuthPassword
    RequestedScopes = $RequestedScopes
}

$authRequestParameters = @{
    Method = "POST"
    Uri = $AuthUrl
    Body = ($authRequestBody | ConvertTo-Json) 
    ContentType = "application/json"
}

# Invoke the Rest API
$authRequestResponse = Invoke-RestMethod @authRequestParameters

# Extract JWT from the JSON Response 
$authToken = $authRequestResponse.token

# The Auth Header needs to be sent for any additional OData request
$authHeader = @{
    Authorization = "Bearer $authToken"
}

The $authHeader can then be passed as the Headers Parameter to Invoke-RestMethod. You can also set a variable name, that Invoke-RestMethod should write the parameter to ($statusCode in the example).

Write-Host "[REQ]"
Write-Host "[REQ] OData Request"
Write-Host "[REQ]"
Write-Host "[REQ]   Description:    $Description"
Write-Host "[REQ]   URL:            $Endpoint"
Write-Host "[REQ]   Scopes:         $RequestedScopes"
Write-Host "[REQ]"

$odataRequestParameters = @{
    Method = "GET"
    Uri = $Endpoint
    Headers = $authHeader
    StatusCodeVariable = 'statusCode'
}

try {

    $oDataResponse = Invoke-RestMethod @odataRequestParameters
    $oDataResponseValue = $oDataResponse.value | ConvertTo-Json

    Write-Host "[RES]    HTTP Status:    $statusCode"  -ForegroundColor Green
    Write-Host "[RES]    Body:           $oDataResponseValue"  -ForegroundColor Green
} catch {
    Write-Host "[ERR] Request failed with StatusCode:" $_.Exception.Response.StatusCode.value__ -ForegroundColor Red
}

We will then put all logic for querying the OData Endpoints into a function Send-ODataRequest, which can then be called with sample OData Requests. We end up with the following PowerShell Script:

<#
.SYNOPSIS
    Example Script for restricting Navigation Properties using the 
    ODataAuthorization library.
.DESCRIPTION
    This script obtains a valid token and proceeds to perform requests to
    the API.
.NOTES
    File Name      : ODataQueries.ps1
    Author         : Philipp Wagner
    Prerequisite   : PowerShell
    Copyright 2023 - MIT License
#>

function Send-ODataRequest {
    param (
        [Parameter(Mandatory)]
        [string]$AuthUrl,

        [Parameter(Mandatory)]
        [string]$AuthEmail,

        [Parameter(Mandatory)]
        [string]$AuthPassword,

        [Parameter(Mandatory)]
        [string]$Description,

        [Parameter(Mandatory)]
        [string]$Endpoint,

        [Parameter(Mandatory)]
        [string]$RequestedScopes
    )

    # Perform /Auth/login to obtain the JWT with Requested Scopes
    $authRequestBody = @{
        Email = $AuthEmail
        Password = $AuthPassword
        RequestedScopes = $RequestedScopes
    }

    $authRequestParameters = @{
        Method = "POST"
        Uri = $AuthUrl
        Body = ($authRequestBody | ConvertTo-Json) 
        ContentType = "application/json"
    }

    # Invoke the Rest API
    $authRequestResponse = Invoke-RestMethod @authRequestParameters

    # Extract JWT from the JSON Response 
    $authToken = $authRequestResponse.token

    # The Auth Header needs to be sent for any additional OData request
    $authHeader = @{
        Authorization = "Bearer $authToken"
    }

    Write-Host "[REQ]"
    Write-Host "[REQ] OData Request"
    Write-Host "[REQ]"
    Write-Host "[REQ]   Description:    $Description"
    Write-Host "[REQ]   URL:            $Endpoint"
    Write-Host "[REQ]   Scopes:         $RequestedScopes"
    Write-Host "[REQ]"

    $odataRequestParameters = @{
        Method = "GET"
        Uri = $Endpoint
        Headers = $authHeader
        StatusCodeVariable = 'statusCode'
    }

    try {

        $oDataResponse = Invoke-RestMethod @odataRequestParameters
        $oDataResponseValue = $oDataResponse.value | ConvertTo-Json

        Write-Host "[RES]    HTTP Status:    $statusCode"  -ForegroundColor Green
        Write-Host "[RES]    Body:           $oDataResponseValue"  -ForegroundColor Green
    } catch {
        Write-Host "[ERR] Request failed with StatusCode:" $_.Exception.Response.StatusCode.value__ -ForegroundColor Red
    }
}

$authUrl = "http://localhost:5124/Auth/login"
$authEmail = "admin@admin.com"
$authPassword = "123456"

$requests = 
    @{
        AuthUrl = $authUrl
        AuthEmail = $authEmail 
        AuthPassword = $authPassword 
        Description = "Get all Products without 'Address' expanded"
        Endpoint = "http://localhost:5124/odata/Products" 
        RequestedScopes = "Products.Read Products.ReadByKey"
    },    
    @{
        AuthUrl = $authUrl
        AuthEmail = $authEmail 
        AuthPassword = $authPassword 
        Description = "Get all Products with 'Address' expanded. Missing Scope: 'Products.ReadAddress'"
        Endpoint = "http://localhost:5124/odata/Products?`$expand=Address" 
        RequestedScopes = "Products.Read Products.ReadByKey"
    },
    @{
        AuthUrl = $authUrl
        AuthEmail = $authEmail 
        AuthPassword = $authPassword 
        Description = "Get all Products with 'Address' expanded. Valid Required Scopes."
        Endpoint = "http://localhost:5124/odata/Products?`$expand=Address" 
        RequestedScopes = "Products.Read Products.ReadByKey Products.ReadAddress"
    }

foreach ( $request in $requests )
{
    Send-ODataRequest @request
}

And we end up with the following output (it will be nicely colored in the Terminal):

PS C:\Users\philipp\source\repos\bytefish\ODataAuthorization\samples\JwtAuthenticationExample\PowershellScripts> .\ODataQueries.ps1
[REQ]
[REQ] OData Request
[REQ]
[REQ]   Description:    Get all Products without 'Address' expanded
[REQ]   URL:            http://localhost:5124/odata/Products
[REQ]   Scopes:         Products.Read Products.ReadByKey
[REQ]
[RES]    HTTP Status:    200
[RES]    Body:           [
  {
    "Id": 1,
    "Name": "Macbook M1",
    "Price": 3000,
    "AddressId": 1
  },
  {
    "Id": 2,
    "Name": "Macbook M2",
    "Price": 3500,
    "AddressId": 1
  },
  {
    "Id": 3,
    "Name": "iPhone 14",
    "Price": 1400,
    "AddressId": 1
  }
]
[REQ]
[REQ] OData Request
[REQ]
[REQ]   Description:    Get all Products with 'Address' expanded. Missing Scope: 'Products.ReadAddress'
[REQ]   URL:            http://localhost:5124/odata/Products?$expand=Address
[REQ]   Scopes:         Products.Read Products.ReadByKey
[REQ]
[ERR] Request failed with StatusCode: 403
[REQ]
[REQ] OData Request
[REQ]
[REQ]   Description:    Get all Products with 'Address' expanded. Valid Required Scopes.
[REQ]   URL:            http://localhost:5124/odata/Products?$expand=Address
[REQ]   Scopes:         Products.Read Products.ReadByKey Products.ReadAddress
[REQ]
[RES]    HTTP Status:    200
[RES]    Body:           [
  {
    "Id": 1,
    "Name": "Macbook M1",
    "Price": 3000,
    "AddressId": 1,
    "Address": {
      "Id": 1,
      "Country": "USA",
      "City": "California"
    }
  },
  {
    "Id": 2,
    "Name": "Macbook M2",
    "Price": 3500,
    "AddressId": 1,
    "Address": {
      "Id": 1,
      "Country": "USA",
      "City": "California"
    }
  },
  {
    "Id": 3,
    "Name": "iPhone 14",
    "Price": 1400,
    "AddressId": 1,
    "Address": {
      "Id": 1,
      "Country": "USA",
      "City": "California"
    }
  }
]