How to Automate Windows 365 Cloud PC Last Login monitoring!

Microsoft

Automate Windows 365 Cloud PC Last Login monitoring!

(Windows 365, Azure Active Directory, Power Automate, MS Graph)

 

Contributors:

Juan José Guirola Sr. (Next Generation Endpoint GBB for Americas)

Bobby Chang (Power Platform GBB for Americas)

 

Enterprises of all sizes are adopting and aligning Windows 365 to solve several business-critical scenarios.  Organizations appreciate the simplicity of the solution, rapid deployment, and enhanced end user experience; offering the opportunity to include new solutions to their services catalog!

Part of the simplicity of Windows 365 is that its management plane is Microsoft Intune.  Leveraging the Windows 365 admin blade in Intune, administrators can perform the initial configuration of the service and perform on going monitoring of Cloud PCs deployed within the enterprise with several reports being made visible through the “Reports” blade, to include Device management, Endpoint Security, Endpoint Analytics, etc.  We have recently introduced a new type of analytical report – Cloud PC utilization report (preview) – which brings visibility to Cloud PCs with low usage.  This is a nice addition to the platform, and a much-needed report.

For some organizations, that level of reporting will suffice.  But if you are looking for a more custom report that aligns to the specific goals and needs of your organization, then keep reading.  This blog will describe how to use the Microsoft Power Platform to automate the reporting of Windows 365 based on your specific criteria and receive notifications via email when the criteria is met.

In our example, we are setting the criteria to report on Cloud PCs that have not been logged on to for 60 days or more.  Let’s get started.

Prerequisites

The following items are required to automate the process and deploy in a production environment: (For personal development and sandbox/testing scenario, you can use the Microsoft 365 Developer Plan and Power Apps Developer Plan).

Azure App Registration with the following permissions: CloudPC.Read.All. For enterprise production scenarios, we would recommend leveraging the Application Lifecycle Management (ALM) capabilities in Power Platform, in order to safely adopt future changes to your processes. However, this is outside of the scope of this blog post.

Register MS Graph in Azure AD

If you have followed our previous BLOG – How to automate Windows 365 Cloud PC self-service requests – you may have already performed these steps.  If so, please proceed to the next section of this BLOG.

Register MS Graph as an Enterprise application in Azure Active Directory.

  1. Log into the Azure portal with appropriate permissions for making application registrations. Global Administrator privileges will provide the permissions to make application registrations; there are other options by following the custom role details in this documentation Custom role permissions for app registration - Azure AD - Microsoft Entra | Microsoft Docs. In the Azure services portal, click Azure Active Directory > Azure Active Directory.

JJGuirola_0-1677715299567.png

 

Figure 1: A screenshot of the Azure Active Directory blade in the Azure services portal.

  1. Select App registrations in the left navigation menu.
  2. Click New registration.

 

  1. Give the application a name, select Single Tenant for the supported account type, and then click Register.

JJGuirola_1-1677715299572.png

 

Figure 2 : A screenshot of the Register an application screen, showing the details that need to be identified for the new application.

  1.       Note your Directory (tenant) ID and Application (client) ID GUIDs and then click on API Permissions.

 

JJGuirola_2-1677715299578.png

 

Figure 3: A screenshot of the recently created application overview with the Application (client) ID and Directory (tenant) ID details highlighted.

  1. Click API permissions in the left navigation menu.
    1. Click Add a Permission.
    2. Select Microsoft.Graph and choose Application permissions. Ensure the following permissions are added:
  • CloudPC.Read.All
  • User.Read
  • User.Read.All
  • Group.Read.All
  • Mail.Send (optional for sending messages via Graph
 
  JJGuirola_3-1677715299579.png

 


  • )

Figure 4: A screenshot of the Select permissions setup.

  1. Once the permissions have been added, click Grant consent.
  2. Click Certificates & secrets in the left navigation menu, and then click New client secret.
    1. Important! Note this secret key and store it somewhere safe, like a key vault. This key will only be visible upon creation. Once you navigate away, you will be unable to expose the key again and will have to generate a new key.

Create the Cloud PC Last Login Monitoring automation!

In this section, we will build the Power Automate flows that will orchestrate the Last Login monitoring reporting process. This decision flow illustrates the end-to-end process of retrieving Cloud PC attribute values from the Microsoft Graph leveraging the Windows 365 API and parse through the LastLoginResult value to compare against our criteria of 60 days or more.

JJGuirola_4-1677715299581.png

 

Figure 5: A flowchart depicting the process for reporting Cloud PC Last Login.

 

To begin, sign into Microsoft Power Automate with your Microsoft 365 organization credentials.

  1. From the left navigation menu, click + Create then:
    1. Click Automated cloud flow.
    2. Name the flow and choose the flow trigger, “Recurrence” from list.
    3. Click Create.
    4. Set your desired Interval.

JJGuirola_5-1677715299583.png

 

Figure 6: A screenshot that shows the Recurrence trigger.

  1. Click on + New step (To add variable for the UPN).
    1. In Choose an operation, type variable.
    2. Select Initialize variable from Actions.
    3. Type Init VARUPN details screen.
    4. Give it a name, e.g., VARUPN and select “String” as Type.
  2. Click + New step (To add variable for the “lastLoginResult” attribute value of the Cloud PC).
    1. Choose an operation, type variable.
    2. Select Initialize variable from Actions.
    3. Give it a name, e.g. lastLoginResult and select “String” as Type.
  3. Click on + New step (To add variable for the “Composed_LastLoginResult_Value” of the Cloud PC).
    1. Search for VAR in Choose an operation.
    2. Select Initialize variable.
    3. Give it a name (e.g. Composed_LastLoginResult) and select “String” as Type.
  4. Click on + New step (To add variable for CurrentDateTime).
    1. Choose an operation, type variable.
    2. Select Initialize variable from Actions.
    3. Give it a name (e.g., DateNow) and select “String” as Type.
    4. In the Value field, Add, Expression, in Fx type utcNow()
  5. Click on + New step (To add variable for DateDifference)
    1. Choose an operation, type variable.
    2. Select Initialize variable from Actions.
    3. Give it a name (e.g., DateDiff) and select “Integer” as Type.
  6. Click on + New step (To add variable for the “Criteria,” which in our example is 60 day +).
    1. Choose an operation, type variable.
    2. Select Initialize variable from Actions.
    3. Give it a name (e.g., More than 60 days) and select “String” as Type.

 

At this point, we need to determine the automated actions, based on the “LastLoginResult” value of the Cloud PC. This can be accomplished by parsing through each Cloud PC LastLoginRestult value and applying a “Condition” action.

 

Let’s add a GET step to the flow to gather Cloud PC attribute value:

  1. Click Add an action.
    1. Important! To add the control to perform Graph API calls against tenant to gather Cloud PC attribute value, search for HTTP.
    2. In the Method field, select GET. Under URI, set it up exactly as illustrated below:

 https://graph.microsoft.com/beta/deviceManagement/virtualEndpoint/cloudPCs? $select=userprincipalname,id,displayName,managedDeviceName,Status,imageDisplayName,lastModifiedDateTime,lastRemoteActionResult,lastLoginResult

 

  1. For Authentication, select Active Directory OAuth.
    1. Leave the authority as default.
    2. Enter your Tenant ID under Tenant, https://graph.microsoft.com under Audience, the AppID under Client ID, and the Secret in the Secret section.
      1.       For production scenarios, you should consider storing your secret in a Key Management solution, like Azure Key Vault
      2.       If you are using Azure Key Vault, then you can first add the Get Secret action from the pre-built Azure Key Vault connector (https://learn.microsoft.com/en-us/connectors/keyvault/#actions) then securely pass your Secret into this step of your automation -

JJGuirola_6-1677715299596.png

 

Figure 7: Example setup for Graph API controls to gather Cloud PC attribute value.

 

  1. Hide your Secret from the Power Automate run history
    1.       Click on the … to the right of the Power Automate HTTP action
    2.       Select Settings

JJGuirola_7-1677715299605.png

 

  •       Turn the toggles to On for “Secure Inputs” and “Secure Outputs” in order to not display your Secret in plain text on the logs or run history

JJGuirola_8-1677715299608.png

 

 

 

  1. Click Add an action, and search for “Parse JSON.”
    1. Under Parse JSON, select Body for the Content field and insert the body of the HTTP request response into the Schema field. Use the following schema:

JJGuirola_9-1677715299610.png

 

Figure 8: A screenshot of completed content and schema details for Parse JSON.

 

{

    "type": "object",

    "properties": {

        "@@odata.context": {

            "type": "string"

        },

        "value": {

            "type": "array",

            "items": {

                "type": "object",

                "properties": {

                    "userPrincipalName": {

                        "type": "string"

                    },

                    "managedDeviceName": {

                        "type": "string"

                    },

                    "id": {

                        "type": "string"

                    },

                    "displayName": {

                        "type": "string"

                    },

                    "imageDisplayName": {

                        "type": "string"

                    },

                    "status": {

                        "type": "string"

                    },

                    "lastModifiedDateTime": {

                        "type": "string"

                    },

                    "lastRemoteActionResult": {},

                    "lastLoginResult": {}

                },

                "required": [

                    "id",

                    "userPrincipalName",

                    "displayName",

                    "imageDisplayName",

                    "managedDeviceName",

                    "status",

                    "lastModifiedDateTime",

                    "lastRemoteActionResult",

                    "lastLoginResult"

                ]

            }

        }

    }

}

 

Note: You can also get this schema by using the Graph explorer to request from the same endpoint. Use the Generate from example button to generate the schema.

 

  1. Click Add action and search for “Apply to each.”
    1. In the Output field, select Value from our Parse JSON step.  Click Add an action and search for “Compose.”
    2. In the Compose step, enter rungraph for: {id}

JJGuirola_10-1677715299611.png

 

Figure 9: Compose control example.

  

  1. Click Add an action and search for “HTTP.”
    1. Configure the HTTP using the same variables for TenantID, APpID, and Secret, as in the previous HTTP action, but using the following URI:

 

https://graph.microsoft.com/beta/deviceManagement/virtualEndpoint/cloudPCs/@{items('Apply_to_each_2'...']}? $select=userprincipalname,id,displayName,managedDeviceName,Status,imageDisplayName,lastModifiedDateTime,lastLoginResult

Example:

JJGuirola_11-1677715299615.png

 

Figure 10: Example setup for retrieving lastLoginResult value for each specific Cloud PC.

  1. Follow the same steps as previously outlined to hide your Secrets from the run history (Click on … > Select Settings > Turn toggles to On for “Secure Inputs” and “Secure Outputs”)

 

  1. Click Add an action, search for “Parse JSON.”
    1. Select Body for the Content field and insert the following into the Schema field:

{

    "type": "object",

    "properties": {

        "@@odata.context": {

            "type": "string"

        },

        "value": {

            "type": "array",

            "items": {

                "type": "object",

                "properties": {

                    "userPrincipalName": {

                        "type": "string"

                    },

                    "managedDeviceName": {

                        "type": "string"

                    },

                    "id": {

                        "type": "string"

                    },

                    "displayName": {

                        "type": "string"

                    },

                    "imageDisplayName": {

                        "type": "string"

                    },

                    "status": {

                        "type": "string"

                    },

                    "lastModifiedDateTime": {

                        "type": "string"

                    },

                    "lastRemoteActionResult": {},

                    "lastLoginResult": {}

                },

                "required": [

                    "id",

                    "userPrincialName",

                    "displayName",

                    "imageDisplayName",

                    "managedDeviceName",

                    "status",

                    "lastModifiedDateTime",

                    "lastRemoteActionResult",

                    "lastLoginResult"

                ]

            }

        }

    }

}

 

   

 

 

JJGuirola_12-1677715299617.png

 

Figure 11: A screenshot of the Parse JSON schema.

 

  1. Click Add an action and search for “Condition”.
    1. Select lastLoginResult under Parse JSON for the value.
    2. Select is not equal to for condition.
    3. Under Add dynamic content, type null as the expression.

JJGuirola_13-1677715299618.png

 

Figure 12: lastLoginResult Condition Expression.

 

At this point we are ready to add logic to the flow based on meeting the criteria of the condition.

 

If yes -

  1. Click Add an action and search for “Set variable”.
    1. Insert a Name (e.g. lastLoginResult)
    2. For Value, select lastLoginResult under Parse JSON2 as the Dynamic content
  2. Click Add an action and search for “Compose”.
    1. Select Compose as the Data Operation.
    2. Enter the following expression in Inputs field:

split(variables('lastLoginResult-Value'),'"')

 

  1. Click Add an action and search for “Compose”.
    1. Select Compose as the Data Operation.
    2. Enter the following expression in Inputs field:

outputs('Compose_3')?[3]

 

  1. Click Add an action and search for “Set Variable”.
    1. Select Set Variable.
    2. Give it a Name (e.g. Composed_LastLoginResult_Value)
    3. Click on Add dynamic content to add Value
    4. Select Outputs under Compose 4 Step.

 

  1. Click Add an action and search for “Set Variable”.
    1. Select Set Variable.
    2. Give it a Name (e.g. DateDiff)
    3. Click on Add dynamic content to add Value
    4. Select Expression and enter the following expression

 

div(sub(ticks(variables('DateNow')),ticks(variables('Composed_LastLoginResult_Value'))),864000000000)

 

Now that we’ve been able to extract the proper number of days since lastlogin, let’s send out the email notifications.

 

  1. Click Add an action and search for “Condition”.
    1. Select DateDiff variable as the value.
    2. Select is greater than as condition.
    3. Enter 60 as the value (or whatever aligns to your criteria)
  2. Click Add an action and search for “Send an email”.
    1. Select Send an email v2.
    2. Provide a name (e.g. More than 60 Days Email notification)
    3. Enter the necessary information to the fields as necessary for your environment.  See below as an example.

JJGuirola_14-1677715299625.png

 

Figure 13: Sample email template.

 

Once you’re past the Apply to Each scope,

  1. Click Add an action, and search for “Terminate.”
    1. Set the Status to Succeeded.
  2. Return to the initial criteria Conditon  to setup the the If no process. Scroll up in the workflow to access this setup.
    1. Click Add an action and search for “Set variable.”
    2. Select Set Variable.
    3. Enter a name (e.g. lastLoginResult-Value)
    4. Value enter Blank

 

The entire flow process should look like the image below.

 

JJGuirola_15-1677715299627.png

 

 

JJGuirola_16-1677715299631.png

 

Once you’ve completed adding in steps to your automation flow, you’re ready to test the solution. You can run a manual test or wait till the schedule task kicks off.  Finally, you should receive an email like the one below:

 

 

Admin Email Notification

 

JJGuirola_17-1677715299636.png

 

 

NOTE: 

WE WILL UPDATE THIS ARTICLE IN THE NEAR FUTURE TO INCLUDE THE ADDITION OF UPDATING A TABLE IN POWER APPS AND A FRONT FACING APPLICATION WHERE ADMINS CAN TAKE ACTION TO RECLAIM WINDOWS 365 LICENSE!  STAY TUNED!!!

 

Continue the conversation by joining us in the Microsoft 365 Tech Community! Whether you have product questions or just want to stay informed with updates on new releases, tools, and blogs, Microsoft 365 Tech Community is your go-to resource to stay connected. 

12 Replies
hello. Thank you for the article. We are not licensed for Power Automate, so I am attempt to do these steps in PowerShell. when I tested the connections in the Graph Explorer, I do not see the lastRemoteActionResult and lastLoginResult properties on the https://graph.microsoft.com/beta/deviceManagement/virtualEndpoint/cloudPCs?

In Graph Explorer, when i run a query of all Cloud PCs these properties are not returned. I tried running
https://graph.microsoft.com/beta/deviceManagement/virtualEndpoint/cloudPCs?$select=userprincipalname...

and the lastLoginResult and lastRemoteActionResult is null for all items.

do you have any advice on how to pull the information?
Hi Dawn. Pay attention to the steps 4 - 5 right under Figure 10 of the article. To get those values for each Cloud PC, you will have to Parse through each Cloud PC and extract that value to a variable. That value is returned as NULL when doing a bulk request, but if you isolate the query to specific Cloud PC id, you will get the necessary value. Hope that helps.
Hello! I have never used Power Automate, but I follow step by step and I end up with a different flow than you show on your example. When I try to add an action after the first condition in "If Yes", it puts it inside of another "Apply to each". Besides this, I wanted to see if there is a ways to add another point besides the "composed last log in". I would like to include, if possible, the TotalUsageInHour less than X, like the report you get from Intune>Devices>Cloud PC Performance

@JJGuirola 

When I try to enter URI:  https://graph.microsoft.com/beta/deviceManagement/virtualEndpoint/cloudPCs? $select=userprincipalname,id,displayName,managedDeviceName,Status,imageDisplayName,lastModifiedDateTime,lastRemoteActionResult,lastLoginResult 

I get error message saying: Enter a valid uri. What am I doing wrong.Skjermbilde 2023-09-05 132618.png

looks like you are copying and pasting the URI directly from the document, which is good and looks correct. If you haven't already, I would suggest trying that URI in MS Graph explorer: https://developer.microsoft.com/en-us/graph/graph-explorer. See if you are able to retrieve the necessary information using the graph directly.
following the steps as documented here should allow you to get the flow as presented in the doc. As for the TotalUsageinHour scenario, short answer is yes it should with the right GraphAPI and parsing calls. However, considering that we have this now natively available in Intune, I recommend leveraging the Intune admin console for that level of granularity. If you feel that there is still a need to have it as part of a custom solution, I would be curious to know your reasons. Thanks.

@JJGuirola 


I used this article sort of as a starter to populate a sharepoint list with information. But total active usage like in the utilization report would be nice. I was inspecting the graph calls for this report, but it doesn't seem as easily available as some of these other api calls.

It's a POST call to https://graph.microsoft.com/beta/deviceManagement/virtualEndpoint/reports/getTotalAggregatedRemoteCo...

Maybe you can tell me where I can find the TotalUsageInHour attribute?

The next Windows feature update is ready and includes reliability, performance, and security improvements.
plese solve my problem
Hello, could it be that something has changed in the meantime on Microsoft graph side of things?

I am only getting null values back on for lastRemoteActionResult and lastLoginResult when running the action in step 5, all the other property values seem to be correct.

https://graph.microsoft.com/beta/deviceManagement/virtualEndpoint/cloudPCs? $select=userprincipalname,id,displayName,managedDeviceName,Status,imageDisplayName,lastModifiedDateTime,lastRemoteActionResult,lastLoginResult

I figured I try the same from the PowerShell function Get-MgBetaDeviceManagementVirtualEndpointCloudPc

Get-MgBetaDeviceManagementVirtualEndpointCloudPc | select-object userprincipalname,id,displayName,managedDeviceName,Status,imageDisplayName,lastModifiedDateTime,lastRemoteActionResult,lastLoginResult

which returns this for all records.

Status : provisioned
ImageDisplayName : Windows 11 Enterprise + Microsoft 365 Apps 22H2
LastModifiedDateTime : 11/1/2023 4:01:15 PM
LastRemoteActionResult : Microsoft.Graph.Beta.PowerShell.Models.MicrosoftGraphCloudPcRemoteActionResult
LastLoginResult : Microsoft.Graph.Beta.PowerShell.Models.MicrosoftGraphCloudPcLoginResult

I am kind of stuck when the LastLoginResult cannot be retrieved.

Thanks in advance.



Hi.
Try appending the id of one of your CPCs to your query.
https://graph.microsoft.com/beta/deviceManagement/virtualEndpoint/cloudPCs/PUT YOUR CPC ID HERE? $select=userprincipalname,id,displayName,managedDeviceName,Status,imageDisplayName,lastModifiedDateTime,lastRemoteActionResult,lastLoginResult

The reason why you are getting NULLS on these attributes is a result of doing a bulk query. To get the values, you have to isolate the query to each individual CPC. This is why in the article I provide the steps to parse through each CPC and extract the lastLoginResult attribute value to a variable. HTH.

@JJGuirola excellent article.  I've managed to use this successfully. You did mention though that you were going to be creating a Power App with some enhancements.  Are you still working on that?  Kepp up the good work!