Azure Functions + PnP.Core + Managed Identity=💙
Tags: azure, security, pnp, csharp
Published: 2022-11-13 15:56:30

> The sample is now available on [PnP Core SDK Samples](https://pnp.github.io/pnpcore/demos/Demo.AzFunction.ManagedIdentity/README.html)

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](https://pnp.github.io/).

If you came across [Granting access via Azure AD App-Only](https://learn.microsoft.com/en-us/sharepoint/dev/solution-guidance/security-apponly-azuread), 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](https://learn.microsoft.com/en-us/security/benchmark/azure/baselines/functions-security-baseline#im-2-manage-application-identities-securely-and-automatically) 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](https://github.com/pnp/pnpcore/blob/675933ce37140ccdcc7bb624072f63929789907c/samples/Demo.AzFunction.ManagedIdentity/Tools/ConfigureHelper.ps1#L2)
```powershell
$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](https://github.com/pnp/pnpcore/blob/675933ce37140ccdcc7bb624072f63929789907c/samples/Demo.AzFunction.ManagedIdentity/Tools/ConfigureHelper.ps1#L39).
```csharp
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](https://pnp.github.io/pnpcore/using-the-sdk/custom-authentication-provider.html).

#### 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](https://learn.microsoft.com/en-us/azure/storage/common/identity-library-acquire-token)

[Azure.Identity.ManagedIdentityCredential](https://learn.microsoft.com/en-us/dotnet/api/azure.identity.managedidentitycredential?view=azure-dotnet) _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](https://pnp.github.io/pnpcore/using-the-sdk/custom-authentication-provider.html)) are defined simply as `https://graph.microsoft.com` for Graph API and `https://<yourtenant>.sharepoint.com/sites/<yoursite>` for SharePoint API.

##### ManagedIdentityTokenProvider.cs
```csharp
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](https://learn.microsoft.com/en-us/azure/azure-functions/functions-dotnet-dependency-injection) which may be used to configure the application to [use](https://pnp.github.io/pnpcore/using-the-sdk/configuring%20authentication.html#configuring-pnp-core-sdk-to-use-the-configured-application) the PnP.Core and PnP.Core.Auth services. 
In this step you configure the Authentication Provider you want to use.
##### Startup.cs
```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](https://github.com/pnp/pnpcore/blob/675933ce37140ccdcc7bb624072f63929789907c/samples/Demo.AzFunction.ManagedIdentity/Tools/ConfigureHelper.ps1#L65) script. 

The correct authentication provider may be registered during dependency injection, based on the presence of the MSI_SECRET variable. 

### Startup.cs
```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
          });
     }
});
```

## 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](https://www.nsa.gov/Press-Room/News-Highlights/Article/Article/3215760/nsa-releases-guidance-on-how-to-protect-against-software-memory-safety-issues/)