Category Archives: Identity

Sharepoint Online and Azure Datafactory using Managed Identity

Let’s face it, Microsoft’s documentation on using Sharepoint as a data source (or sink) in ADF is pretty bad. And it doesn’t even describe how to use the Managed Identity of ADF, who still wants to register separate app credentials? Noooooo you don’t.

So here’s an example how to use Managed Identity to read a json file from a given SpO site, using minimal permissions given to ADF.

  1. Enable MI in datafactory, I’m assuming you know how to do this.
  2. Give ADF Sites.Selected Graph permissions, e.g. like this:
Param(
    [Parameter(Mandatory=$true)][String]$displayName="{NAMEOFADFINSTANCE}",
    [Parameter(Mandatory=$true)][String]$role="Sites.Selected"
)
Connect-AzureAD 
$Msi = (Get-AzureADServicePrincipal -Filter "displayName eq '$displayName'")
Start-Sleep -Seconds 10
$baseSPN = Get-AzureADServicePrincipal -Filter "appId eq '00000003-0000-0000-c000-000000000000'"
$AppRole = $baseSPN.AppRoles | Where-Object {$_.Value -eq $role -and $_.AllowedMemberTypes -contains "Application"}
New-AzureAdServiceAppRoleAssignment -ObjectId $Msi.ObjectId -PrincipalId $Msi.ObjectId -ResourceId $baseSPN.ObjectId -Id $AppRole.Id
$Msi.AppId
  1. Go to https://yourtenant.sharepoint.com/sites/yoursite/_api/site/id and copy the Edm Guid:
get sharepoint site ID / guid
  1. Go to https://developer.microsoft.com/en-us/graph/graph-explorer. Log in at the top right using a user with sufficient permissions, set the mode to POST, add the EDM guid in the URL and create the request body as follows (the id in the body can be found back in step 2, $Msi.AppId)
authorize managed identity on sharepoint site
  1. Create a REST linked service in ADF with Managed Identity auth:
create graph rest linked identity
  1. Create data source in ADF of type REST
  1. Replace the “uri” with the direct url of the file you want to read (or use an alternative method). Example uri:
    https://graph.microsoft.com/v1.0/sites/ee614b6d-de15-4101-1c12-14e4cd3186a9/drive/root:/General/Input_files/test.json:/content

Guest report & cleanup new features

My script / runbook to automatically report on stale Guests and clean them up has received some updates

  • Exclusion Groups

Guests in Exclusion groups will never be deleted, even if they are in an Inclusion Group

  • Inclusion Groups

Guests in Inclusion Groups will be deleted if they meet the age requirements, all guests not in an inclusion group will be ignored

  • ReadOnly mode

Will pretend to delete guests are per configured settings, but won’t actually delete anything

download from git: https://gitlab.com/Lieben/assortedFunctions/-/blob/master/get-AzureAdInactiveGuestUsers.ps1?ref_type=heads

Programmatically grant admin consent to a service principal

Most articles and e.g. az module commands allow you to do an admin consent on an application object.

However, Service Principals have the same option in the Azure Portal:

In my scenario I have control over both the hosting tenant of this multi-tenant app registration, so I could use the requiredResourceAccess property to read all Oauth2permissiongrants and approleAssignments from the source app registration to re-apply it to the service principal in the consuming tenant.

The result is similar to consenting through the admin portal but does not require user interaction / is fully headless, ideal for when you’re adding scopes/roles to an application and don’t want to have to do a manual reconsent in all managed tenants.

Here’s the code to to programmatic admin consent:

https://gitlab.com/Lieben/assortedFunctions/-/blob/master/grant-adminConsentForServicePrincipal.ps1

It requires DelegatedPermissionGrant.ReadWrite.All and AppRoleAssignment.ReadWrite.All graph permissions for the calling principal (user or application).

If you don’t have access to the source tenant (e.g. multi tenant), you can also simply create a hashtable with the required permissions (manual definition or export from the application manifest).

Dynamic membership rule for Teams Room accounts

Teams Room accounts are usually excluded from conditional access. To do so, they have to be in a security group, which of course we don’t want to do manually.

Most companies choose to use a naming standard and simply use that as a rule to create an exclusion group. This is easy to circumvent, I can create a guest user / get invited with the right name et voila zero CA policies!

A better way is to identify the accounts based on their assigned licenses, e.g. Teams Rooms Basic (6af4b3d6-14bb-4a2a-960c-6c902aad34f3). This, however, is not supported as an Azure AD group membership rule as this is stored in the AssignedLicenses property which will throw an “Unsupported Property” error.

The assignedPlans property however does contain the GUID we need.

The following Azure AD Group dynamic membership rule only matches users that have a Teams Room Basic, Teams Room Standard or Teams Room Pro license:

(
	(
		user.assignedPlans -any (
			assignedPlan.servicePlanId -eq "8081ca9c-188c-4b49-a8e5-c23b5e9463a8"
			-and 
			assignedPlan.capabilityStatus -eq "Enabled"
		)
	) -or 
	(
		user.assignedPlans -any (
			assignedPlan.servicePlanId -eq "ec17f317-f4bc-451e-b2da-0167e5c260f9"
			-and 
			assignedPlan.capabilityStatus -eq "Enabled"
		)
	) -or 
	(
		user.assignedPlans -any (
			assignedPlan.servicePlanId -eq "92c6b761-01de-457a-9dd9-793a975238f7"
			-and 
			assignedPlan.capabilityStatus -eq "Enabled"
		)
	)
) -and not (
	user.assignedPlans -all (assignedPlan.servicePlanId -eq "")
)

if you want to do something similar for other licenses, here are the options/combinations:

https://github.com/MicrosoftDocs/entra-docs/blob/main/docs/identity/users/licensing-service-plan-reference.md

Powershell Cert based authentication against the Graph API using a certificate from Keyvault

In automation scenarios it is common to use a service principal (app based) to work with the Graph API, or in my example, with PNP PowerShell against SharePoint (but both scenario’s work the same).

First, you’d need a client certificate, e.g. like this:

$folder = "c:\users\JosLieben\Desktop"
$cert=New-SelfSignedCertificate -Subject "CN=JOS" -CertStoreLocation "Cert:\CurrentUser\My"  -KeyExportPolicy Exportable -KeySpec Signature -HashAlgorithm "SHA256" -Provider "Microsoft Enhanced RSA and AES Cryptographic Provider"
Export-Certificate -Cert $cert -FilePath "$folder\jos.cer" 
Export-PfxCertificate -Password (ConvertTo-SecureString $clientCertPwd -AsPlainText -Force) -Cert $cert -FilePath "$folder\jos.pfx"

You’d then upload the .cer file as a certificate on your service principal to let Azure AD recognize your cert as a valid ‘password’ for your app registration.

Next you’d upload your .pfx file into Keyvault.

Finally, you can use Powershell to construct an access token for a given scope:

$tenantId = "YOURTENANTID"
$clientId = "YOURCLIENTID"
$scope = "https://graph.microsoft.com/.default" #or, e.g. https://$($tenantName)-admin.sharepoint.com/.default openid profile offline_access
$secret = Get-AzKeyVaultSecret -VaultName "YOURKEYVAULTNAME" -Name cippCert -AsPlainText
$clientCert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2 -ArgumentList @([Convert]::FromBase64String($secret),"",[System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::Exportable)

$header = @{
    alg = "RS256"
    typ = "JWT"
    x5t = [System.Convert]::ToBase64String(($clientCert.GetCertHash()))
} | ConvertTo-Json -Compress

$claimsPayload = @{
    aud = "https://login.microsoftonline.com/$tenantId/oauth2/token"
    exp = [math]::Round(((New-TimeSpan -Start ((Get-Date "1970-01-01T00:00:00Z" ).ToUniversalTime()) -End (Get-Date).ToUniversalTime().AddMinutes(2)).TotalSeconds),0)
    iss = $clientId
    jti = (New-Guid).Guid
    nbf = [math]::Round(((New-TimeSpan -Start ((Get-Date "1970-01-01T00:00:00Z" ).ToUniversalTime()) -End ((Get-Date).ToUniversalTime())).TotalSeconds),0)
    sub = $clientId
} | ConvertTo-Json -Compress

$headerjsonbase64 = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($header)).Split('=')[0].Replace('+', '-').Replace('/', '_')
$claimsPayloadjsonbase64 = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($claimsPayload)).Split('=')[0].Replace('+', '-').Replace('/', '_')

$preJwt = $headerjsonbase64 + "." + $claimsPayloadjsonbase64
$toSign = [System.Text.Encoding]::UTF8.GetBytes($preJwt)
$privateKey = $clientCert.PrivateKey
$alg = [Security.Cryptography.HashAlgorithmName]::SHA256
$padding = [Security.Cryptography.RSASignaturePadding]::Pkcs1
$signature = [Convert]::ToBase64String($privateKey.SignData($toSign,$alg,$padding)) -replace '\+','-' -replace '/','_' -replace '='

$jwt = $headerjsonbase64 + "." + $claimsPayloadjsonbase64 + "." + $signature

$Authbody = @{
    'tenant' = $tenantId
    'scope' = $scope
    'client_assertion_type' = 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer'
    'client_id'     = $clientId
    'grant_type'    = 'client_credentials'
    'client_assertion' = $jwt
}        

$accessToken = (Invoke-RestMethod -Method post -Uri "https://login.microsoftonline.com/$($tenantid)/oauth2/v2.0/token" -Body $Authbody -ErrorAction Stop).accesstoken

The token can then be used to used to call Graph. And an example that shows how to use a sharepoint scoped token for the Sharepoint PNP PowerShell moddule:

#use scope: https://$($tenantName)-admin.sharepoint.com/.default

connect-PnPOnline -Url "https://$($tenantName)-admin.sharepoint.com" -AccessToken $accessToken -ReturnConnection