Deploy SPFx app using pipeline's Workload Identity federation
Tags: azure, devops, security
Published: 2024-02-18 12:02:11

My previous post on using [Azure DevOps Pipeline with Workload Identity federation](https://dev.to/kkazala/azure-devops-pipeline-with-workload-identity-federation-2gpj) described how to use a context of already authenticated service principal (the identity of service connection used by the pipeline), to refresh the existing token and exchange it for a SPO token.

The example I provided was very simple, and probably not a very useful one. On top of that, @eduardpaul pointed out that it doesn't work for deploying apps to the app catalog. 
>I utilized the `Sites.FullControl.All` permissions for both Graph and SharePoint APIs, along with `AppCatalog.ReadWrite.All` for testing purposes (global app catalog). [...] I consistently encountered a "(401) Unauthorized" error as soon as the code reached the `Add-PnPApp` section (the only different line from your example). Interestingly, a previous app registration with identical permissions (but using a pfx+password) was functioning correctly.

I couldn't sleep if I didn't check it out. 

## The test: deploy SPFx app

### Service Connection

![Dev connection](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/egbz0pi49vsm9dsgt3v5.png)

Please note the url displayed on the bottom- it contains the app id (`3f1d79ad-xxxx-xxxx-xxxx-51799f7bb56d`) I'm configuring in the next step. 

### API Permissions
Although I really don't like using `Sites.FullControl.All`, I'm going to be a bit lazy here. 

![API Permissions](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hw1f5afwobmodhpkrikm.png)

### The pipeline

```yaml
variables:
- name: tenantName
  value: "{your-tenant-name}"
- name: siteName
  value: "AppCatalog"
- name: spfxPackage
  value: "spfx/spfx-1-16-1-pnpm.sppkg"

steps:

- task: AzurePowerShell@5
  name: DeploySPFx
  inputs:
    azureSubscription: DEV_Connection
    azurePowerShellVersion: latestVersion
    ScriptType: InlineScript
    Inline: |
      Write-Host "##[group]Install/Import  PS modules"
      Install-Module PnP.PowerShell -Scope "CurrentUser" -Verbose -AllowClobber -Force
      Write-Host "##[endgroup]"

      Write-Host "##[group]Who am I"
      $azContext = (Get-AzContext).Account.Id
      $sp = Get-AzADServicePrincipal -ApplicationId $azContext
      Write-Host "##[debug] ServicePrincipal: $($sp.Id)"
      Write-Host "##[endgroup]"

      $url = "https://$(tenantName).sharepoint.com"
      $path="$(Build.SourcesDirectory)/$(spfxPackage)"

      try {
        $azAccessToken = Get-AzAccessToken -ResourceUrl $url
        $conn = Connect-PnPOnline -Url "$url/sites/$(siteName)" -AccessToken $azAccessToken.Token -ReturnConnection

        Write-Host "##[debug]Get-PnPConnection"
        Write-Host $conn.Url

        $packageInSite = Add-PnPApp -Path $path -Overwrite  -Publish -SkipFeatureDeployment  -Connection $conn
       
      }
      catch {
          Write-Host "##[error]$($_.Exception.Message)"
      }
  displayName: Deploy spfx
```

As you see, I'm using the **DEV_Connection** service connection. The authentication is done by the pipeline, the identity exists in the same tenant as my SPO site. 

### The run

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/e0bnda1puvtq6kjiot7j.png)

Seems like everything worked out. 
The Id you see here (` bd6a8b8e-xxxx-xxxx-xxxx-91ad2d479534`) is different than the id you saw above. It's still the same identity, though. One is the application id, and the other one is the object id. 

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/k20mkd4mq8em1svflw7g.png)

### Did it deploy?

Let's check the app catalog: 


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/cq4j4rkgkw35str1irjr.png)

Seems like it did. The Federated Identity added and published the package successfully. 


## "Works for me" :/
I really don't know, @eduardpaul, what the problem with your deployment might be. I hate giving you the "works for me" answer 🙈, but... it really does 

One thing that comes to my mind is that maybe you have to **wait**:
>_"Managed identity tokens are cached by the underlying Azure infrastructure for performance and resiliency purposes: the back-end services for managed identities maintain a cache per resource URI for around 24 hours. It can take several hours for changes to a managed identity's permissions to take effect, for example. Today, it is not possible to force a managed identity's token to be refreshed before its expiry. For more information, see Limitation of using managed identities for authorization."_ [
Are managed identities tokens cached?](https://learn.microsoft.com/en-us/entra/identity/managed-identities-azure-resources/managed-identities-faq#are-managed-identities-tokens-cached)
This refers to API permissions (`Sites.Selected`, `Sites.FullControl.All`) that you grant using `New-MgServicePrincipalAppRoleAssignment`. 
Permissions granted to SPO site take effect immediately (at least for now, according to my tests)

## Sites.Selected

Using `Sites.FullControl.All` is not only "quick'n'dirty" but also rather difficult to get in enterprise environment. Myself, even if I could convince global admin that I need it and they can trust me, I think I would never request it. It certainly has its use when testing, as it helps us to isolate the problem, but once we are sure the code works, it's time to go back to minimum required permissions.

The good news is that `Sites.Selected` works as well. Just make sure you grant `FullControl` permissions on the AppCatalog site, using for example [Grant-PnPAzureADAppSitePermission](https://pnp.github.io/powershell/cmdlets/Grant-PnPAzureADAppSitePermission.html)

`Grant-PnPAzureADAppSitePermission -AppId $clientId -DisplayName $displayName -Permissions FullControl`