Continuous Integration and Continuous Deployment (CI/CD) have become a common task within projects these days. This is also true for building extensions for Microsoft 365 using the SharePoint Framework (SFPx). My main tool for deploying SPFx packages to SharePoint Online, is the M365 CLI. I have also seen many others using this great tool for managing M365 services. It is a great addition to my toolbox and especially in CI/CD.
A few years ago Microsoft started with the so called ‘Resource Specific Consent’ (RSC). In short it provides a mechanism to consent on more granular pieces of permissions. More info on RSC can be found here. Before all this, in order to deploy a SPFx package to SharePoint Online (Tenant App catalog or Site Collection App catalog), you had to create an App registration with a secret or certificate and add Graph and/or SharePoint permission Sites.FullControll.All or Sites.ReadWrite.All. From a security perspective this means your App is able to have access to all Site Collections in your tenant. Especially in bigger Enterprises you are not able to get a consent on this permission, but it was the only way to automate the deployment of SPFx packages. With RSC, Microsoft introduced a new permission called Sites.Selected. Basically, when you Admin consent to this permission, nothing is really consented yet. It only tells the App has permissions to selected sites. The actual permission needs to be set and consented on the Site Collection itself. This way you can give granular permissions to one or more specific Site Collections. In this case your Tenant App Catalog Site Collection or your Site Collection App Catalog you wish to deploy your package to.
GitHub Actions and the M365 CLI
Last week on a project I am working on, we wanted to change our permissions to the Sites.Selected permission scope. We use GitHub and GitHub Actions for our pipelines and the M365 CLI plugins to do the deployment of our SPFx packages. We were stumbling on an issue, where we were getting an ‘Access Denied’ error when using Sites.Selected. We used the Microsoft Documentation and everything seemed to be configured correctly. We used an App registration in Entra ID with a certificate and Sites.Selected permission for both Graph and SharePoint. We use the ‘pnp/action-cli-login@v3’ action to login to our tenant using the M365 CLI:
- name: Login to tenant
uses: pnp/action-cli-login@v3
with:
TENANT: ${{ secrets.CLIMICROSOFT365_TENANT }}
APP_ID: ${{ secrets.CLIMICROSOFT365_ENTRAAPPID }}
CERTIFICATE_ENCODED: ${{ secrets.CLIMICROSOFT365_CERT }}
CERTIFICATE_PASSWORD: ${{ secrets.CLIMICROSOFT365_CERT_PASSWORD }}
Nothing really fancy here. Just login to our tenant with the right information from our App registration. For the deployment of our SPFx package we use the ‘pop/action-cli-deploy@5.0.0 action:
- name: Deploy app to a site collection
uses: pnp/action-cli-deploy@v5.0.0
with:
APP_FILE_PATH: "{{github.WORKSPACE}}/sharepoint/solution/${{vars.SP_APP_NAME}}.sppkg"
SCOPE: sitecollection
SITE_COLLECTION_URL: ${{vars.SP_SITE_URL}}
SKIP_FEATURE_DEPLOYMENT: true
OVERWRITE: true
Also pretty straightforward, but we were still getting the ‘Access Denied’ Error. So we started investigating and checking the consent on the specified Site Collection etc. After we set verbose logging running the GitHub Action, we saw in the token we received, that the SharePoint Site Url was empty after the login command. This is because the M365 CLI tries to retrieve the root site url during login through this GET request:
https://graph.microsoft.com/v1.0/sites/root?$select=webUrl
But with the Sites.Selected and only consent Full Control permission on the Site Collection App Catalog, we did not have permission to retrieve the root site webUrl. This caused that the token we had was lacking information needed to deploy our package and thus failing with an ‘Access Denied’ error.
Solution
When thinking more deeply about the issue, we came up with 3 solutions to ‘workaround’ this issue and solve it.
- Move back to Sites.FullControl.All permissions
- Consent permission on the root Site Collection
- Instruct the M365 CLI spo service to use our root Site Collection Url.
Moving back to Sites.FullControll.All, was not an option as from a security perspective that is not what we wanted anymore. And it shouldn’t be necessary anymore. Consenting permissions on the root Site Collection was also not an option we wanted. Our intranet is located there, so exposing that data to our App, was not really what we wanted too. So we tried a few things and investigated option number 3 in order to instruct the M365 CLI spo service to use our root Site Collection Url.
After some ‘googling’ we stumbled on the M365 spo set command and from the documentation it was actually just telling us that this could solve our issue. It states:
CLI for Microsoft 365 automatically discovers the URL of the root SharePoint site collection/SharePoint tenant admin site (whichever is needed to run the particular command). In specific cases, like when managing multi-geo Microsoft 365 tenants, it could be desirable to make the CLI manage the specific geography. For such cases, you can use this command to explicitly specify the SPO URL that should be used when executing SPO commands.
Because the M365 CLI was not able to discover our root Site Collection Url during the login process, we are able to set it manually with the M365 spo set -url command. So we added a run block between our 2 above mentioned pipeline actions to set our root site Url:
- name: Login to tenant
uses: pnp/action-cli-login@v3
with:
TENANT: ${{ secrets.CLIMICROSOFT365_TENANT }}
APP_ID: ${{ secrets.CLIMICROSOFT365_ENTRAAPPID }}
CERTIFICATE_ENCODED: ${{ secrets.CLIMICROSOFT365_CERT }}
CERTIFICATE_PASSWORD: ${{ secrets.CLIMICROSOFT365_CERT_PASSWORD }}
- name: M365 spo set root site collection url
run: |
m365 spo set --url https://yourtenantname.sharepoint.com
- name: Deploy app to a site collection
uses: pnp/action-cli-deploy@v5.0.0
with:
APP_FILE_PATH: "{{github.WORKSPACE}}/sharepoint/solution/${{vars.SP_APP_NAME}}.sppkg"
SCOPE: sitecollection
SITE_COLLECTION_URL: ${{vars.SP_SITE_URL}}
SKIP_FEATURE_DEPLOYMENT: true
OVERWRITE: true
And voila our pipeline worked as expected with the least privileged permissions set on our App registration.
I hope this can help others struggling with this issue!
Keep coding and sharing is caring!




Leave a comment