Script: - Bulk Assign Users to SaaS Application using Graph API & ADAL

Assigning users permissions to access to azure active directory SaaS Applications can be quite a mundane task especially if you haven't purchased Azure Active Directory Premium which would enable you to assign permissions to a group which i would honestly recommend as a long term sustainable solution for this but if you have a requirement to bulk assign users to an application then the following might come in handy!

How to use the module

To Execute the following command you need to replace the information within the 'body' with the information of the application you wish to assign to users.

First, you need to get a list of your applications within the directory which you can do by using graph explorer and the following resource endpoint. *remember to replace 'tenant.onmicrosoft.com' with the name of your tenant.

https://graph.windows.net/tenant.onmicrosoft.com/servicePrincipals?api-version=1.5

Find the application within the output in this scenario we are using 'twitter' and you need to take the following attribute as you will need the value for the body as this will be your ResourceID:

"objectId": "2f37a4ce-893f-4392-b4ae-e0df9fbfe365",

and the following value as this will be your ID

"appRoles": [
{
"allowedMemberTypes": [
"User"
],
"description": "msiam_access",
"displayName": "msiam_access",
"id": "d84db298-28e2-4b2c-ab52-e70012cbabbf",
"isEnabled": true,
"value": null
}
],

The PrincipalID value is the User Object but in this script as we will be doing this in bulk we shall parse that value in to each request body during the ForEach Loop we do using the final command.

Download the PSM1 and place it in your chosen directory, we shall be using c:scripts as our working directory in this walkthrough which is a module that can be imported into your powershell session using the following command

 Import-Module c:scriptsGraphAddUserToApplication.psm1

Once the script module has been imported, you then need to run the following function which will load in ADAL

 Load-ActiveDirectoryAuthenticationLibrary

Once this has been done, you then need to set the below global variable, this will launch a window in which you will authenticate as an user whom is a global administrator in the directory in which your users and application is located.

 $global:authenticationResult = Get-AuthenticationResult

Once that has been done, you can then proceed with the following command. The CSV file will have a single header called UserPrincipalName and then a list of UserPrincipalNames (i recommend you do this against a few users in the directory initially to ensure it works as expected).

 $csv = Import-CSV C:ScriptsAADUser.csv | % {$user = get-aaduser -Id $_.UserPrincipalName;Set-AppRoleAssignments -userId $user.Objectid}

Once completed, you should find that your users have now been assigned the application you wanted to assign.

The Module (functions) Explained!....  

The first function Load-AzureActiveDirectoryAuthenticationLibrary  first checks the presence of ADAL.Net libraries in the Nugets folder in user’s My Documents folder. If it doesn’t find the library it downloads nuget.exe from https://www.nuget.org and installs the Microsoft.IdentityModel.Clients.ActiveDirectory nuget. It then Loads the two assemblies that make up the ADAL.net SDK i.e. Microsoft.IdentityModel.Clients.ActiveDirectory.dll and Microsoft.IdentityModel.Clients.ActiveDirectory.WindowsForms.dll

 function Load-ActiveDirectoryAuthenticationLibrary(){
  $moduleDirPath = [Environment]::GetFolderPath("MyDocuments") + "WindowsPowerShellModules"
  $modulePath = $moduleDirPath + "AADGraph"
  if(-not (Test-Path ($modulePath+"Nugets"))) {New-Item -Path ($modulePath+"Nugets") -ItemType "Directory" | out-null}
  $adalPackageDirectories = (Get-ChildItem -Path ($modulePath+"Nugets") -Filter "Microsoft.IdentityModel.Clients.ActiveDirectory*" -Directory)
  if($adalPackageDirectories.Length -eq 0){
    Write-Host "Active Directory Authentication Library Nuget doesn't exist. Downloading now ..." -ForegroundColor Yellow
    if(-not(Test-Path ($modulePath + "Nugetsnuget.exe")))
    {
      Write-Host "nuget.exe not found. Downloading from https://www.nuget.org/nuget.exe ..." -ForegroundColor Yellow
      $wc = New-Object System.Net.WebClient
      $wc.DownloadFile("https://www.nuget.org/nuget.exe",$modulePath + "Nugetsnuget.exe");
    }
    $nugetDownloadExpression = $modulePath + "Nugetsnuget.exe install Microsoft.IdentityModel.Clients.ActiveDirectory -Version 2.14.201151115 -OutputDirectory " + $modulePath + "Nugets | out-null"
    Invoke-Expression $nugetDownloadExpression
  }
  $adalPackageDirectories = (Get-ChildItem -Path ($modulePath+"Nugets") -Filter "Microsoft.IdentityModel.Clients.ActiveDirectory*" -Directory)
  $ADAL_Assembly = (Get-ChildItem "Microsoft.IdentityModel.Clients.ActiveDirectory.dll" -Path $adalPackageDirectories[$adalPackageDirectories.length-1].FullName -Recurse)
  $ADAL_WindowsForms_Assembly = (Get-ChildItem "Microsoft.IdentityModel.Clients.ActiveDirectory.WindowsForms.dll" -Path $adalPackageDirectories[$adalPackageDirectories.length-1].FullName -Recurse)
  if($ADAL_Assembly.Length -gt 0 -and $ADAL_WindowsForms_Assembly.Length -gt 0){
    Write-Host "Loading ADAL Assemblies ..." -ForegroundColor Green
    [System.Reflection.Assembly]::LoadFrom($ADAL_Assembly[0].FullName) | out-null
    [System.Reflection.Assembly]::LoadFrom($ADAL_WindowsForms_Assembly.FullName) | out-null
    return $true
  }
  else{
    Write-Host "Fixing Active Directory Authentication Library package directories ..." -ForegroundColor Yellow
    $adalPackageDirectories | Remove-Item -Recurse -Force | Out-Null
    Write-Host "Not able to load ADAL assembly. Delete the Nugets folder under" $modulePath ", restart PowerShell session and try again ..."
    return $false
  }
}

The Get-AuthenticationResult function calls the AquireToken method provided by ADAL.net SDK to authenticate the user and returns the authentication results that contains the access token to access the Graph API.

 function Get-AuthenticationResult($tenant = "common", $env="prod"){
  $clientId = "1950a258-227b-4e31-a9cf-717495945fc2"
  $redirectUri = "urn:ietf:wg:oauth:2.0:oob"
  $resourceClientId = "00000002-0000-0000-c000-000000000000"
  $resourceAppIdURI = "https://graph.windows.net/"
  $authority = "https://login.windows.net/" + $tenant
  if($env.ToLower() -eq "china"){$resourceAppIdURI = "https://graph.chinacloudapi.cn/"; $authority = "https://login.chinacloudapi.cn/" + $tenant}
  $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority,$false
  $authResult = $authContext.AcquireToken($resourceAppIdURI, $clientId, $redirectUri, [Microsoft.IdentityModel.Clients.ActiveDirectory.PromptBehavior]::Always)
  return $authResult
}

The Get-AADObject function receives as input the entity type e.g. “users” or “applications”. It then constructs the entity URI and employs the Invoke-RESTMethod base PowerShell cmdlet to invoke an HTTP GET on Graph API. The function retrieves the access token from the global authentication result variable and adds an Authorization header to the REST method call. Invoke-RestMethod cmdlets does the heavy lifting of creating an HTTPClient invoking the API and converting the returned JSON into objects that the Get-AADObject emits on the PowerShell pipeline

 function Get-AADObject([string]$type) {
  $objects = $null
  if($authenticationResult -ne $null){
    $header = $authenticationResult.CreateAuthorizationHeader()
    $uri = [string]::Format("https://graph.windows.net/{0}/{1}?api-version=2013-04-05",$authenticationResult.TenantId, $type)
    Write-Host HTTP GET $uri -ForegroundColor Cyan
    $result = Invoke-RestMethod -Method Get -Uri $uri -Headers @{"Authorization"=$header;"Content-Type"="application/json"}
    if ($result -ne $null) {
      $objects = $result.Value
    }
  }
  else{
    Write-Host "Not connected to an AAD tenant. First run Connect-AAD." -ForegroundColor Yellow
  }
  return $objects
}

The Get-AADObjectById is similar to Get-AADObject except that it invokes HTTP GET on a specific Graph API object.

 function Get-AADObjectById([string]$type, [string]$id) {
  $object = $null
  if($global:authenticationResult -ne $null){
    $header = $authenticationResult.CreateAuthorizationHeader()
    $uri = [string]::Format("https://graph.windows.net/{0}/{1}/{2}?api-version=2013-04-05",$authenticationResult.TenantId, $type.Trim(), $id.Trim())
    Write-Host HTTP GET $uri -ForegroundColor Cyan
    $object = Invoke-RestMethod -Method Get -Uri $uri -Headers @{"Authorization"=$header;"Content-Type"="application/json"}
  }
  else{
    Write-Host "Not connected to an AAD tenant. First run Connect-AAD." -ForegroundColor Yellow
  }
  return $object
}

The Get-AADObject and Get-AADObjectById generic functions do the work of calling the RESTful Graph APIs. Get-AADUser (and other Get-AAD* cmdlets in this module) are simple cmldets that leverage these generic functions. The function definition is more formal, with the parameter attribute defining whether the Id parameter is mandatory, as well as providing a HelpMessage for the parameter.

 function Get-AADUser {
  [CmdletBinding()]
  param (
    [parameter(Mandatory=$false,
    ValueFromPipeline=$true,
    HelpMessage="Either the ObjectId or the UserPrincipalName of the User.")]
    [string]
    $Id
  )
  PROCESS {
    if($Id -ne "") {
      Get-AADObjectById -Type "users" -Id $id
    }
    else {
      Get-AADObject -Type "users"
    }
  }
}

The Set-AppRoleAssignments function uses similar code from the previous functions Get-AADObject and Get-AADObjectByID but in this function we also include a body and change the graph endpoint slightly so that it send the body to the graph endpoint appRoleAssignments. This will then be used to add the user (PrincipalID) to the service principal defined by the ID and ResourceID.

 function Set-AppRoleAssignments($userID) {
  $objects = $null
  if($authenticationResult -ne $null){
    $header = $authenticationResult.CreateAuthorizationHeader()
    $uri = [string]::Format("https://graph.windows.net/{0}/users/{1}/appRoleAssignments?api-version=1.5",$authenticationResult.TenantId, $userID)

#ResourceId is the objectId of the application servicePrincipal that get created in the tenant in the previous command.#
#ID is the default role id of SaaS Application. The value is d84db298-28e2-4b2c-ab52-e70012cbabbf in all tenants.########
#PrincipalId is the objectId of the principal (user or group) that is being assigned to the app.###################
#You need to replace the ID and Resource ID in the 'Body'##########################################################

$body = @"
{
    "id":  "d84db298-28e2-4b2c-ab52-e70012cbabbf",
    "principalId":  "$userID",
    "resourceId":  "2f37a4ce-893f-4392-b4ae-e0df9fbfe365"
}
"@

    Write-Host HTTP GET $uri -ForegroundColor Cyan
    $result = Invoke-RestMethod -Method Post -Uri $uri -Headers @{"Authorization"=$header;"Content-Type"="application/json"} -Body $Body
    if ($result -ne $null) {
      $objects = $result.Value
    }
  }
  else{
    Write-Host "Not connected to an AAD tenant. First run Connect-AAD." -ForegroundColor Yellow
  }
  return $objects
}

This was built upon work that had already been done by Dushyant whom is a colleague that works in the Microsoft Engineering Team, further information around AADGraphPowerShell can be found over on his blog! check it out.

I hope that this comes in handy! but remember, this is ideally to help if you need to assign a bulk user set to an application quickly it isn't designed to run as an automated process to manage user assignment this should be done using Group Application Assignment which is a feature of which is part of Azure Active Directory Premium!

Enjoy, and remember Test! before using in production environment!

James.