The sample is now available on PnP Core SDK Samples
When implementing Azure Functions/Azure Runbooks, that work with SharePoint Online, you may use PnP.Core, which "provides a unified object model for working with SharePoint Online and Teams which is agnostic to the underlying APIs being called". PnP.Core.
If you came across Granting access via Azure AD App-Only, you already know that you should use Azure AD App-Only to authenticate your application to SharePoint Online.
But do you really have to use App Registration? Azure Functions and Runbooks both support Managed Identity which is the recommended approach to enhance authentication security.
Setting up app-only access using Managed Identity.
Managed Identity may be easily enabled using UI, PowerShell, CLI, or even Bicep templates. Unlike App Registration, you won't need to create client secrets or certificates, which also means you don't have to think about rotating them.
API Permissions
Granting API Permissions to the Managed Identity cannot be done using Azure Portal, but you may use PowerShell instead: Set-AzureADPermissions
$GraphAppId = "00000003-0000-0000-c000-000000000000" # Microsoft Graph
$SPOAppId = "00000003-0000-0ff1-ce00-000000000000" # SharePoint Online
#Retrieve the Azure AD Service Principal instance for the Microsoft Graph (00000003-0000-0000-c000-000000000000) or SharePoint Online (00000003-0000-0ff1-ce00-000000000000).
$servicePrincipal_Graph = Get-AzureADServicePrincipal -Filter "appId eq '$GraphAppId'"
$servicePrincipal_SPO = Get-AzureADServicePrincipal -Filter "appId eq '$SPOAppId'"
$SPN = Get-AzADServicePrincipal -Filter "displayName eq '$appDisplayName'"
Write-Host "App $appDisplayName created with client id: $($SPN.AppId)"
$permissionName = "Sites.Selected"
$appRole_GraphId = ($servicePrincipal_Graph.AppRoles | Where-Object { $_.AllowedMemberTypes -eq "Application" -and $_.Value -eq $permissionName }).Id
$appRole_SPOId = ($servicePrincipal_SPO.AppRoles | Where-Object { $_.AllowedMemberTypes -eq "Application" -and $_.Value -eq $permissionName }).Id
# Grant API Permissions
New-AzureAdServiceAppRoleAssignment -ObjectId $($SPN.Id) -PrincipalId $($SPN.Id) -ResourceId $($servicePrincipal_Graph.ObjectId) -Id $appRole_GraphId
New-AzureAdServiceAppRoleAssignment -ObjectId $($SPN.Id) -PrincipalId $($SPN.Id) -ResourceId $($servicePrincipal_SPO.ObjectId) -Id $appRole_SPOId
Always use Minimum Required Permissions. If your code needs access to a specific SPO site only, use
Sites.Selected
API Permissions only. Then, you will grant Read/Write or FullControl permissions to a specific SPO site only: Set-PnPSiteAccess.
if ($permission -ne 'FullControl' ) {
Grant-PnPAzureADAppSitePermission -AppId $appId -DisplayName $appName -Site $siteUrl -Permissions $Permissions
}
else {
Grant-PnPAzureADAppSitePermission -AppId $appId -DisplayName $appName -Site $siteUrl -Permissions Write
$PermissionId = Get-PnPAzureADAppSitePermission -AppIdentity $appId
Set-PnPAzureADAppSitePermission -Site $siteurl -PermissionId $(($PermissionId).Id) -Permissions FullControl
}
Authenticating with Managed Identity
PnP.Core authentication doesn't, as of the time of writing, natively support Managed Identity Authentication, and it's recommended to write a custom Authentication Provider.
ManagedIdentityTokenProvider
The custom ManagedIdentityTokenProvider
is using Azure.Identity
client library to acquire AccessToken.
"The Azure Identity client library simplifies the process of getting an OAuth 2.0 access token for authorization with Azure Active Directory (Azure AD) via the Azure SDK. The latest versions of the Azure Storage client libraries for .NET, Java, Python, JavaScript, and Go integrate with the Azure Identity libraries for each of those languages to provide a simple and secure means to acquire an access token for authorization of Azure Storage requests." Use the Azure Identity library to get an access token for authorization
Azure.Identity.ManagedIdentityCredential authenticates with an Azure managed identity in any hosting environment which supports managed identities. This credential defaults to using a system-assigned identity.
The scopes (GetRelevantScopes
in the sample) are defined simply as https://graph.microsoft.com
for Graph API and https://<yourtenant>.sharepoint.com/sites/<yoursite>
for SharePoint API.
ManagedIdentityTokenProvider.cs
public async Task<string> GetAccessTokenAsync(Uri resource, string[] scopes)
{
//...
// var credential = new DefaultAzureCredential();
var credential = new Azure.Identity.ChainedTokenCredential(
new ManagedIdentityCredential(),
new EnvironmentCredential()
);
var accessToken = await credential.GetTokenAsync(new Azure.Core.TokenRequestContext(scopes));
return accessToken.Token;
}
//....
private string[] GetRelevantScopes(Uri resourceUri)
{
if (resourceUri.ToString() == "https://graph.microsoft.com")
{
return new[] { $"{resourceUri}" };
}
else
{
string resource = $"{resourceUri.Scheme}://{resourceUri.DnsSafeHost}";
return new[] { $"{resource}" };
}
}
Dependency injection
Azure Functions supports dependency injection which may be used to configure the application to use the PnP.Core and PnP.Core.Auth services.
In this step you configure the Authentication Provider you want to use.
Startup.cs
builder.Services.AddPnPCore(options =>
{
var siteUrl= "https://<tenantname>.sharepoint.com/sites/<siteName>"
var authProvider = new ManagedIdentityTokenProvider();
// Set it as default
options.DefaultAuthenticationProvider = authProvider;
// Add a default configuration with the site configured in app settings
options.Sites.Add("Default",
new PnPCoreSiteOptions
{
SiteUrl = siteUrl,
AuthenticationProvider = authProvider
});
});
Debugging locally
When debugging your code, you obviously cannot use the Managed Identity of your Azure service. In this case you need to use the App Registration.
This App Registration, however, will only be registered in your Azure Development environment, and may be deleted as soon as the application is complete.
The creation of the AppRegistration and granting API permissions is presented in the Add-AppRegistration script.
The correct authentication provider may be registered during dependency injection, based on the presence of the MSI_SECRET variable.
Startup.cs
// When MSI is enabled for an App Service, two environment variables MSI_ENDPOINT and MSI_SECRET are available
public bool isMSI = !String.IsNullOrEmpty(Environment.GetEnvironmentVariable("MSI_SECRET"));
//If Managed Identity is configured
if (appConfig.isMSI )
{
var siteUrl= "https://<tenantname>.sharepoint.com/sites/<siteName>"
var authProvider = new ManagedIdentityTokenProvider();
// set it as default
options.DefaultAuthenticationProvider = authProvider;
// Add a default configuration with the site configured in app settings
options.Sites.Add("Default",
new PnPCoreSiteOptions
{
SiteUrl = siteUrl,
AuthenticationProvider = authProvider
});
}
else
{
Console.WriteLine("Local DEV using cert auth");
var appConfigCert = new AppConfigCert();
// Configure an authentication provider with certificate (Required for app only)
// App-only authentication against SharePoint Online requires certificate based authentication for calling the "classic" SharePoint REST/CSOM APIs. The SharePoint Graph calls can work with clientid+secret, but since PnP Core SDK requires both type of APIs (as not all features are exposed via the Graph APIs) you need to use certificate based auth.
var authProvider = new X509CertificateAuthenticationProvider(...);
// And set it as default
options.DefaultAuthenticationProvider = authProvider;
// Add a default configuration with the site configured in app settings
options.Sites.Add("Default",
new PnP.Core.Services.Builder.Configuration.PnPCoreSiteOptions
{
SiteUrl = siteUrl,
AuthenticationProvider = authProvider
});
}
});
⚠️ IMPORTANT
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?
Did you know?
NSA advises organizations to consider making a strategic shift from
programming languages that provide little or no inherent memory protection, such as
C/C++, to a memory safe language when possible. Some examples of memory safe
languages are C#, Go, Java, Ruby™, and Swift®.
NSA Releases Guidance on How to Protect Against Software Memory Safety Issues
Top comments (4)
I have tried this but ended up in getting exception
SharePoint Rest service exception
1.RequestAsync(ApiCall apiCall, HttpMethod method, String operationName)at PnP.Core.Services.BatchClient.ExecuteSharePointRestInteractiveAsync(Batch batch)
at PnP.Core.Services.BatchClient.ExecuteBatch(Batch batch)
at PnP.Core.Model.BaseDataModel
at PnP.Core.Services.PnPContextFactory.InitializeContextAsync(PnPContext context, PnPContextOptions options)
at PnP.Core.Services.PnPContextFactory.CreateAsync(Uri url, IAuthenticationProvider authenticationProvider, CancellationToken cancellationToken, PnPContextOptions options)
at PnP.Core.Services.PnPContextFactory.CreateAsync(Uri url, IAuthenticationProvider authenticationProvider, PnPContextOptions options)
ID3035: The request was not valid or is malformed.`
I have no idea what is wrong... If only those exceptions would tell anything :/
@adiu72 Im getting the same error. Have you managed to solve the error?
No… I gave up and moved on to new projects ;)
Let's have a look into it, shall we?
It still works for me, when using my old application, and when creating new one from the scratch.
Can you run
Get-PnPAzureADAppSitePermission -AppIdentity $appId -Site $siteUrl
and see what results you get?Please remember that after assigning permissions to managed identity, you need to wait:
Did you start with the project from PnP Samples?