AAD Password Grant PowerShell class for use with Business Central

After my post yesterday, I have a quick way to register Business Central APIs for AAD authentication. That’s working well and I can use Postman to test APIs but I need to be able to call APIs in PowerShell.

After a lot of time spent reading Microsoft articles on OAuth 2.0 like this and some articles on Graph, I Googled around the problem and found a solution on reddit/r/PowerShell.

The result is a class that you can use to get the authentication token using a password grant request.

It’s on my GitHub and below :-

#  References.
#  https://docs.microsoft.com/en-us/azure/active-directory/develop/v1-protocols-oauth-code
#  https://www.reddit.com/r/PowerShell/comments/9clts3/powershell_automation_with_oauth2/
#

#
# Class to get an OAuth 2.0 authentication token from BC using a Password Grant.
#
class AADPasswordGrant {
    [string]$token                  #Token that we need to get to Authenticate with later
    [string]$tenantId               #BC Tenant ID
    [string]$clientId               #The ApplicationId that was registed for BC in AAD.   
    [string]$username               #BC username
    [string]$password               #BC Password 
    [System.Security.SecureString]$securePasswordStr #BC Password we will convert to a secure string later
    [string]$securePasswordBStr     #BSTR version of the secure password  
    [string]$clientSecret           #Key that was generated when registring BC in AAD
    [string]$grantType      = "password"  #This must be password so we are not challenged or have to use a form.  
    [string]$callbackUrl    = "https://businesscentral.dynamics.com/"              #The same callback registered for BC in AAD
    [string]$accessTokenUrl = "https://login.windows.net/{tenantId}/oauth2/token"  #Url to request the token from
    [string]$resourceUrl    = "https://api.businesscentral.dynamics.com"           #The resource we want to talk to
    [string]$scopeUrl       = "https://api.businesscentral.dynamics.com"           #The resource we want to talk to
   
    AADPasswordGrant([string]$tenandId, [string]$clientId, [string]$clientSecret, [string]$userName, [string]$password) {
        $this.tenantId     = $tenandId
        $this.clientId     = $clientId
        $this.clientSecret = $clientSecret
        $this.userName     = $userName
        $this.securePasswordStr  = (ConvertTo-SecureString -String $password -AsPlainText -Force)
        $this.securePasswordBStr = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto([System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($this.securePasswordStr))
        $this.accessTokenUrl     = $this.accessTokenUrl.Replace('{tenantId}', $tenandId)
    }

    [void]TryGetAuthorisationToken () {
        $body = @{
            grant_type    = $this.grantType
            username      = $this.userName
            password      = $this.securePasswordBStr
            client_id     = $this.clientId
            client_secret = $this.clientSecret
            scope         = $this.scopeUrl
            redirect_uri  = $this.callbackUrl
            resource      = $this.resourceUrl
        }
        $authResult = Invoke-RestMethod -Method Post -Uri $this.accessTokenUrl -Body $body
        $this.token =$authResult.access_token
    }
}

You can test the class with the code below, using a BC tenant of your own. The values here are, of course, fudged and provided for example only.

#
# Test the Class Here
#
$tenantId     = 'fa75f4db-5231-6eb5-9ec4-ddb74cd648e'      #Your BC tennantID
$clientId     = 'b94639c8-553a-6a7d-ad54-d36463d3729'      #The id that BC is registered with in AAD  
$clientSecret = '5P00Zg29GGg6rnllUg0Fad9SSFC8lWVGJyOnsF0=' #The secret key that was created when registering BC for AAD Auth
$username     = 'bcuser@domain.onmicrosoft.com'            #Username for the passowrd grant
$password     = 'user.password@1234'                       #Password in plain text

[AADPasswordGrant]$aadPasswordGrant = [AADPasswordGrant]::new($tenantId, $clientId, $clientSecret, $username, $password)
$aadPasswordGrant.TryGetAuthorisationToken()

$companiesUrl = "https://api.businesscentral.dynamics.com/v1.0/api/microsoft/automation/beta/companies"
$requestHeaders = @{ 'Authorization' = 'Bearer ' + $aadPasswordGrant.token }
$result = Invoke-RestMethod -Uri $usersUrl -Headers $requestHeaders -Method Get
$result.value | Out-GridView

Hope it helps you out

/cal;

Use PowerShell to setup AAD Authentication for Business Central APIs

After spending some time trying to play with the Automation APIs for BC it became clear that using the Basic Authentication with Username and Password was not going to work for me.

I have read Vjeko’s article: How do I: Really set up Azure Active Directory based authentication for Business Central APIs and know that navcontainerhelper does setup AAD authentication setup for you.

I will probably have to set this up a few times for colleagues and clients, so I thought that it would save a bit of time in the future if I hacked this script together from Freddy’s work and tested its usage with the help of Vjeko’s article.

Thanks and credit to Vjeko and Freddy for their efforts.

I have it on GitHub here

You will need to install Nuget Package Provider the AzureAD Package (if you dont have them already)

Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force -WarningAction Ignore | Out-Null
Install-Package AzureAD -Force -WarningAction Ignore | Out-Null

You can now use the script below to setup for your BC tenant

$bcAppDisplayName = "BC OAuth 2.0"
$bcSignOnURL      = "https://businesscentral.dynamics.com/"
$bcAuthURL        = "https://login.windows.net/{bcDirectoryId}/oauth2/authorize?resource=https://api.businesscentral.dynamics.com"
$bcAccessTokenURL = "https://login.windows.net/{bcDirectoryId}/oauth2/token?resource=https://api.businesscentral.dynamics.com"

function Create-AesKey {
    $aesManaged = New-Object "System.Security.Cryptography.AesManaged"
    $aesManaged.Mode = [System.Security.Cryptography.CipherMode]::CBC
    $aesManaged.Padding = [System.Security.Cryptography.PaddingMode]::Zeros
    $aesManaged.BlockSize = 128
    $aesManaged.KeySize = 256
    $aesManaged.GenerateKey()
    [System.Convert]::ToBase64String($aesManaged.Key)
}

$AesKey = Create-AesKey

# Login to AzureRm
$AadAdminCredential = Get-Credential
$account = Connect-AzureAD -Credential $AadAdminCredential
$bcDirectoryId = $account.Tenant.Id

#Delete the old one if it is there
Get-AzureADApplication -All $true | Where-Object { $_.DisplayName.Contains($bcAppDisplayName) } | Remove-AzureADApplication

#Create New One
$ssoAdApp = New-AzureADApplication -DisplayName $bcAppDisplayName -Homepage $bcSignOnURL -ReplyUrls ($bcSignOnURL)

# Add a key to the AAD App Properties
$SsoAdAppId = $ssoAdApp.AppId.ToString()
$AdProperties = @{}
$AdProperties["AadTenant"] = $account.TenantId
$AdProperties["SsoAdAppId"] = $SsoAdAppId
$startDate = Get-Date
New-AzureADApplicationPasswordCredential -ObjectId $ssoAdApp.ObjectId `
                                         -Value $AesKey `
                                         -StartDate $startDate `
                                         -EndDate $startDate.AddYears(10) | Out-Null
#
#Set the permissions
#

# Windows Azure Active Directory -> Delegated permissions for Sign in and read user profile (User.Read)
$req1 = New-Object -TypeName "Microsoft.Open.AzureAD.Model.RequiredResourceAccess" 
$req1.ResourceAppId = "00000002-0000-0000-c000-000000000000"
$req1.ResourceAccess = New-Object -TypeName "Microsoft.Open.AzureAD.Model.ResourceAccess" -ArgumentList "311a71cc-e848-46a1-bdf8-97ff7156d8e6","Scope"

# Dynamics 365 Business Central -> Delegated permissions for Access as the signed-in user (Financials.ReadWrite.All)
$req2 = New-Object -TypeName "Microsoft.Open.AzureAD.Model.RequiredResourceAccess" 
$req2.ResourceAppId = "996def3d-b36c-4153-8607-a6fd3c01b89f"
$req2.ResourceAccess = New-Object -TypeName "Microsoft.Open.AzureAD.Model.ResourceAccess" -ArgumentList "2fb13c28-9d89-417f-9af2-ec3065bc16e6","Scope"

Set-AzureADApplication -ObjectId $ssoAdApp.ObjectId -RequiredResourceAccess @($req1, $req2)

#Write out information for the developer
Write-Host "AesKey / Client Secret" $AesKey
Write-Host "Directory Id" $bcDirectoryId
Write-Host "Application Id / Client ID" $ssoAdApp.AppId
Write-Host "Call Back Url" $bcSignOnURL
Write-Host "Auth URL" $bcAuthURL.Replace('{bcDirectoryId}', $bcDirectoryId)
Write-Host "Access Token URL" $bcAccessTokenURL.Replace('{bcDirectoryId}', $bcDirectoryId)

/cal;

Updating a PowerShell module

I needed to update my navcontainerhelper to be able to use the new features that Freddy had blogged about here and in this tweet :-

 

To do that I did the following :-

I checked my current navcontainerhelper version by running the below commands from an elevated PowerShell ISE

Import-Module navcontainerhelper
Get-Module navcontainerhelper | Select Version

The output told me I was running a rather old version : 0.3.1.4

Version
-------
0.3.1.4

So, to update it I uninstalled and reinstalled it with :-

Uninstall-Module navcontainerhelper -RequiredVersion '0.3.1.4'
Install-Module navcontainerhelper

After accepting the warnings and then closing and re-opening my PowerShell ISE, I could check that my version had been updated by running the Import-Module and Get-Module from my first step.

Version
-------
0.5.0.6

I have had to used this method to update some of my Office 365 and Azure PowerShell modules in the past to get access to some of the new commands.

Now to experiment with running tests using the new navcontainerhelper commands…

/cal;