Wednesday, September 30, 2026

Pin a SharePoint Site to the Microsoft Teams Left Rail (No Code)

This is a no-code, step-by-step guide. You package a SharePoint page as a Teams app, upload it, and pin it for everyone in your organisation.

Why do this?

Most organisations already have an intranet on SharePoint, yet people spend their day in Microsoft Teams. Putting the intranet one click away on the Teams app bar (the icons down the left side) raises traffic to the intranet and saves people switching windows.

In this guide you build a small Teams app with one personal tab that loads a SharePoint page. You then upload the app in the Teams admin center and pin it with an app setup policy, so every user sees the icon without installing anything.

What you end up with: an "Intranet" icon in the Teams left rail. Clicking it opens the SharePoint site home page inside Teams, already signed in.

Prerequisites

RequirementDetails
Admin roleTeams Administrator (or Global Administrator) to upload custom apps and edit setup policies
SharePoint siteA modern SharePoint site the target users can already read
Custom apps allowedOrg-wide app settings must allow custom apps to be uploaded
ToolsAny text editor and a zip tool. No Visual Studio, SPFx, or Azure resources are needed

How it works

PieceRole
Teams app manifestDescribes the app: its name, its icons and one static (personal) tab
teamslogon.aspxA SharePoint page that signs the user in through Teams, then redirects to the page given in the dest parameter
webApplicationInfoPoints at the SharePoint Online resource (00000003-0000-0ff1-ce00-000000000000) so single sign-on works in the Teams desktop client
App setup policyPins the app to the app bar for everyone the policy applies to

Step 1 – Find the SharePoint site and its home page

  1. Open the SharePoint admin center and go to Sites > Active sites.
  2. Search for the site and note its URL (in this example, /sites/work).
Step 1a – Locate the site in SharePoint admin center > Active sites

Step 1a – Locate the site in SharePoint admin center > Active sites

  1. Open the site and confirm which page is the home page. For most modern sites it is SitePages/Home.aspx.
Step 1b – The SharePoint site home page that will appear in Teams

Step 1b – The SharePoint site home page that will appear in Teams

Tip: to confirm the home page, open the browser developer console on the site and run fetch('/sites/work/_api/web/WelcomePage',{headers:{accept:'application/json'}}).then(r=>r.json()).

Step 2 – Create the Teams app package

A Teams app package is a .zip file that holds three files, all at the root of the zip with no folders:

FileSpecification
manifest.jsonApp definition (shown below)
color.png192 × 192 px full-colour icon
outline.png32 × 32 px icon, white on a transparent background

Here is the manifest. Replace contoso with your tenant name, /sites/work with your site path, and <new-guid> with a new GUID (in PowerShell, run [guid]::NewGuid()).

{
  "$schema": "https://developer.microsoft.com/en-us/json-schemas/teams/v1.17/MicrosoftTeams.schema.json",
  "manifestVersion": "1.17",
  "version": "1.0.0",
  "id": "<new-guid>",
  "developer": {
    "name": "IT",
    "websiteUrl": "https://contoso.sharepoint.com/sites/work",
    "privacyUrl": "https://contoso.sharepoint.com/sites/work",
    "termsOfUseUrl": "https://contoso.sharepoint.com/sites/work"
  },
  "name": { "short": "Intranet", "full": "Intranet (SharePoint)" },
  "description": {
    "short": "Company intranet SharePoint site in Teams",
    "full": "Opens the intranet SharePoint site home page inside Microsoft Teams."
  },
  "icons": { "color": "color.png", "outline": "outline.png" },
  "accentColor": "#0078D4",
  "staticTabs": [{
    "entityId": "intranethome",
    "name": "Intranet",
    "contentUrl": "https://contoso.sharepoint.com/_layouts/15/teamslogon.aspx?spfx=true&dest=https://contoso.sharepoint.com/sites/work/SitePages/Home.aspx",
    "websiteUrl": "https://contoso.sharepoint.com/sites/work/SitePages/Home.aspx",
    "scopes": ["personal"]
  }],
  "permissions": ["identity"],
  "validDomains": [
    "contoso.sharepoint.com",
    "*.login.microsoftonline.com",
    "*.sharepoint.com",
    "spoprod-a.akamaihd.net",
    "resourceseng.blob.core.windows.net"
  ],
  "webApplicationInfo": {
    "id": "00000003-0000-0ff1-ce00-000000000000",
    "resource": "https://contoso.sharepoint.com"
  }
}

The key line is contentUrl. It points at /_layouts/15/teamslogon.aspx?spfx=true&dest=<page URL>. Microsoft documents this pattern for SharePoint content hosted in Teams. It signs the user in to SharePoint through Teams single sign-on, so they don't get a blank frame or a second sign-in prompt.

Zip the three files (select the files themselves, not the folder that contains them). Name the zip something like IntranetTeamsApp.zip.

Step 3 – Upload the app in the Teams admin center

  1. Go to Teams admin center > Teams apps > Manage apps.
Step 3a – Teams admin center > Teams apps > Manage apps

Step 3a – Teams admin center > Teams apps > Manage apps

  1. Open the Actions menu and choose Upload new app.
Step 3b – Actions > Upload new app

Step 3b – Actions > Upload new app

  1. Select Upload and choose your .zip file.
Step 3c – The Upload a custom app dialog

Step 3c – The Upload a custom app dialog

  1. When New app added appears, select this link to open the app.
Step 3d – The app was uploaded

Step 3d – The app was uploaded

  1. Check the app details. Available to should read Everyone (org-wide default). If the app shows as Blocked, select Allow.
Step 3e – The app's details page, showing it available to everyone

Step 3e – The app's details page, showing it available to everyone

Step 4 – Pin the app with an app setup policy

  1. Go to Teams apps > Setup policies and open Global (Org-wide default). To reach only a pilot group, create a custom policy instead and assign it to that group.
Step 4a – The App setup policies page

Step 4a – The App setup policies page

  1. Scroll to Pinned apps.
Step 4b – Settings for the Global (Org-wide default) policy

Step 4b – Settings for the Global (Org-wide default) policy

  1. Select + Add apps.
Step 4c – Pinned apps > Add apps

Step 4c – Pinned apps > Add apps

  1. Search for the app name, hover over it, and choose Select.
Step 4d – Search for the custom app in the Add pinned apps pane

Step 4d – Search for the custom app in the Add pinned apps pane

  1. Check that it appears under Apps to add, then select Add.
Step 4e – The app is queued under Apps to add

Step 4e – The app is queued under Apps to add

  1. The app now appears in the App bar list. Use Move up or Move down, or drag the handle, to change its position.
Step 4f – The Intranet app in the pinned App bar list

Step 4f – The Intranet app in the pinned App bar list

  1. Select Save, then Confirm in the warning that changes to the Global policy affect the whole organisation.
Step 4g – Confirm the change to the Global policy

Step 4g – Confirm the change to the Global policy

Step 5 – Test it in Teams

Open Teams on the web or on the desktop. The new icon appears in the left app bar. Click it and the SharePoint home page loads inside Teams, signed in and fully usable. Users can read news and open links, and page authors can even use Edit.

Step 5 – The SharePoint site running inside Teams from the pinned icon

Step 5 – The SharePoint site running inside Teams from the pinned icon

Rollout timing: Microsoft states that setup policy changes can take a few hours to take effect. Teams on the web usually picks them up first. On desktop, signing out and back in speeds things up.

Troubleshooting

SymptomCause / fix
"This app cannot be found" right after uploadingThe app catalog hasn't finished propagating. Wait a few minutes and reload Teams
Blank white frame in the tabcontentUrl doesn't use teamslogon.aspx, or the domain is missing from validDomains
Repeated sign-in prompts on desktopwebApplicationInfo is missing or has the wrong resource. It must be your own tenant's https://<tenant>.sharepoint.com
App is missing from the Add pinned apps searchThe app is blocked or not yet available. Allow it under Manage apps, and check that the tab scope is personal
Icon doesn't appear on mobileMobile clients need at least two pinned apps in the policy
You need to change the target pageEdit contentUrl and websiteUrl, raise version (for example to 1.0.1), re-zip, then use Manage apps > app > Upload file

Alternatives worth knowing

  • Viva Connections: Microsoft's supported way to bring the SharePoint home site into Teams, with a dashboard, feed and resources. Choose it when the page is your organisation's home site.
  • Website tab in a channel: quick for one team, but it appears only inside that channel and can't be pinned to the app bar.
  • SPFx personal app: choose this when you need custom code rather than an existing SharePoint page.

Summary

A three-file Teams app package, one upload and one setup-policy edit put any SharePoint site in the Teams left rail for the whole organisation. You don't write code or host anything, and single sign-on works on web and desktop. Repeat the steps with a new GUID and name to add more sites.

References

Sunday, September 27, 2026

Connect to SharePoint Online with Connect-PnPOnline

Goal: Starting from a fresh Windows PC, finish by running one PowerShell command that reads the title of a SharePoint site.
Who it's for: Anyone. You don't need PowerShell experience, just copy, paste and follow the screenshots.


At a glance

ItemDetails
⏱️ Time needed15–20 minutes (first time)
💻 ComputerWindows 10 / 11
👤 AccountA Microsoft 365 work account with access to the SharePoint site
🔑 One-time admin helpSomeone with Global Administrator (or an admin role that can grant consent) for Step 3
🎯 End result(Get-PnPWeb).Title prints your site title

The workflow

Workflow: install PowerShell 7, install PnP.PowerShell, register the Entra ID app, connect, read the site title
Workflow: install PowerShell 7, install PnP.PowerShell, register the Entra ID app, connect, read the site title

Key terms (30-second glossary)

TermWhat it means in plain English
PowerShell 7The modern command window from Microsoft. Not the same as the built-in "Windows PowerShell 5.1".
PnP.PowerShellA free add-on (module) with 700+ SharePoint and Microsoft 365 commands.
Connect-PnPOnlineThe command that signs you in to a SharePoint site. Every other PnP command needs it first.
Entra ID app registrationAn "ID card" for PnP PowerShell in your tenant. Since September 2024, every tenant needs its own.
Client IDThe ID number of that app registration. You pass it to Connect-PnPOnline.
Admin consentA one-time approval by an admin that allows the app to access SharePoint.

Step 1: Install PowerShell 7

Why not the built-in PowerShell?

Windows PowerShell 5.1PowerShell 7
Comes with Windows✅ Yes❌ Install once
Runs PnP.PowerShell 3.x❌ No✅ Yes (needs 7.4.0 or later)
Window title"Windows PowerShell""PowerShell" / "PowerShell 7"
Start commandpowershellpwsh

If you run PnP commands in the old one, you get this error:

Windows PowerShell 5.1 does not recognise Connect-PnPOnline
Windows PowerShell 5.1 does not recognise Connect-PnPOnline

Install it

#Action
1Open Start → type PowerShell → open Windows PowerShell (any window works for this step)
2Check the latest version: winget search --id Microsoft.PowerShell --exact
3Install: winget install --id Microsoft.PowerShell --source winget
4Close the window. Open Start → type PowerShell 7 → open it
5Confirm the version: $PSVersionTable.PSVersion → Major must be 7
winget search --id Microsoft.PowerShell --exact
winget install --id Microsoft.PowerShell --source winget
winget search and install PowerShell 7
winget search and install PowerShell 7
pwsh
$PSVersionTable.PSVersion
Switch to PowerShell 7 and confirm the version
Switch to PowerShell 7 and confirm the version

💡 Tip: Already inside Windows PowerShell? Just type pwsh and press Enter to switch to PowerShell 7 in the same window.


Step 2: Install the PnP.PowerShell module

Run this inside PowerShell 7:

Install-Module PnP.PowerShell -Scope CurrentUser -Force
Get-Module PnP.PowerShell -ListAvailable | Select-Object Name, Version
PartMeaning
-Scope CurrentUserInstalls for you only, so no admin rights are needed
-ForceSkips the "untrusted repository" prompt
Get-Module ... -ListAvailableProves it installed and shows the version
PnP.PowerShell installed
PnP.PowerShell installed

⚠️ Common mistake: Installing the module from Windows PowerShell 5.1 puts it in a different folder. PowerShell 7 then says Connect-PnPOnline is not recognized. Always install from PowerShell 7.


Step 3: Create the Entra ID app registration (one time per tenant)

Pick one option.

Option A: Azure portalOption B: One command
Best forVisual learners, full controlSpeed
Clicks~151 command + sign-in
NeedsAdmin who can grant consentSame

Option A: Azure portal (click-by-click)

A1. Open App registrations

#Action
1Go to https://portal.azure.com and sign in
2In the top search bar type App registrations → open it
3Click + New registration
App registrations → New registration
App registrations → New registration

A2. Fill in the registration form

FieldValue
NamePnP.PowerShell (any name works)
Supported account typesSingle tenant only (default)
Redirect URI → platformPublic client/native (mobile & desktop)
Redirect URI → valuehttp://localhost

Click Register.

Register an application form
Register an application form

A3. Copy the Client ID

On the Overview page, copy Application (client) ID and save it in Notepad. You'll need it in Step 4. The Directory (tenant) ID is here too.

Overview page: Application (client) ID
Overview page: Application (client) ID

A4. Add the SharePoint permission

#Action
1Left menu → API permissions → + Add a permission
2Choose SharePoint
3Choose Delegated permissions (you sign in as yourself)
4Search AllSites → tick the permission you need (see table below)
5Click Add permissions
SharePoint → Delegated → AllSites permissions
SharePoint → Delegated → AllSites permissions
PermissionWhat you can doAdmin consent?
AllSites.ReadRead sites, lists, items (enough for this guide)No
AllSites.WriteRead + write list itemsNo
AllSites.ManageRead + write items and listsNo
AllSites.FullControlEverything, including site settingsYes

🔒 Least privilege: Start with AllSites.Read. Add more only when you need it.

A5. Grant admin consent

Click ✓ Grant admin consent for <your org> → Yes. The Status column turns green: Granted for …

API permissions with admin consent granted
API permissions with admin consent granted

A6. Check the Authentication settings

WhereWhat to check
Authentication → Redirect URI configurationMobile and desktop applications → http://localhost
Authentication → SettingsAllow public client flows = Enabled (needed for -DeviceLogin)
Redirect URI http://localhost
Redirect URI http://localhost
Allow public client flows enabled
Allow public client flows enabled

Option B: One command (PnP does it for you)

Run in PowerShell 7 and sign in with an admin account when the browser opens:

Register-PnPEntraIDAppForInteractiveLogin -ApplicationName "PnP.PowerShell" -Tenant "contoso.onmicrosoft.com"
What it does automatically
Creates the app registration✅
Adds redirect URI + default permissions✅
Prompts you to grant consent✅
Prints the Client ID at the end✅ Copy it

💡 Add -SharePointDelegatePermissions "AllSites.Read" to choose the exact permission.
Add -DeviceLogin if no browser pops up (sign in with a code instead).


Step 4: Connect to the SharePoint site

Replace the two values, then run:

Connect-PnPOnline -Url "https://contoso.sharepoint.com/sites/pub" -ClientId "<your-client-id>" -Interactive
ParameterValue to use
-UrlFull site address from your browser, e.g. https://contoso.sharepoint.com/sites/pub
-ClientIdThe Application (client) ID from Step 3
-InteractiveOpens a Microsoft sign-in window → sign in with your work account

No output = success. PowerShell just returns to the prompt.

No browser pop-up? Use a device code instead

Connect-PnPOnline -Url "https://contoso.sharepoint.com/sites/pub" -ClientId "<your-client-id>" -Tenant "contoso.onmicrosoft.com" -DeviceLogin
#Action
1PowerShell shows a code, e.g. ABCD1234
2Open https://microsoft.com/devicelogin in any browser
3Enter the code → sign in → return to PowerShell

Step 5: Read the site title 🎯

(Get-PnPWeb).Title
Connect-PnPOnline and site title output
Connect-PnPOnline and site title output

✅ You did it. You're connected, and PowerShell printed the site title.

Bonus: a few more read-only commands to try

CommandShows
Get-PnPWeb -Includes Created | Select Title, Url, CreatedTitle, address, creation date
Get-PnPList | Select Title, ItemCountAll lists and libraries with item counts
Get-PnPSiteCollectionAdminSite collection admins
Disconnect-PnPOnlineSigns out of the session

Save it as a reusable script

Save as Get-SiteTitle.ps1, then run .\Get-SiteTitle.ps1 from PowerShell 7:

param(
    [string]$SiteUrl  = "https://contoso.sharepoint.com/sites/pub",
    [string]$ClientId = "<your-client-id>",
    [string]$Tenant   = "contoso.onmicrosoft.com",
    [switch]$DeviceLogin
)

if (-not (Get-Module -ListAvailable PnP.PowerShell)) {
    Install-Module PnP.PowerShell -Scope CurrentUser -Force
}
Import-Module PnP.PowerShell

if ($DeviceLogin) {
    Connect-PnPOnline -Url $SiteUrl -ClientId $ClientId -Tenant $Tenant -DeviceLogin
} else {
    Connect-PnPOnline -Url $SiteUrl -ClientId $ClientId -Interactive
}

$web = Get-PnPWeb -Includes Title, Url, Created
Write-Host "Title   : $($web.Title)"
Write-Host "Url     : $($web.Url)"
Write-Host "Created : $($web.Created)"

Disconnect-PnPOnline

💡 Skip typing -ClientId every time. Save it once as an environment variable, then open a new PowerShell 7 window:

[Environment]::SetEnvironmentVariable("ENTRAID_APP_ID", "<your-client-id>", "User")

Troubleshooting

SymptomCauseFix
Connect-PnPOnline is not recognizedYou're in Windows PowerShell 5.1, or the module was installed thereType pwsh, then re-run Install-Module PnP.PowerShell -Scope CurrentUser -Force
pwsh is not recognized right after installPATH not refreshedClose and reopen the window, or open PowerShell 7 from Start
AADSTS700016: Application ... not foundWrong Client ID or wrong tenantCopy the Client ID again from Overview
AADSTS65001: ... has not consentedAdmin consent missingAPI permissions → Grant admin consent
AADSTS7000218: ... client_assertion or client_secretPublic client flows disabled (device login)Authentication → Settings → Allow public client flows = Enabled
403 Forbidden / Access deniedYour account has no access to that siteAsk the site owner to add you as a member/visitor
running scripts is disabled on this systemExecution policySet-ExecutionPolicy -Scope CurrentUser RemoteSigned
Sign-in window never appearsPop-up blocked / remote sessionUse -DeviceLogin

Cheat sheet

TaskCommand
Install PowerShell 7winget install --id Microsoft.PowerShell --source winget
Switch to PowerShell 7pwsh
Check version$PSVersionTable.PSVersion
Install PnPInstall-Module PnP.PowerShell -Scope CurrentUser -Force
Update PnPUpdate-Module PnP.PowerShell
Create the app (one command)Register-PnPEntraIDAppForInteractiveLogin -ApplicationName "PnP.PowerShell" -Tenant "contoso.onmicrosoft.com"
Connect (browser)Connect-PnPOnline -Url "<site-url>" -ClientId "<client-id>" -Interactive
Connect (device code)Connect-PnPOnline -Url "<site-url>" -ClientId "<client-id>" -Tenant "<tenant>.onmicrosoft.com" -DeviceLogin
Site title(Get-PnPWeb).Title
Sign outDisconnect-PnPOnline

References

SourceLink
Install PowerShell 7 on Windows (Microsoft Learn)https://learn.microsoft.com/powershell/scripting/install/install-powershell-on-windows
PnP PowerShell: Installationhttps://pnp.github.io/powershell/articles/installation.html
PnP PowerShell: Register an Entra ID applicationhttps://pnp.github.io/powershell/articles/registerapplication.html
Connect-PnPOnline referencehttps://pnp.github.io/powershell/cmdlets/Connect-PnPOnline.html
Grant tenant-wide admin consent (Microsoft Learn)https://learn.microsoft.com/entra/identity/enterprise-apps/grant-admin-consent

Tags: #SharePointOnline #PnPPowerShell #PowerShell7 #EntraID #Microsoft365 #M365Admin

Friday, September 25, 2026

Assigning Microsoft Graph Permissions to a Managed Identity: Application vs Delegated

Assigning Microsoft Graph Permissions to a Managed Identity: Application vs Delegated (with ServiceMessage Examples)

You've enabled a system-assigned managed identity on an Azure Automation Account, and the runbook needs to read Microsoft 365 Message Center posts through Microsoft Graph. You open Entra ID → Enterprise applications → your identity → Permissions and there's no Add a permission button, and Grant admin consent is greyed out.

This article explains why, how to assign the permission with PowerShell, and why one permission people commonly ask for (ServiceMessageViewpoint.Write) fails with a confusing Edm.Guid error.


1. Why the portal can't do it

A managed identity is a service principal with no app registration behind it. In the portal, the API permissions blade (where you click Add a permission → Microsoft Graph) exists only on app registrations. The enterprise app Permissions blade just shows what has already been granted.

To give a managed identity Graph permissions, you create the grant directly on its service principal:

Permission typeGraph object createdPowerShell cmdletWorks for managed identity at runtime?
Application (app role)appRoleAssignmentNew-MgServicePrincipalAppRoleAssignment✅ Yes
Delegated (scope)oauth2PermissionGrantNew-MgOauth2PermissionGrant❌ No, a managed identity has no signed-in user

You can do this with PowerShell, the Azure CLI (az rest), or Graph Explorer. All three call the same Graph API.


2. Know your permission before you assign it

The service message permissions are a good example of how application and delegated permissions differ. From the Microsoft Graph permissions reference:

PermissionApplicationDelegatedPurpose
ServiceMessage.Read.All✅ 1b620472-6534-4fe6-9df2-4680e8aa28ec✅Read Message Center posts
ServiceHealth.Read.All✅ 79c261e0-fe76-4144-aad5-bdc68fbe4037✅Read service health and incidents
ServiceMessageViewpoint.Write❌ none✅Mark posts read, archived or favourite for the signed-in user

Two things to note:

  • There is no ServiceMessage.ReadWrite.All. For app-only automation, ServiceMessage.Read.All is the highest service message permission available.
  • ServiceMessageViewpoint.Write is delegated only. Read, archived and favourite status is stored per user, so Graph doesn't offer it for app-only access.

Tip: Permission names are the same in both lists, but their GUIDs differ. Always look up the ID from the correct collection: AppRoles for application permissions, Oauth2PermissionScopes for delegated ones.


3. Prerequisites

ItemRequirement
RoleGlobal Administrator or Privileged Role Administrator
ModulesMicrosoft.Graph.Applications, Microsoft.Graph.Identity.SignIns
IdentityManaged identity enabled on the resource (e.g. Automation Account → Identity → System assigned → On)

4. Assign an application permission (the one your runbook can use)

Run these blocks one at a time. Replace aa-automation-demo with your resource name.

# 1. Install and import the module (first time only)
Install-Module Microsoft.Graph.Applications -Scope CurrentUser -Force -AllowClobber
Import-Module Microsoft.Graph.Applications

# 2. Sign in
Connect-MgGraph -Scopes "Application.Read.All","AppRoleAssignment.ReadWrite.All" -NoWelcome

# 3. Get the managed identity's service principal
$miName = "aa-automation-demo"
$miSp = Get-MgServicePrincipal -Filter "displayName eq '$miName'"
$miSp | Select-Object DisplayName, Id, ServicePrincipalType     # expect: ManagedIdentity

# 4. Get the Microsoft Graph service principal
$graphSp = Get-MgServicePrincipal -Filter "appId eq '00000003-0000-0000-c000-000000000000'"

# 5. Find the application permission (app role)
$role = $graphSp.AppRoles | Where-Object { $_.Value -eq "ServiceMessage.Read.All" -and $_.AllowedMemberTypes -contains "Application" }
$role | Select-Object Value, Id                                  # must return a row

# 6. Check whether it's already assigned
$existing = Get-MgServicePrincipalAppRoleAssignment -ServicePrincipalId $miSp.Id -All |
    Where-Object { $_.ResourceId -eq $graphSp.Id -and $_.AppRoleId -eq $role.Id }

# 7. Assign it
if (-not $role) {
    Write-Warning "Not an application permission. Check the name, or use the delegated section."
} elseif ($existing) {
    Write-Host "Already assigned" -ForegroundColor Yellow
} else {
    New-MgServicePrincipalAppRoleAssignment -ServicePrincipalId $miSp.Id -PrincipalId $miSp.Id -ResourceId $graphSp.Id -AppRoleId $role.Id
    Write-Host "Assigned $($role.Value)" -ForegroundColor Green
}

# 8. Check the result
Get-MgServicePrincipalAppRoleAssignment -ServicePrincipalId $miSp.Id -All |
    Where-Object { $_.ResourceId -eq $graphSp.Id } |
    ForEach-Object { $rid = $_.AppRoleId; ($graphSp.AppRoles | Where-Object { $_.Id -eq $rid }).Value }

Disconnect-MgGraph

Refresh Enterprise applications → your identity → Permissions in the portal. The permission shows with type Application.


5. The Edm.Guid error explained

If you put ServiceMessageViewpoint.Write into step 5 above, you'll get:

New-MgServicePrincipalAppRoleAssignment : Cannot convert the literal '' to the expected type 'Edm.Guid'.
Status: 400 (BadRequest)
ErrorCode: Request_BadRequest
SymptomCause
Step 5 returns nothingThe permission isn't in $graphSp.AppRoles because it's delegated-only
$role.Id is emptyGraph receives appRoleId: ""
Edm.Guid 400 errorAn empty string can't be converted to a GUID

To confirm which list a permission belongs to:

# Application permissions
$graphSp.AppRoles | Where-Object { $_.Value -like "ServiceMessage*" } | Select-Object Value, Id

# Delegated permissions
$graphSp.Oauth2PermissionScopes | Where-Object { $_.Value -like "ServiceMessage*" } | Select-Object Value, Id

6. Granting a delegated permission to a managed identity (and why it won't help)

You can grant a delegated scope to a managed identity's service principal with an admin-consent (AllPrincipals) oauth2PermissionGrant. It will then appear on the Permissions blade with type Delegated:

Import-Module Microsoft.Graph.Identity.SignIns
Connect-MgGraph -Scopes "Application.Read.All","DelegatedPermissionGrant.ReadWrite.All" -NoWelcome

$miSp    = Get-MgServicePrincipal -Filter "displayName eq 'aa-automation-demo'"
$graphSp = Get-MgServicePrincipal -Filter "appId eq '00000003-0000-0000-c000-000000000000'"
$scopeName = "ServiceMessageViewpoint.Write"

$grant = Get-MgOauth2PermissionGrant -All |
    Where-Object { $_.ClientId -eq $miSp.Id -and $_.ResourceId -eq $graphSp.Id -and $_.ConsentType -eq "AllPrincipals" }

if (-not $grant) {
    New-MgOauth2PermissionGrant -BodyParameter @{
        clientId    = $miSp.Id
        consentType = "AllPrincipals"
        resourceId  = $graphSp.Id
        scope       = $scopeName
    }
} elseif (($grant.Scope -split " ") -contains $scopeName) {
    Write-Host "Already granted" -ForegroundColor Yellow
} else {
    Update-MgOauth2PermissionGrant -OAuth2PermissionGrantId $grant.Id -Scope (($grant.Scope.Trim() + " " + $scopeName).Trim())
}

Disconnect-MgGraph

But the runbook still can't use it. A managed identity always gets an app-only token, and app-only tokens contain roles (application permissions), never scp (delegated scopes). Calls to markRead or archive from the runbook will still return 403.


7. Marking Message Center posts read or archived (the delegated way)

If you need viewpoint actions, run them as a signed-in admin:

Connect-MgGraph -Scopes "ServiceMessage.Read.All","ServiceMessageViewpoint.Write" -NoWelcome

$body = @{ messageIds = @("MC000001","MC000002") } | ConvertTo-Json
$base = "https://graph.microsoft.com/v1.0/admin/serviceAnnouncement/messages"

Invoke-MgGraphRequest -Method POST -Uri "$base/markRead" -Body $body -ContentType "application/json"
Invoke-MgGraphRequest -Method POST -Uri "$base/archive"  -Body $body -ContentType "application/json"
ActionEndpoint
Mark read / unread/markRead, /markUnread
Archive / unarchive/archive, /unarchive
Favourite / unfavourite/favorite, /unfavorite

Status changes apply only to the signed-in user. Other admins still see the posts unchanged.


8. Auditing: who added a permission, and how?

Every assignment is logged. Go to Enterprise applications → your identity → Audit logs and look for:

ActivityMeaning
Add app role assignment to service principalAn application permission was assigned
Add delegated permission grantA delegated scope was granted

The User-Agent field shows the tool that was used. For example, AZURECLI/… cloud-shell means Azure CLI in Cloud Shell, and a Graph PowerShell SDK agent string means Microsoft.Graph PowerShell.


9. Removing a permission

# Application permission
$assignment = Get-MgServicePrincipalAppRoleAssignment -ServicePrincipalId $miSp.Id -All |
    Where-Object { $_.AppRoleId -eq $role.Id }
Remove-MgServicePrincipalAppRoleAssignment -ServicePrincipalId $miSp.Id -AppRoleAssignmentId $assignment.Id

# Delegated grant (entire grant)
Remove-MgOauth2PermissionGrant -OAuth2PermissionGrantId $grant.Id

You can also use Review permissions on the enterprise app's Permissions blade.


10. Key takeaways

#Takeaway
1The portal can't add Graph permissions to a managed identity. Use PowerShell, the CLI or Graph Explorer
2Application permission → New-MgServicePrincipalAppRoleAssignment; delegated → New-MgOauth2PermissionGrant
3Look up permission IDs from AppRoles (application) or Oauth2PermissionScopes (delegated)
4Cannot convert the literal '' to 'Edm.Guid' usually means the permission isn't an application permission
5ServiceMessageViewpoint.Write is delegated only, so a managed identity can't use it at runtime
6Managed identity tokens are cached, so allow time before a new permission takes effect in a running job

References

Featured Post

Pin a SharePoint Site to the Microsoft Teams Left Rail (No Code)

This is a no-code, step-by-step guide. You package a SharePoint page as a Teams app, upload it, and pin it for everyone in your organisation...

Popular posts