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

spol_file_download_1

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.

spol_file_download_2

When you click on Create you will be presented with a permission consent dialog. Press Trust It to grant the permissions:

spol_file_download_3

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

spol_file_download_4

I hope you find this post helpful. Let me know your thoughts in the comments section.