Deploy SPFx app package to SharePoint from Azure DevOps with modern authentication

As you can see from my last posts I got heavily involved in dealing with SharePoint modern authentication in the recent past. To repeat once again:


If your tenant has turned off legacy authentication you cannot simply authenticate with PowerShell and UserCredentials anymore. To check this run the following script:

Connect-SPOService -Url "https://<Your-Tenant>"

$tenantsettings = Get-SPOTenant


And potentially modify this setting (turn on: $true)

Set-SPOTenant -LegacyAuthProtocolsEnabled $true

In one of my last posts I showed how modern authentication is handled in a PowerShell script, especially in an Azure Automation environment where you can store and retrieve the necessary self-signed certificate as an Azure Automation asset.

Now another challenge: Inside an Azure DevOps Release pipeline you have another one or two PowerShell tasks. In my scenario two, one for uploading the app package to the app catalog, a second one to upload the assets to a SharePoint library, that is your Office 365 public CDN.

In Azure DevOps Release (and Build) pipelines you have no capability to simply store a certificate as an asset. Have it in the source code might not be an option to not distribute it to any developer’s environment. I only found the way to store variables (strings!), if anyone else has a better idea, speak up please 🙂

Fortunately this is also a working scenario as you can connect to SharePoint with another variant of the Connect-PnPOnline cmdlet providing a PEMCertificate and a PEMPrivateKey, both represented by large strings. According to my last post both can be simply extracted from a certificate once it is created by the New-PnPAzureCertificate cmdlet.

$Password = "******"
$secPassword = ConvertTo-SecureString -String $Password -AsPlainText -Force
$cert = New-PnPAzureCertificate -Out "AzureAutomationSPOAccess.pfx" - `
 -ValidYears 10 `
 -CertificatePassword $secPassword
 -CommonName "AzureAutomationSPOAccess" `
 -Country "DE" `
 -State "Bavaria"
$PEMCert = $cert.Certificate
$PEMSecret = $cert.PrivateKey 
Write-Host $PEMCert
Write-Host $PEMSecret

On top you also have to output the KeyCredentials and combine them with an Azure AD App registration.

Release pipeline basic configuration

In the past I mentioned Elio Struyf as a good resource for SPFx Azure DevOps Build&Release Pipelines but today I would also like to mention the blogpost of Giuliano de Luca which lists up all the necessary steps and options in a fantastic way.

In my simple scenario I picked most of the basics from him but I prefer to use a Release Pipeline only in staging environments (as you have to package for every stage again (changes in write-manifest.json for instance). But to simplify we will cover only one stage:

Simple Azure DevOps SPFx Release Pipeline (1 Stage)
Azure DevOps Release Pipeline – The Tasks

The first tasks are quite obvious. At first we follow a recommendation from Elio to use Node version 8 which significantly improves performance. Afterwards we run npm install, bundle and package our solution.

Here I want to give you a special hint on dealing with larger projects:

There I tend to use larger Git repositories, meaning not every SPFx solution has it’s own like the typical standard deployment demo scenario. That leads to the point where the standard file structure is not valid anymore. When entering the System.DefaultWorkingDirectory followed by your Artifact folder (“MM” in my example, see 1st screenshot) we come to different solution directories below:

For those solution directories (assume today you want to deploy Webpart1 but tomorrow Webpart2 and you all have them in one Git repository and all are handled by one configured Release pipeline) I created a Release variable localPath that can be set at releasetime.

Azure DevOps Release variables

This leds us to the point where we can relate to this in the npm install or our gulp tasks when it comes to a local file path:

npm install
gulp bundle task (gulp package-solution quite similar)

So depending on which webpart we want to deploy according to our variables above we would hand in different “packageFile” (we use it later!) and different “localPath” values and the task will find it’s package.json (for npm install) as well as it’s gulpfile.js

PowerShell Tasks and modern authentication

But now let’s come to the main point of this post. Let’s create the two PowerShell tasks to upload our app package and the assets to the library that represents our Office 365 public CDN. At first let me clarify which task to use as there are more outside (in the past for instance I used one from the marketplace). I now switched to the official Microsoft task.

Azure DevOps Task Template PowerShell (by Microsoft)

The first task is to upload the package to the app catalog. It is quite simple as beyond the script we do not really need to configure anything else. I skipped to give a working directory (I use cd in script instead) or provide environment variables here. Maybe worth to investigate in future.

Azure DevOps PowerShell task Deploy to App catalog

As the complete script did not fit into the screenshot here once again:

Write-Host ("Adding package " + $($env:packageFile) + " to the AppCatalog at https://" + $($env:tenant) + "" + $($env:catalogsite))
cd $env:System_DefaultWorkingDirectory/MM/$env:localPath/sharepoint/solution/
Install-PackageProvider -Name NuGet -Force -Scope "CurrentUser" 
Install-Module SharePointPnPPowerShellOnline -Scope "CurrentUser" -Verbose -AllowClobber -Force
Import-Module SharePointPnPPowerShellOnline -Scope "Local" -WarningAction SilentlyContinue

Write-Host ("Connecting to Tenant " + $($env:tenant) + " with AppID " + $($env:SPOAppID))

Connect-PnPOnline –Url https://$(tenant)$(catalogsite) -Tenant "$(tenant)" -ClientId $(SPOAppID) -PEMCertificate "$(PEMCertificate)" -PEMPrivateKey "$(PEMPrivateKey)"

Write-Output 'Connected to SPO'

Add-PnPApp -Path $env:packageFile -Publish -Overwrite
Write-Host Finished upload app package $($env:packageFile)

For debug reasons I left a couple of Write-Host but they are not necessary of course. Anyway the first shows quite well how to reference our release variables from above ($env:<ReleaseVariableName>).

After the first output we switch our current directory into our current solution and the folder where the .sppkg file resides.

Afterwards we install our PnP-PowerShell module with 3 lines of code.

But then it comes to the ‘magic’ point of this post, the modern authentication with PnP Online:

    –Url https://$(tenant)$(catalogsite) 
    -Tenant "$(tenant)" 
    -ClientId $(SPOAppID) 
    -PEMCertificate "$(PEMCertificate)" 
    -PEMPrivateKey "$(PEMPrivateKey)"

We build our Url from known and obvious parts combined with our release variables, the Tenant as well.

We then hand in the ID of our App Registration and finally
our PEMCertificate and PEMPrivateKey string retrieved from our variable.

Finally I simplified the things again and added a simple Add-PnPApp cmdlet assuming SkipFeatureDeployment is always false. Giuliano de Luca shows a cool optional handling for this in his code repository.

The next task is quite the same:

Azure DevOps PowerShell task Upload to Office 365 public CDN
Write-Host ("Adding package " + $($env:packageFile)+ " to the AppCatalog at https://"+ $($env:tenant) + "" + $($env:catalogsite))
cd $env:System_DefaultWorkingDirectory/MM/$env:localPath/

$cdnConfig = Get-Content -Raw -Path ("config\copy-assets.json") | ConvertFrom-Json
$bundlePath = $cdnConfig.deployCdnPath
$files = Get-ChildItem $bundlePath\*.*

Write-Output "Connecting to CDN-Site $env:CDNSite"

Connect-PnPOnline –Url $env:CDNSite -Tenant "$(tenant)" -ClientId $(SPOAppID) -PEMCertificate "$(PEMCertificate)" -PEMPrivateKey "$(PEMPrivateKey)"

foreach ($file in $files) {
    $fullPath = $file.DirectoryName + "\" + $file.Name
    Write-Output "Uploading file $fullPath to folder $env:CDNLib"
    $f = Add-PnPFile -Path $fullPath -Folder $env:CDNLib

We do not need to install PnP Powershell anymore but now only cd to a different directory. Then we grab a JSon file from our repository to get a value from there. An elegant way to omit some release variables.

We once again authenticate with our certificate values to SharePoint.

Finally we use the (retrieved from Json config file) path value where our bundles reside to grab all files from there. We then iterate them one by one and upload them to the library/folder inside our CDN site.

Markus is a SharePoint architect and technical consultant with focus on latest technology stack in Microsoft 365 and SharePoint Online development. He loves the new SharePoint Framework as well as some backend stuff around Azure Automation or Azure Functions and also has a passion for Microsoft Graph.
He works for Avanade as an expert for Microsoft 365 Dev and is based in Munich.
Although if partially inspired by his daily work opinions are always personal.

2 thoughts on “Deploy SPFx app package to SharePoint from Azure DevOps with modern authentication

Leave a Reply

Fill in your details below or click an icon to log in: Logo

You are commenting using your account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s