Uploading documents into SharePoint with custom metadata
Posted 01 March 2025, 15:00 | by Ben Duguid | Perma-link
We recently had a requirement to bulk upload over ten thousand items into some SharePoint Document Libraries, and populate a number of custom fields, using a Service Principal rather than delegated auth as a user, configured with the lowest privilege levels we could get away with.
This should be relatively simple, but was complicated by there being a couple of different ways to connect programmatically to SharePoint, which have different levels of support depending on whether you're connecting via Application or Delegated permissions, so here's what we ended up doing.
Setting up the Service Principal
First, find a friendly Entra ID admin who has at least Privileged Role Administrator permissions, as we'll be granting permissions against Microsoft Graph app roles, and they will also need SharePoint administrator rights (specifically, Site Collections Administrator).
Create an App Registration
Within Entra ID, create a new App Registration, with an appropriate name, and leave the other settings as they are ("Supported Account Types" should be single tenant, "Redirect URI" can be left blank).
From the overview pane, make a note of the Application (client) ID and the Directory (tenant) ID as we'll need those later.
Create a new client secret and make a note of it securely as you won't be able to access it again later.
On the API permissions blade, Add a permission, select the big "Microsoft Graph" option, then "Application permissions", and search for Sites.Selected
(you'll need to expand "Sites" to see it) - select it, noting that Admin Consent is required and "Add permissions". Then "Grant admin consent for [tenant]".
You now have a service principal that can be configured to access specific sites within SharePoint, so let's do that.
Configure specific site access
For the rest of the process, we'll be in PowerShell, working with MS Graph, so make sure you've got those modules installed:
Get-Module -Name Microsoft.Graph.* -ListAvailable
This should return a list of installed MS Graph modules, if not, install them for your current user:
If these aren't installed, run:
Install-Module Microsoft.Graph -Scope CurrentUser
You're now ready to connect to MS Graph and grant permissions to the Service Principal
# Update these variables as needed:
$tenantId = "<tenant Id as GUID>"
$hostName = "<unique part of SharePoint Hostname>" # i.e. https://<hostname>.sharepoint.com
$sitePath = "/sites/<site URI>"
$appName = "<app Name>"
# Connect to MS Graph
# Grants the current user Application Read/Write, Directory Access and
# Sites Full Control.
# This will bounce you through a browser based authentication process, and
# may prompt you to grant admin consent for the MS Graph CLI - accept as
# appropriate (i.e. "This account only", etc.).
Connect-MgGraph -TenantId $tenantId -Scopes "Application.ReadWrite.All", "Directory.AccessAsUser.All", "Sites.FullControl.All"
# Check the Site collection:
$site = Invoke-MgGraphRequest -Method GET -Uri "https://graph.microsoft.com/v1.0/sites/$hostName.sharepoint.com:$sitePath"
$site.Id #should return something like: hostname.sharepoint.com,GUID,GUID
$app = Get-MgApplication -Filter "displayName eq 'IntranetArchive_Migration'"
$app.AppId
# Create the request for access
$body = @{"roles" = @("write"); "grantedToIdentities" = @(@{ "application" = @{ "id" = $app.AppId; "displayName" = $app.DisplayName } }) } | ConvertTo-Json -Depth 10
Invoke-MgGraphRequest -Method POST -Uri "https://graph.microsoft.com/v1.0/sites/$($site.Id)/permissions" -Body $body
You now have a service principal set up with write access to a single site within your SharePoint instance, meeting our needs for least privileges.
Uploading Files to SharePoint with a Service Principal
We're now ready to start uploading some files into our Document Libraries. This is a two step process - first we're uploading the file, then we upload the metadata into the columns on the document library. This will require us to interact with the Library as both a Drive and a List, so there's a little bit of setup needed.
As we're using a Service Principal, we need to connect to EntraID first, and then grab an access token to access the MS Graph with the permissions that were granted to the app registration. The following scripts assume a clean PowerShell terminal from the Admin setup, so we start by setting up a few similar variables
$clientId = "<app Id as GUID>"
# Prompt the user to enter the client secret securely
$clientSecret = Read-Host -Prompt "Client Secret" -AsSecureString
$tenantId = "<tenant Id as GUID>"
$hostName = "<unique part of SharePoint Hostname>" # i.e. https://<hostname>.sharepoint.com
$siteName = "<site name from URL>"
$libraryName = "<library name from URL>"
# Ensure we don't save the credentials in the history
Disable-AzContextAutosave -Scope Process | Out-Null
# Create a credential object
$credential = New-Object -TypeName "System.Management.Automation.PSCredential" -ArgumentList $clientId,$clientSecret
# Connect to Azure as the service principal
Connect-AzAccount -Credential $credential -Tenant $TenantId -ServicePrincipal
# Request an access token to connect to MS Graph
$graphToken = Get-AzAccessToken -Resource "https://graph.microsoft.com" -AsSecureString
# Connect to MS Graph (without a welcome message)
Connect-MgGraph -AccessToken $graphToken.Token -NoWelcome
# Get the Site ID for the site
$site = Invoke-MgGraphRequest -Method Get -Uri "https://graph.microsoft.com/v1.0/sites/$hostName.sharepoint.com:/sites/$siteName`?`$select=id"
# Get the Id, Name and internal SharePoint Ids of all the document
# libraries in the site, this will include the default libraries.
$drives = Invoke-MgGraphRequest -Method Get -Uri "https://graph.microsoft.com/v1.0/sites/$($site.id)/drives?`$select=id,name,sharePointIds" -OutputType psobject
# Filter the list down to just the one we're interested in, and save the
# Drive Id and the underlying List Id
$drive = $drives.value | Where-Object { $_.name -eq $libraryName }
$driveId = $drive.Id
$listId = $drive.sharePointIds.listId
At this point, we have enough information to start uploading some files into SharePoint - I'm going to skip loading a CSV of metadata via Import-CSV
and looping through the records for now to keep this focused on the core topic, so I'm going to hard code the file path and metadata for this example
$documentPath = "c:\temp\uploads\"
$fileName = "exampleFile.pdf"
$filePath = Join-Path -Path $documentPath -ChildPath $fileName
$uploadUri = "https://graph.microsoft.com/v1.0/drives/$driveId/items/root:/$($fileName):/content"
# Using raw access here, as loading the file into a stream, and then
# converting to JSON as required for the Invoke-MgGraphRequest
# wasn't obvious...
$uploadResult = Invoke-RestMethod -Uri $uploadUri -Method Put -InFile $LocalFilePath -ContentType 'multipart/form-data' -Verbose -Authentication OAuth -Token $graphToken.Token
# Grab the internal SharePoint IDs of the uploaded file
$fileIds = Invoke-MgGraphRequest -Method GET -Uri "https://graph.microsoft.com/v1.0/drives/$driveId/items/$($uploadResult.id)?`$select=sharepointids,name,id" -OutputType psobject
# We need the internal List Item Id of the file within the list, this is an incrementing integer
$itemId = $fileIds.sharepointIds.listItemId
# Create a custom object to hold the metadata
# ArticleType is a "Choice" field that only allows a single choice.
# LocationTags is a "Choice" field that allows multiple choices, including "Write In"
$metadata = @{
Title = "An Example Upload"
Authors = "Ben Duguid"
PublicationDate = Get-Date -Date "2025-03-01T14:30:00Z"
ArticleType = "News"
"[email protected]" = "Collection(Edm.String)"
LocationTags = "United Kingdom|Europe|".Split("|", [System.StringSplitOptions]::RemoveEmptyEntries)
}
$metadataAsJson = $metadata | ConvertTo-Json -Depth 10
# Work with the underlying list to update the columns
Invoke-MgGraphRequest -Method Patch -Uri "https://graph.microsoft.com/v1.0/sites/$($site.id)/lists/$listId/items/$itemId/fields" -ContentType "application/json" -body $metadataAsJson -OutputType PSObject
And there you have it - you should now have an example file uploaded into your Document Library along with some metadata in both standard and custom fields.
Filed under: PowerShell, SharePoint