AadSupportFunctions.psm1

<#
.SYNOPSIS
Manually consent to permissions
 
.DESCRIPTION
Manually consent to permissions. This works for Admin consent, user consent, delegated permissions, and application permissions.
 
EXAMPLES
# Add Admin Consent for a Delegated Permission
Add-AadConsent -ClientId b330d711-77c4-463b-a391-6b3fbef74ffd -ResourceId "Microsoft Graph" -PermissionType Delegated -ClaimValue User.Read
 
# Add User Consent for single user (new OAuth2PermissionGrant)
Add-AadConsent -ClientId b330d711-77c4-463b-a391-6b3fbef74ffd -ResourceId "Microsoft Graph" -PermissionType Delegated -ClaimValue User.Read -UserId john@contoso.com
 
# Update User Consent for all users (existing OAuth2PermissionGrant)
Add-AadConsent -ClientId b330d711-77c4-463b-a391-6b3fbef74ffd -ResourceId "Microsoft Graph" -ConsentType User -PermissionType Delegated -ClaimValue Directory.AccessAsUser.All
 
# Add Admin Consent for Application Permission
Add-AadConsent -ClientId b330d711-77c4-463b-a391-6b3fbef74ffd -ResourceId "Microsoft Graph" -PermissionType Application -ClaimValue User.Read.All
 
 
.PARAMETER ClientId
Identifier for the Enterprise App (ServicePrincipal) we will be consenting to.
 
.PARAMETER ResourceId
Identifier for the Resource (ServicePrincipal) we will be consenting permissions to.
 
.PARAMETER UserId
If you set a UserId, then it will use User Consent
 
.PARAMETER ClaimValue
This is the permission you want to add. We also accept multiple values here as space delimitted list.
 
.PARAMETER ConsentType
Specify if this is a 'Admin' consent or 'User' consent
Default value if none is specified is 'Admin'
 
.PARAMETER PermissionType
Specify if this is a 'Delegated' permission or 'Application' permission
 
.NOTES
General notes
#>

function Add-AadConsent {
    [CmdletBinding(DefaultParameterSetName="DefaultSet")] 
    param (
        [Parameter(mandatory=$true, Position=0, ValueFromPipeline = $true)]
        [string]$ClientId,

        [Parameter(mandatory=$true)]
        [string]$ResourceId,

        [Parameter(mandatory=$true)]
        [string]$ClaimValue,

        [Parameter(mandatory=$false)]
        [ValidateSet('Admin','User')]
        $ConsentType="Admin",

        [Parameter(mandatory=$false)]
        [ValidateSet('Delegated','Application')]
        $PermissionType="Delegated",

        [string]$UserId
    )

    #Required but ignored parameter
    $Expires = (Get-AadDateTime -AddMonths 12)

    # Parameter validations
    if($UserId)
    {
        $ConsentType = "User"
    }

    if($UserId -and $PermissionType -eq "Application")
    {
        throw "You can't provide a UserId and set PermissionType to 'Application'"
    }

    $TenantDomain = $Global:AadSupport.Session.TenantId

    # --------------------------------------------------
    # Check if signed in user is Global Admin (As only global admins can perform admin consent)
    $isGlobalAdmin = Invoke-AadCommand -Command {
        Param(
            $AccountId
        )
        $SignedInUser = Get-AzureAdUser -ObjectId $AccountId
        $SignedInUserObjectId = $SignedInUser.ObjectId
        $GlobalAdminRoleIds = (Get-AzureAdDirectoryRole | where { $_.displayName -eq 'Global Administrator' -or $_.displayName -eq 'Application Administrator' }).ObjectId
        
        foreach($GlobalAdminRoleId in $GlobalAdminRoleIds)
        {
            if( (Get-AzureAdDirectoryRoleMember -ObjectId $GlobalAdminRoleId).ObjectId -contains $SignedInUserObjectId )
            {
                return $true
            }
        }
        
        
    } -Parameters $Global:AadSupport.Session.AccountId
    

    if (-not $isGlobalAdmin)  
    {  
        Write-Host "Your account '$($Global:AadSupport.Session.AccountId)' is not a Global Admin in $TenantDomain."
        throw "Exception: 'Global Administrator' or 'Application Administrator' role REQUIRED\r\n * Application Administrator can only perform consent for delegated permissions"
    } 

    # ++++++++++++++++++++++++
    # GET CLIENT ID
    # ++++++++++++++++++++++++
    $Client = $null

    $client = Get-AadServicePrincipal -Id $ClientId
    $ClientId = $client.ObjectId
    Write-Verbose "ClientId: $ClientId"

    # ++++++++++++++++++++++++
    # GET RESOURCE ID
    # ++++++++++++++++++++++++
    $Resource = $null

    $resource =  Get-AadServicePrincipal -Id $ResourceId
    

    $ResourceId = $resource.ObjectId
    Write-Verbose "ResourceId: $ResourceId"

    # ++++++++++++++++++++++++
    # GET PRINCIPAL ID
    # ++++++++++++++++++++++++
    $User = $null
    $PrincipalId = $null
    if($UserId)
    {
        $User = Invoke-AadCommand -Command {
            Param($UserId)
            Get-AzureADUser -ObjectId $UserId
        } -Parameters $UserId
    
        $PrincipalId = ($User).ObjectId
    
        if (-not $PrincipalId) {
            throw "'$UserId' does not exist in '$TenantDomain'"
        }
    }
    Write-Verbose "PrincipalId: $PrincipalId"

    # START
    $RequestedPermissions = $ClaimValue.Split(" ").Split(";").Split(",")

    $ConsentedPermissions = $null
    
    if($PermissionType -eq "Delegated")
    {
        # Check if OAuth2PermissionGrant already exists
        if(!$ConsentedPermissions)
        {
            $ConsentedPermissions = Get-AadConsent `
            -ClientId $ClientId `
            -ResourceId $ResourceId `
            -ConsentType $ConsentType `
            -PermissionType $PermissionType `
            -UserId $PrincipalId
        }

        Write-Verbose "Showing Consented Permissions..."
        if($ConsentedPermissions)
        {
            Write-Verbose ($ConsentedPermissions|ConvertTo-Json)
        }
        else
        {
            Write-Verbose "NONE."
        }
        

        foreach($RequestedPermission in $RequestedPermissions)
        {
            Write-Verbose "Checking RequestedPermission $RequestedPermission"

            # Oauth2PermissionGrant exists.
            # Lets go update it.
            if($ConsentedPermissions)
            {
                foreach($Permission in $ConsentedPermissions)
                {
                    Write-Verbose "Checking ConsentedPermission $($Permission.ClaimValue)"

                    if($Permission.ClaimValue)
                    {
                        $CurrentScopes = $Permission.ClaimValue.Split(" ")
                    }
                    else
                    {
                        $CurrentScopes = ""
                    }
                    
                    # Skip of Permission is already there
                    if($CurrentScopes -contains $RequestedPermission)
                    {
                        Write-Host "$RequestedPermission already exists on OAuth2PermissinGrant $($Permission.Id)"
                        continue
                    }

                    $NewScopes += $CurrentScopes 
                    $NewScopes += $RequestedPermission

                    $MsGraphUrl = "$($Global:AadSupport.Resources.MsGraph)/beta/oauth2PermissionGrants/$($Permission.Id)"
 
                    $JsonBody = @{
                        scope = [string]::Join(' ', $NewScopes)
                    } | ConvertTo-Json -Compress

                    Write-Verbose "JsonBody..."
                    Write-Verbose $JsonBody

                    Write-Verbose "Updating existing OAuth2PermissionGrant"

                    Invoke-AadProtectedApi `
                    -Client $Global:AadSupport.Clients.AzureAdPowershell.ClientId `
                    -Resource $Global:AadSupport.Resources.MsGraph `
                    -Endpoint $MsGraphUrl -Method PATCH `
                    -Body $JsonBody
                } 
            }
    
            # Oauth2PermissionGrant does not exist.
            # Lets go create one.
            else
            {
                # Lets start building the OAuth2PermissionGrant
                $newPermissionGrant = @{}
                $newPermissionGrant.Add("startTime", [System.DateTime]::UtcNow.AddMinutes(-5).ToString("o"))
                $newPermissionGrant.Add("expiryTime", $Expires)
                $newPermissionGrant.Add("clientId", $ClientId)
                $newPermissionGrant.Add("resourceId", $ResourceId)
                $newPermissionGrant.Add("scope", $ClaimValue)
                if ($PrincipalId) {
                    $newPermissionGrant.Add("principalId", $PrincipalId)
                    $newPermissionGrant.Add("consentType", "Principal")
                }
                else {
                    $newPermissionGrant.Add("consentType", "AllPrincipals")
                }

                $MsGraphUrl = "$($Global:AadSupport.Resources.MsGraph)/beta/oauth2PermissionGrants"
                
                $JsonBody = $newPermissionGrant | ConvertTo-Json -Compress

                Write-Verbose "JsonBody..."
                Write-Verbose $JsonBody

                Write-Verbose "Adding OAuth2PermissionGrant"

                Invoke-AadProtectedApi `
                        -Client $Global:AadSupport.Clients.AzureAdPowershell.ClientId `
                        -Resource $Global:AadSupport.Resources.MsGraph `
                        -Endpoint $MsGraphUrl -Method POST `
                        -Body $JsonBody
            }
    
            
            
        }
    }
    
    
    if($PermissionType -eq "Application")
    {
        foreach($RequestedPermission in $RequestedPermissions)
        {
            $ConsentedPermissions = Get-AadConsent `
            -ClientId $ClientId `
            -ResourceId $ResourceId `
            -PermissionType $PermissionType `
            -ClaimValue $RequestedPermission
    
            if($ConsentedPermissions)
            {
                Write-Host "$RequestedPermission already exists!"
                continue
            }
    
            else
            {
                # ------------------------------------------------
                # ADD ROLES (App Role Assignment)
    
                $AppRoles = $resource.AppRoles | Sort-Object Value
                
                # Get the Role ID
                $RoleId = ($AppRoles | where {$_.Value -eq $RequestedPermission}).Id 

                if($RoleId)
                {
                    Write-Verbose "Adding AppRole assignment"

                    Invoke-AadCommand -Command {
                        Param($Params)
                        New-AzureADServiceAppRoleAssignment -ObjectId $Params.ClientObjectId -Id $Params.RoleId -ResourceId $Params.ResourceObjectId -PrincipalId $Params.ClientObjectId
                    } -Parameters @{
                        ClientObjectId = $ClientId
                        RoleId = $RoleId
                        ResourceObjectId = $ResourceId
                    }
                }
                
                else 
                {
                    Write-Host "'$RoleId' not found on $($resource.DisplayName)" -ForegroundColor Yellow
                }
                
            }
        }
    }
}
<#
.SYNOPSIS
Connect to the Azure AD Support PowerShell module. This will use the same sign-in session to access different Microsoft resources.
 
.DESCRIPTION
Connect to the Azure AD Support PowerShell module. This will use the same sign-in session to access different Microsoft resources.
 
Example 1: Log in with your admin account...
Connect-AadSupport
 
Example 2: Log in to a specific tenant...
Connect-AadSupport -TenantId contoso.onmicrosoft.com
 
Example 3: Log in to a specific instance...
Connect-AadSupport -AzureEnvironmentName AzureCloud
Connect-AadSupport -AzureEnvironmentName AzureGermanyCloud
Connect-AadSupport -AzureEnvironmentName AzureChinaCloud
Connect-AadSupport -AzureEnvironmentName AzureUSGovernment
 
.PARAMETER TenantId
Provide the Tenant ID you want to authenticate to.
 
.PARAMETER AccountId
Provide the Account ID you want to authenticate with.
 
.PARAMETER AzureEnvironmentName
Specifies the name of the Azure environment. The acceptable values for this parameter are:
 
        - AzureCloud
        - AzureChinaCloud
        - AzureUSGovernment
        - AzureGermanyCloud
 
        The default value is AzureCloud.
 
.PARAMETER LogPath
The path where the log file for this PowerShell session is written to. Provide a value here if you need to
deviate from the default PowerShell log file location.
 
.NOTES
General notes
#>


function Connect-AadSupport
{
    [CmdletBinding(DefaultParameterSetName='DefaultSet')]
    param (
        [string]$TenantId = "Common",


        [string]$AccountId,

        [Parameter(mandatory=$true, ParameterSetName = 'UserPrincipalSet')]
        [string]$Password,

        [ValidateSet("AzureCloud","AzureGermanyCloud","AzureUSGovernment","AzureChinaCloud")]
        [string]$AzureEnvironmentName = "AzureCloud",

        [Parameter(mandatory=$true, ParameterSetName = 'ServicePrincipalSet')]
        [string]$ClientId,

        [Parameter(mandatory=$true, ParameterSetName = 'ServicePrincipalSet')]
        [string]$ClientSecret,

        [Switch]$EnableLogging
    )

    New-AadSupportSession

    if($EnableLogging)
    {
        $Global:AadSupport.Logging.Enabled = $true
    }
    else
    {
        $Global:AadSupport.Logging.Enabled = $false
    }

    if($LogPath)
    {
        $Global:AadSupport.Logging.Path = $LogPath
    }

    switch($AzureEnvironmentName)
    {
        "AzureCloud" 
        {
            $Global:AadSupport.Session.AadInstance = "https://login.microsoftonline.com"
            $Global:AadSupport.Resources.AadGraph = "https://graph.windows.net"
            $Global:AadSupport.Resources.MsGraph = "https://graph.microsoft.com"
            $Global:AadSupport.Resources.AzureRmApi = "https://management.azure.com"
            $Global:AadSupport.Resources.AzureServiceApi = "https://management.core.windows.net"
            $Global:AadSupport.Resources.KeyVault = "https://vault.azure.net"
        }

        "AzureChinaCloud"
        {
            $Global:AadSupport.Session.AadInstance = "https://login.chinacloudapi.cn" #https://login.partner.microsoftonline.cn
            $Global:AadSupport.Resources.AadGraph = "https://graph.chinacloudapi.cn"
            $Global:AadSupport.Resources.MsGraph = "https://microsoftgraph.chinacloudapi.cn"
            $Global:AadSupport.Resources.AzureRmApi = "https://management.chinacloudapi.cn"
            $Global:AadSupport.Resources.AzureServiceApi = "https://management.core.chinacloudapi.cn"
            $Global:AadSupport.Resources.KeyVault = "https://vault.azure.cn"
        }

        "AzureUSGovernment"
        {
            $Global:AadSupport.Session.AadInstance = "https://login.microsoftonline.us"
            $Global:AadSupport.Resources.AadGraph = "https://graph.windows.net"
            $Global:AadSupport.Resources.MsGraph = "https://graph.microsoft.us" #DOD https://dod-graph.microsoft.us
            $Global:AadSupport.Resources.AzureRmApi = "https://management.usgovcloudapi.net/"
            $Global:AadSupport.Resources.AzureServiceApi = "https://management.core.usgovcloudapi.net/"
            $Global:AadSupport.Resources.KeyVault = "https://vault.usgovcloudapi.net"
        }

        "AzureGermanyCloud"
        {
            $Global:AadSupport.Session.AadInstance = "https://login.microsoftonline.de"
            $Global:AadSupport.Resources.AadGraph = "https://graph.cloudapi.de/"
            $Global:AadSupport.Resources.MsGraph = "https://graph.microsoft.de"
            $Global:AadSupport.Resources.AzureRmApi = "https://management.microsoftazure.de/"
            $Global:AadSupport.Resources.AzureServiceApi = "https://management.core.cloudapi.de/"
            $Global:AadSupport.Resources.KeyVault = "https://vault.microsoftazure.de"
        }

        default 
        {
            $Global:AadSupport.Session.AadInstance = "https://login.microsoftonline.com"
            $Global:AadSupport.Resources.AadGraph = "https://graph.windows.net"
            $Global:AadSupport.Resources.MsGraph = "https://graph.microsoft.com"
            $Global:AadSupport.Resources.AzureRmApi = "https://management.azure.com"
            $Global:AadSupport.Resources.AzureServiceApi = "https://management.core.windows.net"
            $Global:AadSupport.Resources.KeyVault = "https://vault.azure.net"
        }
    }

    Write-Host ""
    Write-Host "Connecting to Azure AD PowerShell (Connect-AzureAD)"
    Write-Host "and Connecting to Azure PowerShell (Connect-AzAccount)"
    Write-Host ""

    # Connect to Azure AD PowerShell
    # Get Current Session Info
    if(!$AccountId)
    {
        $AccountId = $Global:AadSupport.Session.AccountId
    }

    # if still null, We want to pass an empty AccountId
    if(!$AccountId)
    {
        $AccountId = ""
    }

    try {

        if(!$AccountId)
        {
            $Prompt = "Always"
        }
        else {
            $Prompt = "Auto"
        }

        

        # Get Token for AAD Graph to be used for Azure AD PowerShell
        $token = $null
        if($Password)
        {
            $token = Get-AadTokenUsingAdal `
            -ResourceId $Global:AadSupport.Resources.AadGraph `
            -ClientId $Global:AadSupport.Clients.AzureAdPowershell.ClientId `
            -Instance $Global:AadSupport.Session.AadInstance `
            -Tenant $TenantId `
            -UserId $AccountId `
            -Password $Password `
            -UseResourceOwnerPasswordCredential `
            -SkipServicePrincipalSearch `
            -HideOutput

            $Global:AadSupport.Session.AccountId = $token.DisplayableId
            $Global:AadSupport.Session.TenantId = $token.TenantId

            $AzureToken = Get-AadTokenUsingAdal `
            -ResourceId $Global:AadSupport.Resources.AzureRmApi `
            -ClientId $Global:AadSupport.Clients.AzurePowershell.ClientId `
            -Instance $Global:AadSupport.Session.AadInstance `
            -Tenant $TenantId `
            -UserId $Global:AadSupport.Session.AccountId `
            -Password $Password `
            -UseResourceOwnerPasswordCredential `
            -SkipServicePrincipalSearch `
            -HideOutput
        }

        elseif($ClientSecret)
        {
            $token = Get-AadTokenUsingAdal `
            -ResourceId $Global:AadSupport.Resources.AadGraph `
            -ClientId $ClientId `
            -ClientSecret $ClientSecret `
            -UseClientCredential `
            -Instance $Global:AadSupport.Session.AadInstance `
            -Tenant $TenantId `
            -SkipServicePrincipalSearch `
            -HideOutput

            $AzureToken = Get-AadTokenUsingAdal `
            -ResourceId $Global:AadSupport.Resources.AzureRmApi `
            -ClientId $ClientId `
            -ClientSecret $ClientSecret `
            -UseClientCredential `
            -Instance $Global:AadSupport.Session.AadInstance `
            -Tenant $TenantId `
            -SkipServicePrincipalSearch `
            -HideOutput
        }

        else 
        { 
            $token = Get-AadTokenUsingAdal `
                -ResourceId $Global:AadSupport.Resources.AadGraph `
                -ClientId $Global:AadSupport.Clients.AzureAdPowershell.ClientId `
                -Redirect $Global:AadSupport.Clients.AzureAdPowershell.RedirectUri `
                -Instance $Global:AadSupport.Session.AadInstance `
                -Tenant $TenantId `
                -UserId $AccountId `
                -Prompt $Prompt `
                -SkipServicePrincipalSearch `
                -HideOutput

            $Global:AadSupport.Session.AccountId = $token.DisplayableId
            $Global:AadSupport.Session.TenantId = $token.TenantId

            $token = Get-AadTokenUsingAdal `
                -ResourceId $Global:AadSupport.Resources.AzureRmApi `
                -ClientId $Global:AadSupport.Clients.AzurePowershell.ClientId `
                -Redirect $Global:AadSupport.Clients.AzurePowershell.RedirectUri `
                -UserId $Global:AadSupport.Session.AccountId `
                -Instance $Global:AadSupport.Session.AadInstance `
                -Tenant $Global:AadSupport.Session.TenantId `
                -Prompt Never `
                -SkipServicePrincipalSearch `
                -HideOutput
        }

        # If we didnt get a token lets stop
        if(!$token)
        {
            Write-Host "Failed to authenticate. User most likely cancelled." -Foreground Red
            return
        }

        Write-Verbose "Token AuthenticationResult..."
        Write-Verbose $($token | ConvertTo-Json)
        $Global:AadSupport.Session.AccountId = $token.DisplayableId
        $Global:AadSupport.Session.TenantId = $token.TenantId

        $Global:AadSupport.Session.Active = $true
    }
    catch {
        throw $_
    } 
}


<#
.SYNOPSIS
Converts a single Base64Encoded certificate (Not Chained Ceritificate) to a Custom PSObject for easy readability
 
.DESCRIPTION
Converts a single Base64Encoded certificate (Not Chained Ceritificate) to a Custom PSObject for easy readability
 
 
.PARAMETER Base64String
The Base64Encoded Certificate
 
.EXAMPLE
ConvertFrom-AadBase64Certificate -Base64String "MIIHkDCCBnigAwIBAgIRALENqydLHXg/u+VM04+dg2QwDQYJKoZIhvcNAQELBQAwgZ..."
 
.NOTES
General notes
#>


function ConvertFrom-AadBase64Certificate

{
    [cmdletbinding(DefaultParameterSetName="Default")]
    param(
        [parameter(Mandatory=$true,Position=0,ValueFromPipeline=$true,ParameterSetName="Default")]
        [String]
        $Base64String,

        [parameter(Mandatory=$true,Position=0,ValueFromPipeline=$true,ParameterSetName="Path")]
        [String]
        $Path,

        [String]$Password
    )

    if($Path -and ![System.IO.Path]::IsPathRooted($Path))
    {
        $LocalPath = Get-Location
        $Path = "$LocalPath\$Path"
    }


    if($Path)
    {
        $bytes = [System.IO.File]::ReadAllBytes("$path")
    }

    if($Base64String)
    {
        # Sometimes a Base64Encoded Cert has been Base64Encoded again (Chained Certs)
        if(-not $Base64String.StartsWith("MII") -and -not $Base64String.StartsWith("-----BEGIN"))
        {
            $Base64String = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($Base64String));
        }

        $bytes = [System.Text.Encoding]::UTF8.GetBytes($Base64String)
    }
    
    $cert = new-object System.Security.Cryptography.X509Certificates.X509Certificate2 -ArgumentList @($bytes,$Password)
    $kid = ConvertFrom-AadThumbprintToBase64String -Thumbprint $cert.Thumbprint
    
    $Properties = @{ 
        Kid = $kid; 
        Thumbprint = $cert.Thumbprint;
        NotAfter = $cert.NotAfter;
        NotBefore = $cert.NotBefore; 
        Subject = $cert.Subject;
        Issuer = $cert.Issuer;
        Certificate = $cert;
    }

    $Object = new-object PSObject -Property $Properties

    return $Object
}
<#
.SYNOPSIS
Converts a Base64Encoded Thumbprint or also known as Key Identifier (Kid) back to its original Thumbprint value
 
.DESCRIPTION
Converts a Base64Encoded Thumbprint or also known as Key Identifier (Kid) back to its original Thumbprint value
 
.PARAMETER Base64String
Base64Encoded version of the Thumbprint
 
.EXAMPLE
ConvertFrom-AadBase64StringToThumbprint -Base64String 'z79RnGljTQa9Zh4ZjLq6UaB4eUM='
 
Output...
CF-BF-51-9C-69-63-4D-06-BD-66-1E-19-8C-BA-BA-51-A0-78-79-43
 
.NOTES
 
#>


Function ConvertFrom-AadBase64String {

    [cmdletbinding()]

    param(
        [parameter(Mandatory=$true, Position=0, ValueFromPipeline = $true)]
        [String] $Base64String
    )

    while($Base64String.Length % 4 -ne 0)
    {
        $Base64String += "="
    }

    #$Bytes =[Convert]::FromBase64String($Base64String.Replace("-","+").Replace("_","/"))

    return [Text.Encoding]::Utf8.GetString([Convert]::FromBase64String($Base64String))

}
<#
.SYNOPSIS
Converts a Base64Encoded Thumbprint or also known as Key Identifier (Kid) back to its original Thumbprint value
 
.DESCRIPTION
Converts a Base64Encoded Thumbprint or also known as Key Identifier (Kid) back to its original Thumbprint value
 
.PARAMETER Base64String
Base64Encoded version of the Thumbprint
 
.EXAMPLE
ConvertFrom-AadBase64StringToThumbprint -Base64String 'z79RnGljTQa9Zh4ZjLq6UaB4eUM='
 
Output...
CF-BF-51-9C-69-63-4D-06-BD-66-1E-19-8C-BA-BA-51-A0-78-79-43
 
.NOTES
 
#>


Function ConvertFrom-AadBase64StringToThumbprint {

    [cmdletbinding()]

    param(
        [parameter(Mandatory=$true, Position=0, ValueFromPipeline = $true)]
        [String] $Base64String
    )

    while($Base64String.Length % 4 -ne 0)
    {
        $Base64String += "="
    }

    $Bytes =[Convert]::FromBase64String($Base64String.Replace("-","+").Replace("_","/"))
    $Thumbprint = [BitConverter]::ToString($Bytes);

    return $Thumbprint
}

function ConvertFrom-AadImmutableId
{
[CmdletBinding()]

param (
    [Parameter(Mandatory = $true,
    HelpMessage="`
Input a ImmutableID-string, Azure CS DN value, GUID-ObjectGuid/ms-DS-ConsistencyGuid, Hex-ObjectGuid/ms-DS-ConsistencyGuid or Decimal-ObjectGuid/ms-DS-ConsistencyGuid`
`
ImmutableID: 2bRnBQ6D80uTz6T14srMPw==`
Decimal: 217 180 103 5 14 131 243 75 147 207 164 245 226 202 204 63`
HEX: D9 B4 67 05 0E 83 F3 4B 93 CF A4 F5 E2 CA CC 3F`
DN: CN={3262526e42513644383075547a3654313473724d50773d3d}`
GUID: 0567b4d9-830e-4bf3-93cf-a4f5e2cacc3f`
`
"
)]
    [string]$Value
)

# identification helper functions

function isGUID ($data) { 
    try {     $guid = [GUID]$data 
        return 1 } 
    catch { return 0 } 
}

function isBase64 ($data) { 
    try {     $decodedII = [system.convert]::frombase64string($data) 
            return 1 } 
    catch { return 0 } 
}

function isHEX ($data) { 
    try {     $decodedHEX = "$data" -split ' ' | foreach-object { if ($_) {[System.Convert]::ToByte($_,16)}}
        return 1 } 
    catch { return 0 } 
}

function isDN ($data) { 
    If ($data.ToLower().StartsWith("cn=")) {
        return 1 }
    else {    return 0 }
}

# conversion functions

function ConvertIItoDecimal ($data) {
    if (isBase64 $data) {
        $dec=([system.convert]::FromBase64String("$data") | ForEach-Object ToString) -join ' '
        return $dec
    }
}

function ConvertIIToHex ($data) {
    if (isBase64 $data) {
        $hex=([system.convert]::FromBase64String("$data") | ForEach-Object ToString X2) -join ' '
        return $hex
    }    
}

function ConvertIIToGuid ($data) {
    if (isBase64 $data) {
        $guid=[system.convert]::FromBase64String("$data")
        return [guid]$guid
    }
}

function ConvertHexToII ($data) {
    if (isHex $data) {
        $bytearray="$data" -split ' ' | foreach-object { if ($_) {[System.Convert]::ToByte($_,16)}}
        $ImmID=[system.convert]::ToBase64String($bytearray)
        return $ImmID
    }
}

function ConvertIIToDN ($data) {
    if (isBase64 $data) {
        $enc = [system.text.encoding]::utf8
        $result = $enc.getbytes($data)
        $dn=$result | foreach { ([convert]::ToString($_,16)) }
        $dn=$dn -join ''
        return $dn
    }
}

function ConvertDNtoII ($data) {
    if (isDN $data) {
        $hexstring = $data.replace("CN={","")
        $hexstring = $hexstring.replace("}","")
        $array = @{}
        $array = $hexstring -split "(..)" | ? {$_}
        $ImmID=$array | FOREACH { [CHAR][BYTE]([CONVERT]::ToInt16($_,16))}
        $ImmID=$ImmID -join ''
        return $ImmID
    }
}

function ConvertGUIDToII ($data) {
    if (isGUID $data) {
        $guid = [GUID]$data
            $bytearray = $guid.tobytearray()
            $ImmID=[system.convert]::ToBase64String($bytearray)
        return $ImmID
    }
}

# from byte string (converted to byte array)
If ( ($value -replace ' ','') -match "^[\d\.]+$") {
    $bytearray=("$value" -split ' ' | foreach-object {[System.Convert]::ToByte($_)})
    $HEXID=($bytearray| ForEach-Object ToString X2) -join ' '

    $identified="1"
    Write-host ""

    $ImmID=ConvertHexToII $HEXID
    $dn=ConvertIIToDN $ImmID
    $GUIDImmutableID = ConvertIIToGuid $ImmID

    Write-Host "HEX: $HEXID" -foregroundColor Green
    Write-Host "ImmutableID: $ImmID" -foregroundColor Green
    Write-Host "DN: CN={$dn}" -foregroundColor Green
    Write-Host "GUID: $GUIDImmutableID " -foregroundColor Green
}

# from hex
If ($value -match " ") {
    If ( ($value -replace ' ','') -match "^[\d\.]+$") {
        Return
    }
    $identified="1"
    Write-host ""

    $ImmID=ConvertHexToII $value
    $dec=ConvertIItoDecimal $ImmID
    $dn=ConvertIIToDN $ImmID
    $GUIDImmutableID = ConvertIIToGuid $ImmID

    Write-Host "Decimal: $dec" -foregroundColor Green
    Write-Host "ImmutableID: $ImmID" -foregroundColor Green
    Write-Host "DN: CN={$dn}" -foregroundColor Green
    Write-Host "GUID: $GUIDImmutableID " -foregroundColor Green
}

# from immutableid
If ($value.EndsWith("==")) {
    $identified="1"
    Write-host ""

    $dn=ConvertIIToDn $Value    
    $HEXID=ConvertIIToHex $Value
    $GUIDImmutableID = ConvertIIToGuid $Value
    $dec=ConvertIItoDecimal $value

    Write-Host "Decimal: $dec" -foregroundColor Green
    Write-Host "HEX: $HEXID" -foregroundColor Green
    Write-Host "DN: CN={$dn}" -foregroundColor Green
    Write-Host "GUID: $GUIDImmutableID " -foregroundColor Green
}

# from dn
If ($value.ToLower().StartsWith("cn=")) {
    $identified="1"
    Write-host ""

    $ImmID=ConvertDNToII $Value
    $HEXID=ConvertIIToHex $ImmID
    $GUIDImmutableID = ConvertIIToGuid $ImmID
    $dec=ConvertIItoDecimal $ImmID

    Write-Host "Decimal: $dec" -foregroundColor Green
    Write-Host "ImmutableID: $ImmID" -foregroundColor Green
    Write-Host "HEX: $HEXID" -foregroundColor Green
    Write-Host "GUID: $GUIDImmutableID " -foregroundColor Green
}

# from guid
if ( isGuid $Value) {
    $identified="1"
    Write-host ""

    $ImmID=ConvertGUIDToII $Value
    $dn=ConvertIIToDN $ImmID
    $HEXID=ConvertIIToHex $ImmID
    $dec=ConvertIItoDecimal $ImmID

    Write-Host "Decimal: $dec" -foregroundColor Green
    Write-Host "ImmutableID: $ImmID" -foregroundColor Green
    Write-Host "HEX: $HEXID" -foregroundColor Green
    Write-Host "DN: CN={$dn}" -foregroundColor Green    
}

If (-not($identified)) {
    Write-host -fore red "You provided a value that was neither an ImmutableID (ended with ==), a DN (started with CN=), a GUID, a HEX-value nor a Decimal-value, please try again."
}

<# Examples
 
ImmutableID: 2bRnBQ6D80uTz6T14srMPw==
Decimal: 217 180 103 5 14 131 243 75 147 207 164 245 226 202 204 63
HEX: D9 B4 67 05 0E 83 F3 4B 93 CF A4 F5 E2 CA CC 3F
DN: CN={3262526e42513644383075547a3654313473724d50773d3d}
GUID: 0567b4d9-830e-4bf3-93cf-a4f5e2cacc3f
 
#>

}

<#
.SYNOPSIS
Convert the long number format from JWT tokens to UTC
 
.DESCRIPTION
Convert the long number format from JWT tokens to UTC
For example convert '1557162946' to '2019-05-06T22:15:46.0000000Z'
 
.PARAMETER JwtNumberDateTime
This is the JWT number format (i.e. 1557162946)
 
.EXAMPLE
ConvertFrom-AadJwtTime 1557162946
 
.NOTES
General notes
#>


function ConvertFrom-AadJwtTime {
    Param (
        [Parameter(ValueFromPipeline = $true,Mandatory=$true)]
        [string]
        $JwtDateTime
    )

    $date = (Get-Date -Date "1/1/1970").AddSeconds($JwtDateTime)
    return $date
}
<#
.SYNOPSIS
Convert a base64Encoded Json Web Token to a PowerShell object.
#
 
.DESCRIPTION
Convert a base64Encoded Json Web Token to a PowerShell object.
 
.PARAMETER Token
Parameter description
 
.EXAMPLE
EXAMPLE 1
"eyJ***" | ConvertFrom-AadJwtToken
 
EXAMPLE 2
ConvertFrom-AadJwtToken -Token "eyJ***"
 
.NOTES
General notes
#>


function ConvertFrom-AadJwtToken {
 
    [cmdletbinding()]
    param([Parameter(ValueFromPipeline = $true,Mandatory=$true)][string]$Token)
 
    #Validate as per https://tools.ietf.org/html/rfc7519
    #Access and ID tokens are fine, Refresh tokens will not work
    if (!$token.Contains(".") -or !$token.StartsWith("eyJ")) { Write-Error "Invalid token" -ErrorAction Stop }
    
    $jwtClaims = [ordered]@{}

    #Header
    $tokenheader = $token.Split(".")[0]
    
    #Fix padding as needed, keep adding "=" until string length modulus 4 reaches 0
    while ($tokenheader.Length % 4) { $tokenheader += "=" }
    
    #Convert from Base64 encoded string to PSObject all at once
    $jwtHeader = ([System.Text.Encoding]::ASCII.GetString([system.convert]::FromBase64String($tokenheader)) | ConvertFrom-Json)

    $jwtHeader.psobject.properties | Foreach { $jwtClaims[$_.Name] = $_.Value }

    #Payload
    $tokenPayload = $token.Split(".")[1]
    
    #Fix padding as needed, keep adding "=" until string length modulus 4 reaches 0
    while ($tokenPayload.Length % 4) { $tokenPayload += "=" }
    
    #Convert from Base64 encoded string to PSObject all at once
    $jwtPayload = ([System.Text.Encoding]::ASCII.GetString([system.convert]::FromBase64String($tokenPayload)) | ConvertFrom-Json)

    $claims = $jwtPayload.psobject.properties

    
    Foreach ($claim in $claims) 
    { 
        if($claim.Name -eq "iat" -or $claim.Name -eq "exp" -or $claim.Name -eq "nbf") 
        {
            $jwtClaims.Add($claim.Name, ($claim.Value | ConvertFrom-AadJwtTime) )
        }

        else 
        { 
            try {
                $jwtClaims.Add($claim.Name,$claim.Value)
            }
            catch {
                Write-Verbose "Duplicate claim $($claim.Name)"
            }
            
        }


    }

    $Object = New-Object -TypeName psobject -Property $jwtClaims

    Write-Verbose "Done converting JWT to PSObject."

    return $Object
}
<#
.SYNOPSIS
Converts a Thumbprint to a Base64Encoded Thumbprint or also known as Key Identifier (Kid)
 
.DESCRIPTION
Converts a Thumbprint to a Base64Encoded Thumbprint or also known as Key Identifier (Kid)
 
.PARAMETER Thumbprint
Provide the Thumbprint to be converted to a Base64Encoded value
 
.EXAMPLE
ConvertFrom-AadThumbprintToBase64String -Base64String 'CF-BF-51-9C-69-63-4D-06-BD-66-1E-19-8C-BA-BA-51-A0-78-79-43'
ConvertFrom-AadThumbprintToBase64String -Base64String 'CFBF519C69634D06BD661E198CBABA51A0787943'
 
Output...
z79RnGljTQa9Zh4ZjLq6UaB4eUM=
 
.NOTES
#>


Function ConvertFrom-AadThumbprintToBase64String {

    [cmdletbinding()]

    param(
        [parameter(Mandatory=$true, Position=0, ValueFromPipeline = $true)]
        [String] $Thumbprint
    )

    $Bytes = Convert-AadThumbprintToByteArray -Thumbprint ($Thumbprint.Replace("-",""))

    $hashedString =[Convert]::ToBase64String($Bytes)

 
    $hashedString = $hashedString.Split('=')[0]
    $hashedString = $hashedString.Replace('+', '-')
    $hashedString = $hashedString.Replace('/', '_')

    return $hashedString
}

<#
.SYNOPSIS
Utility to convert strings to Base 64 Encoded Strings. By default this will also remove formatting from 'Carriage Return', 'New Line', 'Tab'
 
.DESCRIPTION
Utility to convert strings to Base 64 Encoded Strings. By default this will also remove formatting from 'Carriage Return', 'New Line', 'Tab'
 
ConvertTo-AadBase64EncodedString -string 'your-string-here' -StripFormatting $true
 
.PARAMETER String
String to encode to Base 64.
 
.PARAMETER StripFormatting
Boolean to determine if formatting items like ('Carriage Return', 'New Line', 'Tab') should be removed.
Default is TRUE. Formatting items will be removed.
 
.EXAMPLE
ConvertTo-AadBase64EncodedString -string 'your-string-here' -StripFormatting $true
 
 
.NOTES
General notes
#>

function ConvertTo-AadBase64EncodedString
{

    Param(
        [Parameter(
            mandatory=$true,
            Position=0,
            ValueFromPipeline = $true
        )]
        [string]$String,
        [bool]$StripFormatting=$true
    )


    if($StripFormatting)
    {
        $String = $String.Replace("`r", "").Replace("`n", "").Replace("`t", "")
    }

    $EncodedString = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($String))
    
    return $EncodedString
}


<#
.SYNOPSIS
Exports all Azure Role Assignments from all subscriptions in which you have read access to.
 
.DESCRIPTION
Exports all Azure Role Assignments from all subscriptions in which you have read access to.
 
This will output a series of files...
* Separate CSV for each group and their Group Memberships
* Separate CSV for each Azure subscription and their Azure Role Assignments
* Single CSV that contains all subscriptions and all Azure ROle Assignments
* Single HTML that contains all subscriptions and all Azure ROle Assignments
 
Output of running this command will look something like this...
 
Skipping 'Access to Azure Active Directory'. This is not going to have Role Assignments.
Analyzing Subscription 'Pay-As-You-Go (Id:92aa81c9-af09-4ea2-ade0-72a1b4073dbe)'
Exported Group Memberships
 > C:\temp\GroupMembers--DynamicGroup.csv
Exported Subscription Role Assignments
 > C:\temp\Subscription--Pay-As-You-Go-Roles.csv
Analyzing Subscription 'Windows Azure MSDN - Visual Studio Ultimate (Id:955107ad-af96-475e-a9e9-0b0474e83982)'
Exported Group Memberships
 > C:\temp\GroupMembers--Application Access - 4.csv
Exported Group Memberships
 > C:\temp\GroupMembers--Group 1.csv
Exported Subscription Role Assignments
 > C:\temp\Subscription--Windows Azure MSDN - Visual Studio Ultimate-Roles.csv
Analyzing Subscription 'Microsoft Azure Internal Consumption (Id:ef8110a7-ab02-4b82-a4d1-4126dcda86e0)'
Exported Subscription Role Assignments
 > C:\temp\Subscription--Microsoft Azure Internal Consumption-Roles.csv
Exported All Role Assignments
 > C:\temp\Subscription--All-Roles.csv
Exported HTML
 > C:\temp\Subscription--All-Roles.html
 
 
Please verify the contents of the exported files.
 
You can use either the 'Subscription--All-Roles.csv' or one of the subscription files to import the Azure Role Assignments into another tenant or into another Azure subscription when running...
Import-AadAzureRoleAssignments
 
.EXAMPLE
Export-AadAzureRoleAssignments
#>


function Export-AadAzureRoleAssignments {
    $RoleAssignments = @()

    #Traverse through each Azure subscription user has access to
    $subscriptions = Invoke-AzureCommand -Command {
      Param($TenantId)
      Get-AzSubscription -TenantId $TenantId
    } -Parameters $Global:AadSupport.Session.TenantId

    Foreach ($sub in $subscriptions) {
        $SubName = $sub.Name
        if ($sub.Name -ne "Access to Azure Active Directory") { # You can't assign roles in Access to Azure Active Directory subscriptions
            
            Write-Host "Analyzing Subscription '$($sub.Name) (Id:$($sub.id))' "
            
            Invoke-AzureCommand -Command {
              Param($SubscriptionId)
              Set-AzContext -SubscriptionId $SubscriptionId | Out-Null
            } -Parameters $sub.id -SubscriptionId $sub.id

            Try {
                $SubRoleAssignments = Invoke-AzureCommand -Command { Get-AzRoleAssignment -IncludeClassicAdministrators } -SubscriptionId $sub.id
                $RoleAssignments += $SubRoleAssignments
            } 
            Catch {
                Write-Output "Failed to collect RBAC permissions for $subname"
            }
            
            #Custom Roles do not display their Name in these results. We are forcing this behavior for improved reporting
            Foreach ($role in $RoleAssignments) {
              $ObjectId = $role.ObjectId
              $DisplayName = $role.DisplayName
              If (-not $role.RoleDefinitionName) {
                $role.RoleDefinitionName = Invoke-AzureCommand {
                  Param($RoleDefinition)
                  (Get-AzRoleDefinition -Id $RoleDefinition).Name
                } -Parameters $role.RoleDefinitionId -SubscriptionId $sub.id
              }
              if ($role.ObjectType -eq "Group" -and !(Test-Path -path "GroupMembers--$DisplayName.csv")) {
                $Members = Invoke-AadCommand -Command {
                  Param($ObjectId)
                  Get-AzureADGroupMember -ObjectId $ObjectId
                } -Parameters $ObjectId
                
                $Path = Get-Location
                $FilePath = "$Path\GroupMembers--$DisplayName.csv"

                $Members | Export-CSV $FilePath -NoTypeInformation -Force
                Write-Host "Exported Group Memberships"
                Write-Host " > $FilePath"
              }
            }
            #Export the Role Assignments to a CSV file labeled by the subscription name

            $Path = Get-Location
            $FilePath = "$Path\Subscription--$SubName-Roles.csv"

            $SubRoleAssignments | Export-CSV $FilePath -NoTypeInformation -Force
            Write-Host "Exported Subscription Role Assignments"
            Write-Host " > $FilePath"
        }

        else {
          Write-Host "Skipping 'Access to Azure Active Directory'. This is not going to have Role Assignments."
        }
    }

    #Export All Role Assignments in to a single CSV file
    $Path = Get-Location
    $FilePath = "$Path\Subscription--All-Roles.csv"

    $RoleAssignments | Export-CSV ".\Subscription--All-Roles.csv" -NoTypeInformation -Force

    Write-Host "Exported All Role Assignments"
    Write-Host " > $FilePath"

    # HTML report
    $a = "<style>"
    $a = $a + "TABLE{border-width: 1px;border-style: solid;border-color: black;border-collapse: collapse;font-family:arial}"
    $a = $a + "TH{border-width: 1px;padding: 5px;border-style: solid;border-color: black;}"
    $a = $a + "TD{border-width: 1px;padding: 5px;border-style: solid;border-color: black;}"
    $a = $a + "</style>"

    $Path = Get-Location
    $FilePath = "$Path\Subscription--All-Roles.html"

    $RoleAssignments | ConvertTo-Html -Head $a| Out-file $FilePath -Force
    Write-Host "Exported HTML"
    Write-Host " > $FilePath"
}
<#
.SYNOPSIS
Gets the admin roles assigned to the specified object (User or ServicePrincipal)
 
.DESCRIPTION
Gets the admin roles assigned to the specified object (User or ServicePrincipal)
 
Example 1: Get Admin Roles for a User or Object based on its ObjectId
Get-AadAdminRolesByObject -ObjectId
 
Example 2: Get Admin Roles for a ServicePrincipal
Get-AadAdminRolesByObject -ServicePrincipalId 'Contoso Web App'
 
Example 3: Get Admin Roles for a user
Get-AadAdminRolesByObject -UserId 'john@contoso.com'
 
.PARAMETER ObjectId
Lookup user or service principal by its ObjectId
 
.PARAMETER ServicePrincipalId
Lookup service principal by any of its Ids (DisplayName, AppId, ObjectId, or SPN)
 
.PARAMETER UserId
Lookup user by any of its Ids ObjectId or UserPrincipalName
 
.NOTES
General notes
#>


function Get-AadAdminRolesByObject {

    param(
        [Parameter(
            ValueFromPipeline = $true,
            ParameterSetName = "ByObjectId")]
        [parameter(ValueFromPipeline=$true)]
        $ObjectId,

        [Parameter(ParameterSetName = "ByServicePrincipalId")]
        $ServicePrincipalId,

        [Parameter(ParameterSetName = "ByUserId")]
        $UserId
    )

    # REQUIRE AadSupport Session
    RequireConnectAadSupport
    # END REGION

    # Search for ServicePrincipal
    if($ServicePrincipalId)
    {
        $sp = Get-AadServicePrincipal -Id $ServicePrincipalId

        If(-not $sp)
        {
           return 
        }

        $ObjectId = $sp.ObjectId
    }

    # Search for User
    if($UserId)
    {
        $user = Invoke-AadCommand -Command {
            Param($UserId)
            Get-AzureADUser -ObjectId $UserId
        } -Parameters $UserId

        If(-not $user)
        {
           return 
        }

        $ObjectId = $user.ObjectId
    }

    $roles = Invoke-AadCommand -Command {
        Get-AzureADDirectoryRole
    }

    $AdminRoleList = @()

    $AadAdminCount = 0
    foreach ($role in $roles) {
        $members = Invoke-AadCommand -Command {
            Param($role)
            Get-AzureADDirectoryRoleMember -ObjectId $role.ObjectId
        } -parameters $role

        foreach ($member in $members) {
            if($member.ObjectId -eq $ObjectId) {
                $AdminRoleList += [PSCustomObject]@{
                    RoleDisplayName = $role.DisplayName;
                    RoleId = $role.ObjectId;
                }
            }
        } 
    }

    # Output Admin Roles
    $ReturnObject = $AdminRoleList | Select-Object RoleDisplayName, RoleId

    return $ReturnObject

}
Set-Alias -Name Get-AadApp -Value Get-AadApplication

<#
.SYNOPSIS
Intelligence to return the Application object by looking up using any of its identifiers.
 
.DESCRIPTION
Intelligence to return the Application object by looking up using any of its identifiers.
 
.PARAMETER Id
Either specify Application Name, Display Name, Object ID, Application/Client ID, or Application Object ID
 
.EXAMPLE
Get-AadApplication -Id 'Contoso Web App'
 
.NOTES
Returns the Application object using Get-AzureAdApplication and filter based on the Id parameter
#>


function Get-AadApplication
{
    [CmdletBinding(DefaultParameterSetName='ByAnyId')]
    param(
        [Parameter(
            mandatory=$true,
            Position=0,
            ValueFromPipeline = $true,
            ParameterSetName = 'ByAnyId'
        
        )]
        $Id,


        [Parameter(
            mandatory=$true,
            ParameterSetName = 'ByAppId'
        )]
        $AppId,

        [Parameter(
            mandatory=$true,
            ParameterSetName = 'ByDisplayName'
        )]
        $DisplayName,

        [Parameter(
            mandatory=$true,
            ParameterSetName = 'ByAppUriId'
        )]
        $AppUriId,

        [Parameter(
            mandatory=$true,
            ParameterSetName = 'ByReplyAddress'
        )]
        $ReplyAddress

        
    )
    

    # REQUIRE AadSupport Session
    RequireConnectAadSupport
    # END REGION

    $Global:Applications = @()
    $app         = $null
    $isGuid     = $null

    # Search By AppId
    if ($AppId) {
        Write-Verbose "Looking for '$AppId'"
        $app = GetAadAppByAppId $AppId
    }

    # Search By ReplyAddress
    if ($ReplyAddress) {
        Write-Verbose "Looking for '$ReplyAddress'"
        $app = GetAadAppByReplyAddress $ReplyAddress
    }

    # Search By AppUriId
    if ($AppUriId) {
        Write-Verbose "Looking for '$AppUriId'"
        $app = GetAadAppByAppUriId $AppUriId
    }

    # Search By DisplayName
    if ($DisplayName) {
        Write-Verbose "Looking for '$DisplayName'"
        $sp = GetAadAppyDisplayName -Id $DisplayName
    }

    try {
        $isGuid = [System.Guid]::Parse($Id)
    } catch {
    }
    
    # Search By All (Any ID)
    if($Id)
    {
        # Search for app based on AppId or ObjectId
        if ($isGuid -and -not $app) {

            # Search for app based on ObjectId
            $app = $null
            $app = try { 
                Invoke-AadCommand -Command {
                    Param($Id)
                    Get-AzureADObjectByObjectId -ObjectId $Id 
                } -Parameters $Id
            } catch {}

            if ($app.ObjectType -eq "Application") {
                Write-Verbose "Application found using ObjectId"
            }

            $appid = $Id
            if ($app.ObjectType -eq "ServicePrincipal") {
                Write-Verbose "Service Principal found! Looking for Application..."
                $appid = $app.AppId
                $app = $null
            }

            # Search for app based on AppId
            if(-not $app)
            {
                $app = GetAadAppByAppId -Id $appid
                if ($app) { return $app }
            }
        } 
        # Search for app based on AppUriId or DisplayName
        if(-not $app) {
            $app = @()
            $app += GetAadAppByDisplayName $Id
            $app += GetAadAppByAppUriId $Id
        }
    }

    $Global:Applications = $null

    # Exit script! Application Not found
    if (-not $app) {
        throw "Azure AD Application '$Id' not found!"
    }

    # Application(s) Found > Only return One result
    if($app.count -gt 1)
    {
        $app = $app | Out-GridView -PassThru -Title "Select Application"
    }
    return $app
}


function GetAadAppByAppId
{
    param(
        [Parameter(
            mandatory=$true,
            ValueFromPipeline = $true)]
        $Id
    )

    Write-Verbose "Searching by AppId"

    try {
        $isGuid = [System.Guid]::Parse($Id)
    } catch {
        throw "Invalid App Id"
    }

    $app = Invoke-AadCommand {
        Param($Id)
        Get-AzureAdApplication -filter "AppId eq '$Id'"
    } -Parameters $Id

    if ($app) { 
        Write-Verbose "Application found using AppId"
        return $app
    }

    return
}


function GetAadAppByReplyAddress
{
    param(
        [Parameter(
            mandatory=$true,
            ValueFromPipeline = $true)]
        $Id
    )

    Write-Verbose "Searching by ReplyUrls"

    $apps = LookupAllApplications

    $app = @()
    $app += $apps | Where-Object {$_.ReplyUrls -contains "$Id"}

    if ($app) { 
        Write-verbose "Application '$Id' found using Reply Address" 
    }

    return $app
}



function GetAadAppByAppUriId
{
    param(
        [Parameter(
            mandatory=$true,
            ValueFromPipeline = $true)]
        $Id
    )

    Write-Verbose "Searching by AppUriId"

    $apps = LookupAllApplications

    $app = @()
    $app += $apps | Where-Object { $_.IdentifierUris -match $Id }

    if ($app) { 
        Write-verbose "Application(s) '$Id' found using AppUriId" 
    }

    return $app
}


function GetAadAppByDisplayName
{
    param(
        [Parameter(
            mandatory=$true,
            ValueFromPipeline = $true)]
        $Id
    )

    Write-Verbose "Searching by DisplayName"

    $app = Invoke-AadCommand {
        Param($Id)
        Get-AzureADApplication -filter "DisplayName eq '$Id'"
    } -Parameters $Id
    
    # Application Not found yet using DisplayName (Do wider search)
    if(-not $app)
    {
        $apps = LookupAllApplications
        $app = @()
        $app += $apps | Where-Object {$_.DisplayName -match "$Id"}
    }

    if ($app) { 
        Write-Verbose "Application(s) '$Id' found using DisplayName"
    }

    return $app
}


function LookupAllApplications()
{
    if(-not $Global:Applications)
    {
        Write-Host "Looking at all Applications to find your app. This might take awhile..."
        $Global:Applications = Invoke-AadCommand {
            Get-AzureADApplication -All $true
        }
    }

    return $Global:Applications
}
<#
.SYNOPSIS
Easily find the value or id of a permission based on the servicePrincipals AppRoles or Oauth2Permissions
 
.DESCRIPTION
Long descriptionEasily find the value or id of a permission based on the servicePrincipals AppRoles or Oauth2Permissions
 
.PARAMETER ResourceId
Provide the Resource Identifier
 
.PARAMETER Permission
Provide the permission you want to look up.
 
EXAMPLES
 
# Lookup Scope/Role Value
Get-AadAppPermissionInfo "Microsoft Graph" -Permission User.Read.All
 
# Lookup Scope Id > This is the id for User.Read Scope
Get-AadAppPermissionInfo "Microsoft Graph" -Permission a154be20-db9c-4678-8ab7-66f6cc099a59
 
# Lookup Role Id > This is the id for User.Read Role
Get-AadAppPermissionInfo "Microsoft Graph" -Permission df021288-bdef-4463-88db-98f22de89214
 
.NOTES
General notes
#>

function Get-AadAppPermissionInfo {
    [CmdletBinding(DefaultParameterSetName="DefaultSet")] 
    param (
        [Parameter(mandatory=$true, Position=0, ValueFromPipeline = $true)]
        [string]$ResourceId,

        [Parameter(mandatory=$true)]
        [string]$Permission
    )

    # Get the servicePrincipal for the resource
    $sp = Get-AadServicePrincipal -Id $ResourceId

    # Lookup the permission in AppRoles
    $Roles = $sp.AppRoles | where {$_.Value -eq $Permission -or $_.id -eq $Permission} | Select Id, Value, Type
    
    # Role found so add the 'Type' property
    if($Roles)
    {
        $Roles.Type = "Role"
    }

    # Lookup the permission in Oauth2Permissions
    $Scopes = $sp.Oauth2Permissions | where {$_.Value -eq $Permission -or $_.id -eq $Permission} | Select Id, Value, Type
    
    # Scope found so add the 'Type' property
    if($Scopes)
    {
        $Scopes.Type = "Scope"
    }

    # Build our results
    $results = @()
    $results += $Roles
    $results += $Scopes

    return $results
}
<#
.SYNOPSIS
Get the App roles assigned to the specified object
 
.DESCRIPTION
Get the App roles assigned to the specified object
 
.PARAMETER ServicePrincipalId
Specify by the Service Principal
 
.PARAMETER ObjectId
Specify by any object ID
 
.PARAMETER ObjectType
Specify the Object type based on Object id specified
 
.PARAMETER UserId
Specify the User
 
.EXAMPLE
Get-AadAppRolesByObject -ServicePrincipalId 'Contoso App'
 
ResourceDisplayName : Microsoft Graph
ResourcePermission : User.ReadWrite.All
DirectAssignment : True
GetsAssignmentBy :
Id : ef7d1fa9-1e37-48fd-bb58-ad10a78cbd18
 
.NOTES
General notes
#>


function Get-AadAppRolesByObject {
    param(
        [Parameter(mandatory=$true, ParameterSetName="ByServicePrincipalId")]
        $ServicePrincipalId,

        [Parameter(mandatory=$true, ParameterSetName="ByObjectId")]
        [parameter(ValueFromPipeline=$true)]
        $ObjectId,

        [Parameter(ParameterSetName="ByObjectId")]
        [parameter(ValueFromPipeline=$true)]
        [ValidateSet("User","ServicePrincipal")]
        $ObjectType,

        [Parameter(mandatory=$true, ParameterSetName="ByUserId")]
        $UserId
    )

    if($ObjectId -and -not $ObjectType)
    {
        $ObjectType = (
            Invoke-AadCommand -Command {
                Param($ObjectId)
                Get-AzureADObjectByObjectId -ObjectIds $ObjectId
            } -Parameters $ObjectId
        ).ObjectType
    }

    if($ServicePrincipalId)
    {
        $sp = (Get-AadServicePrincipal -Id $ServicePrincipalId)
        $ObjectId = $sp.ObjectId

        if($sp.count -gt 1)
        {
            throw "'$ServicePrincipalId' query returned more than one result. Please provide a unique Service Principal Identifier"
        }

        if(-not $ObjectId)
        {
            throw "'$ServicePrincipalId' not found in '$TenantDomain'"
        }

        $ObjectType = "ServicePrincipal"
    }

    $TenantDomain = $Global:AadSupport.Session.TenantId


    $AppRoleList = @()

    if($ObjectType -eq "ServicePrincipal")
    {
        $AppRoles = Invoke-AadCommand -Command {
            Param($ObjectId)
            Get-AzureADServiceAppRoleAssignedTo -ObjectId $ObjectId
        } -Parameters $ObjectId
    }

    if($UserId)
    {
        $User = (
            Invoke-AadCommand -Command {
                Param($UserId)
                Get-AzureADUser -ObjectId $UserId
            } -Parameters $UserId
        )

        $ObjectId = $User.ObjectId

        if(-not $ObjectId)
        {
            throw "'$UserId' not found in '$TenantDomain'"
        }

        $ObjectType = "User"
    }

    if($ObjectType -eq "User")
    {
        $AppRoles = Invoke-AadCommand -Command {
            Param($ObjectId)
            Get-AzureADUserAppRoleAssignment -ObjectId $ObjectId
        } -Parameters $ObjectId
    }


    foreach ($AppRole in $AppRoles) {
        if($ObjectId -eq $AppRole.PrincipalId)
        {
            $DirectAssignment = $true
        }
        else {
            $DirectAssignment = $false
            $GetsAssignmentBy = "$($AppRole.PrincipalDisplayName) ($($AppRole.PrincipalId))"
        }

        $resource = (
            Invoke-AadCommand -Command {
                Param($AppRole)
                Get-AzureADServicePrincipal -ObjectId $AppRole.ResourceId
            } -Parameters $AppRole
        ).AppRoles | Where-Object { $_.Id -eq $AppRole.Id }

        $AppRoleList += [PSCustomObject]@{
            ResourceDisplayName = $AppRole.ResourceDisplayName;
            ResourcePermission = $resource.Value
            DirectAssignment = $DirectAssignment
            GetsAssignmentBy = $GetsAssignmentBy
            Id = $AppRole.PrincipalId   
        }
    }

    # Output App Roles

    return $AppRoleList
}
<#
.SYNOPSIS
Get the Azure Roles assigned to the specified object
 
.DESCRIPTION
Get the Azure Roles assigned to the specified object
 
.PARAMETER ServicePrincipalId
Specify by the Service Principal
 
.PARAMETER ObjectId
Specify by any object ID
 
.PARAMETER ObjectType
Specify the Object type based on Object id specified
 
.PARAMETER UserId
Specify the User
 
.PARAMETER ServicePrincipalName
Specify the ServicePrincipalName
 
.PARAMETER SigninName
Specify the SigninName
 
.EXAMPLE
Get-AadAzureRoleAssignments -ServicePrincipalId 'Contoso App'
 
ResourceDisplayName : Microsoft Graph
ResourcePermission : User.ReadWrite.All
DirectAssignment : True
GetsAssignmentBy :
Id : ef7d1fa9-1e37-48fd-bb58-ad10a78cbd18
 
.NOTES
General notes
#>

function Get-AadAzureRoleAssignments {
    # THIS FUNCTION IS STANDALONE
    [CmdletBinding(DefaultParameterSetName="ByObject")]
    param(
        [Parameter(ParameterSetName="ByObject",Mandatory=$true)]
        [Parameter(ParameterSetName="ByServicePrincipalObject",Mandatory=$true)]
        [parameter(ValueFromPipeline=$true)]
        [string]$ObjectId,

        [Parameter(ParameterSetName="ByObject")]
        [Parameter(ParameterSetName="ByServicePrincipalObject",Mandatory=$false)]
        [parameter(ValueFromPipeline=$true)]
        [string]$ObjectType,

        [Parameter(ParameterSetName="ByServicePrincipalId",Mandatory=$true)]
        [string]$ServicePrincipalId,

        [Parameter(ParameterSetName="ByUserId",Mandatory=$true)]
        [string]$UserId,

        [Parameter(ParameterSetName="ByServicePrincipalName",Mandatory=$true)]
        [Parameter(ParameterSetName="ByServicePrincipalObject",Mandatory=$false)]
        [string]$ServicePrincipalName,

        [Parameter(ParameterSetName="BySigninName",Mandatory=$true)]
        [string]$SigninName
    )

    # REQUIRE AadSupport Session
    RequireConnectAadSupport
    # END REGION

    $TenantId = $Global:AadSupport.Session.TenantId

    # If ObjectId is used, need to determine what object type it is and set appropraite variables
    if($ObjectId -and (-not $ObjectType -or $ServicePrincipalName))
    {
        $Object = Invoke-AadCommand -Command {
            Param($ObjectId)
            Get-AzureADObjectByObjectId -ObjectIds $ObjectId
        } -Parameters $ObjectId

        # If no object found throw error
        if(-not $Object)
        {
            throw "'$ObjectId' not found in '$TenantId'"
        }
    }
    elseif($ServicePrincipalName)
    {
        $Object = Get-AadServicePrincipal -ServicePrincipalName $ServicePrincipalName
    }

    if(-not $ObjectType)
    {
        # Get Object Type
        $ObjectType = $Object.ObjectType
        if ($ObjectType -eq "ServicePrincipal")
        {
            $ServicePrincipalName = $Object.ServicePrincipalNames[0]
        }

        if ($ObjectType -eq "User")
        {
            $SigninName = $Object.UserPrincipalName
        }
    }


    if($ServicePrincipalId)
    {
        $sp = (Get-AadServicePrincipal -Id $ServicePrincipalId)
        $ServicePrincipalName = $sp.ServicePrincipalNames[0]
        $ObjectId = $sp.ObjectId

        if($sp.count -gt 1)
        {
            throw "'$ServicePrincipalId' query returned more than one result. Please provide a unique Service Principal Identifier"
        }

        if(-not $ServicePrincipalName)
        {
            throw "'$ServicePrincipalId' not found in '$TenantId'"
        }
    }

    if($UserId)
    {
        $User = (
            Invoke-AadCommand -Command {
                Param($UserId)
                Get-AzureADUser -ObjectId $UserId
            } -Parameters $UserId
        )
        $SigninName = $User.UserPrincipalName

        if(-not $UserId)
        {
            throw "'$UserId' not found in '$TenantId'"
        }
    }

    $subscriptions = Invoke-AzureCommand -Command {
        Param($TenantId)
         Get-AzSubscription -TenantId $TenantId
    } -Parameters $Global:AadSupport.Session.TenantId

    $result = @()
    foreach($sub in $subscriptions) {
        
        if($sub.Name -ne "Access to Azure Active Directory")
        {
            $context = Invoke-AzureCommand -Command { 
                Param($Params)
                #$context = Get-AzSubscription -Subscription $Params.SubscriptionId -TenantId $Params.TenantId
                Set-AzContext -Tenant $Params.TenantId -Subscription $Params.SubscriptionId | Out-Null
            } -Parameters @{
                SubscriptionId = $sub.id
                TenantId = $TenantId
            } -SubscriptionId $sub.id
            
            # Get Role Assignments
            if($ServicePrincipalName)
            {
                $RoleAssignments = @()
                $AzRoleAssignments = Invoke-AzureCommand -Command { Get-AzRoleAssignment } -SubscriptionId $sub.id

                # Get Direct Assignments
                $RoleAssignments += Invoke-AzureCommand -Command {
                    Param($ServicePrincipalName)
                    Get-AzRoleAssignment -ServicePrincipalName $ServicePrincipalName
                } -Parameters $ServicePrincipalName -SubscriptionId $sub.id

                # Get groups assigned to Azure RBAC (we need to check each group)
                $GroupIdsCheck = ($AzRoleAssignments | where {$_.ObjectType -eq "Group"}).ObjectId

                $Groups = New-Object Microsoft.Open.AzureAD.Model.GroupIdsForMembershipCheck
                $Groups.GroupIds = $GroupIdsCheck

                # Check if ServicePrincipal is a member of any of those groups
                if($GroupIdsCheck)
                {
                    $IsMemberOf = Invoke-AadCommand -Command {
                        Param($Params)
                        Select-AzureADGroupIdsServicePrincipalIsMemberOf -ObjectId $Params.ObjectId -GroupIdsForMembershipCheck $Params.Groups
                    } -Parameters @{
                        ObjectId = $ObjectId
                        Groups = $Groups
                    }

                    # Get Azure RBAC for the groups in which the Service Principal is a member of
                    foreach($GroupId in $IsMemberOf)
                    {
                        $RoleAssignments += $AzRoleAssignments | Where-Object { $_.ObjectId -eq $GroupId }
                    }
                }
            }

            if($SigninName)
            {
                $RoleAssignments = Invoke-AzureCommand -Command {
                    Param($SigninName)
                    Get-AzRoleAssignment -SignInName $SigninName -ExpandPrincipalGroups
                } -Parameters $SigninName -SubscriptionId $sub.id
            }
            
            if($IsMemberOf)
            {
                $DirectAssignment = $false
            }
            else {
                $DirectAssignment = $true
            }

            # If Role Assignment > generate output
            if($RoleAssignments)
            {
                foreach($Role in $RoleAssignments)
                {
                    if($IsMemberOf)
                    {
                        $AssignedDisplayName = (
                            Invoke-AadCommand -Command {
                                Param($Role)
                                Get-AzureADObjectByObjectId -ObjectIds $Role.ObjectId
                            } -Parameters $Role
                            
                        ).DisplayName

                        $GetsAssignmentBy = "$($AssignedDisplayName) ($($Role.ObjectId))"
                    }

                    $ResourceName = ""
                    if($Role.Scope)
                    {
                        $Split = $Role.Scope.Split("/")
                        $ResourceName = $Split[$Split.Lenth-1]
                    }
                    
                    $CustomResult = [pscustomobject]@{
                        Scope = $Role.Scope
                        RoleName = $Role.RoleDefinitionName
                        ResourceName = $ResourceName
                        DirectAssignment = $DirectAssignment
                        GetsAssignmentBy = $GetsAssignmentBy
                    }
    
                    $result += $CustomResult 
                }
            }
        }
    }

    return $result
}
<#
.SYNOPSIS
    Get a list of consented permissions based using the specified parameters to filter
 
.DESCRIPTION
    Get a list of consented permissions based using the specified parameters to filter
 
    Get-AadConsent Returns the following Object with properties
    PermissionType | Expected values: Role, Scope | Role if Application permission, Scope if Delegated permission
    ClientName | Name of the client
    ClientId | Service Principal Object ID of the client
    ResourceName | Name of the resource
    ResourceId | Service Principal Object ID of the resource
    PrincipalId | Service Principal Object ID of the user
    ClaimValue | List of scopes or role claim values
    Id | Id of the OAuth2PermissionGrant/AppRole
    ConsentType | Expected values: AdminConsent, UserConsent
 
.PARAMETER ClientId
    Filter based on the ClientId. This is the Enterprise App (Client app) in which the consented permissions are applied on.
 
.PARAMETER ResourceId
    Filter based on the ResourceId. This is the resource in which the client has permissions on.
 
.PARAMETER UserId
    Filter based on the UserId. User in which that has consented to the app.
 
.PARAMETER ClaimValue
    Filter based on the scope or role value.
 
.PARAMETER ConsentType
    Filter based on the Consent Type. Available options...
    'Admin','User', 'All'
 
.PARAMETER PermissionType
    Filter based on the Permission Type. Available options...
    'Delegated','Application', 'All'
 
.EXAMPLE
    Example 1: See a list of all consents for a app
    PS C:\> Get-AadConsent -ClientId 'Contoso App' | Format-List
 
.EXAMPLE
    Example 2: See a list of User Consents for a app
    PS C:\> Get-AadConsent -ClientId 'Contoso App' -UserId john@contoso.com | Format-List
#>

function Get-AadConsent
{
    
    [CmdletBinding(DefaultParameterSetName='Default')]
    Param(
        [string]$ClientId,
        [string]$ResourceId,
        [string]$UserId,

        [ValidateSet('Admin','User', 'All')]
        $ConsentType = 'All',

        [ValidateSet('Delegated','Application', 'All')]
        $PermissionType = 'All',

        [string]$ClaimValue
    )

    $PrincipalId = $UserId

    # Parameter Validations
    if(!$ClientId -and !$ResourceId -and !$PrincipalId)
    {
        throw "You must specify at least one of the properties : ClientId or ResourceId or PrincipalId"
    }

    if( ($Principald -and $PermissionType -eq 'Application') -or ($PermissionType -eq 'Application' -and $ConsentType -eq 'User') )
    {
        throw "You can't have PermissionType 'Application' and specify a user."
    }

    if($ClaimValue -and -not $ResourceId)
    {
        throw "You must provide a 'ResoureId' when using 'ClaimValue'"
    }

    # Start building the OAuth2PermissionGrant Filter
    $GrantFilterBuilder = @()

    if($ConsentType -eq "User" -and !$UserId)
    {
        $GrantFilterBuilder += "consentType eq 'Principal'"
    }
    elseif($ConsentType -eq "Admin")
    {
        $GrantFilterBuilder += "consentType eq 'AllPrincipals'"
    }

    $GrantUri = "$($Global:AadSupport.Resources.MsGraph)/beta/oauth2PermissionGrants?`$top=999"

    $Resource = $null
    if($ResourceId)
    {
        $Resource = Get-AadServicePrincipal -Id $ResourceId
        if(!$Resource)
        {
            throw "$ResourceId not found!"
        }

        $RoleUri = "$($Global:AadSupport.Resources.MsGraph)/beta/servicePrincipals/$($Resource.ObjectId)/appRoleAssignedTo?`$top=999"

        $GrantFilterBuilder += "resourceId eq '$($Resource.ObjectId)'"
    }


    $Client = $null
    if($ClientId)
    {
        $Client = Get-AadServicePrincipal -Id $ClientId
        if(!$Client)
        {
            throw "$ClientId not found!"
        }
        
        $RoleUri = "$($Global:AadSupport.Resources.MsGraph)/beta/servicePrincipals/$($Client.ObjectId)/appRoleAssignments?`$top=999"
        
        $GrantFilterBuilder += "clientId eq '$($Client.ObjectId)'"
    }


    $Principal = $null
    if($PrincipalId)
    {
        $Principal = Invoke-AadCommand -Command {
            Param($PrincipalId)
            Get-AzureAdUser -ObjectId $PrincipalId
        } -Parameters  $PrincipalId

        if(!$Principal)
        {
            throw "$PrincipalId not found!"
        }

        $GrantFilterBuilder += "principalId eq '$($Principal.ObjectId)'"
    }

    # CREATE FILTER FOR OAUTH2PERMISSION GRANTS
    if($GrantFilterBuilder)
    {
        $GrantFilter = "`&`$filter="
    }

    foreach($item in $GrantFilterBuilder)
    {
        if($item -ne $GrantFilterBuilder[0])
        {
            $GrantFilter += " and "
        }
        $GrantFilter += $item
    }


    # ------------------------------------------------
    # GET OAUTH2PERMISSIONGRANT

    <#
        {
            "@odata.context": "https://graph.microsoft.com/beta/$metadata#oauth2PermissionGrants",
            "value": [
                {
                    "clientId": "7f5c5913-b683-4b71-a57b-008cd3de72e5",
                    "consentType": "AllPrincipals",
                    "expiryTime": "2021-02-11T22:51:28.9886344Z",
                    "id": "E1lcf4O2cUulewCM095y5Yb6Xxl6wYJDqt2ei2ygkbw",
                    "principalId": null,
                    "resourceId": "195ffa86-c17a-4382-aadd-9e8b6ca091bc",
                    "scope": "user_impersonation",
                    "startTime": "0001-01-01T00:00:00Z"
                }
            ]
        }
    #>


    if($PermissionType -eq 'All' -or $PermissionType -eq "Delegated")
    {
        $Grants =  Invoke-AadProtectedApi `
        -Client $Global:AadSupport.Clients.AzureAdPowershell.ClientId `
        -Resource $Global:AadSupport.Resources.MsGraph `
        -Endpoint $GrantUri+$GrantFilter -Method "GET"
    }

    $Permissions = [PSCustomObject]@()

    if($Grants)
    {
        
        foreach($item in $Grants)
        {
            Show-AadSupportStatusBar
            # Skip condition if we only want to see UserConsent
            if($ConsentType -eq "User" -and $item.consentType -eq "AllPrincipals")
            {
                continue
            }

            # Skip condition if we only want to see AdminConsent
            if($ConsentType -eq "Admin" -and $item.consentType -eq "Principal")
            {
                continue
            }

            $Permission = @{
                PermissionType = "Delegated"
            }

            $ItemClient = $null
            if($item.clientId -and $Client.ObjectId -ne $item.clientId)
            {
                $ItemClient = Get-AadServicePrincipal -Id $item.clientId
            }
            else {
                $ItemClient = $Client
            }

            $ItemResource = $null
            if($item.resourceId -and $Resource.ObjectId -ne $item.resourceId)
            {
                $ItemResource = Get-AadServicePrincipal -Id $item.resourceId
            }
            else {
                $ItemResource = $Resource
            }

            $ItemPrincipal = $null
            if(!$item.principalId)
            {
                $Permission.ConsentType = "AdminConsent"
            }
            else {
                if($item.principalId -and $Principal.ObjectId -ne $item.principalId)
                {
                    $ItemPrincipal = Invoke-AadCommand -Command {
                        Param($PrincipalId)
                        Get-AzureAdUser -ObjectId $PrincipalId
                    } -Parameters  $item.principalId
                }
                else {
                    $ItemPrincipal = $Principal
                }

                $Permission.ConsentType = "UserConsent"
            }

            $Permission.ClientName = $ItemClient.DisplayName
            $Permission.ClientId = $ItemClient.ObjectId
            $Permission.ResourceName = $ItemResource.DisplayName
            $Permission.ResourceId = $ItemResource.ObjectId
            $Permission.PrincipalId = $ItemPrincipal.userPrincipalName
            $Permission.ClaimValue = $item.scope
            $Permission.Id = $item.id

            $PermissionObject = New-Object -TypeName 'PSObject' -Property $Permission

            $Permissions += $PermissionObject
        }
    }

    


    # ------------------------------------------------
    # GET ROLES

    # NOTE: AppRoleAssignments in MS Graph does not support filtering
    
    if(($PermissionType -eq 'All' -or $PermissionType -eq "Application") -and $ConsentType -ne 'User' -and !$PrincipalId)
    {
        $RoleAssignments =  Invoke-AadProtectedApi `
        -Client $Global:AadSupport.Clients.AzureAdPowershell.ClientId `
        -Resource $Global:AadSupport.Resources.MsGraph `
        -Endpoint $RoleUri -Method "GET"
    }
  
    if($RoleAssignments)
    {
        if($Client) {
            $RoleAssignments = $RoleAssignments | where-object {$_.principalId -eq $Client.ObjectId }
        }

        if($Resource) {
            $RoleAssignments = $RoleAssignments | where-object {$_.resourceId -eq $Resource.ObjectId }
        }
        
        foreach($item in $RoleAssignments) 
        {
            Show-AadSupportStatusBar
            

            $Permission = @{
                PermissionType = "Application"
            }
    
            $ItemClient = $null
            if($item.clientId -and $Client.ObjectId -ne $item.principalId)
            {
                $ItemClient = Get-AadServicePrincipal -Id $item.principalId
            }
            else {
                $ItemClient = $Client
            }
    
            $ItemResource = $null
            if($item.resourceId -and $Resource.ObjectId -ne $item.resourceId)
            {
                $ItemResource = Get-AadServicePrincipal -Id $item.resourceId
            }
            else {
                $ItemResource = $Resource
            }
    
            $Permission.ClientName = $ItemClient.DisplayName
            $Permission.ClientId = $ItemClient.ObjectId
            $Permission.ResourceName = $ItemResource.DisplayName
            $Permission.ResourceId = $ItemResource.ObjectId
            $RoleName = ($ItemResource.AppRoles | Where-Object {$_.id -eq $item.appRoleId }).Value
            $Permission.ClaimValue = $RoleName
            $Permission.Id = $item.id
            $Permission.ConsentType = "AdminConsent"
            $Permission.PrincipalId = $null
    
            $PermissionObject = New-Object -TypeName 'PSObject' -Property $Permission
    
            $Permissions += $PermissionObject
        }
    }

    if($ClaimValue)
    {
        return $Permissions | where {(" "+$_.ClaimValue+" ") -match (" "+$ClaimValue+" ")}
    }

    return $Permissions
}

<#
.SYNOPSIS
Azure AD uses the UTC time (Coordinated Universal Time) and Universal Sortable DateTime Pattern.
 
.DESCRIPTION
Azure AD uses the UTC time (Coordinated Universal Time) and Universal Sortable DateTime Pattern.
 
UTC is the worlds synchronized time and set to the GMT time zone.
Universal Sortable DateTime Pattern looks like this: yyyy-MM-dd'T'HH:mm:ss.SSSZ
 
.PARAMETER DateTime
Specify DateTime you want to convert to UTC
 
.PARAMETER AddDays
Add or subtract days from current DateTime or specified DateTime
 
.PARAMETER AddHours
Add or subtract hours from current DateTime or specified DateTime
 
.PARAMETER AddMinutes
Add or subtract minutes from current DateTime or specified DateTime
 
.EXAMPLE
Get-AadDateTime
Get-AadDateTime -DateTime "01/20/2019"
Get-AadDateTime -AddDays 7 -AddHours 12 -AddMinutes 30
Get-AadDateTime -DateTime "01/20/2019" -AddDays -7
 
.NOTES
General notes
#>


function Get-AadDateTime {
    param 
    (
        [Parameter(
            mandatory=$false,
            ValueFromPipeline = $true)]
        $DateTime, 
        $AddDays, 
        $AddHours, 
        $AddMinutes,
        $AddYears,
        $AddMonths
    )

    if ($DateTime) 
    {
        $date = (Get-Date $DateTime).ToUniversalTime()
    }
    
    else
    {
        $date = (Get-Date).ToUniversalTime()
    }


    if ($AddDays) {
        $date = $date.AddDays($AddDays)
    }

    if ($AddHours) {
        $date = $date.AddHours($AddHours)
    }

    if ($AddMinutes) {
        $date = $date.AddMinutes($AddMinutes)
    }

    if ($AddYears) {
        $date = $date.AddYears($AddYears)
    }

    if ($AddMonths) {
        $date = $date.AddMonths($AddMonths)
    }

    return (Get-Date $date -Format o)
}

<#
.SYNOPSIS
Gets the Azure AD Discovery Keys
 
.DESCRIPTION
Gets the Azure AD Discovery Keys
 
PS C:\>Get-AadDiscoveryKeys
Downloading configuration from 'https://login.microsoftonline.com/aa00d1fa-5269-4e1c-b06d-30868371d2c5/.well-known/openid-configuration'
Downloading signing keys from 'https://login.microsoftonline.com/common/discovery/keys'
 
ApplicationId :
Kid : HlC0R12skxNZ1WQwmjOF_6t_tDE
Use : sig
x5t : HlC0R12skxNZ1WQwmjOF_6t_tDE
kty : RSA
Modulus : vq_3TOSbrUzpGPHFEwjmeoE_Zu3-wU4vaeEvjQzUHXwIefy8bDuMav6OzUiEXhjLX5JRkGhds3lNGR3CSZgartIKWv5Vrc7F2YcBcgz
                rpO06kVcewRMjdhrPYfUfO6QklAOSCcPq4RUhEvkGEwbAw3awclve1KuhpX6fOIInP8Gp8hrFDd_neBR3AY03JrZpezBdQoE24UHgAl
                HGb2UZ2KKjl3rLDMPh9HecjTiga3SbdcrhTAOYHYb4LwCSrThrHSyZFBxzTwQMS0NEyKV7_-ADrFunf9cuVcGpQZkvdwODl4tY-l2sd
                3WpoD_gMDpoFJVojjzF07ovrfntM4o8Bw
Exponent : AQAB
Certificate : @{Subject=CN=accounts.accesscontrol.windows.net; Kid=HlC0R12skxNZ1WQwmjOF_6t_tDE; NotAfter=12/24/2024
                6:00:00 PM; Issuer=CN=accounts.accesscontrol.windows.net; Certificate=[Subject]
                  CN=accounts.accesscontrol.windows.net
 
                [Issuer]
                  CN=accounts.accesscontrol.windows.net
 
                [Serial Number]
                  19BE4B61B2A8DC874CD0742C8EFFA612
 
                [Not Before]
                  12/25/2019 6:00:00 PM
 
                [Not After]
                  12/24/2024 6:00:00 PM
 
                [Thumbprint]
                  1E50B4475DAC931359D564309A3385FFAB7FB431
                ; Thumbprint=1E50B4475DAC931359D564309A3385FFAB7FB431; NotBefore=12/25/2019 6:00:00 PM}
Thumbprint : 1E50B4475DAC931359D564309A3385FFAB7FB431
x5c : MIIDBTCCAe2gAwIBAgIQGb5LYbKo3IdM0HQsjv+mEjANBgkqhkiG9w0BAQsFADAtMSswKQYDVQQDEyJhY2NvdW50cy5hY2Nlc3Njb25
                0cm9sLndpbmRvd3MubmV0MB4XDTE5MTIyNjAwMDAwMFoXDTI0MTIyNTAwMDAwMFowLTErMCkGA1UEAxMiYWNjb3VudHMuYWNjZXNzY2
                9udHJvbC53aW5kb3dzLm5ldDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL6v90zkm61M6RjxxRMI5nqBP2bt/sFOL2nhL
                40M1B18CHn8vGw7jGr+js1IhF4Yy1+SUZBoXbN5TRkdwkmYGq7SClr+Va3OxdmHAXIM66TtOpFXHsETI3Yaz2H1HzukJJQDkgnD6uEV
                IRL5BhMGwMN2sHJb3tSroaV+nziCJz/BqfIaxQ3f53gUdwGNNya2aXswXUKBNuFB4AJRxm9lGdiio5d6ywzD4fR3nI04oGt0m3XK4Uw
                DmB2G+C8Akq04ax0smRQcc08EDEtDRMile//gA6xbp3/XLlXBqUGZL3cDg5eLWPpdrHd1qaA/4DA6aBSVaI48xdO6L6357TOKPAcCAw
                EAAaMhMB8wHQYDVR0OBBYEFMGZU4IfHXk8nigJzTMM45KzMjeVMA0GCSqGSIb3DQEBCwUAA4IBAQAMJF5kk0gj119v4wbQTr9sQr9SS
                7ALfmIBQaeWjWRZvmXbEnMMA46y9nShV+d3cFrIrxuz7ynd3PU0+2HP4217VHO3rFyNbNnp4IB+BJa+hW/Hi54X+m/QPztDFCdiP1zY
                Wr7DNEvnebuAMAJ+W0I08h5yIcX6Z0TTZcrWc72Qyi2Y2MuYDN+AVvQ1WZWsU4gbnUK7oj8bYnLfzWWuhfks2vC5Sbx9+79j+36HtsQ
                nYe9ouxQ5vfNxm7wcLTQQulU16lnD0yObvr1hfteKfuW2/Ynoy5Z2ntIyCbGxiulaPLrFTW4gUhYgnteB5CwGw1C5vhv0Aa+XZouHVh
                oOLhWF
 
.PARAMETER Tenant
Specify the tenant. This would be required if getting specific information about an app
 
.PARAMETER AadInstance
Specify the Azure AD Instance i.e. https://login.microsoftonline.com or https://login.microsoftonline.us
 
.PARAMETER Issuer
You can specify the full Issuer. This would be required to correctly get discovery keys for Azure AD B2C
 
.PARAMETER ApplicationId
Specify the Application ID
 
.EXAMPLE
Get-AadDiscoveryKeys
 
.EXAMPLE
Get-AadDiscoveryKeys -Tenant contoso.onmicrosoft.com -ApplicationId bcdeb54f-733b-4657-8948-0f39934c2a53
 
.EXAMPLE
Get-AadDiscoveryKeys -Issuer "https://williamfiddesb2c.b2clogin.com/tfp/williamfiddesb2c.onmicrosoft.com/B2C_1_V2_SUSI_DefaultPage/v2.0/"
 
.NOTES
General notes
#>

function Get-AadDiscoveryKeys
{
    [CmdletBinding(DefaultParameterSetName='Default')]
    param(
        [Parameter(ParameterSetName = 'SetTenantAndInstance')]
        [string]$Tenant, 

        [Parameter(ParameterSetName = 'SetTenantAndInstance')]
        [string]$AadInstance,

        [Parameter(ParameterSetName = 'SetIssuer')]
        [string]$Issuer,

        [string]$ApplicationId
    ) 

    if($Issuer)
    {
        $Configuration = (Get-AadOpenIdConnectConfiguration -Issuer $Issuer -ApplicationId $ApplicationId)
    }
    else
    {
        $Configuration = (Get-AadOpenIdConnectConfiguration -Tenant $Tenant -AadInstance $AadInstance -ApplicationId $ApplicationId)
    }

    if(!$Configuration)
    {
      throw "$Issuer is not valid"
    }

    $KeyUrl = $Configuration.jwks_uri

    if(!$KeyUrl)
    {
      throw "$KeyUrl not found"
    }

    # Get the Discovery Keys
    Write-Host "Downloading signing keys from '$KeyUrl'"
    $Keys = (ConvertFrom-Json (Invoke-WebRequest $KeyUrl).Content).Keys
    if(!$Keys)
    {
      throw "$KeyUrl is not valid"
    }
    
    # Build the Output object
    $ReturnObject = @()

    foreach($Key in $Keys)
    {
        $Object = [pscustomobject]@{} 

        $Object | Add-Member -NotePropertyName ApplicationId -NotePropertyValue $Configuration.ApplicationId
        $Object | Add-Member -NotePropertyName Kid -NotePropertyValue $Key.kid
        $Object | Add-Member -NotePropertyName Use -NotePropertyValue $Key.use
        $Object | Add-Member -NotePropertyName x5t -NotePropertyValue $Key.x5t
        $Object | Add-Member -NotePropertyName kty -NotePropertyValue $Key.kty
        $Object | Add-Member -NotePropertyName Modulus -NotePropertyValue $Key.n
        $Object | Add-Member -NotePropertyName Exponent -NotePropertyValue $Key.e

        if($Key.x5c)
        {
            $Certificate = ConvertFrom-AadBase64Certificate -Base64String $Key.x5c[0]
            $Thumbprint = $Certificate.Thumbprint

            $Object | Add-Member -NotePropertyName Certificate -NotePropertyValue $Certificate
            $Object | Add-Member -NotePropertyName Thumbprint -NotePropertyValue $Thumbprint
            $Object | Add-Member -NotePropertyName x5c -NotePropertyValue $Key.x5c[0]
        }

        $ReturnObject += $Object
    }

    return $ReturnObject
}


function Test-Get-AadDiscoveryKeys
{
    # Provide no info
    Get-AadDiscoveryKeys 

    # Provide a tenant
    Get-AadDiscoveryKeys -Tenant "williamfiddesb2c.onmicrosoft.com"

    # Provide a instance
    Get-AadDiscoveryKeys -AadInstance "https://login.microsoftonline.us"

    # Provide a Issuer with a appid
    Get-AadDiscoveryKeys -Issuer https://login.microsoftonline.com/williamfiddes.onmicrosoft.com/.well-known/openid-configuration?appid=bcdeb54f-733b-4657-8948-0f39934c2a53

    # Provide a appid
    Get-AadDiscoveryKeys -Tenant "williamfiddes.onmicrosoft.com" -ApplicationId bcdeb54f-733b-4657-8948-0f39934c2a53

    # Provide a B2C Issuer
    Get-AadDiscoveryKeys -Issuer "https://williamfiddesb2c.b2clogin.com/tfp/williamfiddesb2c.onmicrosoft.com/B2C_1_V2_SUSI_DefaultPage/v2.0/.well-known/openid-configuration"
    
}
function Get-AadKeyVaultAccessByObject {

    param(
        [Parameter(
            ValueFromPipeline = $true,
            ParameterSetName = "ByObjectId")]
        $ObjectId,

        [Parameter(ParameterSetName = "ByObjectId")]
        [parameter(ValueFromPipeline=$true)]
        [ValidateSet("User","ServicePrincipal")]
        [string]$ObjectType,

        [Parameter(ParameterSetName = "ByServicePrincipalId")]
        $ServicePrincipalId,

        [Parameter(ParameterSetName = "ByUserId")]
        $UserId
    )

    if($ObjectId -and -not $ObjectType)
    {
        $ObjectType = Invoke-AadCommand -Command {
            Param($ObjectId)
             (Get-AzureADObjectByObjectId -ObjectIds $ObjectId).ObjectType
        } -Parameters $ObjectId
    }

    # Search for ServicePrincipal
    if($ServicePrincipalId)
    {
        $ObjectType = "ServicePrincipal"
        $sp = Get-AadServicePrincipal -Id $ServicePrincipalId

        If(-not $sp)
        {
            return 
        }

        $ObjectId = $sp.ObjectId
    }
    
    # Search for User
    if($UserId)
    {
        $ObjectType = "User"

        $user = $ObjectType = Invoke-AadCommand -Command {
            Param($UserId)
            Get-AzureADUser -ObjectId $UserId
        } -Parameters $UserId

        If(-not $user)
        {
            return 
        }

        $ObjectId = $user.ObjectId
    }


    $subscriptions = Invoke-AzureCommand -Command {
        Param($TenantId)
        Get-AzSubscription -TenantId $TenantId
      } -Parameters $Global:AadSupport.Session.TenantId

    $result = @()
    foreach($sub in $subscriptions) {
        
        if($sub.Name -ne "Access to Azure Active Directory")
        {
            Write-Verbose "Checking Subscription '$($sub.Name) (Id:$($sub.id))' "
            <#
            Invoke-AzureCommand -Command {
                Param($SubscriptionId)
                Select-AzSubscription -SubscriptionId $SubscriptionId | Out-Null
            } -Parameters $sub.id -SubscriptionId $sub.id
            #>

            
            $KeyVaults = Invoke-AzureCommand -Command { Get-AzKeyVault } -SubscriptionId $sub.id
            foreach($KeyVaultItem in $KeyVaults)
            {
                $KeyVaultName = $KeyVaultItem.VaultName
                Write-Verbose "Checking Key Vault '$KeyVaultName'"
                $kv = Invoke-AzureCommand -Command {
                    Param($KeyVaultName)
                     Get-AzKeyVault -VaultName $KeyVaultName
                } -Parameters $KeyVaultName -SubscriptionId $sub.id

                foreach($policy in $kv.AccessPolicies)
                {
                    $PolicyAssignedObject = Invoke-AadCommand -Command {
                        Param($ObjectIds)
                        (Get-AzureADObjectByObjectId -ObjectIds $ObjectIds)
                    } -Parameters $policy.ObjectId

                    if($PolicyAssignedObject.ObjectType -eq "Group")
                    {
                        # Check if User/ServicePrincipal is a member of the group
                        $Groups = New-Object Microsoft.Open.AzureAD.Model.GroupIdsForMembershipCheck
                        $Groups.GroupIds = $policy.ObjectId

                        if($ObjectType -eq "User")
                        {
                            $IsMemberOf = Invoke-AadCommand -Command {
                                Param($Params)
                                Select-AzureADGroupIdsUserIsMemberOf -ObjectId $Params.ObjectId -GroupIdsForMembershipCheck $Params.Groups
                            } -Parameters @{
                                ObjectId = $ObjectId
                                Groups = $Groups
                            }
                        }

                        if($ObjectType -eq "ServicePrincipal")
                        {
                            $IsMemberOf = Invoke-AadCommand -Command {
                                Param($Params)
                                Select-AzureADGroupIdsServicePrincipalIsMemberOf -ObjectId $Params.ObjectId -GroupIdsForMembershipCheck $Params.Groups
                            } -Parameters @{
                                ObjectId = $ObjectId
                                Groups = $Groups
                            }
                        }
                        
                        # I only want to show group info if the object is assigned through group membership
                        if($IsMemberOf)
                        {
                            $DirectAssignment = $false
                            $GroupDisplayName = $PolicyAssignedObject.DisplayName
                            $GroupObjectId = $PolicyAssignedObject.ObjectId
                        }
                        else {
                            $DirectAssignment = $true
                        }
                    }

                    if($policy.ObjectId -eq $ObjectId -or $IsMemberOf)
                    {
                        $CustomResult = [ordered]@{}
                        $CustomResult.KeyVaultName = $KeyVaultName
                        $CustomResult.PermissionsToSecrets = $policy.PermissionsToSecrets
                        $CustomResult.PermissionsToKeys = $policy.PermissionsToKeys
                        $CustomResult.PermissionsToCertificates = $policy.PermissionsToCertificates
                        $CustomResult.PermissionsToStorage = $policy.PermissionsToStorage
                        $CustomResult.DirectAssignment = $DirectAssignment
                        $CustomResult.GetsAssignmentBy = "$GroupDisplayName ($GroupObjectId)"
                        
                        $result += $CustomResult
                    }
                }
            }
        }
    }

    return $result
}
function Get-AadObjectCount
{
    $MsGraphEndpoint = $Global:AadSupport.Resources.MsGraph

    Write-Host "Getting User Count."
    $UserCount = Invoke-AadCommand -Command {
        (Get-AzureADUser -All $true).Count
    }

    $UserDeletedCount = 0
    $UserDeletedCount = (Invoke-AadProtectedApi -Client $Global:AadSupport.Clients.AzureAdPowershell.ClientId -Resource $MsGraphEndpoint -Endpoint "$MsGraphEndpoint/v1.0/directory/deletedItems/Microsoft.Graph.User").Count

    Write-Host "Getting Group Count."
    $GroupCount = Invoke-AadCommand -Command {
        (Get-AzureADGroup -All $true).Count
    }

    $GroupDeletedCount = 0
    $GroupDeletedCount = (Invoke-AadProtectedApi -Client $Global:AadSupport.Clients.AzureAdPowershell.ClientId -Resource $MsGraphEndpoint -Endpoint "$MsGraphEndpoint/v1.0/directory/deletedItems/Microsoft.Graph.Group").Count

    Write-Host "Getting Device Count."
    $DeviceCount = Invoke-AadCommand -Command {
        (Get-AzureADDevice -All $true).Count
    }

    $DeviceDeletedCount = 0
    $DeviceDeletedCount = (Invoke-AadProtectedApi -Client $Global:AadSupport.Clients.AzureAdPowershell.ClientId -Resource $MsGraphEndpoint -Endpoint "$MsGraphEndpoint/v1.0/directory/deletedItems/Microsoft.Graph.Device").Count

    Write-Host "Getting Contact (Organizational) Count."
    $ContactCount = Invoke-AadCommand -Command {
        (Get-AzureADContact -All $true).Count
    }

    $ContactDeletedCount = 0
    $ContactDeletedCount = (Invoke-AadProtectedApi -Client $Global:AadSupport.Clients.AzureAdPowershell.ClientId -Resource $MsGraphEndpoint -Endpoint "$MsGraphEndpoint/beta/directory/deletedItems/Microsoft.Graph.orgContact").Count
    
    Write-Host "Getting Application Count."
    $AppCount = Invoke-AadCommand -Command {
        (Get-AzureADApplication -All $true).Count
    }

    $AppDeletedCount = 0
    $AppDeletedCount = (Invoke-AadProtectedApi -Client $Global:AadSupport.Clients.AzureAdPowershell.ClientId -Resource $MsGraphEndpoint -Endpoint "$MsGraphEndpoint/beta/directory/deletedItems/Microsoft.Graph.Application").Count
    
    Write-Host "Getting ServicePrincipal Count. This one might take a while."
    $SpCount = Invoke-AadCommand -Command {
        (Get-AzureADServicePrincipal -All $true).Count
    }

    $SpDeletedCount = 0
    $SpDeletedCount = (Invoke-AadProtectedApi -Client $Global:AadSupport.Clients.AzureAdPowershell.ClientId -Resource $MsGraphEndpoint -Endpoint "$MsGraphEndpoint/beta/directory/deletedItems/Microsoft.Graph.ServicePrincipal").Count
    
    
    Write-Host "Getting OAuth2PermissionGrant Count. This one might take a while."
    $OAuth2PermissionGrantCount = Invoke-AadCommand -Command {
        (Get-AzureADOAuth2PermissionGrant -All $true).Count
    }

    Write-Host "Getting Directory Role Count. This one might take a while."
    $DirectoryRoleCount = Invoke-AadCommand -Command {
        (Get-AzureADDirectoryRole).Count
    }

    $PolicyCount = 0
    if($Global:AadSupport.Powershell.Modules.AzureAd.Name -eq "AzureADPreview")
    {
        Write-Host "Getting Azure AD Policy Count."
        $PolicyCount = Invoke-AadCommand -Command {
            (Get-AzureADPolicy -All $true).Count
        }

        if($PolicyCount.Exception)
        {
            $PolicyCount = 0
        }
    }
    

    $TotalActiveCount = $UserCount +$GroupCount +$DeviceCount +$ContactCount +$AppCount +$SpCount +$OAuth2PermissionGrantCount +$DirectoryRoleCount +$PolicyCount
    $TotalDeletedCount = $UserDeletedCount +$GroupDeletedCount +$DeviceDeletedCount +$ContactDeletedCount + $AppDeletedCount +$SpDeletedCount

    $TotalCount = $TotalActiveCount + $TotalDeletedCount

    $TotalValidCount = $TotalActiveCount + [System.Math]::Ceiling($TotalDeletedCount/4)

    Write-Host ""
    Write-Host "Summary..."
    Write-Host " - Users: $UserCount"
    Write-Host " - Deleted Users: $UserDeletedCount"
    Write-Host " - Groups: $GroupCount"
    Write-Host " - Deleted Groups: $GroupDeletedCount"
    Write-Host " - Devices: $DeviceCount"
    Write-Host " - Deleted Devices: $DeviceDeletedCount"
    Write-Host " - Contacts: $ContactCount"
    Write-Host " - Deleted Contacts: $ContactDeletedCount"
    Write-Host " - Applications: $AppCount"
    Write-Host " - Deleted Applications: $AppDeletedCount"
    Write-Host " - ServicePrincipals: $SpCount"
    Write-Host " - Deleted ServicePrincipals: $SpDeletedCount"
    Write-Host " - OAuth2 Permission Grants: $OAuth2PermissionGrantCount"
    Write-Host " - Directory Roles: $DirectoryRoleCount"
    Write-Host " - Azure AD Policies: $PolicyCount (AzureAdPreview Required)"
    Write-Host ""
    Write-Host "Total Count: $TotalCount"
    Write-Host ""
    Write-Host "Note: Deleted objects only count for 1/4 against Object Quota."
    Write-Host "Total Count against Directory Quota: $TotalValidCount"
    Write-Host ""
}

<#
.SYNOPSIS
Gets the Azure AD Open Id Connect Configuration
 
.DESCRIPTION
Gets the Azure AD Open Id Connect Configuration
 
PS C:\>Get-AadOpenIdConnectConfiguration
Downloading configuration from 'https://login.microsoftonline.com/common/.well-known/openid-configuration'
 
token_endpoint : https://login.microsoftonline.com/common/oauth2/token
token_endpoint_auth_methods_supported : {client_secret_post, private_key_jwt, client_secret_basic}
jwks_uri : https://login.microsoftonline.com/common/discovery/keys
response_modes_supported : {query, fragment, form_post}
subject_types_supported : {pairwise}
id_token_signing_alg_values_supported : {RS256}
response_types_supported : {code, id_token, code id_token, token id_token...}
scopes_supported : {openid}
issuer : https://sts.windows.net/{tenantid}/
microsoft_multi_refresh_token : True
authorization_endpoint : https://login.microsoftonline.com/common/oauth2/authorize
http_logout_supported : True
frontchannel_logout_supported : True
end_session_endpoint : https://login.microsoftonline.com/common/oauth2/logout
claims_supported : {sub, iss, cloud_instance_name, cloud_instance_host_name...}
check_session_iframe : https://login.microsoftonline.com/common/oauth2/checksession
userinfo_endpoint : https://login.microsoftonline.com/common/openid/userinfo
tenant_region_scope :
cloud_instance_name : microsoftonline.com
cloud_graph_host_name : graph.windows.net
msgraph_host : graph.microsoft.com
rbac_url : https://pas.windows.net
ApplicationId :
 
.PARAMETER Tenant
Specify the tenant. This would be required if getting specific information about an app
 
.PARAMETER AadInstance
Specify the Azure AD Instance i.e. https://login.microsoftonline.com or https://login.microsoftonline.us
 
.PARAMETER Issuer
You can specify the full Issuer. This would be required to correctly get Open Id Connect Configuration for Azure AD B2C
 
.PARAMETER ApplicationId
Specify the Application ID
 
.EXAMPLE
Get-AadOpenIdConnectConfiguration
 
.EXAMPLE
Get-AadOpenIdConnectConfiguration -Tenant contoso.onmicrosoft.com -ApplicationId bcdeb54f-733b-4657-8948-0f39934c2a53
 
.EXAMPLE
Get-AadOpenIdConnectConfiguration -Issuer "https://williamfiddesb2c.b2clogin.com/tfp/williamfiddesb2c.onmicrosoft.com/B2C_1_V2_SUSI_DefaultPage/v2.0/"
 
.NOTES
General notes
#>

function Get-AadOpenIdConnectConfiguration
{
    [CmdletBinding(DefaultParameterSetName='Default')]
    param(
        [Parameter(ParameterSetName = 'SetTenantAndInstance')]
        [string]$Tenant, 

        [Parameter(ParameterSetName = 'SetTenantAndInstance')]
        [string]$AadInstance,

        [Parameter(ParameterSetName = 'SetIssuer')]
        [string]$Issuer,

        [string]$ApplicationId
    ) 

    # Populate Tenant info
    if($Global:AadSupport.Session.Active -and -not $Tenant)
    {
        $Tenant = $Global:AadSupport.Session.TenantId
    }

    if(-not $Tenant)
    {
        $Tenant = "common"
    }

    # Populate AadInstance info
    if($Global:AadSupport.Session -and -not $AadInstance)
    {
        $AadInstance = $Global:AadSupport.Session.AadInstance
    }

    if(-not $AadInstance)
    {
        $AadInstance = "https://login.microsoftonline.com"
    }

    # Set the Open ID Connect Configuration Endpoint
    if(!$Issuer)
    {
        $Issuer = "$AadInstance/$Tenant"
    }
    elseif($Issuer.LastIndexOf("/") -eq $Issuer.Length-1)
    {
        $Issuer = $Issuer.Substring(0,$Issuer.Length-1)
    }

    $Url = $Issuer

    if(!$Issuer.Contains("/.well-known/openid-configuration"))
    {
        $Url += "/.well-known/openid-configuration"
    }
    

    if($ApplicationId -and !$Issuer.Contains("appid="))
    {
        $Url += "?appid=$ApplicationId"
    }
    elseif($ApplicationId -and $Issuer.Contains("appid="))
    {
        Write-Warning "Using Application ID provided in Issuer"
    }
    elseif($Issuer.Contains("appid="))
    {
        $ApplicationId = ($Issuer.Split("?")[1].Split("&") | where {$_ -match 'appid'}).Split("=")[1]
    }

    # Get the Discovery Keys
    Write-Host "Downloading configuration from '$Url'"
    $Configuration = (ConvertFrom-Json (Invoke-WebRequest $Url).Content)
    $Configuration | Add-Member -Type NoteProperty -Name "ApplicationId" -Value $ApplicationId
    

    return $Configuration
}
 
function Test-Get-AadOpenIdConnectConfiguration
{
    # Provide no issuer
    Get-AadOpenIdConnectConfiguration 

    # Provide a tenant
    Get-AadOpenIdConnectConfiguration -Tenant "williamfiddesb2c.onmicrosoft.com"

    # Provide a instance
    Get-AadOpenIdConnectConfiguration -AadInstance "https://login.microsoftonline.us"

    # Provide a AAD Issuer
    Get-AadOpenIdConnectConfiguration -Issuer "https://login.microsoftonline.com/williamfiddes.onmicrosoft.com"

    # Provide a B2C Issuer
    Get-AadOpenIdConnectConfiguration -Issuer "https://williamfiddesb2c.b2clogin.com/tfp/williamfiddesb2c.onmicrosoft.com/B2C_1_V2_SUSI_DefaultPage/v2.0/.well-known/openid-configuration"
    
    # Provide a Issuer with a appid
    Get-AadOpenIdConnectConfiguration -Issuer https://login.microsoftonline.com/williamfiddes.onmicrosoft.com/.well-known/openid-configuration?appid=bcdeb54f-733b-4657-8948-0f39934c2a53

    # Provide a appid
    Get-AadOpenIdConnectConfiguration -Tenant "williamfiddes.onmicrosoft.com" -ApplicationId bcdeb54f-733b-4657-8948-0f39934c2a53

    # Show Warning
    Get-AadOpenIdConnectConfiguration -Issuer https://login.microsoftonline.com/williamfiddes.onmicrosoft.com/.well-known/openid-configuration?appid=bcdeb54f-733b-4657-8948-0f39934c2a53 -ApplicationId bcdeb54f-733b-4657-8948-0f39934c2a53
}
function Get-AadReportCredentialsExpiringSoon
{
    param
    (
        [string]$Days = 45
    )

    # REQUIRE AadSupport Session
    RequireConnectAadSupport
    # END REGION

    $apps = @()

    Write-Host "This might take a while."
    Write-Host "Getting Service Principals..."
    $apps += Invoke-AadCommand -Command {
        Get-AzureADServicePrincipal -All $true
    }

    Write-Host "Getting Applications..."
    $apps += Invoke-AadCommand -Command {
        Get-AzureADApplication -All $true
    }
    
    $ReturnObject = @()
    foreach($app in $apps)
    {

        foreach($AppCredential in $app.PasswordCredentials)
        {
            $Object = [pscustomobject]@{
                ObjectType = $app.ObjectType
                DisplayName = $app.DisplayName
                ObjectId = $app.ObjectId
                AppId = $app.AppId
                CredentialType = "PasswordCredentials"
                CustomKeyIdentifier = $AppCredential.CustomKeyIdentifier
                EndDate = $AppCredential.EndDate
                StartDate = $AppCredential.StartDate
                KeyId = $AppCredential.KeyId
            }
            $ReturnObject += $Object
        }

        foreach($AppCredential in $app.KeyCredentials)
        {
            $Object = [pscustomobject]@{
                DisplayName = $app.DisplayName
                EndDate = $AppCredential.EndDate
                ObjectType = $app.ObjectType
                CredentialType = "KeyCredentials"
                StartDate = $AppCredential.StartDate
                ObjectId = $app.ObjectId
                KeyId = $AppCredential.KeyId
                CustomKeyIdentifier = $AppCredential.CustomKeyIdentifier
            }
            $ReturnObject += $Object
        }

        
    }

    return $ReturnObject | Sort-Object EndDate | Where-Object {$_.EndDate -lt (Get-Date).AddDays($Days) }
}
function Get-AadReportMfaEnabled
{
    [CmdletBinding()]
    param()

    $ScriptBlock = { 
        Get-MsolUser -All | where {$_.StrongAuthenticationRequirements.Count -eq 1} | Select-Object -Property UserPrincipalName
    }

    Invoke-MSOnlineCommand -Command  $ScriptBlock
   
}


function Get-AadReportMfaEnrolled
{
    [CmdletBinding()]
    param()

    $ScriptBlock = { 
        Get-MsolUser -All | where {$_.StrongAuthenticationMethods -ne $null} | Select-Object -Property UserPrincipalName 
    }

    Invoke-MSOnlineCommand -Command  $ScriptBlock
   
}


function Get-AadReportMfaNotEnrolled
{
    [CmdletBinding()]
    param()

    $ScriptBlock = { 
        Get-MsolUser -All | where {$_.StrongAuthenticationMethods.Count -eq 0} | Select-Object -Property UserPrincipalName
    }

    Invoke-MSOnlineCommand -Command  $ScriptBlock
   
}


<#
.SYNOPSIS
Intelligence to return the service principal object by looking up using any of its identifiers.
 
.DESCRIPTION
Intelligence to return the service principal object by looking up using any of its identifiers.
 
.PARAMETER Id
Either specify Service Principal (SP) Name, SP Display Name, SP Object ID, Application/Client ID, or Application Object ID
 
.EXAMPLE
Get-AadServicePrincipal -Id 'Contoso Web App'
 
.NOTES
Returns the Service Pricpal object using Get-AzureAdServicePradmin@wiincipal and filter based on the Id parameter
#>


function Get-AadServicePrincipal
{
    [CmdletBinding(DefaultParameterSetName='ByAnyId')]
    param(
        [Parameter(
            mandatory=$true,
            Position=0,
            ValueFromPipeline = $true,
            ParameterSetName = 'ByAnyId'
        
        )]
        $Id,


        [Parameter(
            mandatory=$true,
            ParameterSetName = 'ByAppId'
        )]
        $AppId,

        [Parameter(
            mandatory=$true,
            ParameterSetName = 'ByDisplayName'
        )]
        $DisplayName,

        [Parameter(
            mandatory=$true,
            ParameterSetName = 'ByServicePrincipalName'
        )]
        $ServicePrincipalName,

        [Parameter(
            mandatory=$true,
            ParameterSetName = 'ByReplyAddress'
        )]
        $ReplyAddress

        
    )

    $Global:ServicePrincipals = @()
    $sp         = $null
    $isGuid     = $null

    # Search By AppId
    if ($AppId) {
        Write-Verbose "Looking for AppId '$AppId'"
        $sp = GetAadSpByAppId $AppId
    }

    # Search By ReplyAddress
    if ($ReplyAddress) {
        Write-Verbose "Looking for '$ReplyAddress'"
        $sp = GetAadSpByReplyAddress $ReplyAddress
    }

    # Search By ServicePrincipalName
    if ($ServicePrincipalName) {
        Write-Verbose "Looking for '$ServicePrincipalName'"
        $sp = GetAadSpByServicePrincipalName -Id $ServicePrincipalName
    }

    # Search By DisplayName
    if ($DisplayName) {
        Write-Verbose "Looking for '$DisplayName'"
        $sp = GetAadSpByDisplayName -Id $DisplayName
    }

    try {
        $isGuid = [System.Guid]::Parse($Id)
    } catch {
    }
    
    # Search By All (Any ID)
    if($Id)
    {
        Write-Verbose "Looking for '$Id'"

        # Search for app based on AppId or ObjectId
        if ($isGuid -and -not $sp) {

            # Search for app based on ObjectId
            $sp = $null
            $sp = try { 
                Invoke-AadCommand -Command {
                    Param($Id)
                    Get-AzureADObjectByObjectId -ObjectId $Id 
                } -Parameters $Id
            } catch {}

            if ($sp.ObjectType -eq "ServicePrincipal") {
                Write-Verbose "Service Principal found using ObjectId"
                return $sp
            }

            $appid = $Id
            if ($sp.ObjectType -eq "Application") {
                Write-Verbose "Application found! Looking for Service Principal..."
                $appid = $sp.AppId
                $sp = $null
            }

            # Search for app based on AppId
            $sp = GetAadSpByAppId -Id $appid
            if ($sp) { return $sp }

        } 

        # Search for app based on ServicePrincipalName
        if(-not $sp) {
            $sp = GetAadSpByServicePrincipalName $Id
        }

        # Search for app based on DisplayName
        if(-not $sp) {
            $sp = GetAadSpByDisplayName $Id
        }
    }

    $Global:ServicePrincipals = $null

    # Exit script! Service Principal Not found
    if (-not $sp) {
        throw "$Id Service Principal not found!"
    }

    # Service Principal(s) Found > Only return One result
    if($sp.count -gt 1)
    {
        $sp = $sp | Out-GridView -PassThru -Title "Select Enterprise App"
    }
    return $sp
}


function GetAadSpByAppId
{
    param(
        [Parameter(
            mandatory=$true,
            ValueFromPipeline = $true)]
        $Id
    )

    Write-Verbose "Searching by AppId"

    try {
        $isGuid = [System.Guid]::Parse($Id)
    } catch {
        throw "Invalid App Id"
    }

    $sp = Invoke-AadCommand -Command {
        Param($AppId)
        Get-AzureADServicePrincipal -filter "AppId eq '$AppId'"
    } -Parameters $Id


    if ($sp) { 
        Write-Verbose "Service Principal found using AppId"
        return $sp
    }

    return
}


function GetAadSpByReplyAddress
{
    param(
        [Parameter(
            mandatory=$true,
            ValueFromPipeline = $true)]
        $Id
    )

    Write-Verbose "Searching by ReplyUrls"

    $sps = LookupAllServicePrincipals

    $sp = @()
    $sp += $sps | Where-Object {$_.ReplyUrls -contains "$Id"}

    if ($sp) { 
        Write-verbose "Service Principal '$Id' found using Reply Address" 
    }

    return $sp
}



function GetAadSpByServicePrincipalName
{
    param(
        [Parameter(
            mandatory=$true,
            ValueFromPipeline = $true)]
        $Id
    )

    Write-Verbose "Searching by ServicePrincipalName"

    $sp = Invoke-AadCommand -Command {
        Param($Id)
        Get-AzureADServicePrincipal -filter "servicePrincipalNames/any(x:x eq '$Id')"
    } -Parameters $Id

    # Service Principal Not found yet using ServicePrincipalName (Do wider search)
    if(-not $sp)
    {
        $sps = LookupAllServicePrincipals
        $sp = @()
        $sp += $sps | Where-Object { $_.ServicePrincipalNames -match $Id }
    }

    if ($sp) { 
        Write-verbose "Service Principal(s) '$Id' found using ServicePrincipalName" 
    }

    return $sp
}


function GetAadSpByDisplayName
{
    param(
        [Parameter(
            mandatory=$true,
            ValueFromPipeline = $true)]
        $Id
    )

    Write-Verbose "Searching by DisplayName"

    $sp = Invoke-AadCommand -Command {
        Param($Id)
        Get-AzureADServicePrincipal -filter "DisplayName eq '$Id'"
    } -Parameters $Id
    
    # Service Principal Not found yet using DisplayName (Do wider search)
    if(-not $sp)
    {
        $sps = LookupAllServicePrincipals
        $sp = @()
        $sp += $sps | Where-Object {$_.DisplayName -match "$Id"}
    }

    if ($sp) { 
        Write-Verbose "Service Principal(s) '$Id' found using DisplayName"
    }

    else
    {
        Write-Verbose "Service Principal(s) '$Id' NOT found using DisplayName"
    }

    return $sp
}


function LookupAllServicePrincipals()
{
    if(-not $Global:ServicePrincipals)
    {
        Write-Verbose "Looking at all Service Principals to find your app. This might take awhile..."
        $Global:ServicePrincipals = Invoke-AadCommand {
            Get-AzureADServicePrincipal -All $true
        }
    }

    return $Global:ServicePrincipals
}

<#
.SYNOPSIS
Gets information for what access a Service Principal/Application has access to.
 
.DESCRIPTION
Gets information for what access a Service Principal/Application has access to.
Gets Azure AD Directory Roles assigned to Service Principal
Gets App Roles assigned to Service Principal
Gets Consented Permissions assigned to Service Principal
Gets Azure Role Assignments assigned to Service Principal (This one may take a while)
Gets Key Vault Access Policies assigned to Service Principal (This one may take a while)
 
.PARAMETER Id
Provide the Service Principal ID
 
.PARAMETER SkipAzureRoleAssignments
Enable switch to skip lookup of Azure Role Assignments.
 
.PARAMETER SkipKeyVaultAccess
Enable switch to skip lookup of Azure Key Vault Access policies.
 
 
.EXAMPLE
Get-AadServicePrincipalAccess -Id 'Your Application Name, AppId, or Service Principal Object Id'
 
.NOTES
General notes
#>

function Get-AadServicePrincipalAccess
{
    param(
        [Parameter(
            mandatory=$true,
            ValueFromPipeline = $true)]
        $Id,

        [switch]$SkipAzureRoleAssignments,
        [switch]$SkipKeyVaultAccess
    )

    # REQUIRE AadSupport Session
    RequireConnectAadSupport
    # END REGION
    

    $TenantDomain = $Global:AadSupport.Session.TenantId

    $sp = (Get-AadServicePrincipal -Id $Id)
    
    if(-not $sp)
    {
        throw "'$Id' not found in '$TenantDomain'"
    }

    Write-Host ""
    Write-Host "Enterprise App (ServicePrincipal)" -ForegroundColor Yellow
    Write-Host "$($sp.DisplayName) | AppId:$($sp.AppId) | ObjectId:$($sp.ObjectId)"
    Write-Host ""

    if($sp.count -gt 1)
    {
        throw "'$Id' query returned more than one result. Please provide a unique Service Principal Identifier"
    }

    Write-Host "Getting Azure AD Directory Roles assigned to Service Principal..."
    $AdminRoles = Get-AadAdminRolesByObject -ObjectId $sp.ObjectId | ConvertTo-Json

    Write-Host "Getting App Roles (Application Permissions) assigned to Service Principal..."
    $AppRoles = Get-AadAppRolesByObject -ObjectId $sp.ObjectId -ObjectType $sp.ObjectType | ConvertTo-Json

    Write-Host "Getting OAuth2PermissionGrants (Delegated Permissions) assigned to Service Principal..."
    $Grants = Get-AadConsent -ClientId $sp.ObjectId -PermissionType Delegated | ConvertTo-Json

    if(-not $SkipKeyVaultAccess)
    {
        Write-Host "Getting Key Vault Access assigned to Service Principal..."
        $KeyVaultAccess = Get-AadKeyVaultAccessByObject -ObjectId $sp.ObjectId | ConvertTo-Json 
    }
    
    if(-not $SkipAzureRoleAssignments)
    {
        Write-Host "Getting Azure Roles assigned to Service Principal..."
        $AzureRoles = Get-AadAzureRoleAssignments -ServicePrincipalName $sp.ServicePrincipalNames[0] -ObjectId $sp.ObjectId | ConvertTo-Json
      
    }

    $Report = [pscustomobject]@{
        PrincipalType = $sp.ObjectType
        PrincipalId = $sp.AppId
        PrincipalDisplayName = $sp.DisplayName
        PrincipalObjectId = $sp.ObjectId
        AzureAdAdminRoles = $AdminRoles;
        ApplicationRoles = $AppRoles;
        ConsentedPermissions = $Grants;
        KeyVaultAccess = $KeyVaultAccess;
        AzureRoleAssignments = $AzureRoles;
    }

    #$ReturnObject = New-Object -TypeName psobject -Property $Report

    return $Report
}







<#
.SYNOPSIS
Gets a list of Service Principals assigned to a Administrator role in Azure AD
 
.DESCRIPTION
Gets a list of Service Principals assigned to a Administrator role in Azure AD
 
.EXAMPLE
Get-AadServicePrincipalAdmins
 
.NOTES
General notes
#>


function Get-AadServicePrincipalAdmins() {
    # REQUIRE AadSupport Session
    RequireConnectAadSupport
    # END REGION

    $roles = Invoke-AadCommand -Command {
        Get-AzureADDirectoryRole | Sort-Object DisplayName
    }

    $servicePrincipalAdmins = $null

    $list = @()

    foreach ($role in $roles) {
        if($role.DisplayName -eq "Directory Readers" -or $role.DisplayName -eq "Directory Writers"){
            continue
        }

        $servicePrincipalAdmins = Invoke-AadCommand -Command {
            Param($RoleObjectId)
            Get-AzureADDirectoryRoleMember -ObjectId $RoleObjectId | where-object {$_.ObjectType -eq 'ServicePrincipal'}
        } -Parameters $role.ObjectId
        
        foreach ($sp in $servicePrincipalAdmins) {
            $item = [PSCustomObject]@{
                DisplayName = $sp.DisplayName
                Id = $sp.ObjectId
                Role = $role.DisplayName
            } 

            $list += $item
        }
    }

    Write-Host "Service Pricipals with Azure AD Admin Roles ($($list.count) Found)." -ForegroundColor Yellow
    $list | Sort-Object DisplayName, Role
}

Set-Alias -Name Get-AadSpAdmins -Value Get-AadServicePrincipalAdmins
<#
.SYNOPSIS
Gets a list of tenant admins.
 
.DESCRIPTION
Gets a list of tenant admins.
 
.PARAMETER Role
Provide the role to lookup
By Default 'Global Administrator' is used
 
.EXAMPLE
Get-AadTenantAdmins
...See a list of company admins...
.EXAMPLE
Get-AadTenantAdmins -Role 'Helpdesk Administrator'
...See a list of password or helpdesk admins...
.EXAMPLE
Get-AadTenantAdmins -Role 8da6f8d3-ef75-42e4-961e-8fba79c29048
...You can also use Role Ids...
.EXAMPLE
Get-AadTenantAdmins -All
...You can see a list of all Admin Roles...
 
.NOTES
General notes
#>


function Get-AadTenantAdmins {
    [CmdletBinding(DefaultParameterSetName='All')]
    param (
        [Parameter(Position=0,ParameterSetName="UseRole")]
        $Role = 'Global Administrator',

        [Parameter(ParameterSetName="GetAll")]
        [switch] $All
    )
    
    begin {
        # REQUIRE AadSupport Session
        RequireConnectAadSupport
        # END REGION

        try {
            $isGuid = [System.Guid]::Parse($Id)
        } catch {
        }
    }
    
    process {
        
        # Get All Roles
        $AadDirectoryRoles = Invoke-AadCommand -Command { Get-AzureADDirectoryRole }

        if($All)
        {
            $_role = $AadDirectoryRoles
        }

        # Look up role based on ObjectID
        elseif($isGuid)
        {
            $_role = $AadDirectoryRoles | Where-Object {$_.ObjectId -eq $role}
        }

        # Look up role based on DisplayName
        else {
            $_role = $AadDirectoryRoles | Where-Object {$_.displayName -eq $role} 
        }


        # Role not found
        if(-not $_role)
        {
            Write-Host "'$Role' role not found." -ForegroundColor Red
            Write-Host "To get a list of valid roles, run..." -ForegroundColor Red
            Write-Host "Get-AzureADDirectoryRole | Sort DisplayName" -ForegroundColor Red
            throw "'$Role' role not found."
        }

        if($_role.Count -gt 1)
        {
            foreach ($role in $_role) {
                $members = Invoke-AadCommand {
                    Param($RoleId)
                    Get-AzureADDirectoryRoleMember -ObjectId $RoleId
                } -Parameters $role.ObjectId

                $admins = $members | Select-Object DisplayName, UserPrincipalName, ObjectType | Sort-Object DisplayName
                $count = $admins.Count
                if ($admins.Count -eq $null) {
                    $count = 1
                }
        
                if ($count -gt 0) {
                    Write-Host "========================================"
                    Write-Host "$($role.DisplayName) ($count)" -ForegroundColor Yellow
                    $admins | Format-Table -AutoSize -HideTableHeaders
                }
            }
            return
        }

        else {
            $members = Invoke-AadCommand {
                Param($RoleId)
                Get-AzureADDirectoryRoleMember -ObjectId $RoleId
            } -Parameters $role.ObjectId
            
            $admins = $members | Select-Object DisplayName, UserPrincipalName 

            # Output list of admins
            Write-Host ""
            Write-Host "Users assigned to the '$Role' role" -ForegroundColor Yellow
            $admins |  Sort-Object DisplayName | format-table
        }
        

        
    }
    
    end {
    }
}
<#
.SYNOPSIS
Get Access Token from Azure AD token endpoint.
 
.DESCRIPTION
Get Access Token from Azure AD token endpoint.
 
.PARAMETER ClientId
Provide Client ID or App ID required to get a access token
 
.PARAMETER ResourceId
Provide App URI ID or Service Principal Name you want to get an access token for.
 
.PARAMETER Scopes
Provide Scopes for the request.
 
.PARAMETER ClientSecret
Provide Client Secret if this is a Web App client.
 
.PARAMETER Redirect
Provide Redirect URI or Redirect URL.
 
.PARAMETER Username
Provide username if using Resource Owner Password grant flow.
 
.PARAMETER Password
Provide Password of resource owner if using Resource Owner Password Credential grant flow.
 
.PARAMETER Tenant
Provide Tenant ID you are authenticating to.
 
.PARAMETER Instance
Provide Azure AD Instance you are connecting to.
 
.PARAMETER UseV2
By default, Azure AD V1 authentication endpoints will be used. Use this if you want to use the V2 Authentication endpoints.
 
.PARAMETER UseResourceOwner
Enable this switch if you want to use the Resource Owner Password Credential grant flow.
 
.PARAMETER UseClientCredential
Enable this switch if you want to use the Client Credential grant flow.
 
.PARAMETER UseRefreshToken
Enable this switch if you want to use the Refresh Token grant flow.
 
.PARAMETER GrantType
Specify the 'grant_type' you want to use.
 - password
 - client_credentials
 - refresh_token
 - authorization_code
 - urn:ietf:params:oauth:grant-type:jwt-bearer
 - urn:ietf:params:oauth:grant-type:saml1_1-bearer
 - urn:ietf:params:oauth:grant-type:saml2-bearer
 
.PARAMETER Code
Specify the 'code' for authorization code flow
 
.PARAMETER Assertion
Specify the 'assertion' you want to use for user assertion
 
.PARAMETER ClientAssertionType
Specify the 'client_assertion_type' you want to use.
 
Available options...
 - urn:ietf:params:oauth:client-assertion-type:jwt-bearer
 
.PARAMETER ClientAssertion
Specify the 'client_assertion' you want to use. This is used for Certificate authentication where the client assertion is signed by a private key.
 
.PARAMETER RequestedTokenUse
Specify the 'requested_token_use' you want to use. For Azure AD, the only acceptable value is 'on_behalf_of'. This is used to identify the on-behalf-of flow is to be used.
 
.PARAMETER RequestedTokenType
Specify the 'requested_token_type'. This is either...
 - urn:ietf:params:oauth:token-type:saml2
 - urn:ietf:params:oauth:token-type:saml1
 
.EXAMPLE
Use Resource Owner Password Credential grant flow
Get-AadToken -UseResourceOwner -ResourceId "https://graph.microsoft.com" -ClientId 5567ba8a-e608-4219-97d8-3d3ea63718e7 -Redirect "https://login.microsoftonline.com/common/oauth2/nativeclient" -Username john@contoso.com -Password P@$$w0rd!
 
To get client only access token
Get-AadToken -UseClientCredential -ResourceId "https://graph.microsoft.com" -ClientId 5567ba8a-e608-4219-97d8-3d3ea63718e7 -ClientSecret "98iwjdc-098343js="
 
.NOTES
General notes
#>


function Get-AadToken
{
    [CmdletBinding(DefaultParameterSetName="All")] 
    param(
        [Parameter(Mandatory=$true, Position=0)]
        [string]$ClientId,

        [Parameter(ParameterSetName="UseResourceOwner", Mandatory=$false)]
        [switch]$UseResourceOwner,

        [Parameter(ParameterSetName="UseClientCredential", Mandatory=$false)]
        [switch]$UseClientCredential,

        [Parameter(ParameterSetName="UseRefreshToken", Mandatory=$false)]
        [switch]$UseRefreshToken,   

        [Parameter(ParameterSetName="UseResourceOwner", Mandatory=$true)]
        [string]$Username = $null,

        [Parameter(ParameterSetName="UseResourceOwner", Mandatory=$true)]
        [string]$Password = $null,


        [string]$ClientSecret = $null,

        [Parameter(ParameterSetName="UseRefreshToken", Mandatory=$true)]
        [string]$RefreshToken,

        [string]$ResourceId = $($Global:AadSupport.Resources.MsGraph),
        
        [Parameter(ParameterSetName="UseClientCredential", Mandatory=$true)]
        [Parameter(ParameterSetName="UseRefreshToken", Mandatory=$false)]
        [Parameter(ParameterSetName="UseResourceOwner", Mandatory=$false)]
        [string]$Tenant,

        [string]$Scopes = "openid email profile offline_access $($Global:AadSupport.Resources.MsGraph)/.default",
        [string]$Redirect = $null,
        [string]$Instance = "https://login.microsoftonline.com",
        
        [switch]$UseV2 = $false,

        [string]$GrantType,
        [string]$Code,
        [string]$Assertion,
        [string]$ClientAssertionType,
        [string]$ClientAssertion,
        [string]$RequestedTokenUse,

        [ValidateSet("urn:ietf:params:oauth:token-type:saml2", "urn:ietf:params:oauth:token-type:saml1")]
        [string]$RequestedTokenType
    )

    # REQUIRE AadSupport Session
    RequireConnectAadSupport
    # END REGION

    $error.Clear()
    $scriptError = $null

    # Get Service Principal
    try {
        $sp = Get-AadServicePrincipal -Id $ClientId

        # Get real Client ID
        $ClientId = $sp.AppId
    }
    catch {
        Write-Host "App '$ClientId' not found" -ForegroundColor Yellow
    }


    if($sp.count -gt 1)
    {
        throw "Found too many results for '$ClientId'. Please specify a unique ClientId."
    }


    # Get Redirect Uri if not one specified
    if(-not $Redirect -and $sp) 
    {
        $Redirect = $sp.ReplyUrls[0]
    }
    

    # Set Grant_Type
    if (-not $UseResourceOwner -and -not $UseClientCredential -and -not $UseRefreshToken -and -not $GrantType)
    {
        do {
            $isValidChoice = $true
            Write-Host ""
            Write-Host "Do you want to..." -ForegroundColor Yellow
            Write-Host "1: Resource Owner (Provice user name and password)"
            Write-Host "2: Client Credential (Provice Client ID and Client Secret)"
            Write-Host "3: Refresh (Provide Refresh Token)"
            Write-Host "Type 'exit' to quit."
            Write-Host ""
            $choice = Read-Host -Prompt "Enter your choice (#)"
            Write-Host ""
            switch ($choice) 
            {
                "1" 
                {
                    $UseResourceOwner = $true 
                    break
                }

                "2" 
                {
                    $UseClientCredential = $true 
                    break
                }

                "3" 
                {
                    $UseRefreshToken = $true
                    break
                }

                "exit" {return}

                "default" 
                {
                    $isValidChoice = $false; 
                    break
                }
            }
        } while (-not $isValidChoice)
    }

    # Requirements for Resource Owner
    if ($UseResourceOwner)
    {
        $GrantType = "password"
        
        if (-not $Username)
        {
            $Username = Read-Host -Prompt "Username"
        }

        if (-not $Password)
        {
            $Password = Read-Host -Prompt "Password"
        }
    }

    # Requirements for ClientCredentials
    if($UseClientCredential)
    {
        $GrantType = "client_credentials"

        if (-not $Tenant -or $Tenant -eq "common")
        {
            $Tenant = Read-Host -Prompt "Tenant (Do not use 'common')"
        }

        if (-not $ClientSecret)
        {
            $ClientSecret = Read-Host -Prompt "ClientSecret"
        }
    }

    # Requirements for Refresh Token
    if($UseRefreshToken)
    {
        $GrantType = "refresh_token"
        if (-not $RefreshToken)
        {
            $RefreshToken = Read-Host -Prompt "Refresh Token"
        }
    }



    # Set default Tenant
    if (-not $Tenant)
    {
        $Tenant = "common"
    }


    # Start initializing the Token result object
    $token = @{}

    # Start initializing the token endpoint POST content
    $body            = @{}


    # Lookup Resource
    try {
        $resource = Get-AadServicePrincipal -Id $ResourceId
        $ResourceId - $resource.AppId
    }
    catch {
        Write-Host "Resource '$ResourceId' not found" -ForegroundColor Yellow
    }


    # Set up if AAD V1 or V2 authentication is used...
    if ($UseV2) {
        # Use V2 endpoint
        $authUrl = "$Instance/$Tenant/oauth2/v2.0/token"
    }

    else {
        # Use V1 endpoint
        $authUrl = "$Instance/$Tenant/oauth2/token"
        $body.resource = "$ResourceId"
    }

    $token.authUrl = $authUrl

    $body.client_id  = $ClientID
    $body.grant_type = $GrantType
    $body.scope      = $scopes
    $body.nonce      = 1234
    $body.state      = 5678

    if ($Redirect)
    {
        $body.redirect_uri = $Redirect
    }

    if ($RefreshToken)
    {
        $body.refresh_token = $RefreshToken
    }

    if($Code)
    {
        $body.code = $Code
    }

    if ($ClientSecret) {
        $body.client_secret = $ClientSecret
    }

    if ($Username -and $Password) {
        
        $body.username   = $Username
        $body.password   = $Password
        $body.grant_type = "password"
    }

    if($ClientAssertionType)
    {
        $body.client_assertion_type = $ClientAssertionType
    }

    if($ClientAssertion)
    {
        $body.client_assertion = $ClientAssertion
    }

    if($Assertion)
    {
        $body.assertion = $Assertion
    }

    if($RequestedTokenUse)
    {
        $body.requested_token_use = $RequestedTokenUse
    }

    if($RequestedTokenType)
    {
        $body.requested_token_type = $RequestedTokenType
    }

    $token.PostContent = $body

    # Sign-in to Azure AD & Get Access Token
    try {
        Write-Verbose "Authenticating to '$authUrl'"
        
        $response = $null
        $response      = Invoke-WebRequest -Method Post -Uri $authUrl -Body $body -verbose

        $content = $null

        if($response.content) {
            $content = $response.content | ConvertFrom-Json
        }
        elseif($response.access_token) {
            $content = $response
        }

        if ($content.access_token)   { $token.AccessToken = $content.access_token }
        if ($content.id_token)       { $token.IdToken = $content.id_token }
        if ($content.refresh_token)  { $token.RefreshToken = $content.refresh_token }
        if ($content.Type)           { $token.Type = $content.Type }
        if ($content.scope)          { $token.Scopes = $content.scope }
        if ($content.expires_in)     { $token.expires_in = $content.expires_in }
        if ($content.ext_expires_in) { $token.ext_expires_in = $content.ext_expires_in }
        if ($content.expires_on)     { $token.expires_on = $content.expires_on }
        if ($content.not_before)     { $token.not_before = $content.not_before }
        if ($content.resource)       { $token.resource = $content.resource }

        if($token.AccessToken)
        {
            if($token.AccessToken.StartsWith("eyJ"))
            {
                $token.AccessTokenClaims = $token.AccessToken | ConvertFrom-AadJwtToken
            }

            else 
            {
                $token.SamlDecoded = ConvertFrom-Base64String -base64String ( Base64UrlDecode($token.AccessToken) )
            }
            
        }

        if($token.IdToken)
        {
            $token.IdTokenClaims = $token.IdToken | ConvertFrom-AadJwtToken
        }
    }

    # Acquire token failed
    Catch {
        
        $scriptError = $_
        $scriptError = $scriptError | ConvertFrom-Json

        $token.Error = $scriptError
        $scriptError | Out-Host

        # AADSTS65001
        if ($scriptError -match "AADSTS65001") {
            $consentUrl = "$Instance/$Tenant/oauth2/authorize?client_id=$ClientId&response_type=code&redirect=$RedirectUri&prompt=admin_consent"
            Write-Host ""
            Write-Host "Consent Required" -ForegroundColor Yellow
            Write-Host ""
            Write-Host "Go to the following url '$consentUrl'" -ForegroundColor Yellow
        }

        # AADSTS50079
        if ($scriptError -match "AADSTS50079") {

            $claims = $scriptError.claims | ConvertFrom-Json
            $capolids = $claims.access_token.capolids.values

            Write-Host ""
            Write-Host "User Interaction Required due to Conditional Access Policy Id '$capolids'" -ForegroundColor Yellow

        }

        return $token
    }

    

    $Object = New-Object PSObject -Property $token

    if($Object.AccessTokenClaims -and -not $HideOutput)
    {
        
        Write-Host ""
        Write-Host "Access Token" -ForegroundColor Yellow
        Write-ObjectToHost $($Object.AccessTokenClaims)
    }
    
    
    return $Object
}






# ##############################################################################################
# CLIENT ASSERTION
# ##############################################################################################
function ClientAssertion 
{
    param($ClientId, $ClientSecret, $ClientAssertion)

    $body            = @{}
    $body.client_id  = $ClientId
    $body.client_secret  = $ClientSecret
    $body.refresh_token  = [System.Web.HttpUtility]::UrlEncode($RefreshToken)
    $body.grant_type = "refresh_token"

    try {
        Write-Host "Authenticating to '$authUrl'" -ForegroundColor Yellow
        $body | ft
        
        $response = $null
        $response      = Invoke-WebRequest -Method Post -Uri $authUrl -Body $body -verbose
    
        $content = $null
    
        if($response.content) {
            $content = $response.content | ConvertFrom-Json
        }
        elseif($response.access_token) {
            $content = $response
        }
        
        $AccessToken = $null
        $AccessToken = $content.access_token
    
        if($AccessToken) {
            Write-Host "Acquired Access Token successfull!"
        }
    }

    Catch {
        $scriptError = $_
    
        if ($response) {
            $scriptError = $scriptError | ConvertFrom-Json
            $scriptError
        } 
        else {
                throw $scriptError
        }
    }
}
# ----------------------------------------------------------------------------
# INFORMATION
# * Used the follow resource as a guide to write this script
# https://github.com/kenakamu/Microsoft.ADAL.PowerShell/blob/master/Microsoft.ADAL.PowerShell/Microsoft.ADAL.PowerShell.psm1
# * At the time of writing this...
# > AzureAD Module Version (AzureAdPreview version 2.0.1.18)
# > Active Directory Authentication Library version 3.19.7

# ----------------------------------------------------------------------------
<#
# SAMPLE USAGE
 
    # CONFIGURATION
    $TenantName = "williamfiddes.onmicrosoft.com"
    $RedirectURI = "urn:ietf:wg:oauth:2.0:oob"
    $ResourceId = "https://graph.microsoft.com"
 
    # If using Native app type
    $ClientID = ""
     
    # If using Web app type
    $ClientID = ""
    $ClientSecret = ""
 
    # ACQUIRE TOKEN
    $aadContext = [AadContext]::new()
    $aadContext.TenantName = $TenantName
    $aadContext.AcquireToken($ResourceId,$ClientId,$RedirectURI,$PromptBehavior)
    #$aadContext.AcquireToken($Resource, $ClientId, $ClientSecret)
    $Headers = $aadContext.CreateAuthenticationHeaders()
#>


<#
.SYNOPSIS
Gets a token using ADAL installed with Azure AD PowerShell Module
 
 
.DESCRIPTION
Gets a token using ADAL installed with Azure AD PowerShell Module
Prompts user to sign-in in order to acquire token.
 
ClientID : "ApplicationId-of-the-client-application"
TenantId : "tenant-id"
AccessTokenClaims : @{ access-token-claims }
Scopes : "scopes-in-access-token"
ExtraQueryParams : "extra-query-parameters-used-to-get-token"
UserId : "login_hint"
PromptBehavior : "prompt-behavior-used-to-get-token"
ExpiresOn : "when-the-token-expires"
Authority : "authority"
IdToken : "id-token"
IdTokenClaims : @{ claims-in-the-id-token }
UniqueId : "object-id-of-the-user"
ReplyAddress : "reply-address-of-client-application"
AccessToken : "access-token"
DisplayableId : "user-principal-name-of-user"
AppId : "ApplicationId-of-the-client-application"
Audience : "ApplicationId-of-the-resource"
Resource : "ApplicationId-of-the-resource"
Roles : "roles-in-the-token"
 
.PARAMETER ResourceId
Set the resource in which you want to get a access token for
 
.PARAMETER ClientId
Set the Client ID/App Id in which you want to get a access token from
 
.PARAMETER Redirect
Set the Redirect URL/URI for which Azure AD will redirect back to your application
 
.PARAMETER Prompt
Sets the prompt behavior for ADAL
'Always','Auto','SelectAccount','RefreshSession','Never'
 
.PARAMETER UserId
Sets the user account to use when authenticating
 
.PARAMETER Password
Sets the password for the user account being used if using the Resource Owner Password Credential method
 
.PARAMETER Tenant
Sets the tenant in which your authenticating to
Used in conjunction with Instance to form the Authority
 
.PARAMETER Instance
Sets the Azure AD Instance tenant in which your authenticating to
Used in conjunction with Tenant to form the Authority
 
.PARAMETER UseClientCredential
Tell ADAL to use the Client Credential flow (Gets a client only access token)
 
.PARAMETER UseResourceOwnerPasswordCredential
Tell ADAL to use the UserPasswordCredential method. This is actually the Windows Integrated Authentication extension method in ADAL and can also be used for the Resource Owner Password Credential flow for non-federated users.
 
.PARAMETER ClientSecret
Sets the Client Secret for ADAL to use during Client Credential flow
 
.EXAMPLE
Prompt user to sign-in to get access token
Get-AadTokenUsingAdal -ResourceId "https://graph.microsoft.com" -ClientId 'Contoso Native App' -Redirect "https://login.microsoftonline.com/common/oauth2/nativeclient"
 
To get client only access token
Get-AadTokenUsingAdal -UseClientCredential -ResourceId "https://graph.microsoft.com" -ClientId 'Contoso Service App' -ClientSecret "98iwjdc-098343js="
 
Use the Resource Owner Password Credential Flow or us WIA (Password required)
Get-AadTokenUsingAdal -UseResourceOwnerPasswordCredential -ResourceId "https://graph.microsoft.com" -ClientId 'Contoso Service App' -UserId 'your-user-name' -Password "your-secret-password"
 
 
.NOTES
General notes
#>


function Get-AadTokenUsingAdal
{
    [CmdletBinding(DefaultParameterSetName="All")] 
    param(
        [Parameter(Mandatory=$true,Position=0)]
        $ClientId,

        $ResourceId = $($Global:AadSupport.Resources.MsGraph),

        [Parameter(ParameterSetName="UserAccessToken", Mandatory=$false)]
        [Parameter(ParameterSetName="ClientAccessToken", Mandatory=$false)]
        $Redirect,

        [Parameter(ParameterSetName="UserAccessToken", Mandatory=$false)]
        [Parameter(ParameterSetName="RopcFlow", Mandatory=$true)]
        $UserId,

        [Parameter(ParameterSetName="UserAccessToken", Mandatory=$false)]
        [Parameter(ParameterSetName="RopcFlow", Mandatory=$true)]
        $Password,

        [Parameter(ParameterSetName="UserAccessToken", Mandatory=$false)]
        $DomainHint,

        [Parameter(Mandatory=$false)]
        [ValidateSet('Always','Auto','SelectAccount','RefreshSession','Never')]
        $Prompt,

        $Tenant,
        $Instance,

        [Parameter(ParameterSetName="ClientAccessToken", Mandatory=$true)]
        [switch]
        $UseClientCredential,

        [Parameter(ParameterSetName="RopcFlow", Mandatory=$true)]
        [switch]
        $UseResourceOwnerPasswordCredential,

        [Parameter(ParameterSetName="ClientAccessToken", Mandatory=$true)]
        $ClientSecret,

        [switch]
        $SkipServicePrincipalSearch,

        [switch]
        $HideOutput
    )

    # Convert Password string to SecuredString
    if($Password)
    {
        [SecureString]$SecuredPassword = ConvertTo-SecureString $Password -AsPlainText -Force
    }

    "Get-AadTokenUsingAdal::Params:Instance:$Instance" | Log-AadSupport
    if(-not $Instance)
    {
        if($Global:AadSupport.Session.AadInstance)
        {
            $Instance = $Global:AadSupport.Session.AadInstance
        }
        else 
        {
            $Instance = "https://login.microsoftonline.com"
        }
    }
    
    if(-not $Tenant)
    {
        if($Global:AadSupport.Session.TenantId)
        {
            $Tenant = $Global:AadSupport.Session.TenantId
        }
        else 
        {
            $Tenant = "common"
        }
    }

    # Get Service Principal
    if(-not $SkipServicePrincipalSearch -and $Global:AadSupport.Session.Active)
    {
        try{
            $sp = Get-AadServicePrincipal -Id $ClientId
            $ClientId = $sp.AppId
        }
        catch {
            Write-Host "App '$ClientId' not found" -ForegroundColor Yellow
        }
        
        if(-not $Redirect -and $sp)
        {
            $Redirect = $sp.ReplyUrls[0]
        }

        # Lookup Resource
        try {
            $resource = Get-AadServicePrincipal -Id $ResourceId
            $ResourceId = $resource.AppId
        }
        catch {
            Write-Host "Resource '$ResourceId' not found" -ForegroundColor Yellow
        }
        
    }    

    if($sp.count -gt 1)
    {
        throw "Found too many results for '$ClientId'. Please specify a unique ClientId."
    }

    if(-not $UserId -and -not $Prompt)
    {
        $UserId = $Global:AadSupport.Session.AccountId
    }

    $ExtraQueryParams = ""
    if ($UserId)
    {
        $ExtraQueryParams += "&login_hint=$UserId"
    }
    if ($DomainHint)
    {
        $ExtraQueryParams += "&domain_hint=$DomainHint"
    }


    # Use the Client Credential Flow
    if ($UseClientCredential)
    {
        if($Tenant -eq "common")
        {
            throw "Invalid Tenant. When using Client Credentials, Tenant is required."
        }
         
        $result = Invoke-AdalCommand -Command { 
            Param($Params)
                
            $result = Get-AadSupportTokenForClient -Authority $Params.Authority -ClientId $Params.ClientId -ResourceId $Params.ResourceId -ClientSecret $Params.ClientSecret
            return $result
        } -Parameters @{
            Authority = "$Instance/$Tenant/"
            ResourceId = $ResourceId
            ClientId = $ClientId 
            ClientSecret = $ClientSecret
        }

    }

    # Use the Resource Owner Password Credential Flow
    if($UseResourceOwnerPasswordCredential)
    {
        $result = Invoke-AdalCommand -Command { 
            Param($Params)
            $result = Get-AadSupportTokenForUserWithPassword -Authority $Params.Authority -ClientId $Params.ClientId -ResourceId $Params.ResourceId -UserId $Params.UserId -Password $Params.Password
            return $result
        } -Parameters @{
            ClientId = $ClientId 
            ResourceId = $ResourceId
            UserId = $UserId
            Password = $SecuredPassword
            Authority = "$Instance/$Tenant/"
        }
    }

    # Determine and use a Interactive flow
    if (-not $UseClientCredential -and -not $UseResourceOwnerPasswordCredential)
    {

        Write-Verbose "Use Interactive flow."
        if($Prompt -ne "Always" -and $Prompt -ne "SelectAccount")
        {
            $result = Invoke-AdalCommand -Command { 
                Param($Params)
                    
                $result = Get-AadSupportTokenForUser -Authority $Params.Authority -ClientId $Params.ClientId -ResourceId $Params.ResourceId -RedirectURI $Params.RedirectURI -PromptBehavior $Params.Prompt -ExtraQueryParameters $Params.ExtraQueryParameters -UserId $Params.UserId
                return $result
            } -Parameters @{
                ClientId = $ClientId 
                ResourceId = $ResourceId
                RedirectURI = $Redirect
                UserId = $UserId
                ExtraQueryParameters = $ExtraQueryParams
                Prompt = $Prompt
                Authority = "$Instance/$Tenant/"
            }
            
        }

        else {
            Write-Host "Authenticating user..." -ForegroundColor Yellow

            $result = Invoke-AdalCommand -Command { 
                Param($Params)
                $result = Get-AadSupportTokenForUser -Authority $Params.Authority -ClientId $Params.ClientId -ResourceId $Params.ResourceId -RedirectURI $Params.RedirectURI -PromptBehavior $Params.Prompt
                return $result
                
            } -Parameters @{
                Authority = "$Instance/$Tenant/"
                ClientId = $ClientId 
                ResourceId = $ResourceId
                RedirectURI = $Redirect
                Prompt = $Prompt
            }

        }
        
    }

    $details = @{}
    $details.AppId = $ClientId
    $details.ReplyAddress = $Redirect
    $details.Audience = $ResourceId

    if($result.Error -or $result.Exception)
    {

        $details.Error = $result.ErrorDetails.Exception.Message

        if (-not $HideOutput)
        {
            Write-Host ""
            Write-Host "Error" -ForegroundColor Yellow
            $AadError = $details.Error.Message
            Write-Host $AadError -ForegroundColor Red

            # If Output is allowed, lets return the error
            return $details
        }

        #Otherwise throw the error to stop the program
        return $result.Exception
    }

    foreach($member in $result | Get-member)
    {
        if ($member.MemberType -eq 'NoteProperty')
        {
            $details[$member.Name] = $result.($member.Name)

        }
        
    }

    $Headers = @{ "Authorization" = "Bearer $($details.AccessToken)" }
    $details.Headers = $Headers
    $details.AccessTokenClaims = $details.AccessToken | ConvertFrom-AadJwtToken
    $details.Scopes = $details.AccessTokenClaims.scp
    $details.Roles = $details.AccessTokenClaims.Roles
    if($details.IdToken) { $details.IdTokenClaims = $details.IdToken | ConvertFrom-AadJwtToken }

    $Object = New-Object -TypeName PSObject -Property $details
    
    if($Object.AccessToken -and -not $HideOutput)
    {
        
        Write-Host ""
        Write-Host "Access Token" -ForegroundColor Yellow
        $AccessTokenClaims = $Object.AccessTokenClaims
        Write-ObjectToHost $AccessTokenClaims
    }

    return $Object
        
       
    #}

    //Write-Host "RUNNING Invoke-AdalCommand"
    //Invoke-AdalCommand -Command $ScriptBlock -Parameters $Params

    Write-Host "END Invoke-AdalCommand"
}

function Test-GetAadTokenUsingAdal {
    Get-AadTokenUsingAdal -UseClientCredential -ClientId 'aadsupport unittest' -ResourceId 'Microsoft Graph' -ClientSecret 'iu?6uN35Mp9b?]]KqMwnpElTBy.S:/5-' -Tenant williamfiddes.onmicrosoft.com
    
}

# ############################################################################
# FEATURE REQUESTS
#
# $AdalAssembly = Allow to set custom Adal Assembly path
# Implement AcquireToken using user credentials
# Implement AcquireToken using OBO
# Implement Options to set additional parameters
# > AAD Instance
# > Set login_hint
# > Set domain_hint
# > Set Prompt Behavior
<#
.SYNOPSIS
Convert a base64Encoded Json Web Token to a PowerShell object.
#
 
.DESCRIPTION
Convert a base64Encoded Json Web Token to a PowerShell object.
 
.PARAMETER Token
Parameter description
 
.EXAMPLE
EXAMPLE 1
"eyJ***" | ConvertFrom-AadJwtToken
 
EXAMPLE 2
ConvertFrom-AadJwtToken -Token "eyJ***"
 
.NOTES
General notes
#>


function Get-AadTrustedSites
{
    $CurrentLocation = Get-Location
    $Key = "HKCU:\Software\Microsoft\Windows\CurrentVersion\Internet Settings\ZoneMap\domains"
    TraverseKey -Key $Key

    Set-Location -Path $CurrentLocation
}

function TraverseKey
{
    Param(
        [parameter(Mandatory=$true, Position=0, ValueFromPipeline = $true)]
        [string]$Key
    )

    $Key = $Key.Replace("HKEY_CURRENT_USER","HKCU:")

    $items = Get-ChildItem -Path $Key
    $items
    
    $Sites = @()

    foreach($item in $items)
    {
        $itemName = $item.Name.Replace("HKEY_CURRENT_USER","HKCU:")
        if($Key -eq $itemName)
        {
            continue
        }

        TraverseKey -Key $item.Name
    }

}
<#
.SYNOPSIS
Gets information for what access a user has access to.
 
.DESCRIPTION
Gets information for what access a user has access to.
 
.PARAMETER Id
Provide the User Principal Name or User Object ID
 
.EXAMPLE
Get-AadUserAccess -Id 'UserPrincipalName or User Object ID'
 
.NOTES
General notes
#>


function Get-AadUserAccess
{
    param(
        [Parameter(
            mandatory=$true,
            ValueFromPipeline = $true)]
        $Id,

        [switch]$SkipAzureRoleAssignments,
        [switch]$SkipKeyVaultAccess
    )

    # REQUIRE AadSupport Session
    RequireConnectAadSupport
    # END REGION
    

    $TenantDomain = $Global:AadSupport.Session.TenantId

    $user = Invoke-AadCommand -Command {
        Param($Id)
        Get-AzureAdUser -ObjectId $Id
    } -Parameters $Id
    
    if(-not $user)
    {
        throw "'$Id' not found in '$TenantDomain'"
    }

    Write-Host ""
    Write-Host "User Account" -ForegroundColor Yellow
    Write-Host "$($user.UserPrincipalName) | ObjectId:$($user.ObjectId)"
    Write-Host ""

    Write-Host "Getting Azure AD Directory Roles assigned to User..."
    $AdminRoles = Get-AadAdminRolesByObject -ObjectId $user.ObjectId | ConvertTo-Json

    Write-Host "Getting App Roles (Application Permissions) assigned to User..."
    $AppRoles = Get-AadAppRolesByObject -ObjectId $user.ObjectId -ObjectType $user.ObjectType | ConvertTo-Json

    Write-Host "Getting OAuth2PermissionGrants (Delegated Permissions) assigned to User..."
    $Grants = Get-AadConsent -UserId $user.ObjectId | ConvertTo-Json

    if(-not $SkipKeyVaultAccess)
    {
        Write-Host "Getting Key Vault Access assigned to User..."
        $KeyVaultAccess = Get-AadKeyVaultAccessByObject -ObjectId $user.ObjectId | ConvertTo-Json 
    }
    
    if(-not $SkipAzureRoleAssignments)
    {
        Write-Host "Getting Azure Roles assigned to User..."
        $AzureRoles = Get-AadAzureRoleAssignments -SigninName $user.UserPrincipalName | ConvertTo-Json
      
    }

    $Report = [pscustomobject]@{
        PrincipalType = $user.ObjectType
        PrincipalId = $user.UserPrincipalName
        PrincipalDisplayName = $user.DisplayName
        PrincipalObjectId = $user.ObjectId
        AzureAdAdminRoles = $AdminRoles;
        ApplicationRoles = $AppRoles;
        ConsentedPermissions = $Grants;
        KeyVaultAccess = $KeyVaultAccess;
        AzureRoleAssignments = $AzureRoles;
    }

    #$ReturnObject = New-Object -TypeName psobject -Property $Report

    return $Report
}
function Get-AadUserInfo
{
    param
    (
        [Parameter(Position=0, Mandatory=$true)]
        [string]$Token
    )

    $response = Invoke-AadProtectedApi -Endpoint "https://login.microsoftonline.com/common/openid/userinfo" -AccessToken $Token

    return $response
}
function Get-AadUserRealm
{
    param
    (
        [Parameter(Mandatory=$true)]
        [string]$UserPrincipalName,

        [string]$AadInstance
    )

    if(-not $AadInstance)
    {
        $AadInstance = $Global:AadSupport.Session.AadInstance
    }
    
    # If AadInstance is still null, lets set a default
    if(-not $AadInstance)
    {
        $AadInstance = "https://login.microsoftonline.com"
    }

    return (Invoke-WebRequest -Uri "$AadInstance/getuserrealm.srf?login=$UserPrincipalName" -UseBasicParsing).Content | ConvertFrom-Json
}
<#
.SYNOPSIS
Import Azure RBAC Role Assignments from a CSV exported using Export-AadAzureRoleAssignments
 
.DESCRIPTION
Import Azure RBAC Role Assignments from a CSV exported using Export-AadAzureRoleAssignments
 
We also try to map external guest accounts to accounts that exist in the tenant you are importing to.
 
When running this cmdlet, you may see the following messages...
* Assigning to scope '/' level not allowed
  > This is a Azure AD tenant setting where the user has enabled 'Access management for Azure resources'
  > https://docs.microsoft.com/en-us/azure/role-based-access-control/elevate-access-global-admin
* This is a Unknown Role Assignment.
  > This object probably does not exist anymore in the Azure AD tenant.
 
.PARAMETER ImportCsv
Provide the Azure subscription CSV file exported from 'Export-AadAzureRoleAssignments'
 
.PARAMETER SubId
Provide the Azure subscription ID you want to import into.
 
.EXAMPLE
Import-AadAzureRoleAssignments -SubId 'efb4bb0c-e454-4530-8753-753f22c8f901' -ImportCsv '.\Subscription--Pay-As-You-Go-Roles.csv'
 
.NOTES
General notes
#>


function Import-AadAzureRoleAssignments {
# Ensure you are signed in to Az Account (Connect-AzAccount)
# Ensure you are signed in to AzureAD (Connect-AzureAD)

# $ImportCSV : Location of CSV file for role assignments
# $SubId : Azure subscription ID to re-apply permissions to
  param (
    [Parameter(Mandatory=$true)]
    [string]$ImportCsv, 
    [Parameter(Mandatory=$true)]
    [string]$SubId
  )

  # REQUIRE AadSupport Session
  if($Global:AadSupportModule)
  {
    RequireConnectAadSupport
  }
  # END REGION
  
  $domains = Invoke-AadCommand -Command { Get-AzureADDomain }
  $InitialDomain = ($domains | where {$_.IsInitial -eq $true}).Name

  $HaveAccess = 0
  try {
    
    Invoke-AzureCommand {
      Param($Params)
      Set-AzContext -Subscription $Params.Subscription -Tenant Params.Tenant | Out-Null
    } -Parameters @{
      Subscription = $SubId
      Tenant = $Global:AadSupport.Session.TenantId 
    } -SubscriptionId $SubId

    $HaveAccess = $True
  } Catch {
    $HaveAccess = $False
    Write-Host "$SubId does not exist or You don't have access to it" -ForegroundColor Red
  }

  If ($HaveAccess) {
      $roles = Import-CSV $ImportCsv
  
      if(-not $roles)
      {
        return
      }

      # Start assigning roles
      foreach ($role in $roles) {
        
        $RoleDefinitionName = $role.RoleDefinitionName
        $RoleDisplayName = $role.DisplayName
        $RoleObjectId = $role.ObjectId
        $RoleScope = $role.scope
        $RoleSignInName = $role.SignInName
        $RoleDefId = $role.RoleDefinitionId

        if($RoleDisplayName)
        {
          Write-Host "Assigning role for '$RoleDisplayName' to '$RoleDefinitionName' @ '$RoleScope'"
        }
        else 
        {
          Write-Host "Assigning role for '$RoleObjectId' to '$RoleDefinitionName' @ '$RoleScope'"
        }
        

        if($RoleDefinitionName -match "ServiceAdministrator" -or $RoleDefinitionName -match "AccountAdministrator")
        {
          Write-Host " -- Skipping assignment for '$RoleDisplayName' to '$RoleDefinitionName'. Not possible with this script."
          Continue
        }

        if ($role.Scope -ne "/") { # Skip Azure AD scope assignments as it is not possible to assign to this scope
      
          # Unknown Role Assignments
          if($role.ObjectType -eq "Unknown")
          {
            Write-Host " -- This is a Unknown Role Assignment. Most likely the Azure AD Object assigned to this was deleted."
            Continue
          }

          # Group Role Assignment
          elseif ($role.ObjectType -eq "Group") {

            $group = Invoke-AadCommand {
              Param($RoleDisplayName)
              Get-AzureAdGroup -Filter "DisplayName eq '$RoleDisplayName'"
            } -Parameters $role.DisplayName

            if ($group.count -eq 1) {
              $GroupId = $group.id
              try{
                
                $assignment = $null
                $assignment = Invoke-AzureCommand -Command {
                  Param($Params)
                  Get-AzRoleAssignment -scope $Params.Scope -ObjectId $Params.ObjectId -RoleDefinitionId $Params.RoleDefinitionId
                } -Parameters @{
                  Scope = $rolescope
                  ObjectId = $GroupId
                  RoleDefinitionId = $RoleDefId
                } -SubscriptionId $SubId

                if(-not $assignment)
                {
                  Invoke-AzureCommand -Command {
                    Param($Params)
                    New-AzRoleAssignment -scope $Params.Scope -ObjectId $Params.ObjectId -RoleDefinitionId $Params.RoleDefinitionId
                  } -Parameters @{
                    Scope = $rolescope
                    ObjectId = $GroupId
                    RoleDefinitionId = $RoleDefId
                  } -SubscriptionId $SubId
                }
              }
              catch{
                Write-Host " -- The role assignment already exists." -ForegroundColor Yellow
              }
              
            } 
            elseif ($group.count -gt 1) { 
              write-Host " -- Could not assign Access Control to Group $roleDisplayName" -ForegroundColor Yellow
              write-Host " -- Multiple groups exist with the same DisplayName; unable to identify which group to assign Access Control" -ForegroundColor Yellow
            } 
            else { 
              write-Host " -- Could not assign Access Control to Group '$roleDisplayName'" -ForegroundColor Yellow
              write-Host " -- No groups exist with that name" -ForegroundColor Yellow
            }

            Continue
          } 

          # User Role Assignment
          elseif ($role.ObjectType -eq "User") {
            $isExternal = $false # User is external and SignInName has underscore to be replaced by @
        
            #Modify SignInName if external user
            $i = $role.SignInName.indexOf("#EXT#")
            if ($i -eq -1) { $i = $role.SignInName.length }
            else {
              $isExternal = $true
            }
            $role.SignInName = $role.SignInName.Substring(0,$i)

            if ($isExternal -eq $true) {
              $ati = $role.SignInName.lastindexOf("_")
              $part1 = $role.SignInName.Substring(0,$ati)
              $part2 = $role.SignInName.Substring(($ati+1))
              $role.SignInName = $part1 + "@" + $part2
            }

            # Look for UPN suffix
            $ati = $role.SignInName.indexOf("@")
            $suffix = $role.SignInName.Substring(($ati+1))

            # Check if user domain is verified, If not then this user is still external user
            $domains = Invoke-AadCommand -Command { Get-AzureAdDomain }
            $DomainExists = ($domains | where {$suffix -match $_.Name}).Count
            if (!$DomainExists) {
                $role.SignInName = $role.SignInName.Replace("@","_")
                $role.SignInName = $role.SignInName + "#EXT#@" + $InitialDomain
            }

            $SignInName = $role.SignInName
            $user = Invoke-AadCommand {
              Param($ObjectId)
              Get-AzureADUser -ObjectId $ObjectId
            } -Parameters $role.SignInName

            $UserObjectId = $user.ObjectId

            if ($RoleDefinitionName -eq "CoAdministrator") {
              $rolescope = "/subscriptions/$subid"
              $RoleDefId = "8e3af657-a8ff-443c-a75c-2fe8c4bcb635"
            }

            if ($user.count -eq 1) {
              try {
                $assignment = $null
                $assignment = Invoke-AzureCommand -Command {
                  Param($Params)
                  Get-AzRoleAssignment -Scope $Params.Scope -ObjectId $Params.ObjectId -RoleDefinitionId $Params.RoleDefinitionId
                } -Parameters @{
                  Scope = $RoleScope
                  ObjectId = $UserObjectId
                  RoleDefinitionId = $RoleDefId
                } -SubscriptionId $SubId

                if(-not $assignment)
                {
                  Invoke-AzureCommand -Command {
                    Param($Params)
                    New-AzRoleAssignment -Scope $Params.Scope -ObjectId $Params.ObjectId -RoleDefinitionId $Params.RoleDefinitionId
                  } -Parameters @{
                    Scope = $RoleScope
                    ObjectId = $UserObjectId
                    RoleDefinitionId = $RoleDefId
                  } -SubscriptionId $SubId
                }
                else {
                  Write-Host " -- Role Assignment already exists."
                }
                
              }
              catch
              {

              }
              
            } elseif ($user.count -eq 0) {
              write-Host " -- Could not assign Access Control to User $roleSignInName" -ForegroundColor Yellow
              write-Host " -- User does not exist or can not be found!"  -ForegroundColor Yellow
            }

            Continue
          } 

          # Service Principal Role Assignment
          elseif ($role.ObjectType -match "ServicePrincipal") 
        
          {
            $sp = Invoke-AadCommand -Command {
              Param($RoleDisplayName)
              Get-AzureAdServicePrincipal -Filter "DisplayName eq '$RoleDisplayName'"
            } -Parameters $RoleDisplayName

            if($sp)
            {
              $assignment = $null
              $assignment = Invoke-AzureCommand -Command {
                Param($Params)
                Get-AzRoleAssignment -scope $Params.Scope -ObjectId $Params.ObjectId -RoleDefinitionId $Params.RoleDefinitionId
              } -Parameters @{
                Scope = $RoleScope
                ObjectId = $sp.ObjectId
                RoleDefinitionId = $RoleDefId
              } -SubscriptionId $SubId
              
              if(-not $assignment)
              {
                Invoke-AzureCommand -Command {
                  Param($Params)
                  New-AzRoleAssignment -scope $Params.Scope -ObjectId $Params.ObjectId -RoleDefinitionId $Params.RoleDefinitionId
                } -Parameters @{
                  Scope = $RoleScope
                  ObjectId = $sp.ObjectId
                  RoleDefinitionId = $RoleDefId
                } -SubscriptionId $SubId
              }
              else
              {
                Write-Host " -- Role Assignment already exists."
              }
              
            } 

            else 
            {
                write-host " -- Did not find Service Principal: $RoleDisplayName" -ForegroundColor Yellow
            }

            Continue
          } 
        } 
        
        else 
        { write-host "Assigning to scope '/' level not allowed" -ForegroundColor Yellow}

      }
  }
}
<#
.SYNOPSIS
Make a API request to a protected AAD resource using a Accesss Token.
 
.DESCRIPTION
Make a API request to a protected AAD resource using a Accesss Token.
 
.PARAMETER Endpoint
This is the API url you are making a request to.
 
.PARAMETER AccessToken
Pass the access token to the protected API
 
.PARAMETER GET
Make a GET request to the protected API
 
.PARAMETER PATCH
Use the PATCH Http Method
 
.PARAMETER POST
Use the POST Http Method
 
.PARAMETER DELETE
Use the DELETE Http Method
 
.PARAMETER PUT
Use the PUT Http Method
 
.PARAMETER Body
Include a body content with your API request
 
.PARAMETER ContentType
Specify a ContentType to use. (Default application/json)
 
.EXAMPLE
Invoke-AadProtectedApi -GET -Endpoint "https://graph.microsoft.com/v1.0/me" -AccessToken "eyJ***"
 
.NOTES
General notes
#>



function Invoke-AadProtectedApi
{
    [CmdletBinding(DefaultParameterSetName="All")] 
    Param(

       [Parameter(Mandatory=$true)]

       [string]$Endpoint,

       [Parameter(ParameterSetName="ProvideToken_Get", Mandatory=$True)]
       [string]$AccessToken,

       [Parameter(ParameterSetName="GetToken_Get", Mandatory=$True)]
       [string]$Client,

       [Parameter(ParameterSetName="GetToken_Get", Mandatory=$True)]
       [string]$Resource,

       [ValidateSet('GET','PATCH','POST','PUT','DELETE')]
       $Method = "GET",

       [string]
       $Body,

       $Headers = $null,
       $ContentType = "application/json",

       [string]$RerunTimes = 1
    ) #end param
    

    # Parameter requirements
    if ( ($Method -eq "POST" -or $Method -eq "PATCH" -or $Method -eq "PUT") -and -not $Body )
    {
        throw "Body required when using POST, PATCH, or PUT"
    }

    if(-not $AccessToken -and -not $Client)
    {
        throw "You must specify either -AccessToken or -Client"
    }


    # Check if Body is file
    if($Body)
    {
        if(Test-Path -Path $Body)
        {
            $content = Get-Content -Path $Body -Raw
            $Body = $content
        }
    }


    if($Client -and $Resource)
    {
        if($Client -eq $Global:AadSupport.Clients.AzureAdPowerShell.ClientId -or $Client -eq $Global:AadSupport.Clients.AzurePowerShell.ClientId )
        {
            $ClientId =  $Client
        }
        else {
            $ClientId = (Get-AadServicePrincipal -Id $Client).AppId
        }

        $ResourceId = (Get-AadServicePrincipal -Id $Resource).AppId
        
        if(!$ClientId) {
            $ClientId = $Client
        }

        if(!$ResourceId) {
            $ResourceId = $Client
        }

        write-verbose "Getting token for $Client and $Resource"
        $token = Get-AadTokenUsingAdal -ClientId $ClientId -ResourceId $ResourceId -UserId $Global:AadSupport.Session.AccountId -Prompt Auto -HideOutput -SkipServicePrincipalSearch
        if($token["Error"]) 
        {
            return $token["Error"]
        }
        write-verbose "$($token)"
        $AccessToken = $token.AccessToken
    }

    #
    # BEGIN
    # Re-run logic
    # Used to help repro Throttling errors or just simply call the API # of times
    #
    if($RerunTimes -gt 0)
    {
        $Params = @{
            Endpoint = $Endpoint
            AccessToken = $AccessToken
            Method = $Method
            Body = $Body
            ContentType = $ContentType
            Headers = $Headers
        }


        $JobGuids = @()
        for ($i=1; $i -le $RerunTimes; $i++)
        {
            $JobGuid = New-Guid
            $JobGuids += $JobGuid

            $job = {
                Param(
                    $Params
                )

                $Method = $Params.Method
                $Endpoint = $Params.Endpoint
                $AccessToken = $Params.AccessToken
                $Body = $Params.Body
                $ContentType = $Params.ContentType
                $Headers = $Params.Headers

                $Result = @{}
                $Result.Content = @()
                $nextLink = $null
            
                $RetryAttempts = 0
                $MaxRetryAttempts = 5
                do {
                    
                    #
                    # Start API
                    #
            
                    if($nextLink)
                    {
                        $endpoint = $nextLink
                        $nextLink = $null
                    }
            
                    try {
                        write-verbose "Making Graph call..."
                        if(!$Headers)
                        {
                            $Headers = @{}
                        }
                        $Headers.Authorization = "Bearer $AccessToken"
                        

                        if ($Method -eq "GET")
                        {
                            $request = Invoke-WebRequest -Headers $Headers  -Uri $endpoint -Method GET -ContentType $ContentType
                        }
            
                        if ($Method -eq "POST" -or $Method -eq "PATCH" -or $Method -eq "PUT")
                        {   
                            $request = Invoke-WebRequest -Headers $Headers -Uri $endpoint -Method $Method -Body $Body -ContentType $ContentType
                        }
            
                        if ($Method -eq "DELETE")
                        {
                            $request = Invoke-WebRequest -Headers $Headers -Uri $endpoint -Method $Method -ContentType $ContentType
                        }
            
                    }
                    catch{
                        $RetryAttempts++
                        Start-Sleep -Milliseconds (1000*$RetryAttempts*2)
            
                        Write-Host "Exception calling API." -ForegroundColor Red
                        if($request.Response.Content)
                        {
                            $String = $request.Response.Content
                        }
            
                        elseif($_.Exception.Response) {
                            $reqstream = $_.Exception.Response.GetResponseStream()
                            $reqstream.Position = 0
            
                            $stream = [System.IO.StreamReader]::new($reqstream)
                            $String = $stream.ReadToEnd()
                            $stream.Close()
                            $reqstream.Close()
                        }
            
                        Write-Host $String -ForegroundColor Yellow
            
                        if($_.Exception.Response.StatusCode -eq "429")
                        {
                            Write-Host "Throttling limit hit: StatusCode 429: Waiting 1 minute. It may take up to 5 minutes" -ForegroundColor Yellow
                            Start-Sleep -Seconds 61
                            Continue
                        }
            
                        elseif($_.Exception.Response.StatusCode -eq "Unauthorized")
                        {
                            Write-Host "Invalid Access Token." -ForegroundColor Yellow
                            throw $_
                        }
            
                        elseif($_.Exception.Response.StatusCode -eq "Forbidden")
                        {
                            Write-Host "Missing Permissions." -ForegroundColor Yellow
                            Start-Sleep -Seconds 300
                            throw $_
                        }
                        
                        
                        if($RetryAttempts -gt $MaxRetryAttempts)
                        {
                            throw $_
                        }
                        
                        Write-Host ""
                        Write-Host "Retrying request!" -ForegroundColor Yellow
                        continue
                    }
                
                    try{
                        
                        $JsonObject = $request.Content | ConvertFrom-Json
                        
                        if($JsonObject.Value)
                        {
                            $Result.Content += $JsonObject.Value
                            if($JsonObject.'@odata.nextLink')
                            {
                                $nextLink = $JsonObject.'@odata.nextLink'
                            }
                            if($JsonObject.'odata.nextLink')
                            {
                                if(-not $nextLink -match "https")
                                {
                                    $nextLink = $JsonObject.'odata.nextLink'
                                    Write-Host "nextLink is not a valid web address."
                                    Write-Host $nextLink
                                }
                            }
                            
                        }
                        else{
                            $Result.Content += $JsonObject
                        }
                        
                    }
                    catch{
                        $Result.Content += $request.Content
                    }
                } while ($nextLink)
                
                $Result.Headers = $request.Headers
                $Result.StatusCode = $request.StatusCode
                $Result.Response = $request
            
                if($Result)
                {
                    Write-Verbose $($request)
                }
                
            
                $ReturnObject = New-Object -TypeName PsCustomObject -Property $Result
            
                if($ReturnObject.Content)
                {
                    $members = $ReturnObject.Content | Get-Member | where {$_.MemberType -eq "NoteProperty"}
                    $ValuePropertyExist = $members.Name.Contains("value")
                }
            
                if(!$ReturnObject.Content.Value -and $ValuePropertyExist)
                {
                    return $null
                }
            
                return $ReturnObject.Content
            }

            Start-Job -ScriptBlock $job -Name $JobGuid -ArgumentList $Params | out-null

        }

        $CompletedJobs = @()
        foreach($JobId in $JobGuids)
        {
            Write-Verbose "Completing JobId: $JobId"
            $CompletedJobs += Wait-Job -Name $JobId | Receive-Job
            $CompletedJobs
        }

        return $CompletedJobs
    }

    # END
    #

   

}
<#
.SYNOPSIS
Quickly create a Self-Signed Certificate to be used for authentication for the Azure AD application.
 
.DESCRIPTION
Quickly create a Self-Signed Certificate to be used for authentication for the Azure AD application.
 
This will export a PFX that will contain the Private key.
This will also export the CER that contains the Public Key which can be uploaded to the app in Azure AD.
 
Once you have uploaded the CER file to Azure AD on the applicatication, you can use the PFX and run the New-AadClientAssertion cmdlet to generate a client assertion.
 
.PARAMETER CertificatePassword
Either specify Service Principal (SP) Name, SP Display Name, SP Object ID, Application/Client ID, or Application Object ID
 
.PARAMETER ClientId
Specify the Azure AD application this certificate is meant for.
 
.PARAMETER CertificateName
When the certificates are exported, you can use this to customize the file name of the certificates.
 
.PARAMETER AddToApplication
This is a switch. When used, it will add the public key certificate to the Application object in Azure AD.
 
.EXAMPLE
New-AadApplicationCertificate -ClientId ac4dff1b-b7d7-4453-a4e4-613210c686c9 -CertificatePassword "SomePassword" -AddToApplication
 
.NOTES
 
#>

function New-AadApplicationCertificate
{
    [CmdletBinding(DefaultParameterSetName='DefaultSet')]
    Param(
        [Parameter(mandatory=$true)]
        [string]$CertificatePassword,

        [Parameter(mandatory=$true, ParameterSetName = 'ClientIdSet')]
        [string]$ClientId,

        [string]$CertificateName,

        [Parameter(mandatory=$false, ParameterSetName = 'ClientIdSet')]
        [switch]$AddToApplication
    )

    if($ClientId)
    {
        # Get Application object from Azure AD
        $app = Get-AadApplication -id $ClientId
    }
    else { $CliendId = "app" }

    # Create self-signed Cert
    $notAfter = (Get-Date).AddYears(2)

    try
    {
        $cert = (New-SelfSignedCertificate -DnsName "cert://$ClientId" -CertStoreLocation "cert:\LocalMachine\My" -KeyExportPolicy Exportable -Provider "Microsoft Enhanced RSA and AES Cryptographic Provider" -NotAfter $notAfter)
        
        Write-Verbose "Cert Hash: $($cert.GetCertHash())"
        Write-Verbose "Cert Thumbprint: $($cert.Thumbprint)"
    }

    catch
    {
        Write-Error "ERROR. Probably need to run as Administrator."
        Write-host $_
        return
    }

    if(!$CertificateName)
    {
        $CertificateName = "aad-$ClientId"
    }

    # Export Public portion of Certificate
    $CerPath = "$CertificateName.cer"
    Export-Certificate -Cert $cert -FilePath $CerPath

    # Export PFC with private key
    $PfxPath = "$CertificateName.pfx"
    $SecuredPwd = ConvertTo-SecureString -String $CertificatePassword -Force -AsPlainText
    Export-PfxCertificate -cert $cert -FilePath "$PfxPath" -Password $SecuredPwd 

    Write-Host ""
    Write-Host "Exported Public Key Certificate to..." -ForeGroundColor Yellow
    Write-Host "$CerPath"

    Write-Host ""
    Write-Host "Exported PFX with Private Key to..." -ForeGroundColor Yellow
    Write-Host "$PfxPath"

    if($AddToApplication)
    {
        $AppObjectId = $app.ObjectId
        $KeyValue = [System.Convert]::ToBase64String($cert.GetRawCertData())

        Write-Verbose "App Object Id: $AppObjectId"
        Write-Verbose "Key Value: $KeyValue"

        invoke-AadCommand -Command {
            Param($Params)
            New-AzureADApplicationKeyCredential -ObjectId $Params.ObjectId -Type AsymmetricX509Cert -Usage Verify -Value $Params.Value 
        } -Parameters @{
            ObjectId = $AppObjectId
            Value = $KeyValue
        }
        
    }


    function GetX509Certificate($certThumbprint){


        $x509cert = Get-ChildItem "Cert:\LocalMachine\My" | Where-Object { $_.Thumbprint -eq $certThumbprint } | Select-Object -First 1
       
        if(!$x509cert){
       
        $x509cert = Get-ChildItem "Cert:\CurrentUser\My" | Where-Object { $_.Thumbprint -eq $certThumbprint } | Select-Object -First 1
       
        }
       
       
       
       Write-Host "Cert = {$x509cert}"
       
        Return $x509cert
       
       
       
       }
}
<#
.SYNOPSIS
Generates a client assertion for Azure AD.
 
.DESCRIPTION
Generates a client assertion for Azure AD. Requires a PFX with a Private Key.
 
You can create one using "New-AadApplicationCertificate"
 
.PARAMETER ClientId
Specify the Azure AD application this client assertion is meant for.
 
.PARAMETER CertificatePath
Path to certificate (PFX)
 
.PARAMETER CertificatePassword
Password for the PFX
 
.PARAMETER Tenant
Specify the tenant name or ID this client assertion is meant for.
 
.EXAMPLE
New-AadClientAssertion -ClientId ac4dff1b-b7d7-4453-a4e4-613210c686c9 -CertificatePath "C:\path\to\certificate.pfx" -CertificatePassword "SomePassword" -Tenant contoso.onmicrosoft.com
 
.NOTES
 
.RELATED
https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/wiki/Client-Assertions
 
#>


function New-AadClientAssertion
{
    [CmdletBinding(DefaultParameterSetName='Default')]
    param(
        [Parameter(mandatory=$true)]
        $ClientId,

        [Parameter(mandatory=$true)]
        [string]$CertificatePath,

        [Parameter(mandatory=$true)]
        [string]$CertificatePassword,

        [Parameter(mandatory=$true)]
        [string]$Tenant  
    )


    if(![System.IO.Path]::IsPathRooted($CertificatePath))
    {
        $LocalPath = Get-Location
        $CertificatePath = "$LocalPath\$CertificatePath"
    }

    Write-Verbose "Looking for $CertificatePath"

    $certificate = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new($CertificatePath, $CertificatePassword)


    $ThumbprintBase64 = ConvertFrom-AadThumbprintToBase64String $certificate.Thumbprint
    
    $nbf = [int][double]::parse((Get-Date -Date $(Get-Date).ToUniversalTime() -UFormat %s))
    $exp = [int][double]::parse((Get-Date -Date $((Get-Date).addseconds($ValidforSeconds).ToUniversalTime()) -UFormat %s)) # Grab Unix Epoch Timestamp and add desired expiration.
    $jti = New-Guid
    $aud = "https://login.microsoftonline.com/$Tenant/oauth2/token"
    $sub = $ClientId
    $iss = $ClientId
   
    [hashtable]$header = @{
        alg = "RS256"; 
        typ = "JWT"; 
        x5t = $ThumbprintBase64; 
        kid = $ThumbprintBase64 
    }

    [hashtable]$payload = @{
        aud = $aud; 
        iss = $iss; 
        sub = $sub; 
        jti = $jti; 
        nbf = $nbf; 
        exp = $exp; 
        tid = "aa00d1fa-5269-4e1c-b06d-30868371d2c5";
    }

    $headerjson = $header | ConvertTo-Json -Compress
    $payloadjson = $payload | ConvertTo-Json -Compress
    
    $headerjsonbase64 = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($headerjson)).Split('=')[0].Replace('+', '-').Replace('/', '_')
    $payloadjsonbase64 = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($payloadjson)).Split('=')[0].Replace('+', '-').Replace('/', '_')

    $ToBeSigned = [System.Text.Encoding]::UTF8.GetBytes($headerjsonbase64 + "." + $payloadjsonbase64)


    $rsa = $certificate.PrivateKey
    if ($null -eq $rsa) { # Requiring the private key to be present; else cannot sign!
        throw "There's no private key in the supplied certificate - cannot sign" 
    }
    else {
        # Overloads tested with RSACryptoServiceProvider, RSACng, RSAOpenSsl
        try { $Signature = [Convert]::ToBase64String($rsa.SignData([byte[]]$ToBeSigned,[Security.Cryptography.HashAlgorithmName]::SHA256,[Security.Cryptography.RSASignaturePadding]::Pkcs1)) -replace '\+','-' -replace '/','_' -replace '=' }
        catch 
        {
            throw "Signing with SHA256 and Pkcs1 padding failed using private key $rsa >> " + $_
        }
    }

    $token = "$headerjsonbase64.$payloadjsonbase64.$Signature"

    Write-Host "Generated Client Assertion..."
    return $token

}
<#
.SYNOPSIS
    Get a list of consented permissions based using the specified parameters to filter
 
.DESCRIPTION
    Revokes a consented permission based on the parameters provided to be used as a filter. At minimum, the ClientId is required.
 
.PARAMETER ClientId
    Filter based on the ClientId. This is the Enterprise App (Client app) in which the consented permissions are applied on.
 
.PARAMETER ResourceId
    Filter based on the ResourceId. This is the resource in which the client has permissions on.
 
.PARAMETER UserId
    Filter based on the UserId. User in which that has consented to the app.
 
.PARAMETER ClaimValue
    Filter based on the scope or role value.
 
.PARAMETER ConsentType
    Filter based on the Consent Type. Available options...
    'Admin','User', 'All'
 
.PARAMETER PermissionType
    Filter based on the Permission Type. Available options...
    'Delegated','Application', 'All'
 
.EXAMPLE
    Example 1: Remove all consented permissions for a app (Removes All Admin and User Consents)
    PS C:\> Revoke-AadConsent -ClientId 'Contoso App'
 
.EXAMPLE
    Example 2: Remove all user consented permissions leaving only the Admin consented permissions
    PS C:\> Revoke-AadConsent -ClientId 'Contoso App' -ConsentType User
 
.EXAMPLE
    Example 3: Revoke a specific permission
    PS C:\> Revoke-AadConsent -ClientId 'Contoso App' -ResourceId 'Microsoft Graph' -ClaimValue Directory.ReadWrite.All
 
.EXAMPLE
    Example 4: Revoke a specific user
    PS C:\> Revoke-AadConsent -ClientId 'Contoso App' -UserId 'john@contoso.com'
 
#>


function Revoke-AadConsent {
    [CmdletBinding(DefaultParameterSetName="All")] 
    param (
        [Parameter(mandatory=$true, Position=0, ValueFromPipeline = $true)]
        [string]$ClientId,
        [string]$ResourceId,
        [string]$ClaimValue,

        [Parameter(ParameterSetName = 'UserId')]
        [string]$UserId,

        [ValidateSet('Admin','User', 'All')]
        $ConsentType = 'All',

        [ValidateSet('Delegated','Application', 'All')]
        $PermissionType = 'All'
    )

    # Parameter validations
    if($ClaimValue -and -not $ResourceId)
    {
        throw "You must provide a 'ResoureId' when using 'ClaimValue'"
    }

    if($ClaimValue -match " " -or $ClaimValue -match ";" -or $ClaimValue -match ",")
    {
        throw "Specifing only one 'ClaimValue' is supported"
    }


    $TenantDomain = $Global:AadSupport.Session.TenantId

    # --------------------------------------------------
    # Check if signed in user is Global Admin (As only global admins can perform admin consent)
    $isGlobalAdmin = Invoke-AadCommand -Command {
        Param(
            $AccountId
        )
        $SignedInUser = Get-AzureAdUser -ObjectId $AccountId
        $SignedInUserObjectId = $SignedInUser.ObjectId
        $GlobalAdminRoleIds = (Get-AzureAdDirectoryRole | where { $_.displayName -eq 'Global Administrator' -or $_.displayName -eq 'Application Administrator' }).ObjectId
        foreach($GlobalAdminRoleId in $GlobalAdminRoleIds)
        {
            if( (Get-AzureAdDirectoryRoleMember -ObjectId $GlobalAdminRoleId).ObjectId -contains $SignedInUserObjectId )
            {
                return $true
            }
        }
    } -Parameters $Global:AadSupport.Session.AccountId
    

    if (-not $isGlobalAdmin)  
    {  
        Write-Host "Your account '$($Global:AadSupport.Session.AccountId)' is not a Global Admin in $TenantDomain."
        throw "Exception: 'Global Administrator' or 'Application Administrator' role REQUIRED"
    } 

    $ConsentedPermissions = Get-AadConsent `
     -ClientId $ClientId `
     -ResourceId $ResourceId `
     -ClaimValue $ClaimValue `
     -ConsentType $ConsentType `
     -PermissionType $PermissionType `
     -UserId $UserId
     
    
    $CountRemovedPermissions = 0

    # Get output ready, lets create a new line
    Write-Host ""

    foreach($Permission in $ConsentedPermissions)
    {
        $MsGraphUrl = "$($Global:AadSupport.Resources.MsGraph)/beta/oauth2PermissionGrants/$($Permission.Id)"

        if($Permission.PermissionType -eq "Delegated")
        {
            $RemoveConsent = $true

            if($ClaimValue -and $Permission.ClaimValue -ne $ClaimValue) {
                $RemoveConsent = $false
            }

            # Remove the OAuth2PermissionGrant Object
            if($RemoveConsent)
            {
                $User = $Permission.PrincipalId
                if(!$User)
                {
                    $User = "AllPrincipals"
                }

                Write-Host "Removing $($Permission.ResourceName) | $($Permission.ConsentType) $($Permission.PermissionType) permission(s): $($Permission.ClaimValue) | User: $User"
        
                $CountRemovedPermissions++
                Invoke-AadProtectedApi `
                -Client $Global:AadSupport.Clients.AzureAdPowershell.ClientId `
                -Resource $Global:AadSupport.Resources.MsGraph `
                -Endpoint $MsGraphUrl -Method DELETE `
            } 

            # Update the OAuth2PermissionGrant Object to remove ClaimValue
            else {
                $CountRemovedPermissions++
                $ClaimValues = $Permission.ClaimValue.Split(" ")
                $ClaimValues = $ClaimValues | where-object {$_ -ne $ClaimValue}
                $NewClaimValues = $ClaimValues -Join " "

                
                $JsonBody = @{
                    scope = $NewClaimValues
                } | ConvertTo-Json -Compress

                Write-Host "Removing $($Permission.ResourceName) | $($Permission.ConsentType) $($Permission.PermissionType) permission: $ClaimValue | $($Permission.PrincipalId)"

                Invoke-AadProtectedApi `
                -Client $Global:AadSupport.Clients.AzureAdPowershell.ClientId `
                -Resource $Global:AadSupport.Resources.MsGraph `
                -Endpoint $MsGraphUrl -Method PATCH `
                -Body $JsonBody

            }


        }

        if($Permission.PermissionType -eq "Application")
        {
            $CountRemovedPermissions++

            Write-Host "Removing $($Permission.ResourceName) | $($Permission.ConsentType) $($Permission.PermissionType) permission: $($Permission.ClaimValue)"

            Invoke-AadCommand -Command {
                Param($Params)
                Remove-AzureADServiceAppRoleAssignment -ObjectId $Params.ObjectId -AppRoleAssignmentId $Params.AppRoleAssignmentId
            } -Parameters @{
                ObjectId = $Permission.ClientId
                AppRoleAssignmentId = $Permission.Id
            }
        }
    } 

    Write-Host ""
    Write-Host "Removed $CountRemovedPermissions permission(s)"
}


function Test-RevokeAadConsent
{
    Remove-Module AadSupportPreview
    Import-Module AadSupportPreview
    Connect-AadSupport

    Add-AadConsent -ClientId 'AadSupport UnitTest' -ResourceId 'Microsoft Graph' -ClaimValue 'User.read' -UserId testuser@williamfiddes.onmicrosoft.com
    Add-AadConsent -ClientId 'AadSupport UnitTest' -ResourceId 'Microsoft Graph' -ClaimValue 'User.read' -UserId testuser2@williamfiddes.onmicrosoft.com

    Revoke-AadConsent -ClientId 'AadSupport UnitTest'
    Revoke-AadConsent -ClientId 'AadSupport UnitTest' -ConsentType User
    Revoke-AadConsent -ClientId 'AadSupport UnitTest' -UserId testuser@williamfiddes.onmicrosoft.com
    Revoke-AadConsent -ClientId 'AadSupport UnitTest' -ConsentType Admin
    Revoke-AadConsent -ClientId 'AadSupport UnitTest' -PermissionType Delegated
    Revoke-AadConsent -ClientId 'AadSupport UnitTest' -PermissionType Application

    Revoke-AadConsent -ClientId 'AadSupport UnitTest' -ResourceId 'Microsoft Graph'
    Revoke-AadConsent -ClientId 'AadSupport UnitTest' -ResourceId 'Microsoft Graph' -UserConsentOnly 
    Revoke-AadConsent -ClientId 'AadSupport UnitTest' -ResourceId 'Microsoft Graph' -UserId testuser@williamfiddes.onmicrosoft.com
    Revoke-AadConsent -ClientId 'AadSupport UnitTest' -ResourceId 'Microsoft Graph' -AdminConsentOnly
    Revoke-AadConsent -ClientId 'AadSupport UnitTest' -ResourceId 'Microsoft Graph' -DelegatedOnly
    Revoke-AadConsent -ClientId 'AadSupport UnitTest' -ResourceId 'Microsoft Graph' -ApplicationOnly

    
}
<#
.SYNOPSIS
# Resolve Admin Consent Issues
 
.DESCRIPTION
# Resolve Admin Consent Issues when the application registration is in a external directory and it not configured correctly.
 
.PARAMETER Id
Identifier for the Enterprise App (ServicePrincipal) we will be consenting to.
 
.PARAMETER ResourceId
Identifier for the Resource (ServicePrincipal) we will be consenting permissions to.
 
.PARAMETER UseMsGraph
Set permission scopes for https://graph.microsoft.com
 
.PARAMETER UseAadGraph
Set permission scopes for https://graph.windows.net
 
.PARAMETER UserId
If you set a UserId, then it will use User Consent
 
.PARAMETER Scopes
scope permissions
 
.PARAMETER Expires
Set the date when these consent scope permissions (OAuth2PermissionGrants) expire.
 
.EXAMPLE
Set-AadConsent -Id 'Your App Name' -Scopes 'User.Read Directory.Read.All' -UseMsGraph
 
Applies Admin Consent for the Microsoft Graph permissions User.Read & Directory.Read.All
 
.EXAMPLE
Set-AadConsent -Id 'Your App Name' -Scopes 'User.Read Directory.Read.All' -UseMsGraph -UserId john@contoso.com
 
Applies User Consent on user john@contoso.com for the Microsoft Graph permissions User.Read & Directory.Read.All
 
.EXAMPLE
Set-AadConsent -Id 'Your App Name' -Scopes 'user_impersonation' -ResourceId 'Custom Api'
 
You can also consent for custom API
 
.NOTES
General notes
#>


function Set-AadConsent {
    [CmdletBinding(DefaultParameterSetName="All")] 
    param (
        [Parameter(mandatory=$true, Position=0, ValueFromPipeline = $true)]
        [string]$ClientId,

        [Parameter(mandatory=$true, ParameterSetName = 'UseOtherResource')]
        [string]$ResourceId,

        [Parameter(mandatory=$true, ParameterSetName = 'UseMsGraph')]
        [switch]$UseMsGraph,

        [Parameter(mandatory=$true, ParameterSetName = 'UseAadGraph')]
        [switch]$UseAadGraph,

        [Parameter(mandatory=$false)]
        [string]$Scopes,

        [Parameter(mandatory=$false)]
        [string]$Roles,

        [string]$UserId,
        $Expires = (Get-AadDateTime -AddMonths 12)
    )


    # This CmdLet shows output
    Write-Host ""
    Write-Host "WARNING - Do not use this as a permanent solution. This is a workaround. " -ForegroundColor Yellow
    Write-Host "WARNING - Please ensure the Application registration is correctly configured." -ForegroundColor Yellow
    Write-Host "WARNING - Do not use this within your own Scipts to programmatically consent" -ForegroundColor Yellow
    Read-Host -Prompt "Press any key to continue or CTRL+C to quit" 
    Write-Host ""

    $TenantDomain = $Global:AadSupport.Session.TenantId

    # --------------------------------------------------
    # Check if signed in user is Global Admin (As only global admins can perform admin consent)
    $SignedInUser = Invoke-AadCommand -Command {
        Param($ObjectId)
        Get-AzureAdUser -ObjectId $ObjectId
    } -Parameters $Global:AadSupport.Session.AccountId
    

    $SignedInUserObjectId = $SignedInUser.ObjectId

    $AllDirectoryRoles = Invoke-AadCommand -Command {
        Get-AzureAdDirectoryRole
    }

    

    $GlobalAdminRoleId = ($AllDirectoryRoles | where { $_.displayName -eq 'Global Administrator' }).ObjectId

    $CompanyAdmins = Invoke-AadCommand -Command {
        Param($GlobalAdminRoleId)
        Get-AzureAdDirectoryRoleMember -ObjectId $GlobalAdminRoleId
    } -Parameters $GlobalAdminRoleId

    $isGlobalAdmin = ($CompanyAdmins).ObjectId -contains $SignedInUserObjectId

    if (-not $isGlobalAdmin)  
    {  
        Write-Host "Your account '$authUserId' is not a Global Admin in $TenantDomain."
        throw "Exception: GLOBAL ADMIN REQUIRED"
    } 

    # Set ConsentType
    $ConsentType = "Admin"
    if($UserId)
    {
        $ConsentType = "User"
    }

    # Require ResourceId
    if (-not $ResourceId -and -not $UseMsGraph -and -not $UseAadGraph)
    {
        $ResourceId = Read-Host -Prompt "ResourceId"
    }


    # --------------------------------------------------
    # GET SERVICE PRINCIPAL Object for app we want to udpate
    $ClientObjectId = $null

    $sp = Get-AadServicePrincipal -Id $ClientId

    if(-not $sp)
    {
        throw "'$ClientId' not found in '$TenantDomain'"
    }

    Write-Host "Enterprise App (ServicePrincipal) we will be updating" -ForegroundColor Yellow
    $sp | Select-Object DisplayName, AppId, ObjectId | Format-Table 

    if($sp.count -gt 1)
    {
        throw "'$ClientId' query returned more than one result. Please provide a unique Service Principal Identifier"
    }

    $ClientObjectId = $sp.ObjectId
    

    # ------------------------------------------------
    # GET PRINCIPAL ID
    # Get User to be used as PrincipalId (If ConsentType = User)
    $Oauth2PrincipalId = $null

    if ($ConsentType -eq "User") {
        $User = Invoke-AadCommand -Command {
            Param($UserId)
            Get-AzureADUser -ObjectId $UserId
        } -Parameters $UserId

        $Oauth2PrincipalId = ($User).ObjectId

        if (-not $Oauth2PrincipalId) {
            throw "'$UserId' does not exist in '$TenantDomain'"
        }
    }


    # ------------------------------------------------
    # GET RESOURCE ID
    $ResourceObjectId = $null

    if ($UseMsGraph) {
        $resource = Invoke-AadCommand -Command {
            Get-AadServicePrincipal -Id '00000003-0000-0000-c000-000000000000'
        }

        $ResourceObjectId = $resource.ObjectId
    }
    elseif ($UseAadGraph) {
        $resource = Invoke-AadCommand -Command {
            Get-AadServicePrincipal -Id '00000002-0000-0000-c000-000000000000'
        }

        $ResourceObjectId = $resource.ObjectId
    }
    elseif ($ResourceId) {
        $resource = (Get-AadServicePrincipal -Id $ResourceId)
        $ResourceObjectId = $resource.ObjectId
    }

    if (-not $resource) {
        throw "'$ResourceId' not found in '$TenantDomain'"
    }

    if($resource.count -gt 1)
    {
        throw "The Resource query returned more than one result. Please provide a unique Service Principal Identifier"
    }

    Write-Host "Resource we will be adding permissions from..." -ForegroundColor Yellow
    $resource | Select-Object DisplayName, AppId, ObjectId | Format-Table  


    # ------------------------------------------------
    # GET SCOPES
    # Get current OAuth2PermissionGrant for Service Principal & Resource
    $OAuth2PermissionGrant = $null
    $SpOAuth2PermissionGrants = Invoke-AadCommand -Command {
        Param($ClientObjectId)
        Get-AzureADServicePrincipalOAuth2PermissionGrant -All $true -ObjectId $ClientObjectId
    } -Parameters $ClientObjectId

    # User Consent
    if ($Oauth2PrincipalId) {
        $OAuth2PermissionGrant =  $SpOAuth2PermissionGrants | Where-Object {$_.ResourceId -eq $ResourceObjectId -and $_.PrincipalId -eq $Oauth2PrincipalId}
    
    # Admin Consent
    } else {
        $OAuth2PermissionGrant = $SpOAuth2PermissionGrants | Where-Object {$_.ResourceId -eq $ResourceObjectId -and $_.ConsentType -eq 'AllPrincipals'}
    }

    # Out-put current permission grant
    if ($OAuth2PermissionGrant) {
        Write-Host "Showing current Permission grant" -ForegroundColor Yellow
        $OAuth2PermissionGrant | Format-List -property *
    }
    else {
        Write-Host "No OAuth2PermissionGrants for $ClientId"
    }


    
    # ------------------------------------------------
    # ROLES SECTION
    if($Roles)
    {
        # ------------------------------------------------
        # GET ROLES
        $CurrentAppRoles = GetAadSpAppRoles -ClientObjectId $ClientObjectId -ResourceObjectId $ResourceObjectId
        $CurrentAppRoles | Format-List

        # ------------------------------------------------
        # REMOVE ROLES
        foreach($role in $CurrentAppRoles)
        {
            Invoke-AadCommand -Command {
                Param($Params)
                Remove-AzureADServiceAppRoleAssignment -ObjectId $Params.ObjectId -AppRoleAssignmentId $Params.AppRoleAssignmentId
            } -Parameters @{
                ObjectId = $ClientObjectId
                AppRoleAssignmentId = $role.RoleAssignedId
            }
            
        }

        # ------------------------------------------------
        # ADD ROLES

        $SortedAppRoles = $resource.AppRoles | Sort-Object Value
        $AddRoles = $Roles.Split(" ")
        foreach($role in ($AddRoles))
        {
            $RoleId = ($SortedAppRoles | where {$_.Value -eq $role}).Id 
            if($RoleId)
            {
                Invoke-AadCommand -Command {
                    Param($Params)
                    New-AzureADServiceAppRoleAssignment -ObjectId $Params.ClientObjectId -Id $Params.RoleId -ResourceId $Params.ResourceObjectId -PrincipalId $Params.ClientObjectId
                } -Parameters @{
                    ClientObjectId = $ClientObjectId
                    RoleId = $RoleId
                    ResourceObjectId = $ResourceObjectId
                }
            }
            
            else 
            {
                Write-Host "'$RoleId' not found on $($resource.DisplayName)" -ForegroundColor Yellow
            }
        }

        # ------------------------------------------------
        # VERIFY ROLES
        GetAadSpAppRoles -ClientObjectId $ClientObjectId -ResourceObjectId $ResourceObjectId | Format-List
    }
    

    # ------------------------------------------------
    # BUILD NEW PERMISSION GRANT

    #Build the permission grant
    if($Scopes -ne $null)
    {
        $newPermissionGrant = @{}
        $newPermissionGrant.Add("startTime", [System.DateTime]::UtcNow.AddMinutes(-5).ToString("o"))
        $newPermissionGrant.Add("expiryTime", $Expires)
        $newPermissionGrant.Add("scope", [string]::Join(' ', $scopes))
    
        if(!$OAuth2PermissionGrant) {
            $newPermissionGrant.Add("clientId", $ClientObjectId)
            $newPermissionGrant.Add("resourceId", $ResourceObjectId)
            if ($Oauth2PrincipalId) {
                $newPermissionGrant.Add("principalId", $Oauth2PrincipalId)
                $newPermissionGrant.Add("consentType", "Principal")
            }
            else {
                $newPermissionGrant.Add("consentType", "AllPrincipals")
            }
        }
    
        $newPermissionGrant = $newPermissionGrant | ConvertTo-Json

        # ------------------------------------------------
        # GET ACCESS TOKEN FOR AAD GRAPH
        $AccessToken = GetTokenForAadGraph

        # ------------------------------------------------
        #Create the admin permission grant via graph api
        if ($OAuth2PermissionGrant) {
            $uri = "https://graph.windows.net/$TenantDomain/oauth2PermissionGrants/$($OAuth2PermissionGrant.ObjectId)/?api-version=1.6"
            Invoke-WebRequest -Uri $uri -Headers @{ "Authorization" = "Bearer " + $AccessToken } -Method Patch -Body $newPermissionGrant -ContentType "application/json" -Verbose | Format-List -Force
        }
        else {
            $uri = "https://graph.windows.net/$TenantDomain/oauth2PermissionGrants?api-version=1.6"
            Invoke-WebRequest -Uri $uri -Headers @{ "Authorization" = "Bearer " + $AccessToken } -Method Post -Body $newPermissionGrant -ContentType "application/json" -Verbose | Format-List -Force

        }

        $AccessToken = $null
    }
    
    # ------------------------------------------------
    # VERIFY UPDATE

    Write-Host "Now showing current permission grant" -ForegroundColor Yellow
    $newOAuth2PermissionGrantResponse = Invoke-AadCommand -Command {
        Param($ClientObjectId)
        Get-AzureADServicePrincipalOAuth2PermissionGrant -All $true -ObjectId $ClientObjectId
    } -Parameters $ClientObjectId

    $newOAuth2PermissionGrant = $newOAuth2PermissionGrantResponse | where {$_.ResourceId -eq $ResourceObjectId -and $_.PrincipalId -eq $Oauth2PrincipalId}
    $newOAuth2PermissionGrant | Format-List

    if ($OAuth2PermissionGrant.scope -ne $newOAuth2PermissionGrant.scope) {
        Write-Host "SUCCESS!" -ForegroundColor Yellow
    }

    else 
    {
        Write-Host "No changes occurred!" -ForegroundColor RED
    }
}



function Set-AadOauth2PermissionsGrant
{
    [CmdletBinding(DefaultParameterSetName='Default')]
    Param(
        [Parameter(Mandatory=$true)]
        [string]$Id,

        [Parameter(Mandatory=$true)]
        [string]$Scope,

        [ValidateSet('SET','ADD','REMOVE')]
        $Method = "SET"
    )

    $Scopes = $Scope.Split(" ").Split(";").Split(",")

    $GraphApiUrl = "$($Global:AadSupport.Resources.MsGraph)/beta/oauth2PermissionGrants/$Id"

    # GET ACCESS TOKEN FOR AAD GRAPH
    $AccessToken = GetTokenForMsGraph

    if(!AccessToken)
    {
        throw "Unable to acquire token."
    }

    

    $Grant = @{}

    if($Method -eq "SET")
    {
        $Grant.scope = $Scope
    }

    else{
        # ------------------------------------------------
        # GET OAUTH2PERMISSIONGRANT

        $Grant =  Invoke-AadProtectedApi `
        -Client $Global:AadSupport.Clients.AzureAdPowershell.ClientId `
        -Resource $MsGraphEndpoint `
        -Endpoint $GraphApiUrl -Method "GET"
    
        if(!Grant)
        {
            throw "OAuth2PermissionGrant not found!"
        }
        
        if($Method -eq "ADD")
        {
            foreach($item in $Scopes)
            {
                if($item.Replace(" ","")) {
                    if(!$item -match $Grant.scope)
                    {
                        $Grant.scope += $item
                    }
                }
            }
        }

        if($Method -eq "REMOVE")
        {
            
        }
    }

    $Body = @{
        scope = $Grant.scope
    } | ConvertTo-Json -Compress

    # ------------------------------------------------
    #Create the admin permission grant via graph api
    Invoke-WebRequest -Uri $MsGraphEndpoint -Headers @{ "Authorization" = "Bearer " + $AccessToken } -Method Patch -Body $Body -ContentType "application/json"

}
<#
.SYNOPSIS
Updates the AadSupport PowerShell Module
 
.DESCRIPTION
Updates the AadSupport PowerShell Module
 
.PARAMETER All
Will also update AzureAd and Az PowerShell Modules
 
 
.EXAMPLE
Update-AadSupport
 
.NOTES
General notes
#>


function Update-AadSupport
{
    param([switch]$All)

    if($All)
    {
        #Azure AD
        $AadRemoteModule = Find-Module -Name AzureAd
        $AadLocalModule = Get-Module -Name AzureAd -ListAvailable

        if($AadLocalModule)
        {
            if($AadRemoteModule.Version.ToString() -ne $AadLocalModule.Version.ToString())
            {
                Uninstall-Module -Name AzureAd -AllVersions -Verbose
                Install-Module -Name AzureAd -AllowClobber -Verbose
            }
        }

        #Azure AD Preview
        $AadPreviewRemoteModule = Find-Module -Name AzureAdPreview
        $AadPreviewLocalModule = Get-Module -Name AzureAdPreview -ListAvailable

        if($AadPreviewLocalModule)
        {
            if($AadPreviewRemoteModule.Version.ToString() -ne $AadPreviewLocalModule.Version.ToString())
            {
                Uninstall-Module -Name AzureAdPreview -AllVersions -Verbose
                Install-Module -Name AzureAdPreview -AllowClobber -Verbose
            }
        }

        #Az
        $AzRemoteModule = Find-Module -Name Az
        $AzLocalModule = Get-Module -Name Az -ListAvailable

        if($AzLocalModule)
        {
            if($AzRemoteModule.Version.ToString() -ne $AzLocalModule.Version.ToString())
            {
                Uninstall-Module -Name Az -AllVersions -Verbose
                Install-Module -Name Az -AllowClobber -Verbose
            }
        }
        
    }

    $modules = Get-Module -Name AadSupport -ListAvailable

    #Ensure all AadSupport Modules are unloaded
    if($modules)
    {
        Remove-Module -Name AadSupport -Verbose

        #Remove all versions of AadSupport
        Uninstall-Module -Name AadSupport -AllVersions -Verbose
    }

    
    #Remove all versions of AadSupport
    Install-Module -Name AadSupport -AllowClobber

    Write-Host "Update Complete." -ForegroundColor Yellow
}

<#
.SYNOPSIS
Quickly validate tokens issued by Azure AD.
 
.DESCRIPTION
Quickly validate tokens issued by Azure AD.
 
WARNING: Access token for Microsoft Graph can not be validated. This is expected and only Microsoft Graph will be able to validate its own token.
 
When running this command while Connected to AadSupport (Connect-AadSupport) we will also lookup the Resource AppId and add the signing keys to the list of keys to be verified.
We do this because sometimes the resource might use a different signing key that is not the standard Azure AD set of keys.
 
The results will look something like this...
 
Validate-AadToken -JwtToken $token.AccessToken
 
Kid in token: xGbI8ASfxBGw3u14fNXYJhG-wlU
 
Getting Keys based on Issuer 'https://sts.windows.net/aa00d1fa-5269-4e1c-b06d-30868371d2c5/'...
Downloading configuration from 'https://sts.windows.net/aa00d1fa-5269-4e1c-b06d-30868371d2c5/.well-known/openid-configuration'
Downloading signing keys from 'https://login.windows.net/common/discovery/keys'
 
Keys Found...
HlC0R12skxNZ1WQwmjOF_6t_tDE
YMELHT0gvb0mxoSDoYfomjqfjYU
M6pX7RHoraLsprfJeRCjSxuURhc
 
Getting Keys for the Resource bcdeb54f-733b-4657-8948-0f39934c2a53...
Downloading configuration from 'https://sts.windows.net/aa00d1fa-5269-4e1c-b06d-30868371d2c5/.well-known/openid-configuration?appid=bcdeb54f-733b-4657-8948-0f39934c2a53'
Downloading signing keys from 'https://login.windows.net/aa00d1fa-5269-4e1c-b06d-30868371d2c5/discovery/keys?appid=bcdeb54f-733b-4657-8948-0f39934c2a53'
 
Keys Found...
xGbI8ASfxBGw3u14fNXYJhG-wlU
 
Checking Discovery Key: HlC0R12skxNZ1WQwmjOF_6t_tDE | Signature validation failed
Checking Discovery Key: YMELHT0gvb0mxoSDoYfomjqfjYU | Signature validation failed
Checking Discovery Key: M6pX7RHoraLsprfJeRCjSxuURhc | Signature validation failed
Checking Discovery Key: xGbI8ASfxBGw3u14fNXYJhG-wlU | Signature is verified
 
.PARAMETER JwtToken
Provide the token to be validated.
 
.PARAMETER Issuer
Issuer you want to use. We will use this to get the Open ID Connect Configuration based on the Issuer.
 
.EXAMPLE
Validate-AadToken -JwtToken $AccessToken
Validate-AadToken -JwtToken $AccessToken -Issuer "https://login.microsoftonline.com/contoso.onmicrosoft.com"
Validate-AadToken -JwtToken $AccessToken -Issuer "https://williamfiddesb2c.b2clogin.com/tfp/williamfiddesb2c.onmicrosoft.com/B2C_1_V2_SUSI_DefaultPage/v2.0/.well-known/openid-configuration"
 
.NOTES
General notes
#>

function Test-AadToken
{
    Param(
        [Parameter(Mandatory=$true, Position=0, ValueFromPipeline = $true)]
        [string]$JwtToken,
        [string]$Issuer
    )

    Write-Host ""    


    # Get claims for OAuth2Token
    $TokenClaims = ConvertFrom-AadJwtToken $JwtToken

    if($TokenClaims.aud -eq $Global:AadSupport.Resources.MsGraph -or $TokenClaims.aud -eq "00000003-0000-0000-c000-000000000000")
    {
        Write-Host "WARNING! Microsoft Graph tokens can't be validated" -ForegroundColor Red
    }

    Write-Host "Kid in token: $($TokenClaims.Kid)" -Foreground Yellow

    # Ensure Issuer is set (We will use this for the Discovery key endpoint)
    if(!$Issuer)
    {
        $Issuer = $TokenClaims.Iss
    }

    Write-Host ""
    Write-Host "Getting Keys based on Issuer '$Issuer'..." -ForegroundColor Yellow
    $SigningKeys = @()
    $IssuerSigningKeys = Get-AadDiscoveryKeys -Issuer $Issuer

    Write-Host ""
    Write-Host "Keys Found..." -ForegroundColor Yellow
    $IssuerSigningKeys.Kid

    if($Global:AadSupport.Session.Active)
    {
        try {
            Write-Host ""
            Write-Host "Getting Keys for '$($TokenClaims.aud)' based from 'aud' claim..." -ForegroundColor Yellow
            $resource = Get-AadServicePrincipal -id $TokenClaims.aud
            $AppSigningKeys = Get-AadDiscoveryKeys -Issuer $Issuer -ApplicationId $resource.AppId

            Write-Host ""
            Write-Host "Keys Found..." -ForegroundColor Yellow
            $AppSigningKeys.Kid
        }
        catch {
            Write-Host "Unable to get keys for '$($TokenClaims.aud)' ServicePrincipal probably does not exist in tenant."
        }

        
    }

    $SigningKeys += $IssuerSigningKeys
    $SigningKeys += $AppSigningKeys

    Write-Host ""
    $ListOfKeysChecked = @()
    foreach($SigningKey in $SigningKeys)
    {
        if(!$ListOfKeysChecked.Contains($SigningKey.Kid) -and $SigningKey.Kid)
        {
            $ListOfKeysChecked += $SigningKey.Kid
            $tokenParts = $JwtToken.Split('.')

            $rsa = New-Object System.Security.Cryptography.RSACryptoServiceProvider
            $rsaParameters = [System.Security.Cryptography.RSAParameters]::new()
            $modulus = Base64UrlDecode -value $SigningKey.Modulus
            $exponent = Base64UrlDecode -value $SigningKey.Exponent
            $rsaParameters.Modulus = [System.Convert]::FromBase64String($modulus)
            $rsaParameters.Exponent = [System.Convert]::FromBase64String($exponent)
        
            $rsa.ImportParameters($rsaParameters)
          
            $sha256 = [System.Security.Cryptography.SHA256]::Create()
            $hash = $sha256.ComputeHash([System.Text.Encoding]::UTF8.GetBytes($tokenParts[0] + '.' + $tokenParts[1]))
          
            $rsaDeformatter = [System.Security.Cryptography.RSAPKCS1SignatureDeformatter]::new($rsa)
            $rsaDeformatter.SetHashAlgorithm("SHA256")
    
            $TokenSignature = Base64UrlDecode -value $tokenParts[2]
            $TokenSignatureBytes = [System.Convert]::FromBase64String($TokenSignature)
            if ($rsaDeformatter.VerifySignature($hash, $TokenSignatureBytes))
            {
                Write-Host "Checking Discovery Key: $($SigningKey.Kid) | Signature is verified" -Foreground Green
            }
        
            else
            {
                Write-Host "Checking Discovery Key: $($SigningKey.Kid) | Signature validation failed" -Foreground Red
            }
        }

    }    
}




# SIG # Begin signature block
# MIIjkgYJKoZIhvcNAQcCoIIjgzCCI38CAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCB0bRIzXx7rJNS5
# Bs/atVcpzusY5vLcRpt9JXodsrS/56CCDYEwggX/MIID56ADAgECAhMzAAAB32vw
# LpKnSrTQAAAAAAHfMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMRMwEQYD
# VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy
# b3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNpZ25p
# bmcgUENBIDIwMTEwHhcNMjAxMjE1MjEzMTQ1WhcNMjExMjAyMjEzMTQ1WjB0MQsw
# CQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9u
# ZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMR4wHAYDVQQDExVNaWNy
# b3NvZnQgQ29ycG9yYXRpb24wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB
# AQC2uxlZEACjqfHkuFyoCwfL25ofI9DZWKt4wEj3JBQ48GPt1UsDv834CcoUUPMn
# s/6CtPoaQ4Thy/kbOOg/zJAnrJeiMQqRe2Lsdb/NSI2gXXX9lad1/yPUDOXo4GNw
# PjXq1JZi+HZV91bUr6ZjzePj1g+bepsqd/HC1XScj0fT3aAxLRykJSzExEBmU9eS
# yuOwUuq+CriudQtWGMdJU650v/KmzfM46Y6lo/MCnnpvz3zEL7PMdUdwqj/nYhGG
# 3UVILxX7tAdMbz7LN+6WOIpT1A41rwaoOVnv+8Ua94HwhjZmu1S73yeV7RZZNxoh
# EegJi9YYssXa7UZUUkCCA+KnAgMBAAGjggF+MIIBejAfBgNVHSUEGDAWBgorBgEE
# AYI3TAgBBggrBgEFBQcDAzAdBgNVHQ4EFgQUOPbML8IdkNGtCfMmVPtvI6VZ8+Mw
# UAYDVR0RBEkwR6RFMEMxKTAnBgNVBAsTIE1pY3Jvc29mdCBPcGVyYXRpb25zIFB1
# ZXJ0byBSaWNvMRYwFAYDVQQFEw0yMzAwMTIrNDYzMDA5MB8GA1UdIwQYMBaAFEhu
# ZOVQBdOCqhc3NyK1bajKdQKVMFQGA1UdHwRNMEswSaBHoEWGQ2h0dHA6Ly93d3cu
# bWljcm9zb2Z0LmNvbS9wa2lvcHMvY3JsL01pY0NvZFNpZ1BDQTIwMTFfMjAxMS0w
# Ny0wOC5jcmwwYQYIKwYBBQUHAQEEVTBTMFEGCCsGAQUFBzAChkVodHRwOi8vd3d3
# Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2NlcnRzL01pY0NvZFNpZ1BDQTIwMTFfMjAx
# MS0wNy0wOC5jcnQwDAYDVR0TAQH/BAIwADANBgkqhkiG9w0BAQsFAAOCAgEAnnqH
# tDyYUFaVAkvAK0eqq6nhoL95SZQu3RnpZ7tdQ89QR3++7A+4hrr7V4xxmkB5BObS
# 0YK+MALE02atjwWgPdpYQ68WdLGroJZHkbZdgERG+7tETFl3aKF4KpoSaGOskZXp
# TPnCaMo2PXoAMVMGpsQEQswimZq3IQ3nRQfBlJ0PoMMcN/+Pks8ZTL1BoPYsJpok
# t6cql59q6CypZYIwgyJ892HpttybHKg1ZtQLUlSXccRMlugPgEcNZJagPEgPYni4
# b11snjRAgf0dyQ0zI9aLXqTxWUU5pCIFiPT0b2wsxzRqCtyGqpkGM8P9GazO8eao
# mVItCYBcJSByBx/pS0cSYwBBHAZxJODUqxSXoSGDvmTfqUJXntnWkL4okok1FiCD
# Z4jpyXOQunb6egIXvkgQ7jb2uO26Ow0m8RwleDvhOMrnHsupiOPbozKroSa6paFt
# VSh89abUSooR8QdZciemmoFhcWkEwFg4spzvYNP4nIs193261WyTaRMZoceGun7G
# CT2Rl653uUj+F+g94c63AhzSq4khdL4HlFIP2ePv29smfUnHtGq6yYFDLnT0q/Y+
# Di3jwloF8EWkkHRtSuXlFUbTmwr/lDDgbpZiKhLS7CBTDj32I0L5i532+uHczw82
# oZDmYmYmIUSMbZOgS65h797rj5JJ6OkeEUJoAVwwggd6MIIFYqADAgECAgphDpDS
# AAAAAAADMA0GCSqGSIb3DQEBCwUAMIGIMQswCQYDVQQGEwJVUzETMBEGA1UECBMK
# V2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0
# IENvcnBvcmF0aW9uMTIwMAYDVQQDEylNaWNyb3NvZnQgUm9vdCBDZXJ0aWZpY2F0
# ZSBBdXRob3JpdHkgMjAxMTAeFw0xMTA3MDgyMDU5MDlaFw0yNjA3MDgyMTA5MDla
# MH4xCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdS
# ZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMT
# H01pY3Jvc29mdCBDb2RlIFNpZ25pbmcgUENBIDIwMTEwggIiMA0GCSqGSIb3DQEB
# AQUAA4ICDwAwggIKAoICAQCr8PpyEBwurdhuqoIQTTS68rZYIZ9CGypr6VpQqrgG
# OBoESbp/wwwe3TdrxhLYC/A4wpkGsMg51QEUMULTiQ15ZId+lGAkbK+eSZzpaF7S
# 35tTsgosw6/ZqSuuegmv15ZZymAaBelmdugyUiYSL+erCFDPs0S3XdjELgN1q2jz
# y23zOlyhFvRGuuA4ZKxuZDV4pqBjDy3TQJP4494HDdVceaVJKecNvqATd76UPe/7
# 4ytaEB9NViiienLgEjq3SV7Y7e1DkYPZe7J7hhvZPrGMXeiJT4Qa8qEvWeSQOy2u
# M1jFtz7+MtOzAz2xsq+SOH7SnYAs9U5WkSE1JcM5bmR/U7qcD60ZI4TL9LoDho33
# X/DQUr+MlIe8wCF0JV8YKLbMJyg4JZg5SjbPfLGSrhwjp6lm7GEfauEoSZ1fiOIl
# XdMhSz5SxLVXPyQD8NF6Wy/VI+NwXQ9RRnez+ADhvKwCgl/bwBWzvRvUVUvnOaEP
# 6SNJvBi4RHxF5MHDcnrgcuck379GmcXvwhxX24ON7E1JMKerjt/sW5+v/N2wZuLB
# l4F77dbtS+dJKacTKKanfWeA5opieF+yL4TXV5xcv3coKPHtbcMojyyPQDdPweGF
# RInECUzF1KVDL3SV9274eCBYLBNdYJWaPk8zhNqwiBfenk70lrC8RqBsmNLg1oiM
# CwIDAQABo4IB7TCCAekwEAYJKwYBBAGCNxUBBAMCAQAwHQYDVR0OBBYEFEhuZOVQ
# BdOCqhc3NyK1bajKdQKVMBkGCSsGAQQBgjcUAgQMHgoAUwB1AGIAQwBBMAsGA1Ud
# DwQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFHItOgIxkEO5FAVO
# 4eqnxzHRI4k0MFoGA1UdHwRTMFEwT6BNoEuGSWh0dHA6Ly9jcmwubWljcm9zb2Z0
# LmNvbS9wa2kvY3JsL3Byb2R1Y3RzL01pY1Jvb0NlckF1dDIwMTFfMjAxMV8wM18y
# Mi5jcmwwXgYIKwYBBQUHAQEEUjBQME4GCCsGAQUFBzAChkJodHRwOi8vd3d3Lm1p
# Y3Jvc29mdC5jb20vcGtpL2NlcnRzL01pY1Jvb0NlckF1dDIwMTFfMjAxMV8wM18y
# Mi5jcnQwgZ8GA1UdIASBlzCBlDCBkQYJKwYBBAGCNy4DMIGDMD8GCCsGAQUFBwIB
# FjNodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2RvY3MvcHJpbWFyeWNw
# cy5odG0wQAYIKwYBBQUHAgIwNB4yIB0ATABlAGcAYQBsAF8AcABvAGwAaQBjAHkA
# XwBzAHQAYQB0AGUAbQBlAG4AdAAuIB0wDQYJKoZIhvcNAQELBQADggIBAGfyhqWY
# 4FR5Gi7T2HRnIpsLlhHhY5KZQpZ90nkMkMFlXy4sPvjDctFtg/6+P+gKyju/R6mj
# 82nbY78iNaWXXWWEkH2LRlBV2AySfNIaSxzzPEKLUtCw/WvjPgcuKZvmPRul1LUd
# d5Q54ulkyUQ9eHoj8xN9ppB0g430yyYCRirCihC7pKkFDJvtaPpoLpWgKj8qa1hJ
# Yx8JaW5amJbkg/TAj/NGK978O9C9Ne9uJa7lryft0N3zDq+ZKJeYTQ49C/IIidYf
# wzIY4vDFLc5bnrRJOQrGCsLGra7lstnbFYhRRVg4MnEnGn+x9Cf43iw6IGmYslmJ
# aG5vp7d0w0AFBqYBKig+gj8TTWYLwLNN9eGPfxxvFX1Fp3blQCplo8NdUmKGwx1j
# NpeG39rz+PIWoZon4c2ll9DuXWNB41sHnIc+BncG0QaxdR8UvmFhtfDcxhsEvt9B
# xw4o7t5lL+yX9qFcltgA1qFGvVnzl6UJS0gQmYAf0AApxbGbpT9Fdx41xtKiop96
# eiL6SJUfq/tHI4D1nvi/a7dLl+LrdXga7Oo3mXkYS//WsyNodeav+vyL6wuA6mk7
# r/ww7QRMjt/fdW1jkT3RnVZOT7+AVyKheBEyIXrvQQqxP/uozKRdwaGIm1dxVk5I
# RcBCyZt2WwqASGv9eZ/BvW1taslScxMNelDNMYIVZzCCFWMCAQEwgZUwfjELMAkG
# A1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQx
# HjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEoMCYGA1UEAxMfTWljcm9z
# b2Z0IENvZGUgU2lnbmluZyBQQ0EgMjAxMQITMwAAAd9r8C6Sp0q00AAAAAAB3zAN
# BglghkgBZQMEAgEFAKCBrjAZBgkqhkiG9w0BCQMxDAYKKwYBBAGCNwIBBDAcBgor
# BgEEAYI3AgELMQ4wDAYKKwYBBAGCNwIBFTAvBgkqhkiG9w0BCQQxIgQgqrHXvLD4
# y1I0ptki8l6TuuNeX/gC+8Husc5s+DwzetIwQgYKKwYBBAGCNwIBDDE0MDKgFIAS
# AE0AaQBjAHIAbwBzAG8AZgB0oRqAGGh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbTAN
# BgkqhkiG9w0BAQEFAASCAQApEQIRQ/ApXWfMN6YZg5T0fFCjs40hB4kZGjGp4Iul
# Oj6SEx9MFmMmtO/vuufrCilwzLHlItJFSrhoAle3DYMK7kZfUjYFMrQNF5jgpUqM
# HeX4HpV5WUaYqU0pZXs9AY12BwLoJiYlxGnKwWP/iZP2shFyUAhNiqaK9yCPVn79
# DzOAG/AkAGUSOaVs0jn5C9/SPsfJ+Bl9PsPgUueZSvftsFbg/hGRtNuD2VgKgICV
# P1F4+/anlfX53K8UEQLY5J9eTvSu19NjQQ9Ls5xcI1bQEK3O1PVxryEkATUFIj8+
# yJWB1SEJzD5k/RiPTpQEzAshpK7eKRuwEO443IbTIUYOoYIS8TCCEu0GCisGAQQB
# gjcDAwExghLdMIIS2QYJKoZIhvcNAQcCoIISyjCCEsYCAQMxDzANBglghkgBZQME
# AgEFADCCAVUGCyqGSIb3DQEJEAEEoIIBRASCAUAwggE8AgEBBgorBgEEAYRZCgMB
# MDEwDQYJYIZIAWUDBAIBBQAEIBmneKX1xy1MCYUxkdT4cQ4K6FRtJkqEU2WV7I/v
# GX25AgZg057/j5MYEzIwMjEwNzA4MTczMTI3LjExOVowBIACAfSggdSkgdEwgc4x
# CzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRt
# b25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xKTAnBgNVBAsTIE1p
# Y3Jvc29mdCBPcGVyYXRpb25zIFB1ZXJ0byBSaWNvMSYwJAYDVQQLEx1UaGFsZXMg
# VFNTIEVTTjowQTU2LUUzMjktNEQ0RDElMCMGA1UEAxMcTWljcm9zb2Z0IFRpbWUt
# U3RhbXAgU2VydmljZaCCDkQwggT1MIID3aADAgECAhMzAAABW3ywujRnN8GnAAAA
# AAFbMA0GCSqGSIb3DQEBCwUAMHwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNo
# aW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29y
# cG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1wIFBDQSAyMDEw
# MB4XDTIxMDExNDE5MDIxNloXDTIyMDQxMTE5MDIxNlowgc4xCzAJBgNVBAYTAlVT
# MRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQK
# ExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xKTAnBgNVBAsTIE1pY3Jvc29mdCBPcGVy
# YXRpb25zIFB1ZXJ0byBSaWNvMSYwJAYDVQQLEx1UaGFsZXMgVFNTIEVTTjowQTU2
# LUUzMjktNEQ0RDElMCMGA1UEAxMcTWljcm9zb2Z0IFRpbWUtU3RhbXAgU2Vydmlj
# ZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMgkf6Xs9dqhesumLltn
# l6lwjiD1jh+Ipz/6j5q5CQzSnbaVuo4KiCiSpr5WtqqVlD7nT/3WX6V6vcpNQV5c
# dtVVwafNpLn3yF+fRNoUWh1Q9u8XGiSX8YzVS8q68JPFiRO4HMzMpLCaSjcfQZId
# 6CiukyLQruKnSFwdGhMxE7GCayaQ8ZDyEPHs/C2x4AAYMFsVOssSdR8jb8fzAek3
# SNlZtVKd0Kb8io+3XkQ54MvUXV9cVL1/eDdXVVBBqOhHzoJsy+c2y/s3W+gEX8Qb
# 9O/bjBkR6hIaOwEAw7Nu40/TMVfwXJ7g5R/HNXCt7c4IajNN4W+CugeysLnYbqRm
# W+kCAwEAAaOCARswggEXMB0GA1UdDgQWBBRl5y01iG23UyBdTH/15TnJmLqrLjAf
# BgNVHSMEGDAWgBTVYzpcijGQ80N7fEYbxTNoWoVtVTBWBgNVHR8ETzBNMEugSaBH
# hkVodHRwOi8vY3JsLm1pY3Jvc29mdC5jb20vcGtpL2NybC9wcm9kdWN0cy9NaWNU
# aW1TdGFQQ0FfMjAxMC0wNy0wMS5jcmwwWgYIKwYBBQUHAQEETjBMMEoGCCsGAQUF
# BzAChj5odHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpL2NlcnRzL01pY1RpbVN0
# YVBDQV8yMDEwLTA3LTAxLmNydDAMBgNVHRMBAf8EAjAAMBMGA1UdJQQMMAoGCCsG
# AQUFBwMIMA0GCSqGSIb3DQEBCwUAA4IBAQCnM2s7phMamc4QdVolrO1ZXRiDMUVd
# gu9/yq8g7kIVl+fklUV2Vlout6+fpOqAGnewMtwenFtagVhVJ8Hau8Nwk+IAhB0B
# 04DobNDw7v4KETARf8KN8gTH6B7RjHhreMDWg7icV0Dsoj8MIA8AirWlwf4nr8pK
# H0n2rETseBJDWc3dbU0ITJEH1RzFhGkW7IzNPQCO165Tp7NLnXp4maZzoVx8PyiO
# NO6fyDZr0yqVuh9OqWH+fPZYQ/YYFyhxy+hHWOuqYpc83Phn1vA0Ae1+Wn4bne6Z
# GjPxRI6sxsMIkdBXD0HJLyN7YfSrbOVAYwjYWOHresGZuvoEaEgDRWUrMIIGcTCC
# BFmgAwIBAgIKYQmBKgAAAAAAAjANBgkqhkiG9w0BAQsFADCBiDELMAkGA1UEBhMC
# VVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNV
# BAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEyMDAGA1UEAxMpTWljcm9zb2Z0IFJv
# b3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTAwHhcNMTAwNzAxMjEzNjU1WhcN
# MjUwNzAxMjE0NjU1WjB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3Rv
# bjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0
# aW9uMSYwJAYDVQQDEx1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMDCCASIw
# DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKkdDbx3EYo6IOz8E5f1+n9plGt0
# VBDVpQoAgoX77XxoSyxfxcPlYcJ2tz5mK1vwFVMnBDEfQRsalR3OCROOfGEwWbEw
# RA/xYIiEVEMM1024OAizQt2TrNZzMFcmgqNFDdDq9UeBzb8kYDJYYEbyWEeGMoQe
# dGFnkV+BVLHPk0ySwcSmXdFhE24oxhr5hoC732H8RsEnHSRnEnIaIYqvS2SJUGKx
# Xf13Hz3wV3WsvYpCTUBR0Q+cBj5nf/VmwAOWRH7v0Ev9buWayrGo8noqCjHw2k4G
# kbaICDXoeByw6ZnNPOcvRLqn9NxkvaQBwSAJk3jN/LzAyURdXhacAQVPIk0CAwEA
# AaOCAeYwggHiMBAGCSsGAQQBgjcVAQQDAgEAMB0GA1UdDgQWBBTVYzpcijGQ80N7
# fEYbxTNoWoVtVTAZBgkrBgEEAYI3FAIEDB4KAFMAdQBiAEMAQTALBgNVHQ8EBAMC
# AYYwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBTV9lbLj+iiXGJo0T2UkFvX
# zpoYxDBWBgNVHR8ETzBNMEugSaBHhkVodHRwOi8vY3JsLm1pY3Jvc29mdC5jb20v
# cGtpL2NybC9wcm9kdWN0cy9NaWNSb29DZXJBdXRfMjAxMC0wNi0yMy5jcmwwWgYI
# KwYBBQUHAQEETjBMMEoGCCsGAQUFBzAChj5odHRwOi8vd3d3Lm1pY3Jvc29mdC5j
# b20vcGtpL2NlcnRzL01pY1Jvb0NlckF1dF8yMDEwLTA2LTIzLmNydDCBoAYDVR0g
# AQH/BIGVMIGSMIGPBgkrBgEEAYI3LgMwgYEwPQYIKwYBBQUHAgEWMWh0dHA6Ly93
# d3cubWljcm9zb2Z0LmNvbS9QS0kvZG9jcy9DUFMvZGVmYXVsdC5odG0wQAYIKwYB
# BQUHAgIwNB4yIB0ATABlAGcAYQBsAF8AUABvAGwAaQBjAHkAXwBTAHQAYQB0AGUA
# bQBlAG4AdAAuIB0wDQYJKoZIhvcNAQELBQADggIBAAfmiFEN4sbgmD+BcQM9naOh
# IW+z66bM9TG+zwXiqf76V20ZMLPCxWbJat/15/B4vceoniXj+bzta1RXCCtRgkQS
# +7lTjMz0YBKKdsxAQEGb3FwX/1z5Xhc1mCRWS3TvQhDIr79/xn/yN31aPxzymXlK
# kVIArzgPF/UveYFl2am1a+THzvbKegBvSzBEJCI8z+0DpZaPWSm8tv0E4XCfMkon
# /VWvL/625Y4zu2JfmttXQOnxzplmkIz/amJ/3cVKC5Em4jnsGUpxY517IW3DnKOi
# PPp/fZZqkHimbdLhnPkd/DjYlPTGpQqWhqS9nhquBEKDuLWAmyI4ILUl5WTs9/S/
# fmNZJQ96LjlXdqJxqgaKD4kWumGnEcua2A5HmoDF0M2n0O99g/DhO3EJ3110mCII
# YdqwUB5vvfHhAN/nMQekkzr3ZUd46PioSKv33nJ+YWtvd6mBy6cJrDm77MbL2IK0
# cs0d9LiFAR6A+xuJKlQ5slvayA1VmXqHczsI5pgt6o3gMy4SKfXAL1QnIffIrE7a
# KLixqduWsqdCosnPGUFN4Ib5KpqjEWYw07t0MkvfY3v1mYovG8chr1m1rtxEPJdQ
# cdeh0sVV42neV8HR3jDA/czmTfsNv11P6Z0eGTgvvM9YBS7vDaBQNdrvCScc1bN+
# NR4Iuto229Nfj950iEkSoYIC0jCCAjsCAQEwgfyhgdSkgdEwgc4xCzAJBgNVBAYT
# AlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYD
# VQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xKTAnBgNVBAsTIE1pY3Jvc29mdCBP
# cGVyYXRpb25zIFB1ZXJ0byBSaWNvMSYwJAYDVQQLEx1UaGFsZXMgVFNTIEVTTjow
# QTU2LUUzMjktNEQ0RDElMCMGA1UEAxMcTWljcm9zb2Z0IFRpbWUtU3RhbXAgU2Vy
# dmljZaIjCgEBMAcGBSsOAwIaAxUACrtBbqYy0r+YGLtUaFVRW/Yh7qaggYMwgYCk
# fjB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMH
# UmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYDVQQD
# Ex1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMDANBgkqhkiG9w0BAQUFAAIF
# AOSROrkwIhgPMjAyMTA3MDgxMjUwMDFaGA8yMDIxMDcwOTEyNTAwMVowdzA9Bgor
# BgEEAYRZCgQBMS8wLTAKAgUA5JE6uQIBADAKAgEAAgIogAIB/zAHAgEAAgIRazAK
# AgUA5JKMOQIBADA2BgorBgEEAYRZCgQCMSgwJjAMBgorBgEEAYRZCgMCoAowCAIB
# AAIDB6EgoQowCAIBAAIDAYagMA0GCSqGSIb3DQEBBQUAA4GBABDYdBH3hVqinT+a
# eZHNa9PDPfWkyvItkoxeFCav2pyydxsQkLx/HPMzub+3mTYDfpiBVPbmV0/ogeIS
# CI27iKXRUGiQVXnxWYaW2T3g+qXro3z+rzFInZzSCcJFzA3mhiHRrNYNPL6TH49i
# yKb2EYgP9+1F2/t4W4MnNc2+JcAyMYIDDTCCAwkCAQEwgZMwfDELMAkGA1UEBhMC
# VVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNV
# BAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRp
# bWUtU3RhbXAgUENBIDIwMTACEzMAAAFbfLC6NGc3wacAAAAAAVswDQYJYIZIAWUD
# BAIBBQCgggFKMBoGCSqGSIb3DQEJAzENBgsqhkiG9w0BCRABBDAvBgkqhkiG9w0B
# CQQxIgQg2KQX2dIEd7FMbomeVvHAxVUt+WPl+sjuBorOaNSONd8wgfoGCyqGSIb3
# DQEJEAIvMYHqMIHnMIHkMIG9BCDJIuCpKGMRh4lCGucGPHCNJ7jq9MTbe3mQ2FtS
# ZLCFGTCBmDCBgKR+MHwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9u
# MRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRp
# b24xJjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1wIFBDQSAyMDEwAhMzAAAB
# W3ywujRnN8GnAAAAAAFbMCIEIHqyPZva7fMxjB+cFIVnb2/HgSH55VLx1UdiJc9R
# mTZjMA0GCSqGSIb3DQEBCwUABIIBACXUSMEni+W5dajPEn4U1NOcVMCAcoBHOIsE
# EZDKjFlOVZfZc0UHRIogykdP2GMFsDfxOEfHngfOYF9ZtF57owpfT3rrEgyDO2GJ
# Ow5mekhepn1GK/PDpSZuGN63mY+HF5qwst0mJIHvoSP6OyYHg10aOMpc4CfCvbHC
# iIEAqZIgd+B37P/3qxnw6n1Av9ZGEBobQ4so5fNDiSIOUxtxeSD8fXNeR2f7WClK
# l3TAds7/RCvv5Y35Qq+n+z76pOnD90QZ0/mQNMDzRHUgeidtlitpTSbxl6YuiReX
# t3lPB+qFg1lBLIMskzQ9gM8aBlq+k9FlVDa2UJI/UFkZUZJnFTs=
# SIG # End signature block