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 http://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 http://www.nuget.org/nuget.exe ..." -ForegroundColor Yellow
      $wc = New-Object System.Net.WebClient
      $wc.DownloadFile("http://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.

Comments (7)

  1. Jim Cooper says:

    James - thanks for posting!

    However, I am encountering a problem in the Set-AppRoleAssignments

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

    I get the following

    Invoke-RestMethod : {"odata.error":{"code":"Request_BadRequest","message":{"lang":"en","value":"Cannot convert a primitive
    value to the expected type 'Edm.Guid'. See the inner exception for more details."}}}

    At C:\TEMP\GraphAddUserToApplication\GraphAddUserToApplication.psm1:97 char:15
    + ... $result = Invoke-RestMethod -Method Post -Uri $uri -Headers @{"Auth ...
    + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo : InvalidOperation: (System.Net.HttpWebRequest:HttpWebRequest) [Invoke-RestMethod], WebExcepti
    on
    + FullyQualifiedErrorId : WebCmdletWebResponseException,Microsoft.PowerShell.Commands.InvokeRestMethodCommand

    Line 97 of ghe GraphAddUsertoApplication.psm1 file is

    $result = Invoke-RestMethod -Method Post -Uri $uri -Headers @{"Authorization"=$header;"Content-Type"="application/json"} -Body $Body

    Any thoughts ?

  2. Rajavarman N says:

    Hi,

    I received the following error while running
    Set-AppRoleAssignments -userId “1ce250c6-xxxx-xxxx-xxxx-f5f3a61bc571”
    Please help to resolve this error.
    Note: I have replaced the AppID in ID field and AppObjectID in ResourceID field in psm1 file

    Error:
    *****
    HTTP GET https://graph.windows.net/882d47f6-xxxx-xxxx-xxxx-43a46416b0f0/users/1ce250c6-xxxx-xxxx-xxxx-f5f3a61bc571/appRo
    leAssignments?api-version=1.5
    Invoke-RestMethod : The ‘Content-Type’ header must be modified using the appropriate property or method.
    Parameter name: name
    At C:\Users\username\Desktop\GraphAddUserToApplication.psm1:97 char:15
    + $result = Invoke-RestMethod -Method Post -Uri $uri -Headers @{“Authorization …
    + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo : NotSpecified: (:) [Invoke-RestMethod], ArgumentException
    + FullyQualifiedErrorId : System.ArgumentException,Microsoft.PowerShell.Commands.InvokeRestMethodCommand

    1. currently in the process of re-writing this module to fix this error and make it more robust for future changes to the service. I shall post an update as soon as possible with the new module and code.

  3. Dan C says:

    Is there a way to automate the $Global:AuthenticationResult so that I can run this as a batch, and not have the log-on window?

    Thanks

    1. currently in the process of re-writing this module so that it works better and resolves some of the issues that have been brought up by other people whom are using this. I appreciate your patience.

  4. Scott W says:

    Hi James,
    This is almost exactly what I'm looking for. My client does have AAD Premium (well, we *can* assign AD groups to SAAS Roles, so I assume they have Prem), but even that can be a PITA for a particular SAAS App they use - unfortunately, it has been designed with a very high level of Group to Role mappings.
    I'm looking for a way (through powershell preferably) to bulk assign AD groups to App Roles. (I haven't got my head around Graph API yet, so I'm looking at the 'native' Azure*.* PowerShell modules)

    With a group's on-premises AD name, I can get the group's AAD objectID (from Get-AzureAdGroup), which I can pipe to Get-AzureAdGroupAppRoleAssignment which I can get properties such as Id (which matches the GUID from the manifest file, which we can update programatically when we add new roles to the App), but I can't see a cmdlet that'll expose the Role for that Group to Role mapping. If I could get the Roles, then I'd have everything I need to take a sync'd AD group and map it to the right App Role.

    Any pointers in the right direction would greatly appreciated.

    Cheers
    Scott.

  5. mike.yang says:

    it works perfectly, thank you so much.

Skip to main content