SharePoint App-Only is the older, but still very relevant, model of setting up app-principals. This model works for both SharePoint Online and SharePoint 2013/2016 on-premises and is ideal to prepare your applications for migration from SharePoint on-premises to SharePoint Online.
Setting up an app-only principal with site collection permission
Navigate to a SharePoint Online site in your tenant and then call the appregnew.aspx page via via https://yoursiteurl/_layouts/15/appregnew.aspx.
In this page click on the Generate button to generate a client id and client secret and fill the remaining information like shown in the screenshot below
Note: Store the retrieved information (client id and client secret) since you will need this in the PowerShell script.
Next step is granting permissions to the newly created principal. Since we are granting site collection scoped permission this granting can be done via the appinv.aspx page on the site itself. You can reach this page via https://yoursiteurl/_layouts/15/appinv.aspx. Once the page is loaded add your client id and look up the created principal:
To grant permissions, you will need to provide the permission XML that describes the needed permissions.
When you click on Create you will be presented with a permission consent dialog. Press Trust It to grant the permissions:
Code Snippet
The below PowerShell script downloads file from the SharePoint online site using the client id and secret generated from the previous section. Modify the Global Tenant Specific and Site/File Path Variable values and execute the PowerShell command.
###### Global Static Variables - Start ######
$grantType = "client_credentials"
$principal = "00000003-0000-0ff1-ce00-000000000000"
###### Global Static Variables - End ######
###### Global Tenant Specific Variables - Start ######
$m365TenantId = "yourtenantguid"
$targetHost = "yourtenantname.sharepoint.com"
$appClientId = "clientid-from-previous-section"
$appClientSecret = "clientsecret-from-previous-section"
###### Global Tenant Specific Variables - End ######
###### Site/File Path Variables - Start ######
$targetFolder = $PSScriptRoot
$siteRelativeUrl = "sites/yoursite"
$folderRelativeUrl = "your-document-library-name"
$fileName = "your-file-name.png"
###### Site/File Path Variables - Start ######
###### Helper Functions - Start ######
function Add-Working-Directory([string]$workingDir, [string]$logDir) {
if (!(Test-Path -Path $workingDir)) {
try {
$suppressOutput = New-Item -ItemType Directory -Path $workingDir -Force -ErrorAction Stop
$msg = "SUCCESS: Folder '$($workingDir)' for CSV files has been created."
Write-Host -ForegroundColor Green $msg
}
catch {
$msg = "ERROR: Failed to create '$($workingDir)'. Script will abort."
Write-Host -ForegroundColor Red $msg
Exit
}
}
if (!(Test-Path -Path $logDir)) {
try {
$suppressOutput = New-Item -ItemType Directory -Path $logDir -Force -ErrorAction Stop
$msg = "SUCCESS: Folder '$($logDir)' for log files has been created."
Write-Host -ForegroundColor Green $msg
}
catch {
$msg = "ERROR: Failed to create log directory '$($logDir)'. Script will abort."
Write-Host -ForegroundColor Red $msg
Exit
}
}
}
function Add-Log([string]$message, [string]$logFile) {
$lineItem = "[$(Get-Date -Format "dd-MMM-yyyy HH:mm:ss") | PID:$($pid) | $($env:username) ] " + $message
Add-Content -Path $logFile $lineItem
}
function Get-AccessToken {
try {
$message = "Getting Accesstoken..."
Add-Log $message $Global:logFile
$tokenEndPoint = "https://accounts.accesscontrol.windows.net/$m365TenantId/tokens/oauth/2"
$client_Id = "$appClientId@$m365TenantId"
$resource = "$principal/$targetHost@$m365TenantId"
$requestHeaders = @{
"Content-Type" = "application/x-www-form-urlencoded"
}
$requestBody = @{
client_id = $client_Id
client_secret = $appClientSecret
grant_type = $grantType
resource = $resource
}
$response = Invoke-RestMethod -Method 'Post' -Uri $tokenEndPoint -Headers $requestHeaders -Body $requestBody
$accesstoken = $response.access_token
$message = "Accesstoken received."
Add-Log $message $Global:logFile
return $accesstoken
}
catch {
$statusCode = $_.Exception.Response.StatusCode.value__
$statusDescription = $_.Exception.Response.StatusDescription
$message = "StatusCode: $statusCode"
Add-Log $message $Global:logFile
$message = "StatusDescription : $statusDescription"
Add-Log $message $Global:logFile
return $null
}
}
function Download-File([string]$fileUrl, [string]$targetFilePath) {
$accessToken = Get-AccessToken
if (![string]::IsNullOrEmpty($accessToken)) {
try {
$fileUri = New-Object System.Uri($fileUrl)
$wc = New-Object System.Net.WebClient
$wc.Headers.Add("Authorization", "Bearer $accessToken")
$job = $wc.DownloadFileTaskAsync($fileUri, $targetFilePath)
$message = "Downloading file $fileUrl at $targetFilePath."
Add-Log $message $Global:logFile
while (!$job.IsCompleted) {
sleep 1
}
if ($job.Status -ne "RanToCompletion") {
$message = "Failed to download file."
Add-Log $message $Global:logFile
}
else {
$message = "File downloaded."
Add-Log $message $Global:logFile
}
}
catch {
$statusCode = $_.Exception.Response.StatusCode.value__
$statusDescription = $_.Exception.Response.StatusDescription
$message = "StatusCode: $statusCode"
Add-Log $message $Global:logFile
$message = "StatusDescription : $statusDescription"
Add-Log $message $Global:logFile
$message = "Failed to download file."
Add-Log $message $Global:logFile
}
}
else {
$message = "Unable to get Accesstoken."
Add-Log $message $Global:logFile
}
}
###### Helper Functions - End ######
###### Main Program - Start ######
###### Log Setup - Start ######
$currentDirectoryPath = Split-Path -Parent $MyInvocation.MyCommand.Definition
$workingDirectory = $currentDirectoryPath
$logDirectoryName = "Logs"
$logDirectory = "$workingDirectory/$logDirectoryName"
$logFileName = "$(Get-Date -Format "yyyyMMddTHHmmss")_downloadjobexecution.log"
$Global:logFile = "$logDirectory/$logFileName"
Add-Working-Directory $workingDirectory $logDirectory
###### Log Setup - Start ######
Write-Host -ForegroundColor Yellow "WARNING: Minimal output will appear on the screen."
Write-Host -ForegroundColor Yellow " Please look at the log file '$($logFile)'"
$message = "**************************************** SCRIPT STARTED ****************************************"
Add-Log $message $Global:logFile
###### Download File - Start ######
$targetFilePath = Join-Path $targetFolder $fileName
$fileUrl = "https://$targetHost/$siteRelativeUrl/_api/Web/GetFolderByServerRelativeUrl('$folderRelativeUrl')/Files('$fileName')/`$value"
Download-File $fileUrl $targetFilePath
###### Download File - End ######
$message = "**************************************** SCRIPT COMPLETED ****************************************"
Add-Log $message $Global:logFile
###### Main Program - End ######
Note : If you get “The request was aborted: Could not create SSL/TLS secure channel.” error, the solution to fix this issue is to add the following line in the beginning of the PowerShell script. The root cause is SharePoint online stopped to support SSL/TLS 1.0 since November 2018. Also Microsoft may change the supported TLS version in the future, you may need to keep track the supported version.
[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12
Demo
I hope you find this post helpful.