Creating an Azure AD Application and Granting Application Admin Consent with Powershell

Written by James McDonald

January 28, 2022

https://blog.nico-schiering.de/granting-azure-ad-admin-consent-programmatically/

https://samcogan.com/provide-admin-consent-fora-azure-ad-applications-programmatically/

Standing on the shoulders of giants here is a script that creates an Azure AD Application grants it access to the MS Graph API and the Mail.Send role ability.

It could be modified to grant access to different resources and permissions

There are two functions that can do an admin consent for Application Permissions

Grant-AdminConsentPermissionsToApp

Grant-OAuth2PermissionsToApp

The first function specifically approves the Application to use Microsoft Graph Mail.Send role with this function you would need to loop through each role to approve each one. The Grant-OAuth2PermissionsToApp function will approve multiple roles at once. I am using auth tokens taken from the context or Get-AzAccessToken check the code for specifics.

I logged in as a Global Administrator to run this script.

# run as admin
If (-NOT ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")) {   
    $arguments = "& '" + $myinvocation.mycommand.definition + "'"
    Start-Process powershell -Verb runAs -ArgumentList $arguments
    Break
}

function logThis ($text, $logFile) {
    "[$(Get-Date -format 'yyyy-MM-dd HH:mm:ss')] $($text)" | out-file $logFile -append
}


# place to save output
$tmpPath = "C:\Temp\Test"
$appName = "My Test App 6";

#Setup Logging
$logFile = "$($tmpPath)\AppInstallationLog.txt"
if (!(Test-Path "$($tmpPath)")) { New-Item -ItemType directory -path "$($tmpPath)" }

logThis "[INFO] ******************* SCRIPT START *******************" $logFile

$error.clear()

#Define Functions:
# working

<# 
 This function can approve an individual role
 So in this case it is approving the application to access the Microsoft Graph API and specifically Mail.Send
 This is NOT doing delegated approval but Application approval (you need this so that a program can send email as any user)
#>
Function Grant-AdminConsentPermissionsToApp {
    
    $token = (Get-AzAccessToken -ResourceUrl "https://graph.microsoft.com/").Token
   
    $msGraphObjectId = (Get-AzureADServicePrincipal -All $true | Where-Object { $_.DisplayName -eq "Microsoft Graph" }).ObjectId
    
    $apiUrl = "https://graph.microsoft.com/v1.0/servicePrincipals/$($msGraphObjectId)/appRoleAssignments"

    # The Object ID of the service principal
    $principalId = (Get-AzureADServicePrincipal -All $true | Where-Object { $_.AppId -eq $newAppId }).ObjectId
    
    # Mail.Send Id
    $appRoleId = ((Get-AzureADServicePrincipal -All $true | Where-Object { $_.DisplayName -eq "Microsoft Graph" }).AppRoles | 
        Where-Object { $_.Value -eq "Mail.Send" }).Id

    $body = @{
        principalId = $principalId
        resourceId  = $msGraphObjectId
        appRoleId   = $appRoleId
    }

    Invoke-RestMethod -Uri $apiUrl -Headers @{Authorization = "Bearer $($token)" }  -Method POST -Body $($body | convertto-json) -ContentType "application/json"
}

Function Grant-OAuth2PermissionsToApp {
    $context = Get-AzContext
        
    if ($null -eq $context) {
        $null = Connect-AZAccount -EA stop
        $context = Get-AzContext
    }
   
    #  get an access token to access resource https://main.iam.ad.ext.azure.com / 74658136-14ec-4630-ad9b-26e160ff0fc6
    $token = [Microsoft.Azure.Commands.Common.Authentication.AzureSession]::Instance.AuthenticationFactory.Authenticate($context.Account, $context.Environment, $context.Tenant.Id, $null, "Never", $null, "74658136-14ec-4630-ad9b-26e160ff0fc6")

    $header = @{
        'Authorization'          = 'Bearer ' + $token.AccessToken
        'X-Requested-With'       = 'XMLHttpRequest'
        'x-ms-client-request-id' = [guid]::NewGuid()
        'x-ms-correlation-id'    = [guid]::NewGuid()
    }
    
    $url = "https://main.iam.ad.ext.azure.com/api/RegisteredApplications/$newAppId/Consent?onBehalfOfAll=true"

    Invoke-RestMethod -Uri $url -Headers $header -Method POST -ErrorAction Stop
}


if (Get-Module -ListAvailable -Name AzureAD) 
{} 
else {
    Install-Module -Name AzureAD
    logThis "[INFO] Installing Module AzureAD" $logFile
}

Write-Host                          "Please Log in with an Azure AD Admin Account"
Connect-AzureAD

Write-Host                          "Creating Application with Mail.Send Permissions within your Azure AD Tenant...Please wait..."

$tenant = Get-AzureADTenantDetail

# first get the MS Graph service principal
$msGraphPrincipal = Get-AzureADServicePrincipal -All $true | Where-Object { $_.DisplayName -eq "Microsoft Graph" }
$requiredResourceAccess = New-Object -TypeName "Microsoft.Open.AzureAD.Model.RequiredResourceAccess"
$requiredResourceAccess.ResourceAppId = $msGraphPrincipal.AppId

$msgraphRole = New-Object -TypeName "Microsoft.Open.AzureAD.Model.ResourceAccess" -ArgumentList "b633e1c5-b582-4048-a93e-9f11b44c7e96", "Role"
# Duplicate each role here you want to add here
#$msgraphRole2 = New-Object -TypeName "Microsoft.Open.AzureAD.Model.ResourceAccess" -ArgumentList "GUID of Role look in manifest in the portal to find this", "Role"

$requiredResourceAccess.ResourceAccess = $msgraphRole #, $msgraphRole2 pass each role  you want to add on this line as an array

$newApp = New-AzureADApplication -DisplayName $appName -RequiredResourceAccess @($requiredResourceAccess)

logThis "[INFO] Application Created" $logFile

# password with 5 year expiry
$appSecret = New-AzureADApplicationPasswordCredential -ObjectId $newApp.ObjectId -CustomKeyIdentifier "$($appName) Key" -EndDate (get-date).AddYears(5)

logThis "[INFO] Secret Key Created" $logFile

# needed for granting admin consent
# create a service principal and associate it with the Azure Application

$svcPrincipal = New-AzureADServicePrincipal -AppId $newApp.AppId -Tags @("WindowsAzureActiveDirectoryIntegratedApp")

logThis "[INFO] Service Principal Created" $logFile

$newAppId = $newApp.AppId
$tenantId = $tenant.ObjectId
$appSecretValue = $appSecret.Value
$tenantDomain = ($tenant.VerifiedDomains | ? { $_._Default -eq $true }).Name


logThis "[INFO] Sleep Started" $logFile
Write-Host "Sleeping 30 seconds"
Start-Sleep -s 30



# this works to do app consent for individual roles but you would need to loop through each role to consent
# Grant-AdminConsentPermissionsToApp

logThis "[FUNCTIONCALL] Grant-OAuth2PermissionsToApp" $logFile
# this bulk approves all the roles
Grant-OAuth2PermissionsToApp

#Write-Out Access Details to Log File
logThis "Application Created Successfully" $logFile
logThis "Application (client) ID: " + $newAppId $logFile
logThis "Directory (tenant) ID: " + $tenantId $logFile
logThis "Client Key: " + $appSecretValue $logFile
logThis "Tenant Domain: " + $tenantDomain $logFile


#Error Collection
if ($error.count -gt 0) {
    logThis "[WARN] Error Count: " + $error.count $logFile
    logThis "[WARN] Error Text: " $logFile
    logThis $error $logFile
    logThis "*** End Error Text ***" $logFile
}
else
{ logThis "[INFO] No Errors" $logFile }

logThis "[INFO] ******************* SCRIPT END *******************" $logFile

Write-Host                          "Application successfully created..."
Write-Host                          " "
Write-Host                          "Application (client) ID:" $newAppId
Write-Host                          "Directory (tenant) ID:" $tenantId
Write-Host                          "Client Key:" $appSecretValue
Write-Host                          "Tenant Domain:" $tenantDomain
Write-Host                          " "
Write-Host                          "Please store the following file safely (i.e. encrypted somewhere):"
Write-Host                          "$($logFile)"
pause

0 Comments

Submit a Comment

Your email address will not be published.

You May Also Like…

List your VSCode Extensions

Ever wondered what extensions you have installed and want to keep a list? This actually is a good way to audit your...

array_merge vs the + operator

<?php $options = [ 'rootNode' => 'response' ]; // array_merge // the same key appearing later will overwrite echo...