Azure AD app registration and scope + roles modification (manifest) as application

As the ‘new’ Graph API does not support application credentials (client_credentials oauth2 flow) when working with most of the ServicePrincipal and Application parts of the Graph API, and I really did not want to work with a user account (background processes, MFA, etc), I had to work something out on some of the older (but still supported) API’s which I gleaned from Msft’s PS modules.

For anyone googling, the following code example allows you to create an azure ad application with serviceprincipal and allows you to modify the manifest of the application (e.g. here is the AppRoles and AppIdentifier).

$tenantId = "75d24247-6221-46a1-a651-530ae36dd399"
$clientId = "62d2235b-2ef6-4d70-b273-401c9eb450b3" #client ID (to call graph api with)
$clientSecret = "xxxxxx" #client secret

[void] [System.Reflection.Assembly]::LoadWithPartialName("System.Web")
$body = @{client_id=$clientId;client_secret=$clientSecret;resource='https://graph.windows.net/';grant_type='client_credentials'}
$headers = @{"Authorization" = "Bearer $((invoke-webrequest -uri "https://login.microsoftonline.com/$tenantId/oauth2/token" `
-Method POST -ContentType "application/x-www-form-urlencoded" -Body $body).Content | convertfrom-json | select access_token -ExpandProperty access_token)"
}

#finding an application or create it if it doesn't exist
$appName = "MyApplication"
$app = (Invoke-RestMethod -Method GET -UseBasicParsing -Uri "https://graph.windows.net/$tenantId/applications?api-version=1.6&`$filter=displayName eq %27$appName%27" -Headers $headers -ContentType "application/json").value
if(!$app){
    $body = [pscustomobject]@{
	    'displayName' = $appName
        'availableToOtherTenants' = $True
    }
    $app = Invoke-RestMethod -Method POST -UseBasicParsing -Uri "https://graph.windows.net/$tenantId/applications?api-version=1.6" -Headers $headers -Body ($body | ConvertTo-Json -Depth 100) -ContentType "application/json"
}

#check/correct the identifier URI of the application's published API
if($app[0].identifierUris -notcontains "api://mydomain/myApi"){
    $body = [PSCustomObject]@{
        "identifierUris" = @("api://mydomain/myApi")
    }
    Invoke-RestMethod -Method PATCH -UseBasicParsing -Uri "https://graph.windows.net/$tenantId/applications/$($app[0].objectId)?api-version=1.6" -ContentType "application/json" -Headers $headers -Body ($body | ConvertTo-Json -Depth 100)
}

#check if a certain approle is present, if not, add it
if(@($app[0].appRoles | Where{$_.displayName -eq "Access To My API"}).Count -eq 0){
    $body = [PSCustomObject]@{
        "appRoles" = @([PSCustomObject]@{
            "allowedMemberTypes" = @("Application","User")
            "description" = "Access To My API"
            "displayName" = "Access To My API"
            "id" = [Guid]::NewGuid()
            "isEnabled" = $True
            "value" = "Api.AccessRead"
            }
        )
    }
    Invoke-RestMethod -Method PATCH -UseBasicParsing -Uri "https://graph.windows.net/$tenantId/applications/$($app[0].objectId)?api-version=1.6" -ContentType "application/json" -Headers $headers -Body ($body | ConvertTo-Json -Depth 100)
}

#finding the serviceprincipal belonging to the application or creating it if it doesn't exist
$sp = (Invoke-RestMethod -Method GET -UseBasicParsing -Uri "https://graph.windows.net/$tenantId/servicePrincipals?api-version=1.6&`$filter=displayName eq %27$appName%27" -Headers $headers -ContentType "application/json").value
if(!$sp){
    ##Adding service principal to application instance
    $body = [pscustomobject]@{
	    'appId' = $app.appId
        'tags' = @("WindowsAzureActiveDirectoryIntegratedApp")
    }
    $sp = Invoke-RestMethod -Method POST -UseBasicParsing -Uri "https://graph.windows.net/$tenantId/servicePrincipals/?api-version=1.6" -Headers $headers -Body ($body | ConvertTo-Json -Depth 100) -ContentType "application/json"
}

Note that, for this code to work, you need to grant your application the Company Administrator role, like this:

	Connect-AzureAD
	$app = Get-AzureADServicePrincipal -SearchString "myapplication"
	$role = Get-AzureADDirectoryRole | Where-Object { $_.DisplayName -eq "Company Administrator" }
Add-AzureADDirectoryRoleMember -ObjectId $role.ObjectId -RefObjectId $app.ObjectId

Using runbooks interactively in PowerApps (Build your own app in < 10 minutes!)

Mostly, the users of my PowerShell scripts are themselves PowerShell users. Sometimes though, the audience is less tech-savvy. In this blog post (with my first EVER video tutorial!) I’ll show you how to give your users a super user friendly interface to your scripts: Microsoft PowerApps.

You’ll need a PowerApps trial or license to follow this tutorial.

When you use the Azure AD group that was created to publish your app to when it is ready for distribution, your users will automatically be granted the correct permissions in Azure to start a runbook, as PowerApps does not use its own identity when interacting with connectors, it impersonates the user identity.

The source code for the runbook is:

Param(
    [String]$searchParameter
)

$uri = "https://techcommunity.microsoft.com/t5/forums/searchpage/tab/message?advanced=false&amp;allow_punctuation=false&amp;q=$searchParameter"

Start-Sleep -s 2

Write-Output "Runbook started, searching for $searchParameter..."

$res = Invoke-WebRequest -Uri $uri -UseBasicParsing -Method GET -ErrorAction Stop

Start-Sleep -s 2

Write-Output "found some results, analyzing...."

$firstHit = $res.Links | where-object {$_.outerHTML -like "*lia-link-navigation*" -and $_.href -like "/t5/*"} | select href -First 1 -ExpandProperty href
$firstHit = "https://techcommunity.microsoft.com/$firstHit"

Start-Sleep -s 2

Write-Output "Retrieving first 100 characters of first result..."

$res = Invoke-WebRequest -Uri $firstHit -UseBasicParsing -Method GET -ErrorAction Stop
$excerpt = $res.Content.Substring(($res.Content.IndexOf("class=`"lia-message-body-content`"")+64),100) -Replace('<[^>]+>','')

Start-Sleep -s 2

Write-Output "Result:"
Write-Output $excerpt
write-Output ""
write-Output ""
write-Output ""
write-Output "source: $firstHit"

The app screen’s OnStart property’s function is:

Set(runbookOutput,Blank());Set(runbookJobId,Blank());Set(runbookActive,false);Set(runbookResult,Blank())

The search button’s function is:

Set(runbookResult,Blank());Set(runbookOutput,Blank());Set(runbookJobId,Blank());Set(runbookActive,true);Set(runbookJobId,'new-searchQuery'.Run(TextInput2.Text).jobid)

The status label’s function is:

If(IsBlank(runbookResult) && runbookActive = false," ",If(runbookActive,"Please wait for job to complete…",Concatenate("Job result: ",runbookResult)))

The timer OnTimerStart function is:

If(runbookActive && Len(runbookJobId) > 5,Set(runbookOutput,'get-searchQueryOutput'.Run(runbookJobId).joboutput))

The timer OnTimerEnd function is:

If(runbookActive && Len(runbookJobId) > 5,Set(runbookResult,'get-searchQueryStatus'.Run(runbookJobId).jobstatus));If(runbookResult = "Completed" Or runbookResult = "Suspended" Or runbookResult = "Stopped",Set(runbookActive,false));

Using Exchange Web Service with Powershell to remove calendar appointments without notification

While helping out a client recently something in their migration went wrong, I ran into an interesting challenge. Calendars of users were merged, many many times. The resulting duplicates that shouldn’t be in user’s calendars could be identified easily: the mailbox was neither the organiser nor invited to these calendar events.

But how to remove them? Graph won’t allow you to do so without notifying the recipients (leading, potentially, to thousands of confused users).

Luckily, EWS DOES allow us to do so, and if you ever need to work with EWS (Office 365 Exchange Online) using Powershell, this code sample could come in handy 🙂

Add-Type -Path "C:\Users\jos\Desktop\net35\Microsoft.Exchange.WebServices.dll"

$Service = [Microsoft.Exchange.WebServices.Data.ExchangeService]::new()
$Service.Credentials = [System.Net.NetworkCredential]::new("admin@onedrivemapper.onmicrosoft.com" , "yourpassword")
$Service.Url = "https://outlook.office365.com/EWS/Exchange.asmx"

$maxDaysIntoTheFuture = 365

function Remove-ObsoleteCalendarItems{
    Param(
        $primaryEmailAddress #eg: admin@onedrivemapper.onmicrosoft.com
    )

    $folderid= new-object Microsoft.Exchange.WebServices.Data.FolderId([Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::Calendar,$primaryEmailAddress)   
    $Calendar = [Microsoft.Exchange.WebServices.Data.Folder]::Bind($Service,$folderid)
    $Recurring = new-object Microsoft.Exchange.WebServices.Data.ExtendedPropertyDefinition([Microsoft.Exchange.WebServices.Data.DefaultExtendedPropertySet]::Appointment, 0x8223,[Microsoft.Exchange.WebServices.Data.MapiPropertyType]::Boolean); 
    $psPropset= new-object Microsoft.Exchange.WebServices.Data.PropertySet([Microsoft.Exchange.WebServices.Data.BasePropertySet]::FirstClassProperties)  
    $psPropset.Add($Recurring)
    $psPropset.RequestedBodyType = [Microsoft.Exchange.WebServices.Data.BodyType]::Text;

    #Define Date to Query 
    $currentDay = 0
    while($True){
        $StartDate = (Get-Date).AddDays($currentDay)
        $EndDate = $StartDate.AddDays(14)  
        $currentDay += 14

        if($currentDay -gt $maxDaysIntoTheFuture){
            break
        }

        $CalendarView = New-Object Microsoft.Exchange.WebServices.Data.CalendarView($StartDate,$EndDate,1000)    
        $fiItems = $service.FindAppointments($Calendar.Id,$CalendarView)
        if($fiItems.Items.Count -gt 0){
            $type = ("System.Collections.Generic.List"+'`'+"1") -as "Type"
            $type = $type.MakeGenericType("Microsoft.Exchange.WebServices.Data.Item" -as "Type")
            $ItemColl = [Activator]::CreateInstance($type)
            foreach($Item in $fiItems.Items){
                $ItemColl.Add($Item)
            } 
            [Void]$service.LoadPropertiesForItems($ItemColl,$psPropset)  
        }

        foreach($Item in $fiItems.Items){  
            if($Item.Organizer.Address -ne $primaryEmailAddress -and $Item.RequiredAttendees.Address -notcontains $primaryEmailAddress -and $Item.OptionalAttendees.Address -notcontains $primaryEmailAddress){
                $Item.RequiredAttendees.Clear() #this also works if no one is invited
                $Item.OptionalAttendees.Clear() #this also works if no one is invited
                $Item.Update([Microsoft.Exchange.WebServices.Data.ConflictResolutionMode]::AlwaysOverwrite,[Microsoft.Exchange.WebServices.Data.SendInvitationsOrCancellationsMode]::SendToNone)
                $Item.Delete([Microsoft.Exchange.WebServices.Data.DeleteMode]::MoveToDeletedItems)
                write-host "deleted item $($Item.Subject) without notifying recipients"
            }
        }
    }
}

The required EWS DLL’s can also be found in my git repository:

Delete User Profiles Older than a Specified Number of Days on System Restart through Intune

The good old Group Policy “Configuration\Policies\Administrative Templates\System\User Profiles\Delete User Profiles Older than a Specified Number of Days on System Restart ” isn’t part of Intune yet.

If you use shared devices in your environment, you can use below script to set the number of days after which a user profile is cleaned up on Windows 10 MDM / Intune managed.

It has to run under SYSTEM context or it won’t be allowed to write the right key.

Download: https://gitlab.com/Lieben/assortedFunctions/blob/master/set-CleanupUserProfilesAfterDays.ps1

Office 365, Azure, Enterprise Mobility and DevOps