Internal communication is good for productivity and helps you boost employee engagement. The Windows 10 Spotlight feature provides beautiful backgrounds on your Lock Screen to promote company events. Companies use different endpoint management platform that enables IT operations and security to fully automate discovery, management and remediation. There is an infinite amount of customization that can be made using these platforms, but I will try to cover how to change Windows Lock Screen in Windows 10 when the user is not on corporate network using a SharePoint Online Document Library image and PowerShell in this post.

Create Document Library in SharePoint Online

Create a document library in SharePoint Online to securely store files where you and your co-workers can find them easily, work on them together, and access them from any device at any time. SharePoint team sites include a document library by default. I am going to use the default Shared Documents library for this example.

You can drag an image file from your computer to upload them to your document library with the modern experience. You can also browse your file using classic version as well.

Grant access to SharePoint Online Document Library using SharePoint App-only principal

This is the older method up setting up app-principals. It allows you to generate a principal with very specific permissions and at different levels such as Tenant, Site Collection, Sub-Site and List level. The downside is that by registering the app via the AppRegNew.aspx, the secret will have a default expiry of only 1 year. Furthermore, from the web interface, you have no way of changing this or even being able to see the expiry date. However, you can view the expiry using PowerShell.

PowerShell Script to Set Lock Screen in User’s Windows 10 OS

###### 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 ######
# Feel free to dynamically construct the targetFolder value from registry entries as per your requirements
$targetFolder = $PSScriptRoot
$global:isTargetFilePathExist = $false
$global:sharePointFileHash = 0
$global:desktopImageHash = 0

$siteRelativeUrl = "sites/yoursite"
$folderRelativeUrl = "your-document-library-name"
$fileName = "your-file-name.jpg"
###### 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 Set-LockScreen([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")

            if ($isTargetFilePathExist -eq $true) {
                $jobGetStream = $wc.OpenRead($fileUri)
                $sharePointFileHash = (Get-FileHash -InputStream $jobGetStream).Hash

                if (!($sharePointFileHash -eq $desktopImageHash)) {
                    $message = "Desktop image is new."
                    Add-Log $message $Global:logFile

                    $jobDownload = $wc.DownloadFileTaskAsync($fileUri, $targetFilePath)

                    $message = "Downloading file $fileUrl at $targetFilePath."
                    Add-Log $message $Global:logFile

                    while (!$jobDownload.IsCompleted) {
                        sleep 1
                    }

                    if ($jobDownload.Status -ne "RanToCompletion") {
                        $message = "Failed to download file."
                        Add-Log $message $Global:logFile
                    }
                    else {
                        $message = "File downloaded."
                        Add-Log $message $Global:logFile
                    }
                }
                else {
                    $message = "No change in desktop image."
                    Add-Log $message $Global:logFile
                }
            }
            else {

                $message = "Image not found."
                Add-Log $message $Global:logFile

                $jobDownload = $wc.DownloadFileTaskAsync($fileUri, $targetFilePath)

                $message = "Downloading file $fileUrl at $targetFilePath."
                Add-Log $message $Global:logFile

                while (!$jobDownload.IsCompleted) {
                    sleep 1
                }

                if ($jobDownload.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"

if (Test-Path $targetFilePath) {
    $desktopImageHash = (Get-FileHash $targetFilePath -Algorithm SHA256).Hash
    $isTargetFilePathExist = $true
}

Set-LockScreen $fileUrl $targetFilePath
###### Download File - End ######

$message = "**************************************** SCRIPT COMPLETED ****************************************"
Add-Log $message $Global:logFile

###### Main Program - End ######

Points to consider

  • Grant App-only permission with read access only at the library level to avoid unauthorized operations
  • Make sure the script is copied to a folder only admin privilege can access and execute
  • Protect Client Secret using Azure key vault kind of service
  • Rotate Client Secret frequently
  • Don’t use any confidential information in the Lock Screen image
  • Scan the image file downloaded from SharePoint for malicious content by workstation antivirus tool

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

I hope you find this post helpful.