DEV Community

Cover image for Azure Functions + PnP.Core + Managed Identity=💙
Kinga
Kinga

Posted on • Edited on

Azure Functions + PnP.Core + Managed Identity=💙

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
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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}" };
     }
}
Enter fullscreen mode Exit fullscreen mode

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
     });
});
Enter fullscreen mode Exit fullscreen mode

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
          });
     }
});
Enter fullscreen mode Exit fullscreen mode

⚠️ 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)

Collapse
 
adiu72 profile image
Adrian Stanisławski • Edited

I have tried this but ended up in getting exception
SharePoint Rest service exception
at PnP.Core.Services.BatchClient.ExecuteSharePointRestInteractiveAsync(Batch batch)
at PnP.Core.Services.BatchClient.ExecuteBatch(Batch batch)
at PnP.Core.Model.BaseDataModel
1.RequestAsync(ApiCall apiCall, HttpMethod method, String operationName)
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 :/

Collapse
 
kolhari profile image
C • Edited

@adiu72 Im getting the same error. Have you managed to solve the error?

Collapse
 
adiu72 profile image
Adrian Stanisławski

No… I gave up and moved on to new projects ;)

Thread Thread
 
kkazala profile image
Kinga • Edited

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:

"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?

This refers to API permissions (Sites.Selected) that you grant using New-MgServicePrincipalAppRoleAssignment.
Permissions granted to SPO site take effect immediately (at least for now, according to my tests)

Did you start with the project from PnP Samples?