Crying Cloud

Generating HTML Reports in PowerShell – Part 4

HTMLReport.jpg

Almost doneI'm sure you would like to have something other than "Your Logo Here" at the top of these reports, well you can.  I have modified the module to accept two methods for rendering logos.  The Module will now to accept a file path to a logo or alternatively you can code your base64 string into the module which is the default if no files are specified.

Pre-requisites Again, we are going to use the code to create the Azure VMs recordset and other session variables we built up in Part 1. The module is available through the PowerShell Gallery or can be installed from PowerShell using install-module -name ReportHTML.  Additionally you can contribute via github ReportHTML which also has and all the Examples scripts for download. You can get the example code for part 4 from these Github links with comments and without comment.

Using images for Logos (Example 12) This example will utilize two jpg files, clientlogo and mainlogo and will encode these into base64 strings in the Get-HTMLClose function.

[powershell] ####### Example 12 ######## # The two logo files are stored in the report path $MainLogoFile = join-path $ReportOutputPath "ACELogo.jpg" $ClientLogoFile = join-path $ReportOutputPath "YourLogo.jpg"

$rpt = @() $rpt += Get-HtmlOpen -TitleText ($ReportName + " Example 12") $rpt += Get-HtmlContentOpen -HeaderText "Size Summary" $rpt += Get-HtmlContentTable ($RMVMArray | group Size | select name, count | sort count -Descending) $rpt += Get-HtmlContentClose

# In this case we are going to swap the logos around using ClientLogoFile and MainLogoFile parameters and switching the files used $rpt += Get-HtmlClose -ClientLogoFile $MainLogoFile -MainLogoFile $ClientLogoFile Test-Report -TestName Example12 [/powershell]

ReportExample12

Change logos in the module (Example 13) This example show how the default option works. There are 5 client logo base64 strings encoded into the module. Simply calling the module with -ClientLogoType ClientLogo1 to ClientLogo5 with use a switch statement to select which logo to use. You can use Powershell or a website to create the encoding string. This string can then be put into the module. This obviously breaks receiving updates so I would recommend not using this but it's there at the moment.  Below shows the code where the ClientLogo and MainLogo strings are located.

LogosInModule

[powershell] ####### Example 13 ######## $rpt = @() $rpt += Get-HtmlOpen -TitleText ($ReportName + " Example 13") $rpt += Get-HtmlContentOpen -HeaderText "Size Summary" $rpt += Get-HtmlContentTable ($RMVMArray | group Size | select name, count | sort count -Descending) $rpt += Get-HtmlContentClose

# We have been using Get-HTMLClose up until now which has a default of ClientLogo1 # In this case we can specify ClientLogo5 $rpt += Get-HtmlClose -ClientLogoType ClientLogo5

Test-Report -TestName Example13 [/powershell]

Using images for Logos (Example 14) This example we can pass in the base 64 string directly making moving a report around without an image file easier. This uses the ClientLogoBase64 and MainLogoBase64 parameters.

Edit Please note I uploaded some bad code relating to get-htmlclose and logos please update to 1.0.0.12.  Apologies for any inconvenience. 

[powershell] ####### Example 14 ######## # for this we need to get the file and create the string. You could do this once and code the base64 string into the script $MainLogoFilePath = join-path $ReportOutputPath "ACELogo.jpg" $ClientLogoFilePath = join-path $ReportOutputPath "YourLogo.jpg" $MainBase64 = [Convert]::ToBase64String((Get-Content $ClientLogoFilePath -Encoding Byte)) $clientBase64 = [Convert]::ToBase64String((Get-Content $MainLogoFilePath -Encoding Byte))

# if you run the $clientBase64 and copy the content into a here string you can create the logo image without access to the file.

$rpt = @() $rpt += Get-HtmlOpen -TitleText ($ReportName + " Example 14") $rpt += Get-HtmlContentOpen -HeaderText "Size Summary" $rpt += Get-HtmlContentTable ($RMVMArray | group Size | select name, count | sort count -Descending) $rpt += Get-HtmlContentClose $rpt += Get-HtmlClose -ClientLogoBase64 $MainBase64 -MainLogoBase64 $MainBase64

Test-Report -TestName Example14 [/powershell]

ReportExample14

Conclusion I really hope you find this useful and hope it can help you generate HTML reports on the fly to make someone's job easier.  There is another function ConvertTo-AdvHTML here that requires some HTML knowledge. However this output could be intertwined into ReportHTML module for advanced usage.  There is also a module here which has column sort which could be merged. There is a lot of potential here for expansion on what is there and a lot of room to improve as well.  Please feel free to post suggestions, contact me or contribute via Github.

Good luck.

Part 1 | Part 2 | Part 3 | Part 4 | More

Generating HTML Reports in PowerShell – Part 3

HTMLReport.jpg

Welcome BackYou're still here, hopefully you got the content working from Part 1 and Part 2 in this series about generating HTML reports in Powershell. Now let's have a look at creating a Pie Chart in the script.  The module is available through the PowerShell Gallery or can be installed from PowerShell using install-module -name ReportHTML.  Additionally you can contribute via github ReportHTML which also has and all the Examples scripts for download.

Pre-requisites Again, we are going to use the code to create the Azure VMs recordset and other session variables we built up in Part 1.  You can get the code for part 3 from these Github links with comments and without comment

Module Changes I have added two functions to the module to help create Pie charts.  The first function creates an object that contains the properties for creating the chart.  There is lot of room for expansion and options to create different chart styles I will discuss at the end of this post. Create-HTMLPieChartObject will create a custom object with default chart properties that we pass to the Create-HTMLPieChart function along with a grouped recordset.  Simply using the Powershell group command on an array will create an array with name and count properties that get used as the data points on the chart.  You can alternatively create your own array with Name and Count headings or use and expression to change the existing headings to name and count, meaning you dont have to use group by function.  Please make sure to update your local module files from Github

Creating a Pie Chart (Example 9) First we will create a very basic chart showing a summary of virtual machine power state.

[powershell] ####### Example 9 ######## # First we create a PieChart Object and load it into a variable $PieChartObject = Create-HTMLPieChartObject

# Have a look at what is in this object. $PieChartObject

# Let's set one property $PieChartObject.Title = "VMs Power State"

$rpt = @() $rpt += Get-HtmlOpen -TitleText ($ReportName + " Example 9") $rpt += Get-HtmlContentOpen -HeaderText "Chart Series" $rpt += Create-HTMLPieChart -PieChartObject $PieChartObject -PieChartData ($RMVMArray | group powerstate) $rpt += Get-HtmlContentClose $rpt += Get-HtmlClose

Test-Report -TestName Example9 [/powershell]

ReportExample9

Pie Chart & additional Properties (Example 10) Let's have a look at some of the properties we can set

[powershell] ####### Example 10 ######## $PieChartObject = Create-HTMLPieChartObject $PieChartObject.Title = "VMs Sizes Deployed"

# There is a lot of data so let's make the pie chart a little bigger and explode the largest value $PieChartObject.Size.Height = 600 $PieChartObject.Size.Width = 600 $PieChartObject.ChartStyle.ExplodeMaxValue = $true

$rpt = @() $rpt += Get-HtmlOpen -TitleText ($ReportName + " Example 10") $rpt += Get-HtmlContentOpen -HeaderText "Chart Series"

# To summarize the data I have simply changed the group by property to size $rpt += Create-HTMLPieChart -PieChartObject $PieChartObject -PieChartData ($RMVMArray | group size) $rpt += Get-HtmlContentClose $rpt += Get-HtmlClose Test-Report -TestName Example10 [/powershell]

ReportExample10

Pie Chart & Results Table (Example 11) Here is a quick example with a pie chart and results table

[powershell] ####### Example 11 ######## $PieChartObject1 = Create-HTMLPieChartObject $PieChartObject.Title = "VMs Powerstate" $PieChartObject2 = Create-HTMLPieChartObject $PieChartObject.Title = "VMs Sizes" $PieChartObject.Size.Height = 800 $PieChartObject.Size.Width = 800 $PieChartObject.ChartStyle.ExplodeMaxValue = $true

$rpt = @() $rpt += Get-HtmlOpen -TitleText ($ReportName + " Example 10") $rpt += Get-HtmlContentOpen -HeaderText "Power Summary" $rpt += Create-HTMLPieChart -PieChartObject $PieChartObject1 -PieChartData ($RMVMArray | group powerstate) $rpt += Get-HtmlContentTable ($RMVMArray | group powerstate | select name, count) $rpt += Get-HtmlContentClose $rpt += Get-HtmlContentOpen -HeaderText "VM Size Summary" $rpt += Create-HTMLPieChart -PieChartObject $PieChartObject2 -PieChartData ($RMVMArray | group size) $rpt += Get-HtmlContentTable ($RMVMArray | group Size | select name, count | sort count -Descending) $rpt += Get-HtmlContentClose $rpt += Get-HtmlClose

Test-Report -TestName Example11 [/powershell]

ReportExample11

More Charting Properties There are a lot of properties that can be added to this object. I even thought about creating a couple of defaults sets of properties, for instance defaults for 'Exploded', 'SLA' or 'DisplayValues'. If you want to help expand this chart object, please contribute and edit the github project. The options for charting can be found at microsoft in the Class Reference

Additionally I would like to create some chart functions for line and bar charts, however I haven't had the time to create those functions recently.  If you want to explore this please contribute to github or you can reach out and we can discuss how to go about this.

Summary We have one component left to cover which is changing the logos at the top left and top right that are displayed on the report. Currently these base64 strings are hard coded in the module.  I will be working to create some more dynamics options before posting about this in Part 4.

Part 1 | Part 2 | Part 3 | Part 4 | More

Generating HTML Reports in PowerShell – Part 2

HTMLReport.jpg

Welcome BackHopefully you found Part 1 in this series about generating HTML reports useful. That was an introduction to the basic functions and how to construct a report using ReportHTML module, available through the PowerShell Gallery or can be installed from PowerShell using install-module -name ReportHTML.  Additionally you can contribute via github ReportHTML. Now we are going to build upon those functions and show what else we can do with this module.

Pre-requisites To continue with these examples you will need the code to create the Azure VMs recordset and other session variables from Part 1.  Here is a link for the code for part 2 with comments Report-AzureVMsExamples_Part2WithComments and code without comments Report-AzureVMsExamples_Part2

Adding HyperLinks (Example 6) In this example we are going to create a Hyperlink as a string of values. To do this we need to wrap the hyperlink and the link name in three strings URL01, URL02 and URL03. The construction of a link will look like this. ("URL01 "+ HyperLink + "URL02" + LinkName + "URL03"). We will do this in the below example and create the structure of a hyperlink to the Azure Virtual Machine for each row in the recordset.

[powershell] ####### Example 6 ######## # We need to set some variables for building the URLs to the Azure Resource $Base = "https://portal.azure.com/#resource/subscriptions/" $SubID = $AzureRMAccount.Context.Subscription.SubscriptionId $RG = "/resourceGroups/" $vm = "/providers/Microsoft.Compute/virtualMachines/"

$rpt = @() $rpt += Get-HtmlOpen -TitleText ($ReportName + " Example 6") $rpt += Get-HtmlContentOpen -HeaderText "Virtual Machines"

# Using the expression we can create an expression for each record. # We string the base URL with the subscription ID add the resource group and VM name, # This link is used between URL01 and URL02 we reuse the VM name for the link name and add URL03 $rpt += Get-HtmlContentTable ($RMVMArray | select ResourceGroup, ` @{Name="Azure VM";Expression={"URL01" + $Base + $SubID + $RG + $_."ResourceGroup" + $vm + $_."Name" + "URL02" + $_."Name" + "URL03" }}, ` location, Size,PowerState, DataDiskCount, ImageSKU ) -GroupBy ResourceGroup

$rpt += Get-HtmlContentClose $rpt += Get-HtmlClose

Test-Report -TestName Example6 [/powershell]

ReportExample6

Adding dynamic row colors (Example 7 & 8) In this example we are going to use an to add a colour name to a recordset, which when passed passed to the table content function will render in HTML with colour. There are three colors you can set, green, yellow and red. The first step is to create an expressions that can be evaluated. For this example we are going to color row based on the number of hard disks attached to the VM. You must use a single quote for this expression, that is '. In the expression the work $this is used to represent the current record. In this case we create three expressions, one for each color.

[powershell] ####### Example 7 ######## $VMs = ($RMVMArray | select ResourceGroup, Name, Location, Size,PowerState, DataDiskCount, ImageSKU )

# You must use single quotes here for the expression $Red = '$this.DataDiskCount -ge 2' $Yellow = '$this.DataDiskCount -eq 1' $Green = '$this.DataDiskCount -lt 1'

# call the function and pass the array and color expressions $VMsColoured = Set-TableRowColor $VMs -Red $Red -Yellow $Yellow -Green $Green

# let's just see what the function did $VMsColoured | select ResourceGroup ,name, datadiskcount , RowColor -first 30

$rpt = @() $rpt += Get-HtmlOpen -TitleText ($ReportName + "Example 7") $rpt += Get-HtmlContentOpen -HeaderText "Virtual Machines" $rpt += Get-HtmlContentTable $VMsColoured $rpt += Get-HtmlContentClose $rpt += Get-HtmlClose

Test-Report -TestName Example7 [/powershell]

ReportExample7

[powershell] ####### Example 8 ######## $rpt = @() $rpt += Get-HtmlOpen -TitleText ($ReportName + "Example 8") $rpt += Get-HtmlContentOpen -HeaderText "Virtual Machines"

# here we can sort the rows so the colours are grouped together $rpt += Get-HtmlContentTable ( $VMsColoured | sort DataDiskCount) $rpt += Get-HtmlContentClose $rpt += Get-HtmlClose

[/powershell]

ReportExample8

Summary I hope this has been a useful and you can see potential for using this module in your own scripting. There is still a few more examples to work through. Stay tuned for Part 3. Additionally I will be loading the project into Github.

Part 1 | Part 2 | Part 3 | Part 4 | More

Generating HTML Reports in PowerShell - Part 1

HTMLReport.jpg

The problemPowerShell is an amazing tool for gathering, collecting, slicing, grouping, filtering and collating data.  However, trying to show that information or several sets of it on one report is not as easy.  A few years we ago built our own solution, we created a set of HTML reporting functions.  I have been using these functions for years to help myself, my team and customers to deliver Powershell data to people that just need the details and not a CSV file or a code snippet. We've now decided to make these available to the rest of you.

This solution This is developed as a Powershell module.  It is now available through the PowerShell Gallery or can be installed from PowerShell using install-module -name ReportHTML. Additionally it can be accessed from github ReportHTML, download and deploy to an appropriate module directory for example 'C:\Users\User\Documents\WindowsPowerShell\Modules\' It can be deployed and run ad-hoc or with your scheduled report Powershell jobs.  There are several ways to build up a report. In this post we will build five example reports based around an Azure VMs array.

The report at a glance This screenshot of a report that shows information about patching results grouped in two sections, successful and failed patches. ReportingSS1

This screenshot of a report that shows multiple collapsible sections with an array of functions from within a file displayed in an open section. ReportingSS2

Here is a more complete report I have created at the end of this series in More, Part 5 just to give you an idea of what is possible.  I provide the script and this sample in the link above.  Please note there is java script to enable hiding of sections which does generate a warning in your browser.  I do explain this later in this post. SystemReportScreenShot

This Blog Series There are lots of different ways to leverage this module including changing the logos, highlighting rows, creating different sections with code loops and much more.  This first article is going to work through 5 examples to get you started.  Here are two versions of the same code detailed below or you can create your own script with the code snippets as we work through the examples. You will need to save the file to a local folder or provide a report output path parameter. - A version without all the comments Report-AzureVMsExamples_Part1 - A version with all the comments Report-AzureVMsExamples_Part1WithComments

Reporting functions summary There are a handful of functions that generate HTML code, you string this code together and then save the content as a file.  This code was originally borrowed from Alan Renouf for a vSphere healthcheck report by Andrew Storrs and myself for a more dynamic reporting style, being able to create reports on the fly with minimal effort. In addition these reports once built can be scheduled to run, dropped on a file share or emailed.  I will outline the main functions and then build a report collecting information about virtual machines from an Azure subscription.  will walk through several examples of how to use the functions to generate different types of reports.

  • Get-HtmlOpen
  • Get-HtmlClose
  • Get-HtmlContentOpen
  • Get-HtmlContentClose
  • Get-HtmlContentTable
  • Get-HtmlContentText
  • Set-TableRowColor
  • New-HTMLPieChartObject
  • New-HTMLPieChart
  • New-HTMLBarChartObject
  • New-HTMLBarChart
  • Get-HTMLColumn1of2
  • Get-HTMLColumn2of2
  • Get-HTMLColumnClose

Let's get started First let's create the header section. This contains some parameters, report file path, reportname, loads the ReportHTML module and checks for an Azure Account and if one isn't present asks displays a login prompt.

[powershell] param ( $ReportOutputPath )

Import-Module ReportHtml Get-Command -Module ReportHtml

$ReportName = "Azure VMs Report"

if (!$ReportOutputPath) { $ReportOutputPath = Split-Path -Path $MyInvocation.MyCommand.Definition -Parent }

# see if we already have a session. If we don't don't re-authN if (!$AzureRMAccount.Context.Tenant) { $AzureRMAccount = Add-AzureRmAccount } [/powershell]

Building a recordset We will need a record set to work with.  I am going to take some code Barry Shilmover shared here and add resource group name as a property to or an array of VMs

[powershell] # Get arrary of VMs from ARM $RMVMs = get-azurermvm

$RMVMArray = @() ; $TotalVMs = $RMVMs.Count; $i =1

# Loop through VMs foreach ($vm in $RMVMs) { # Tracking progress Write-Progress -PercentComplete ($i / $TotalVMs * 100) -Activity "Building VM array" -CurrentOperation ($vm.Name + " in resource group " + $vm.ResourceGroupName)

# Get VM Status (for Power State) $vmStatus = Get-AzurermVM -Name $vm.Name -ResourceGroupName $vm.ResourceGroupName -Status

# Generate Array $RMVMArray += New-Object PSObject -Property @{`

# Collect Properties ResourceGroup = $vm.ResourceGroupName Name = $vm.Name; PowerState = (get-culture).TextInfo.ToTitleCase(($vmStatus.statuses)[1].code.split("/")[1]); Location = $vm.Location; Tags = $vm.Tags Size = $vm.HardwareProfile.VmSize; ImageSKU = $vm.StorageProfile.ImageReference.Sku; OSType = $vm.StorageProfile.OsDisk.OsType; OSDiskSizeGB = $vm.StorageProfile.OsDisk.DiskSizeGB; DataDiskCount = $vm.StorageProfile.DataDisks.Count; DataDisks = $vm.StorageProfile.DataDisks; } $I++ } [/powershell]

Testing the report We are just going to write a short function to generate and invoke the report file

[powershell] Function Test-Report { param ($TestName) $rptFile = join-path $ReportOutputPath ($ReportName.replace(" ","") + "-$TestName" + ".mht") $rpt | Set-Content -Path $rptFile -Force Invoke-Item $rptFile sleep 1 } [/powershell]

The process of building the report Let's run this line to see what happens and what output we get.

[powershell] Get-HtmlContentOpen -HeaderText "Virtual Machines" [/powershell]

You should see this HTML as output. Each function generates HTML code, incorporating the parameters you pass it. We are going to collect all this code into an array variable called $rpt.

[html]

<div class="section">

<div class="header"> <a name="Virtual Machines">Virtual Machines</a> </div>

<div class="content" style="background-color:#ffffff;"> [/html]

Building a Basic Report (Example 1) Let's generate a very quick report before we dive into some of the other features and functions.  We want to display the VM array in an table.

[powershell] ####### Example 1 ####### # Create an empty array for HTML strings $rpt = @()

# note from here on we always append to the $rpt array variable. # First, let's add the HTML header information including report title $rpt += Get-HtmlOpen -TitleText $ReportName

# This content open function add a section header $rpt += Get-HtmlContentOpen -HeaderText "Virtual Machines"

# This creates an HTML table of whatever array you pass into the function $rpt += Get-HtmlContentTable $RMVMArray

# This content close function closes the section $rpt += Get-HtmlContentClose

# This HTML close adds HTML footer $rpt += Get-HtmlClose

# Now let's test what we have Test-Report -TestName Example1 [/powershell]

RptExample1

Allow Blocked Content Depending on your browser settings you may receive an warning asking if you want to 'Allow blocked content'.  This is the Javascript function to optionally hide sections of the report. You can click allow blocked content or change your IE settings. ReportBlockContent

Order and grouping data (Example 2) Here we will select specific columns from the array, with the column we are grouping by being the first in the select statement

[powershell] ####### Example 2 ######## $rpt = @() $rpt += Get-HtmlOpen -TitleText $ReportName $rpt += Get-HtmlContentOpen -HeaderText "Virtual Machines"

# here we are going to filter the recordset, reorder the columns and group the results by location. # The value you group by must be first in the select statement $rpt += Get-HtmlContentTable ($RMVMArray | select Location, ResourceGroup, Name, Size,PowerState,DataDiskCount, ImageSKU ) -GroupBy Location $rpt += Get-HtmlContentClose $rpt += Get-HtmlClose

Test-Report -TestName Example2 [/powershell]

ReportExample2

Creating more sections and hiding them (Example 3) Let's create a summary section and a section about VM size counts and we will hide two sections

[powershell] ####### Example 3 ######## $rpt = @() $rpt += Get-HtmlOpen -TitleText $ReportName

# adding the summary section $rpt += Get-HtmlContentOpen -HeaderText "Summary Information" $rpt += Get-HtmlContenttext -Heading "Total VMs" -Detail ( $RMVMArray.Count) $rpt += Get-HtmlContenttext -Heading "VM Power State" -Detail ("Running " + ($RMVMArray | ? {$_.PowerState -eq 'Running'} | measure ).count + " / Deallocated " + ($RMVMArray | ? {$_.PowerState -eq 'Deallocated'} | measure ).count) $rpt += Get-HtmlContenttext -Heading "Total Data Disks" -Detail $RMVMArray.datadisks.count $rpt += Get-HtmlContentClose

# adding the VM size section. Note the -IsHidden switch $rpt += Get-HtmlContentOpen -HeaderText "VM Size Summary" -IsHidden $rpt += Get-HtmlContenttable ($RMVMArray | group size | select Name, Count | sort count -Descending ) -Fixed $rpt += Get-HtmlContentClose

# Note I have also added the -IsHidden Switch here $rpt += Get-HtmlContentOpen -HeaderText "Virtual Machines" -IsHidden $rpt += Get-HtmlContentTable ($RMVMArray | select Location, ResourceGroup, Name, Size,PowerState, DataDiskCount, ImageSKU ) -GroupBy Location $rpt += Get-HtmlContentClose $rpt += Get-HtmlClose

Test-Report -TestName Example3 [/powershell]

ReportExample3

Looping with foreach and section background shading (Example 4) We are going group the recordset by location and add a foreach loop.

[powershell] ####### Example 4 ######## $rpt = @() $rpt += Get-HtmlOpen -TitleText $ReportName $rpt += Get-HtmlContentOpen -HeaderText "Summary Information" $rpt += Get-HtmlContenttext -Heading "Total VMs" -Detail ( $RMVMArray.Count) $rpt += Get-HtmlContenttext -Heading "VM Power State" -Detail ("Running " + ($RMVMArray | ? {$_.PowerState -eq 'Running'} | measure ).count + " / Deallocated " + ($RMVMArray | ? {$_.PowerState -eq 'Deallocated'} | measure ).count) $rpt += Get-HtmlContenttext -Heading "Total Data Disks" -Detail $RMVMArray.datadisks.count $rpt += Get-HtmlContentClose $rpt += Get-HtmlContentOpen -HeaderText "VM Size Summary" -IsHidden $rpt += Get-HtmlContenttable ($RMVMArray | group size | select Name, Count | sort count -Descending ) -Fixed $rpt += Get-HtmlContentClose

# We are introducing -BackgroundShade 2 so that we can clearly see the sections. # This helps with larger reports and many when there are many levels to the sections $rpt += Get-HtmlContentOpen -HeaderText "Virtual Machines by location" -IsHidden

# adding the foreach loop for the group recordset. foreach ($Group in ($RMVMArray | select Location, ResourceGroup, Name, Size,PowerState, DataDiskCount, ImageSKU | group location ) ) {

#for every group that exists for a location we will create an HTML section. I have also specified the -BackgroupShade to 1 $rpt += Get-HtmlContentOpen -HeaderText ("Virtual Machines for location '" + $group.Name +"'") -IsHidden -BackgroundShade 1

# Each recordset may have different data in the columns and therefore create different width in the table columns. # We would like it to look the same. We can use the -Fixed switch to produce evenly space columns for the table $rpt += Get-HtmlContentTable ($Group.Group | select ResourceGroup, Name, Size,PowerState, DataDiskCount, ImageSKU ) -Fixed $rpt += Get-HtmlContentClose } $rpt += Get-HtmlContentClose $rpt += Get-HtmlClose

Test-Report -TestName Example4 [/powershell]

ReportExample4

Filtering Sections based on Conditions (Example 5) This will cover adding some IF conditions to the syntax to display a section or not.

[powershell] ####### Example 5 ######## $rpt = @() $rpt += Get-HtmlOpen -TitleText ($ReportName + "Example 5") $rpt += Get-HtmlContentOpen -HeaderText "Summary Information" -BackgroundShade 1 $rpt += Get-HtmlContenttext -Heading "Total VMs" -Detail ( $RMVMArray.Count) $rpt += Get-HtmlContenttext -Heading "VM Power State" -Detail ("Running " + ($RMVMArray | ? {$_.PowerState -eq 'Running'} | measure ).count + " / Deallocated " + ($RMVMArray | ? {$_.PowerState -eq 'Deallocated'} | measure ).count) $rpt += Get-HtmlContenttext -Heading "Total Data Disks" -Detail $RMVMArray.datadisks.count $rpt += Get-HtmlContentClose $rpt += Get-HtmlContentOpen -HeaderText "VM Size Summary" -IsHidden -BackgroundShade 1 $rpt += Get-HtmlContenttable ($RMVMArray | group size | select Name, Count | sort count -Descending ) -Fixed $rpt += Get-HtmlContentClose $rpt += Get-HtmlContentOpen -HeaderText "Virtual Machines by location" -BackgroundShade 3 foreach ($Group in ($RMVMArray | select Location, ResourceGroup, Name, Size,PowerState, DataDiskCount, ImageSKU | group location ) ) {

# Here we are creating a group to use for the IF condition, so we can create sections for VMs by powerstate, Running or Deallocated $PowerState = $Group.Group | group PowerState $rpt += Get-HtmlContentOpen -HeaderText ("Virtual Machines for location '" + $group.Name +"' - " + $Group.Group.Count + " VMs") -IsHidden -BackgroundShade 2

# If there are VMs in the running group, continue and create a section for them if (($PowerState | ? {$_.name -eq 'running'})) { $rpt += Get-HtmlContentOpen -HeaderText ("Running Virtual Machines") -BackgroundShade 1 $rpt += Get-HtmlContentTable ($Group.Group | where {$_.PowerState -eq "Running"} | select ResourceGroup, Name, Size, DataDiskCount, ImageSKU ) -Fixed $rpt += Get-HtmlContentClose }

# If there are VMs in the running group, continue and create a section for them if (($PowerState | ? {$_.name -eq 'Deallocated'})) { $rpt += Get-HtmlContentOpen -HeaderText ("Deallocated") -BackgroundShade 1 -IsHidden $rpt += Get-HtmlContentTable ($Group.Group | where {$_.PowerState -eq "Deallocated"} | select ResourceGroup, Name, Size, DataDiskCount, ImageSKU)-Fixed $rpt += Get-HtmlContentClose } $rpt += Get-HtmlContentClose } $rpt += Get-HtmlContentClose $rpt += Get-HtmlClose

Test-Report -TestName Example5 [/powershell]

HTML Reporting Example 5

Summary I hope you have had success working through these examples and can find a use for this code.  Part 2 in this series will move into some more techniques and reporting functions. Please share any questions or issues you have executing this content.

Part 1 | Part 2 | Part 3 | Part 4 | More

Azure, Azure Active Directory, and PowerShell. The Hard Way

poshoauth.png

In my opinion, a fundamental shift for Windows IT professionals occurred with the release of Exchange 2007.  This established PowerShell as the tool for managing and configuring Microsoft enterprise products and systems going forward.  I seem to remember hearing a story at the time that a mandate was established for every enterprisey product going forward; each GUI action would have a corresponding PowerShell execution.  If anyone remembers the Exchange 2007 console, you could see that in action.  I won’t bother corroborating this story, because the end results are self-evident.  I can’t stress how important this was.  Engineers and administrators with development and advanced scripting skills were spared the further indignity of committing crimes against Win32 and COM+ across a hodgepodge of usually awful languages.  Windows administrators for whom automation and scripting only meant batch files, a clear path forward was presented.

PowerShell and Leaky Abstractions

For roughly two years now, the scope of my work has been mostly comprised of Azure integration and automation.  Azure proved to be no exception to the PowerShell new world order. I entered with wide-eyed optimism and I quickly discovered a great deal of things, usually of a more advanced nature, that could not be done in the portal and purportedly only via PowerShell. As I continue to receive product briefings, I have developed a bit of a pedantic pet-peeve.  PowerShell is always front and center in the presentations when referencing management, configuration, and automation.  However, I continue to see a general hand wave given as to the underlying technologies (e.g. WMI/CIM, REST API) and requirements.  I absolutely understand the intent, PowerShell has always been meant to provide a truly powerful environment in a manner that was highly accessible and friendly to the IT professional.  It has been a resounding success in that regard.  A general concern, I have, is that of too much abstraction.  There is a direct correlation between your frustration level and how far your understanding of what is going on is when an inevitable edge case is hit and the abstraction leaks.

Getting Back to the Point

All of that is a really long preface to the actual point of this post. I’ve never been a fan of the Azure Cmdlets for a number of reasons, most of which I don’t necessarily impugn the decisions made by Microsoft. To be honest, I think  both Switch-AzureMode (for those that remember) and the rapid release cadence that has introduced many understandably unavoidable breaking changes has really prejudiced me; as a result I tend to use the REST API almost exclusively. The fact is, modern systems and especially all of the micro-service architectures being touted are all powered by REST API. In the case of the Microsoft cloud, with only a few notable exceptions, authentication and authorization is handled via Azure Active Directory. It behooves the engineer or developer focused on Microsoft technologies to have a cursory understanding.  Azure Active Directory, Azure, and Office 365 are intrinsically linked. Every Azure and/or Office 365 Subscription is linked with an Azure AD tenant as the primary identity provider. The modern web seems to have adopted OAuth as an authorization standard and Azure AD can greatly streamline the authorization of web applications and API. The management and other API surfaces of Azure (and Azure Stack) and Office 365 have always taken advantage of this. The term you’ve likely heard thrown around is Bearer Token. That is more accurately described as an authorization header on the HTTP request containing a JWT (JSON Web Token).  My largest issue with the Azure and PowerShell automation has been the necessity to jump through hoops to simply obtain that token via PowerShell.  In 2016 a somewhat disingenuously Cmdlet named Get-AzureStackToken in the AzureRM.AzureStackAdmin module finally appeared.  I’m certain a large portion of the potential reading audience has used a tool like Fiddler, Postman, or even more recently resources.azure.com to either inspect or interact with these services.  Those who have can feel free to skip the straight to where this applies to PowerShell.

There are two types of applications you can create within Azure AD, each of with are identified with a unique Client Id and valid redirect URI(s) as the most relevant properties we’ll focus on.

Web Applications

  • Web applications in Azure Active Directory are OAuth2 confidential clients and likely the most appropriate option for modern (web) use cases.

  • Tokens are obtained on behalf of a user using the OAuth2 authorization grant flow. An authorization code or id token will be supplied to the specified redirect URI.

  • If needed, client credentials (a rolling secret key) can be used to obtain tokens on behalf of the user or on it’s own from the web application itself.

Native Applications

  • Native applications in Azure Active Directory are OAuth2 public clients (e.g. an application on a desktop or mobile device).

  • These applications can obtain a token directly (with managed organizational accounts) or use the authorization grant flow, but application level permissions are not applicable.

Getting to the PowerShell

I will focus primarily on the Native application type as it is most relevant to PowerShell. Most of the content will use Cmdlets from a module that will be available with this post.   The module is heavily derived/inspired by the ADAL libraries, has no external dependencies and accept a friendly PSCredential (with the appropriate rights) for any user authentication.  The Azure Cmdlets use a Native application with a Client Id of 1950a258-227b-4e31-a9cf-717495945fc2 and a redirect URI of urn:ietf:wg:oauth:2.0:oob (the prescribed default for native applications).   We’ll use this for our first attempt at obtaining a token for use against Azure Resource Manager or the legacy Service Management API.  A peculiar detail of Azure management is that this one of the scenarios a token is fungible for disparate endpoints. I always use https://management.core.windows.net as my audience regardless of whether I will be working with ARM or SM.  A token obtained from that audience will work the same as one from https://management.azure.com .

If all you would like is a snippet to obtain a token using the Azure, I’ll offer you a chance to bail out now:


$Resource='https://management.core.windows.net'
$PoshClientId="1950a258-227b-4e31-a9cf-717495945fc2"
$TenantId="yourdomain.com"
$UserName="username@$TenantId"
$Password="asecurepassword"|ConvertTo-SecureString -AsPlainText -Force
$Credential=New-Object pscredential($UserName,$Password)
Get-AzureStackToken -Resource $Resource -AadTenantId $TenantId -ClientId $PoshClientId -Credential $Credential -Authority "https://login.microsoftonline.com/$TenantId" 

A good deal of the functionality around provisioning applications and service principals has come to the Azure Cmdlets.  You can now create applications, service principals from the applications, and role assignments to the service principals. To create an application, in this case one that would own a subscription, you would write something like this:


$ApplicationSecret="ASuperSecretPassword!"
$TenantId='e05b8b95-8c85-49af-9867-f8ac0a257778'
$SubscriptionId='bc3661fe-08f5-4b87-8529-9190f94c163e'
$AppDisplayName='The Subscription Owning Web App'
$HomePage='https://azurefieldnotes.com'
$IdentifierUris=@('https://whereeveryouwant.com')
$NewWebApp=New-AzureRmADApplication -DisplayName $AppDisplayName -HomePage $HomePage `
    -IdentifierUris $IdentifierUris -StartDate (Get-Date) -EndDate (Get-Date).AddYears(1) `
    -Password $ApplicationSecret
$WebAppServicePrincipal=New-AzureRmADServicePrincipal -ApplicationId $NewWebApp.ApplicationId
$NewRoleAssignment=New-AzureRmRoleAssignment -ObjectId $NewWebApp.Id -RoleDefinitionName 'owner' -Scope "/subscriptions/$SubscriptionId"
$ServicePrincipalCred=New-Object PScredential($NewWebApp.ApplicationId,($ApplicationSecret|ConvertTo-SecureString -AsPlainText -Force))
Add-AzureRmAccount -Credential $ServicePrincipalCred -TenantId $TenantId -ServicePrincipal 

For those that stuck around, let’s take a look at obtaining JWT(s), inspecting them, and putting them to use.

I added a method for decoding the tokens, so we will have a look at the access token.  A JWT is comprised of a header, payload, and signature.  I will leave explaining the claims within the payload to identity experts.

Now that we have a token, let's use it for something useful, in this case we will ask Azure (ARM) for our associated subscriptions.

Examining the OAuth2 Flow

If you are not interested in what is going on behind the scenes feel free to skip ahead.  Each application exposes a standard set of endpoints and I will not discuss the v2.0 endpoint as I do not have enough experience using it.  There are two endpoints in particular to make note of, https://login.microsoftonline.com/{tenantid}/oauth2/authorize and https://login.microsoftonline.com/{tenantid}/oauth2/token, where {tenantid} represents the tenant id (guid or domain name) e.g. yourcompany.com or common for multi-tenant applications.  Azure AD obviously supports federation and the directing traffic to the appropriate authorization endpoint is guided by a user realm detection API of various versions at https://login.microsoftonline.com/common/UserRealm.  If we inspect the result for a fully managed Azure AD account we see general tenant detail.

If we take a look at a federated user we will see a little difference, the AuthURL property.

userrealm federated

This show us the location of our federated authentication endpoint. The token will actually be requested via a SAML user assertion that is received from an STS, in this case ADFS.

The OAuth specification uses the request parameter collection for token and authorization code responses. A username and password combination can be used to directly request a token in the fully managed scenario public client scenario.

A POST request can go directly to the Token endpoint with the following query parameters:

client_id

The Application Id

resource

The Resource URI to access

grant_type

password

username

The username

password

The password

The ADFS/WSTrust will entail sending a SOAP request to the WSTrust endpoint to authenticate and use that response to create the assertion that is exchanged for an access token.  Through user realm detection we can find the ADFS username/password endpoint.  A SOAP envelope can be sent to  endpoint to receive a security token response, containing the assertions needed.

A POST request is sent to the Username/Password endpoint for ADFS with the following envelope with noteable values encased in {}:

<s:Envelope xmlns:s='http://www.w3.org/2003/05/soap-envelope' 
    xmlns:a='http://www.w3.org/2005/08/addressing' 
    xmlns:u='http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd'>
    <s:Header>
        <a:Action s:mustUnderstand='1'>http://docs.oasis-open.org/ws-sx/ws-trust/200512/RST/Issue</a:Action>
        <a:messageID>urn:uuid:{Unique Identifier for the Request}</a:messageID>
        <a:ReplyTo>
            <a:Address>http://www.w3.org/2005/08/addressing/anonymous</a:Address>
        </a:ReplyTo>        <!-- The Username Password WSTrust Endpoint -->
        <a:To s:mustUnderstand='1'>{Username/Password Uri}</a:To>
        <o:Security s:mustUnderstand='1' 
            xmlns:o='http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd'>            <!-- The token length requested -->
            <u:Timestamp u:Id='_0'>
                <u:Created>{Token Start Time}</u:Created>
                <u:Expires>{Token Expiry Time}</u:Expires>
            </u:Timestamp>            <!-- The username and password used -->
            <o:UsernameToken u:Id='uuid-{Unique Identifier for the Request}'>
                <o:Username>{UserName to Authenticate}</o:Username>
                <o:Password>{Password to Authenticate}</o:Password>
            </o:UsernameToken>
        </o:Security>
    </s:Header>
    <s:Body>
        <trust:RequestSecurityToken xmlns:trust='http://docs.oasis-open.org/ws-sx/ws-trust/200512'>
            <wsp:AppliesTo xmlns:wsp='http://schemas.xmlsoap.org/ws/2004/09/policy'>
                <a:EndpointReference>
                    <a:Address>urn:federation:MicrosoftOnline</a:Address>
                </a:EndpointReference>
            </wsp:AppliesTo>
            <trust:KeyType>http://docs.oasis-open.org/ws-sx/ws-trust/200512/Bearer</trust:KeyType>
            <trust:RequestType>http://docs.oasis-open.org/ws-sx/ws-trust/200512/Issue</trust:RequestType>
        </trust:RequestSecurityToken>
    </s:Body>
</s:Envelope>

The token response is inspected for SAML assertion types (urn:oasis:names:tc:SAML:1.0:assertion or urn:oasis:names:tc:SAML:2.0:assertion) to find the matching token used for the OAuth token request.

<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope" 
    xmlns:a="http://www.w3.org/2005/08/addressing" 
    xmlns:u="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">
    <s:Header>
        <a:Action s:mustUnderstand="1">http://docs.oasis-open.org/ws-sx/ws-trust/200512/RSTRC/IssueFinal</a:Action>
        <o:Security s:mustUnderstand="1" 
            xmlns:o="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd">
            <u:Timestamp u:Id="_0">
                <u:Created>2016-01-03T01:34:41.640Z</u:Created>
                <u:Expires>2016-01-03T01:39:41.640Z</u:Expires>
            </u:Timestamp>
        </o:Security>
    </s:Header>
    <s:Body>
        <trust:RequestSecurityTokenResponseCollection xmlns:trust="http://docs.oasis-open.org/ws-sx/ws-trust/200512">            <!-- Our Desired Token Response -->
            <trust:RequestSecurityTokenResponse>
                <trust:Lifetime>
                    <wsu:Created xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">2016-01-03T01:34:41.622Z</wsu:Created>
                    <wsu:Expires xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">2016-01-03T02:34:41.622Z</wsu:Expires>
                </trust:Lifetime>
                <wsp:AppliesTo xmlns:wsp="http://schemas.xmlsoap.org/ws/2004/09/policy">
                    <wsa:EndpointReference xmlns:wsa="http://www.w3.org/2005/08/addressing">
                        <wsa:Address>urn:federation:MicrosoftOnline</wsa:Address>
                    </wsa:EndpointReference>
                </wsp:AppliesTo>
                <trust:RequestedSecurityToken>                    <!-- The Assertion -->
                    <saml:Assertion MajorVersion="1" MinorVersion="1" AssertionID="_e3b09f2a-8b57-4350-b1e1-20a8f07b3d3b" Issuer="http://adfs.howtopimpacloud.com/adfs/services/trust" IssueInstant="2016-08-03T01:34:41.640Z" 
                        xmlns:saml="urn:oasis:names:tc:SAML:1.0:assertion">
                        <saml:Conditions NotBefore="2016-01-03T01:34:41.622Z" NotOnOrAfter="2016-01-03T02:34:41.622Z">
                            <saml:AudienceRestrictionCondition>
                                <saml:Audience>urn:federation:MicrosoftOnline</saml:Audience>
                            </saml:AudienceRestrictionCondition>
                        </saml:Conditions>
                        <saml:AttributeStatement>
                            <saml:Subject>
                                <saml:NameIdentifier Format="urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified">130WEAH65kG8zfGrZFNlBQ==</saml:NameIdentifier>
                                <saml:SubjectConfirmation>
                                    <saml:ConfirmationMethod>urn:oasis:names:tc:SAML:1.0:cm:bearer</saml:ConfirmationMethod>
                                </saml:SubjectConfirmation>
                            </saml:Subject>
                            <saml:Attribute AttributeName="UPN" AttributeNamespace="http://schemas.xmlsoap.org/claims">
                                <saml:AttributeValue>chris@howtopimpacloud.com</saml:AttributeValue>
                            </saml:Attribute>
                            <saml:Attribute AttributeName="ImmutableID" AttributeNamespace="http://schemas.microsoft.com/LiveID/Federation/2008/05">
                                <saml:AttributeValue>130WEAH65kG8zfGrZEFlBQ==</saml:AttributeValue>
                            </saml:Attribute>
                        </saml:AttributeStatement>
                        <saml:AuthenticationStatement AuthenticationMethod="urn:oasis:names:tc:SAML:1.0:am:password" AuthenticationInstant="2016-08-03T01:34:41.607Z">
                            <saml:Subject>
                                <saml:NameIdentifier Format="urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified">130WEAH65kG8sfGrZENlBQ==</saml:NameIdentifier>
                                <saml:SubjectConfirmation>
                                    <saml:ConfirmationMethod>urn:oasis:names:tc:SAML:1.0:cm:bearer</saml:ConfirmationMethod>
                                </saml:SubjectConfirmation>
                            </saml:Subject>
                        </saml:AuthenticationStatement>
                        <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
                            <ds:SignedInfo>
                                <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#" />
                                <ds:SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1" />
                                <ds:Reference URI="#_e3b09f2a-8b57-4350-b1e1-20a8f07b3d3b">
                                    <ds:Transforms>
                                        <ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature" />
                                        <ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#" />
                                    </ds:Transforms>
                                    <ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1" />
                                    <ds:DigestValue>itvzbQhlzA8CIZsMneHVR15FJlY=</ds:DigestValue>
                                </ds:Reference>
                            </ds:SignedInfo>
                            <ds:SignatureValue>gBCGUmhQrJxVpCxVsy2L1qh1kMklVVMoILvYJ5a8NOlezNUx3JNlEP7wZ389uxumP3sL7waKYfNUyVjmEpPkpqxdxrxVu5h1BDBK9WqzOICnFkt6JPx42+cyAhj3T7Nudeg8CP5A9ewRCLZu2jVd/JEHXQ8TvELH56oD5RUldzm0seb8ruxbaMKDjYFuE7X9U5sCMMuglU3WZDC3v6aqmUxpSd9Kelhddleu33XEBv7CQNw84JCud3B+CC7dUwtGxwv11Mk/P0t1fGbfs+I6aSMTecKq9YmscqP9tB8ZouD42jhjhYysOQSdulStmUi6gVzQz+c2l2taa5Amd+JCPg==</ds:SignatureValue>
                            <KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
                                <X509Data>
                                    <X509Certificate>MIIC4DCDAcigAwIBAgIQaYQ6QyYqcrBBmOHSGy0E1DANBgkqhkiG9w0BAQsFADArMSkwJwYDVQQDEyBBREZTIFNpZ25pbmcgLSBhZGZzLmNpLmF2YWhjLmNvbTAgFw0xNjA2MDQwNjA4MDdaGA8yMTE2MDUxMTA2MDgwN1owKzEpMCcGA1UEAxMgQURGUyBTaWduaW5nIC0gYWRmcy5jaS5hdmFoYy5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDH9J6/oWYAR8Y98QnacNouKyIBdtZbosEz0HyJVyrxVqKq2AsPvCEO3WFm9Gmt/xQN9PuLidZpgICAe8Ukuv4h/NldgmgtD64mObFNuEM5pzAPRXUv6FWlVE4fnUpIiD1gC0bbQ7Tzv/cVgfUChCDpFu3ePDTs/tv07ee22jXtoyT3N7tsbIX47xBMKgF9ItN9Oyqi0JyQHZghVQ1ebNOMH3/zNdl0WcZ+Pl+osD3iufoH6H+qC9XY09B5YOWy8fJoqf+HFeSWZCHH5vJJfsPTsSilvLHCpMGlrMFaTBKqmv+m9Z3FtbzOcnKHS5PJVAymqLctkH+HbFzaDblaSRhhAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAFB0E2Cj+O24aPM61JsCXLIAB28q4h4qLxMwV+ypYjFxxcQ5GzgqaPJ7BARCnW1gm3PyvNfUut9RYrT9wTJlBVY9WDBoX33jsS87riMj+JONXJ7lG/zAozxs0xIiW+PNlFdOt7xyvYstrFgPJS1E05jhiZ2PR8MS20uSlMNkVPinpz4seyyMQeM+1GbpbDE1EwwtEVKgatJN7t6nAn9mw8cHIk1et7CYOGeWCnMA9EljzNiD8wEwsG51aKfuvGrPK8Q8N/G89SPgstpe0Te5+EtWT6latXfpCwdNWxvinH49SKKa25l1VoLLNwKiQF6vK1Iw0F7dP7QkO5YdE7/MTDU=</X509Certificate>
                                </X509Data>
                            </KeyInfo>
                        </ds:Signature>
                    </saml:Assertion>
                </trust:RequestedSecurityToken>
                <trust:RequestedAttachedReference>
                    <o:SecurityTokenReference k:TokenType="http://docs.oasis-open.org/wss/oasis-wss-saml-token-profile-1.1#SAMLV1.1" 
                        xmlns:o="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd" 
                        xmlns:k="http://docs.oasis-open.org/wss/oasis-wss-wssecurity-secext-1.1.xsd">
                        <o:KeyIdentifier ValueType="http://docs.oasis-open.org/wss/oasis-wss-saml-token-profile-1.0#SAMLAssertionID">_e3b09f2a-8b57-4350-b1e1-20a8f07b3d3b</o:KeyIdentifier>
                    </o:SecurityTokenReference>
                </trust:RequestedAttachedReference>
                <trust:RequestedUnattachedReference>
                    <o:SecurityTokenReference k:TokenType="http://docs.oasis-open.org/wss/oasis-wss-saml-token-profile-1.1#SAMLV1.1" 
                        xmlns:o="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd" 
                        xmlns:k="http://docs.oasis-open.org/wss/oasis-wss-wssecurity-secext-1.1.xsd">
                        <o:KeyIdentifier ValueType="http://docs.oasis-open.org/wss/oasis-wss-saml-token-profile-1.0#SAMLAssertionID">_e3b09f2a-8b57-4350-b1e1-20a8f07b3d3b</o:KeyIdentifier>
                    </o:SecurityTokenReference>
                </trust:RequestedUnattachedReference>
                <trust:TokenType>urn:oasis:names:tc:SAML:1.0:assertion</trust:TokenType>
                <trust:RequestType>http://docs.oasis-open.org/ws-sx/ws-trust/200512/Issue</trust:RequestType>
                <trust:KeyType>http://docs.oasis-open.org/ws-sx/ws-trust/200512/Bearer</trust:KeyType>
            </trust:RequestSecurityTokenResponse>
        </trust:RequestSecurityTokenResponseCollection>
    </s:Body>
</s:Envelope>

A POST request is sent to the Token endpoint with the following query parameters:

client_id

The Application Id

resource

The Resource URI to access

assertion

The base64 encoded SAML token

grant_type

urn:ietf:params:oauth:grant-type:saml1_1-bearer urn:ietf:params:oauth:grant-type:saml2-bearer

scope

openid

A GET request is sent to the Authorize endpoint with some similar query parameters:

client_id

The Application Id

redirect_uri

The location within the application to handle the authorization code

response_type

code

prompt

login consent admin_consent

scope

optional scope for access (app uri or openid scope)

The endpoint should redirect you to the appropriate login screen via user realm detection.  Once the user login is completed, the code is added to the redirect address as either query parameters (default) or a form POST.  Once the code is retrieved it can be exchanged for a token. A POST request is sent to the Token endpoint as demonstrated before with some slightly different parameters:

client_id

The Application Id

resource

The Resource URI to access

code

The authorization code

grant_type

authorization_code

scope

previous scope

client_secret

required if confidential client

Tying it All Together

To try to show some value for your reading time, lets explore how this can be used as the solutions you support and deploy become more tightly integrated with the Microsoft cloud.  We'll start by creating a new Native application in the legacy portal.

appnative1
appnative2

I used https://itdoesnotmatter here, but you might as well follow the guidance of using urn:ietf:wg:oauth:2.0:oob.  We will now grant permissions to Azure Active Directory and Azure Service Management (for ARM too).

ADPermissions
ADServiceMgmt

I will avoid discussing configuring the application to be multi-tenant as the processes I outline are identical, it is simply a matter of the targeted tenant.  You should end up with something looking like this.

Native

Let's now try to go get a token for our new application and put it to use.  This should look exactly the same as retrieving the previous token.


$AuthCode=Approve-AzureADApplication -ClientId $NewClientId -RedirectUri 'https://itdoesnotmatter/' -TenantId sendthewolf.com -AdminConsent

nativefirstattempt

Epic failure!  Unfortunately we run into a common annoyance, the application must be consented to interactively.  I do not know of any tooling that exists to make this easy.  I added a function to make this a little easier and it supports a switch of AdminConsent to approve the application for all users within the tenant.  And step through the consent process to receive an authorization code.


$AuthCode=Approve-AzureADApplication -ClientId $NewClientId -RedirectUri 'https://itdoesnotmatter/' -TenantId sendthewolf.com -AdminConsent

Approve
Approve App

Once the authorization code is obtained it can be exchanged for a token, for which I provided another function.  That token can now be used in the exact same manner as the Azure Cmdlet application.


$TokenResult=Get-AzureADAccessTokenFromCode 'https://management.core.windows.net/' -ClientId $NewClientId -RedirectUri 'https://itdoesnotmatter/' -TenantId sendthewolf.com -AuthorizationCode $AuthCode

Authorize2

If you wanted to handle some Azure Active Directory objects, we can target a different audience, and execute actions appropriate to the account's privilege level.   In the following example we will create a new user.


$GraphBaseUri="https://graph.windows.net/"
$GraphUriBuilder=New-Object System.UriBuilder($GraphBaseUri)
$GraphUriBuilder.Path="$TenantId/users"
$GraphUriBuilder.Query="api-version=1.6"
$NewUserJSON=@"
{
    "accountEnabled": true, 
    "displayName": "Johnny Law", 
    "mailNickName" : "thelaw", 
    "passwordProfile": { 
        "password": "Password1234!", 
        "forceChangePasswordNextLogin": false 
    }, 
    "userPrincipalName": "johhny.law@$TenantId" 
}
"@
$AuthResult=Get-AzureADUserToken -Resource $GraphBaseUri -ClientId $NewClientId -Credential $Credential -TenantId $TenantId
$AuthHeaders=@{Authorization="Bearer $($AuthResult.access_token)"}
$NewUser=Invoke-RestMethod -Uri $GraphUriBuilder.Uri -Method Post -Headers $AuthHeaders -Body $NewUserJSON -ContentType "application/json"

If we want to continue the “fun” with Office 365 we can apply the exact sample approach with the Office 365 Sharepoint Online application permissions.  In the interest of moving along and with no regard for constraining access, we will configure the permissions in the following manner.

sharepoint

We’ll now do some querying of the Office 365 SharePoint video API with some more script.


$SharepointUri='https://yourdomain.sharepoint.com/'
$SpUriBuilder=New-Object System.UriBuilder($SharepointUri)
$SpUriBuilder.Path="_api/VideoService.Discover"
$AuthResult=Get-AzureADUserToken -Resource $SharepointUri -ClientId $NewClientId -Credential $Credential
$Headers=@{Authorization="Bearer $($AuthResult.access_token)";Accept="application/json";}
$VideoDisco=Invoke-RestMethod -Uri $SpUriBuilder.Uri -Headers $Headers $VideoDisco|Format-List
$VideoChannelId="306488ae-5562-4d3e-a19f-fdb367928b96"
$VideoPortalUrl=$VideoDisco.VideoPortalUrl
$ChannelUrlBuilder=New-Object System.UriBuilder($VideoPortalUrl)
$ChannelUrlBuilder.Path+="/_api/VideoService/Channels"
$ChannelOData=Invoke-RestMethod -Uri $ChannelUrlBuilder.Uri -Headers $Headers
$ChannelRoot=$ChannelUrlBuilder.Path
foreach ($Channel in $ChannelOData.Value)
{  
    $VideoUriBuilder=New-Object System.UriBuilder($Channel.'odata.id')
    $VideoUriBuilder.Path+="/Videos"
    Invoke-RestMethod -Uri $VideoUriBuilder.Uri -Headers $Headers|Select-Object -ExpandProperty value
}

We should see some output that looks like this:

spvideos

I’ve had Enough! Please Just Show me the Code.

For those who have endured or even skipped straight here, I present the following module for any use your dare apply.  The standard liability waiver applies and it is presented primarily for educational purposes.  It came from a need to access the assortment of Microsoft cloud API in environments where we could not always ensure the plethora of correct Cmdlets are installed.  Initially, being a .Net guy, I just wrapped standard use cases around ADAL .Net.  I really wanted to make sure that I really understood OAuth and OpenId Connect authorization flows as is relates to Azure Active Directory.  The entire theme of this lengthy tome is to emphasize the importance of having a relatively advanced understanding of these concepts.  Regardless of your milieu, if it has a significant Microsoft component, the demand to both integrate and support the integration(s) of numerous offerings will only grow larger.  The module is primarily targeted at the Native Client application type, however there is support for the client secret and implicit authorization flows.  There are also a few utility methods that are exposed as they may have some diagnostic use or otherwise.  The module exposes the following methods all of which support Get-Help:

  • Approve-AzureADApplication

    • Approves an Azure AD Application Interactively and returns the Authorization Code

    • ConvertFrom-EncodedJWT

      • Converts an encoded JWT to an object representation

      • Get-AzureADAccessTokenFromCode

        • Retrieves an access token from a consent authorization code

        • Get-AzureADClientToken

          • Retrieves an access token as a an OAuth confidential client

          • Get-AzureADUserToken

            • Retrieves an access token as a an OAuth public client

            • Get-AzureADImplicitFlowToken

              • Retrieves an access token interactively for a web application with OAuth implicit flow enabled

              • Get-AzureADOpenIdConfiguration

                • Retrieves the OpenId connect configuration for the specified application

                • Get-AzureADUserRealm

                  • Retrieves a the aggregate user realm data for the specified user principal name(s

                  • Get-WSTrustUserRealmDetails

                    • Retrieves the WSFederation details for a given user prinicpal name

Get it here: Azure AD Module

I hope you find it useful and remember not to fear doing things the hard way every so often.