Skip to content

Commit

Permalink
Feature #3029 : support for Azure AD Workload identity
Browse files Browse the repository at this point in the history
  • Loading branch information
Gautam Sheth authored and KoenZomers committed Sep 3, 2023
1 parent 4e857d7 commit 227f9c9
Show file tree
Hide file tree
Showing 10 changed files with 258 additions and 30 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,8 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/).
- Added `Remove-PnPFileSharingLink` and `Remove-PnPFolderSharingLink` cmdlets to delete sharing links associated with files and folders. [#3200](https://github.com/pnp/powershell/pull/3200)
- Added `Get-PnPUnfurlLink` cmdlet to support unfurling a link to get more information about the link. [#3224](https://github.com/pnp/powershell/pull/3224)
- Added `ListsShowHeaderAndNavigation` parameter to always show lists with site elements intact when specified in `Set-PnPTenantSite` and `Set-PnPSite` cmdlets. [#3233](https://github.com/pnp/powershell/pull/3233)
- Added `-AzureADWorkloadIdentity` parameter to `Connect-PnPOnline` cmdlet to support Azure AD Workload Identity authentication. [#3097](https://github.com/pnp/powershell/pull/3097)
- Added Managed identity support for Power Platform related cmdlets. [#3097](https://github.com/pnp/powershell/pull/3097)

### Fixed

Expand Down
42 changes: 38 additions & 4 deletions documentation/Connect-PnPOnline.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,18 @@ Connect-PnPOnline [-ReturnConnection] [-Url] <String> [-EnvironmentVariable] [-C
[-TransformationOnPrem] [-ValidateConnection] [-MicrosoftGraphEndPoint <string>] [-AzureADLoginEndPoint <string>] [-Connection <PnPConnection>]
```

### Azure AD Workload Identity
```powershell
Connect-PnPOnline [-ReturnConnection] [-ValidateConnection] [-Url] <String>
[-AzureADWorkloadIdentity] [-Connection <PnPConnection>]
```

### Azure AD Workload Identity
```powershell
Connect-PnPOnline [-ReturnConnection] [-ValidateConnection] [-Url] <String>
[-AzureADWorkloadIdentity] [-Connection <PnPConnection>]
```

## DESCRIPTION
Connects to a SharePoint site or another API and creates a context that is required for the other PnP Cmdlets.
See https://pnp.github.io/powershell/articles/connecting.html for more information on the options to connect.
Expand Down Expand Up @@ -262,6 +274,14 @@ Connect-PnPOnline -Url contoso.sharepoint.com -AzureEnvironment Custom -Microsof
```

Use this method to connect to a custom Azure Environment. You can also specify the `MicrosoftGraphEndPoint` and `AzureADLoginEndPoint` parameters if applicable. If specified, then these values will be used to make requests to Graph and to retrieve access token.

### EXAMPLE 18
```powershell
Connect-PnPOnline -Url contoso.sharepoint.com -AzureADWorkloadIdentity
```

This example uses Azure AD Workload Identity to retrieve access tokens. For more information about this, please refer to this article, [Azure AD Workload Identity](https://azure.github.io/azure-workload-identity/docs/introduction.html). We are following the guidance mentioned in [this sample](https://github.com/Azure/azure-workload-identity/blob/main/examples/msal-net/akvdotnet/TokenCredential.cs) to retrieve the access tokens.

## PARAMETERS

### -AccessToken
Expand Down Expand Up @@ -524,7 +544,7 @@ Returns the connection for use with the -Connection parameter on cmdlets. It wil
```yaml
Type: SwitchParameter
Parameter Sets: Credentials, SharePoint ACS (Legacy) App Only, App-Only with Azure Active Directory, App-Only with Azure Active Directory using a certificate from the Windows Certificate Management Store by thumbprint, SPO Management Shell Credentials, PnP Management Shell / DeviceLogin, Web Login for Multi Factor Authentication, Interactive for Multi Factor Authentication, Access Token, Environment Variable
Parameter Sets: Credentials, SharePoint ACS (Legacy) App Only, App-Only with Azure Active Directory, App-Only with Azure Active Directory using a certificate from the Windows Certificate Management Store by thumbprint, SPO Management Shell Credentials, PnP Management Shell / DeviceLogin, Web Login for Multi Factor Authentication, Interactive for Multi Factor Authentication, Access Token, Environment Variable, Azure AD Workload Identity
Aliases:

Required: False
Expand Down Expand Up @@ -585,10 +605,10 @@ The Url of the site collection or subsite to connect to, i.e. tenant.sharepoint.
```yaml
Type: String
Parameter Sets: Credentials, SharePoint ACS (Legacy) App Only, App-Only with Azure Active Directory, App-Only with Azure Active Directory using a certificate from the Windows Certificate Management Store by thumbprint, SPO Management Shell Credentials, PnP Management Shell / DeviceLogin, Web Login for Multi Factor Authentication, Interactive for Multi Factor Authentication, Access Token, Environment Variable
Parameter Sets: Credentials, SharePoint ACS (Legacy) App Only, App-Only with Azure Active Directory, App-Only with Azure Active Directory using a certificate from the Windows Certificate Management Store by thumbprint, SPO Management Shell Credentials, PnP Management Shell / DeviceLogin, Web Login for Multi Factor Authentication, Interactive for Multi Factor Authentication, Access Token, Environment Variable, Azure AD Workload Identity
Aliases:

Required: True (Except when using -ManagedIdentity)
Required: True (Except when using -ManagedIdentity and -AzureADWorkloadIdentity)
Position: 0
Default value: None
Accept pipeline input: True (ByValue)
Expand All @@ -600,7 +620,7 @@ When provided, the cmdlet will check to ensure the SharePoint Online site specif

```yaml
Type: SwitchParameter
Parameter Sets: Credentials, SharePoint ACS (Legacy) App Only, App-Only with Azure Active Directory, App-Only with Azure Active Directory using a certificate from the Windows Certificate Management Store by thumbprint, SPO Management Shell Credentials, PnP Management Shell / DeviceLogin, Web Login for Multi Factor Authentication, Interactive for Multi Factor Authentication, Access Token, Environment Variable
Parameter Sets: Credentials, SharePoint ACS (Legacy) App Only, App-Only with Azure Active Directory, App-Only with Azure Active Directory using a certificate from the Windows Certificate Management Store by thumbprint, SPO Management Shell Credentials, PnP Management Shell / DeviceLogin, Web Login for Multi Factor Authentication, Interactive for Multi Factor Authentication, Access Token, Environment Variable, Azure AD Workload Identity
Aliases:
Required: False
Expand Down Expand Up @@ -811,6 +831,20 @@ Accept pipeline input: False
Accept wildcard characters: False
```

### -AzureADWorkloadIdentity
Connects using the Azure AD Workload Identity.

```yaml
Type: SwitchParameter
Parameter Sets: Azure AD Workload Identity
Required: False
Position: Named
Default value: None
Accept pipeline input: False
Accept wildcard characters: False
```

## RELATED LINKS

[Microsoft 365 Patterns and Practices](https://aka.ms/m365pnp)
18 changes: 17 additions & 1 deletion src/Commands/Base/ConnectOnline.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ public class ConnectOnline : BasePSCmdlet
private const string ParameterSet_USERASSIGNEDMANAGEDIDENTITYBYAZURERESOURCEID = "User Assigned Managed Identity by Azure Resource Id";
private const string ParameterSet_INTERACTIVE = "Interactive login for Multi Factor Authentication";
private const string ParameterSet_ENVIRONMENTVARIABLE = "Environment Variable";
private const string ParameterSet_AZUREAD_WORKLOAD_IDENTITY = "Azure AD Workload Identity";

private const string SPOManagementClientId = "9bc3ab49-b65d-410a-85ad-de819febfddc";
private const string SPOManagementRedirectUri = "https://oauth.spops.microsoft.com/";
Expand All @@ -56,6 +57,7 @@ public class ConnectOnline : BasePSCmdlet
[Parameter(Mandatory = false, ParameterSetName = ParameterSet_USERASSIGNEDMANAGEDIDENTITYBYCLIENTID)]
[Parameter(Mandatory = false, ParameterSetName = ParameterSet_USERASSIGNEDMANAGEDIDENTITYBYPRINCIPALID)]
[Parameter(Mandatory = false, ParameterSetName = ParameterSet_USERASSIGNEDMANAGEDIDENTITYBYAZURERESOURCEID)]
[Parameter(Mandatory = false, ParameterSetName = ParameterSet_AZUREAD_WORKLOAD_IDENTITY)]
public SwitchParameter ReturnConnection;

[Parameter(Mandatory = false, ParameterSetName = ParameterSet_CREDENTIALS, ValueFromPipeline = true)]
Expand All @@ -68,6 +70,7 @@ public class ConnectOnline : BasePSCmdlet
[Parameter(Mandatory = false, ParameterSetName = ParameterSet_INTERACTIVE, ValueFromPipeline = true)]
[Parameter(Mandatory = false, ParameterSetName = ParameterSet_ACCESSTOKEN, ValueFromPipeline = true)]
[Parameter(Mandatory = false, ParameterSetName = ParameterSet_ENVIRONMENTVARIABLE, ValueFromPipeline = true)]
[Parameter(Mandatory = false, ParameterSetName = ParameterSet_AZUREAD_WORKLOAD_IDENTITY, ValueFromPipeline = true)]
public SwitchParameter ValidateConnection;

[Parameter(Mandatory = true, Position = 0, ParameterSetName = ParameterSet_CREDENTIALS, ValueFromPipeline = true)]
Expand All @@ -84,6 +87,7 @@ public class ConnectOnline : BasePSCmdlet
[Parameter(Mandatory = false, Position = 0, ParameterSetName = ParameterSet_USERASSIGNEDMANAGEDIDENTITYBYPRINCIPALID, ValueFromPipeline = true)]
[Parameter(Mandatory = false, Position = 0, ParameterSetName = ParameterSet_USERASSIGNEDMANAGEDIDENTITYBYAZURERESOURCEID, ValueFromPipeline = true)]
[Parameter(Mandatory = true, Position = 0, ParameterSetName = ParameterSet_ENVIRONMENTVARIABLE, ValueFromPipeline = true)]
[Parameter(Mandatory = false, Position = 0, ParameterSetName = ParameterSet_AZUREAD_WORKLOAD_IDENTITY, ValueFromPipeline = true)]
public string Url;

[Parameter(Mandatory = false, ParameterSetName = ParameterSet_CREDENTIALS)]
Expand All @@ -94,6 +98,7 @@ public class ConnectOnline : BasePSCmdlet
[Parameter(Mandatory = false, ParameterSetName = ParameterSet_DEVICELOGIN)]
[Parameter(Mandatory = false, ParameterSetName = ParameterSet_INTERACTIVE)]
[Parameter(Mandatory = false, ParameterSetName = ParameterSet_ENVIRONMENTVARIABLE)]
[Parameter(Mandatory = false, ParameterSetName = ParameterSet_AZUREAD_WORKLOAD_IDENTITY)]
public PnPConnection Connection = PnPConnection.Current;

[Parameter(Mandatory = false, ParameterSetName = ParameterSet_CREDENTIALS)]
Expand Down Expand Up @@ -204,7 +209,6 @@ public class ConnectOnline : BasePSCmdlet
[Parameter(Mandatory = true, ParameterSetName = ParameterSet_USERASSIGNEDMANAGEDIDENTITYBYCLIENTID)]
[Parameter(Mandatory = true, ParameterSetName = ParameterSet_USERASSIGNEDMANAGEDIDENTITYBYPRINCIPALID)]
[Parameter(Mandatory = true, ParameterSetName = ParameterSet_USERASSIGNEDMANAGEDIDENTITYBYAZURERESOURCEID)]

public SwitchParameter ManagedIdentity;

[Parameter(Mandatory = true, ParameterSetName = ParameterSet_USERASSIGNEDMANAGEDIDENTITYBYPRINCIPALID)]
Expand Down Expand Up @@ -257,6 +261,9 @@ public class ConnectOnline : BasePSCmdlet
[Parameter(Mandatory = false, ParameterSetName = ParameterSet_ENVIRONMENTVARIABLE)]
public string AzureADLoginEndPoint;

[Parameter(Mandatory = true, ParameterSetName = ParameterSet_AZUREAD_WORKLOAD_IDENTITY)]
public SwitchParameter AzureADWorkloadIdentity;

protected override void ProcessRecord()
{
cancellationTokenSource = new CancellationTokenSource();
Expand Down Expand Up @@ -346,6 +353,9 @@ protected void Connect(ref CancellationToken cancellationToken)
case ParameterSet_ENVIRONMENTVARIABLE:
newConnection = ConnectEnvironmentVariable();
break;
case ParameterSet_AZUREAD_WORKLOAD_IDENTITY:
newConnection = ConnectAzureADWorkloadIdentity();
break;
}

// Ensure a connection instance has been created by now
Expand Down Expand Up @@ -724,6 +734,12 @@ private PnPConnection ConnectEnvironmentVariable(InitializationType initializati
return null;
}

private PnPConnection ConnectAzureADWorkloadIdentity()
{
WriteVerbose("Connecting using Azure AD Workload Identity");
return PnPConnection.CreateWithAzureADWorkloadIdentity(this, Url, TenantAdminUrl);
}

#endregion

#region Helper methods
Expand Down
24 changes: 21 additions & 3 deletions src/Commands/Base/PnPAzureManagementApiCmdlet.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
using System.Management.Automation;
using System;
using System.Management.Automation;
using System.Net.Http;
using Microsoft.SharePoint.Client;
using PnP.PowerShell.Commands.Model;

namespace PnP.PowerShell.Commands.Base
{
Expand All @@ -15,10 +18,25 @@ public string AccessToken
{
get
{
if (Connection?.Context != null)
if (Connection != null)
{
return TokenHandler.GetAccessToken(this, "https://management.azure.com/.default", Connection);
if (Connection.ConnectionMethod == ConnectionMethod.ManagedIdentity)
{
return TokenHandler.GetManagedIdentityTokenAsync(this, Connection.HttpClient, "https://management.azure.com", Connection.UserAssignedManagedIdentityObjectId, Connection.UserAssignedManagedIdentityClientId, Connection.UserAssignedManagedIdentityAzureResourceId).GetAwaiter().GetResult();
}
else if (Connection.ConnectionMethod == ConnectionMethod.AzureADWorkloadIdentity)
{
return TokenHandler.GetAzureADWorkloadIdentityTokenAsync(this, "https://management.azure.com/.default").GetAwaiter().GetResult();
}
else
{
if (Connection.Context != null)
{
return TokenHandler.GetAccessToken(this, "https://management.azure.com/.default", Connection);
}
}
}
WriteVerbose("Unable to acquire token for resource: https://management.azure.com/");
return null;
}
}
Expand Down
56 changes: 55 additions & 1 deletion src/Commands/Base/PnPConnection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -615,6 +615,49 @@ internal static PnPConnection CreateWithInteractiveLogin(Uri uri, string clientI
}
}

/// <summary>
/// Creates a PnPConnection using a Azure AD Workload Identity
/// </summary>
/// <param name="cmdlet">PowerShell instance hosting this execution</param>
/// <param name="url">Url to the SharePoint Online site to connect to</param>
/// <param name="tenantAdminUrl">Url to the SharePoint Online Admin Center site to connect to</param>
/// <returns>Instantiated PnPConnection</returns>
internal static PnPConnection CreateWithAzureADWorkloadIdentity(Cmdlet cmdlet, string url, string tenantAdminUrl)
{
string defaultResource = "https://graph.microsoft.com/.default";
if (url != null)
{
var resourceUri = new Uri(url);
defaultResource = $"{resourceUri.Scheme}://{resourceUri.Authority}/.default";
}

cmdlet.WriteVerbose("Acquiring token for resource " + defaultResource);
var accessToken = TokenHandler.GetAzureADWorkloadIdentityTokenAsync(cmdlet, defaultResource).GetAwaiter().GetResult();

using (var authManager = new PnP.Framework.AuthenticationManager(new System.Net.NetworkCredential("", accessToken).SecurePassword))
{
PnPClientContext context = null;
ConnectionType connectionType = ConnectionType.O365;
if (url != null)
{
context = PnPClientContext.ConvertFrom(authManager.GetContext(url.ToString()));
context.ApplicationName = Resources.ApplicationName;
context.DisableReturnValueCache = true;
context.ExecutingWebRequest += (sender, e) =>
{
e.WebRequestExecutor.WebRequest.UserAgent = $"NONISV|SharePointPnP|PnPPS/{((AssemblyFileVersionAttribute)Assembly.GetExecutingAssembly().GetCustomAttribute(typeof(AssemblyFileVersionAttribute))).Version} ({System.Environment.OSVersion.VersionString})";
};
if (IsTenantAdminSite(context))
{
connectionType = ConnectionType.TenantAdmin;
}
}

var connection = new PnPConnection(context, connectionType, null, url != null ? url.ToString() : null, tenantAdminUrl, PnPPSVersionTag, InitializationType.AzureADWorkloadIdentity);

return connection;
}
}
#endregion

#region Constructors
Expand All @@ -636,6 +679,17 @@ private PnPConnection(ClientContext context,
{
InitializeTelemetry(context, initializationType);
var coreAssembly = Assembly.GetExecutingAssembly();

var connectionMethod = ConnectionMethod.Credentials;
if(initializationType == InitializationType.AzureADWorkloadIdentity)
{
connectionMethod = ConnectionMethod.AzureADWorkloadIdentity;
}
else if(initializationType == InitializationType.ManagedIdentity)
{
connectionMethod = ConnectionMethod.ManagedIdentity;
}

if (context != null)
{
Context = context;
Expand All @@ -650,7 +704,7 @@ private PnPConnection(ClientContext context,
{
Url = (new Uri(url)).AbsoluteUri;
}
ConnectionMethod = initializationType == InitializationType.ManagedIdentity ? ConnectionMethod.ManagedIdentity : ConnectionMethod.Credentials;
ConnectionMethod = connectionMethod;
ClientId = PnPManagementShellClientId;
}

Expand Down
33 changes: 21 additions & 12 deletions src/Commands/Base/PnPGraphCmdlet.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,22 +44,31 @@ public string AccessToken
{
get
{
if (Connection?.ConnectionMethod == ConnectionMethod.ManagedIdentity)
if (Connection != null)
{
WriteVerbose("Acquiring token for resource " + Connection.GraphEndPoint + " using Managed Identity");
var accessToken = TokenHandler.GetManagedIdentityTokenAsync(this, Connection.HttpClient, $"https://{Connection.GraphEndPoint}/", Connection.UserAssignedManagedIdentityObjectId, Connection.UserAssignedManagedIdentityClientId, Connection.UserAssignedManagedIdentityAzureResourceId).GetAwaiter().GetResult();

return accessToken;
}
else
{
if (Connection?.Context != null)
{
var accessToken = TokenHandler.GetAccessToken(this, $"https://{Connection.GraphEndPoint}/.default", Connection);
if (Connection.ConnectionMethod == ConnectionMethod.ManagedIdentity)
{
WriteVerbose("Acquiring token for resource " + Connection.GraphEndPoint + " using Managed Identity");
var accessToken = TokenHandler.GetManagedIdentityTokenAsync(this, Connection.HttpClient, $"https://{Connection.GraphEndPoint}/", Connection.UserAssignedManagedIdentityObjectId, Connection.UserAssignedManagedIdentityClientId, Connection.UserAssignedManagedIdentityAzureResourceId).GetAwaiter().GetResult();

return accessToken;
}
}
else if (Connection.ConnectionMethod == ConnectionMethod.AzureADWorkloadIdentity)
{
WriteVerbose("Acquiring token for resource " + Connection.GraphEndPoint + " using Azure AD Workload Identity");
var accessToken = TokenHandler.GetAzureADWorkloadIdentityTokenAsync(this, $"https://{Connection.GraphEndPoint}/.default").GetAwaiter().GetResult();

return accessToken;
}
else
{
if (Connection.Context != null)
{
var accessToken = TokenHandler.GetAccessToken(this, $"https://{Connection.GraphEndPoint}/.default", Connection);
return accessToken;
}
}
}
WriteVerbose("Unable to acquire token for resource " + Connection.GraphEndPoint);
return null;
}
Expand Down
Loading

0 comments on commit 227f9c9

Please sign in to comment.