{"id":38992,"date":"2020-04-27T06:00:07","date_gmt":"2020-04-27T13:00:07","guid":{"rendered":"https:\/\/devblogs.microsoft.com\/premier-developer\/?p=38992"},"modified":"2020-04-04T11:19:44","modified_gmt":"2020-04-04T18:19:44","slug":"azure-active-directory-automating-guest-user-management","status":"publish","type":"post","link":"https:\/\/devblogs.microsoft.com\/premier-developer\/azure-active-directory-automating-guest-user-management\/","title":{"rendered":"Azure Active Directory: Automating Guest User Management"},"content":{"rendered":"<p>Application Development Manager <strong>Francis Lacroix<\/strong> discusses how to use Azure Automation and Microsoft Graph to determine which users are inviting guests into Azure Active Directory, audit guest logins, and disable unused guest identities.<\/p>\n<hr \/>\n<p>While Azure AD offers many functions and features for managing Guest Users and their permissions, there are still some business specific processes that may not be supported. Though the use of Azure Automation and Microsoft Graph, it is possible to augment the built-in features to achieve business goals.<\/p>\n<p>In this blog post, we will focus on two goals:<\/p>\n<ol>\n<li><strong>Track and maintain the inviter for guests<\/strong>. We will be using the Manager field on the Azure AD Guest User to track the inviter. This will allow us to track and audit who has invited each guest user, and integrate this information into other processes.<\/li>\n<li><strong>Audit Guest logins and disable unused guest users<\/strong>. We will be tracking logins for guest users, and if there are no logins in the past week, the guest user account will be disabled and and email sent to the inviter.<\/li>\n<\/ol>\n<h2>Assumptions<\/h2>\n<p>As with many custom solutions, there are multiple ways of approaching it. As this work is based on a Proof of Concept for a Premier Developer customer, the solution presented in this blog post is built around certain assumptions:<\/p>\n<ol>\n<li><strong>Using an Automation Account<\/strong>. There are different means of building automated processes in Azure. Azure Automation was selected since it uses PowerShell, which is the primary audience at the customer. Azure Functions could have also been used.<\/li>\n<li><strong>Using PowerShell Runbooks<\/strong>. Since the target audience was already comfortable with PowerShell, this was selected as the Runbook type.<\/li>\n<li><strong>Using Azure AD PowerShell Module<\/strong>. Targeting the Microsoft Graph API directly would have worked also, but since this solution is built around PowerShell, we have opted to use the AzureAD PowerShell module.<\/li>\n<\/ol>\n<h2>The Scripts<\/h2>\n<p>For those simply interested in the scripts, they can be found here:<\/p>\n<ul>\n<li><a href=\"https:\/\/dev.azure.com\/francislacroix\/_git\/CodeShare?path=%2FGuestUserAutomationBlog%2FSetInviterAsManager.ps1\">SetInviterAsManager.ps1<\/a><\/li>\n<li><a href=\"https:\/\/dev.azure.com\/francislacroix\/_git\/CodeShare?path=%2FGuestUserAutomationBlog%2FMonitorInactiveGuestAccounts.ps1\">MonitorInactiveGuestAccounts.ps1<\/a><\/li>\n<\/ul>\n<p>However, if you\u2019re interested in setting up your Automation Account and understanding what is being done in the scripts, please read the rest of this post.<\/p>\n<h2>Building the Automation Account<\/h2>\n<p>The first step is to create the Automation Account itself. If you have an existing Automation Account you want to use for this task, you can skip this step as long as it is properly configured. Creating an Automation Account is a straightforward process:<\/p>\n<ul>\n<li><strong>Name<\/strong>: Provide a name for the Automation Account. We chose: \u201cGuestUserManagement\u201d.<\/li>\n<li><strong>Subscription<\/strong>: Select the subscription to create the account in. Consider a subscription you have permissions to create resources in. Also, if you chose to create a \u201cRun As Account\u201d (more on this later), you will need to be Owner or equivalent on the subscription.<\/li>\n<li><strong>Resource group<\/strong>: We created a new \u201cGuestUserManagement\u201d group, but any resource group you have the proper permissions to will work.<\/li>\n<li><strong>Location<\/strong>: Select a Location.<\/li>\n<li><strong>Create Azure Run As account<\/strong>: Selecting <strong>Yes<\/strong> will create a Service Principal, generate a self-signed certificate for it and assign it Contributor role on the subscription selected above. In order to create the Run As Account, you need to have permissions to create Service Principals in Azure AD and permissions to assign Contributor role in the subscription. If you select <strong>No<\/strong>, you will not require any of those permissions, but you will need to provide credentials used to connect to Azure AD when configuring the Automation account. We will provide instructions on how to do this when we discuss it.<\/li>\n<\/ul>\n<p>For more information on Run As Accounts, please see: <a href=\"https:\/\/docs.microsoft.com\/azure\/automation\/manage-runas-account\">https:\/\/docs.microsoft.com\/azure\/automation\/manage-runas-account<\/a><\/p>\n<p><img decoding=\"async\" width=\"304\" height=\"419\" class=\"wp-image-38993\" src=\"https:\/\/devblogs.microsoft.com\/premier-developer\/wp-content\/uploads\/sites\/31\/2020\/04\/word-image-7.png\" srcset=\"https:\/\/devblogs.microsoft.com\/premier-developer\/wp-content\/uploads\/sites\/31\/2020\/04\/word-image-7.png 304w, https:\/\/devblogs.microsoft.com\/premier-developer\/wp-content\/uploads\/sites\/31\/2020\/04\/word-image-7-218x300.png 218w\" sizes=\"(max-width: 304px) 100vw, 304px\" \/><\/p>\n<h2>Configuring the Automation Account \u2013 PowerShell Modules<\/h2>\n<p>Once the Automation Account is created, we need to configure it before we can run our Runbooks (scripts). The first item we will configure is the PowerShell modules. As mentioned earlier, we will be using the AzureAD PowerShell module to perform our operations. Therefore, we will add it to the Automation Account.<\/p>\n<p>First, in the Automation Account, navigate to the <strong>Modules<\/strong> page in the <strong>Shared Resources<\/strong> section.<\/p>\n<p><img decoding=\"async\" width=\"220\" height=\"213\" class=\"wp-image-38994\" src=\"https:\/\/devblogs.microsoft.com\/premier-developer\/wp-content\/uploads\/sites\/31\/2020\/04\/word-image-8.png\" \/><\/p>\n<p>The AzureAD Module is part of the PowerShell Gallery, therefore you will need to select <strong>Browse gallery <\/strong>in the header bar.<\/p>\n<p>Once the Gallery page is open, search for <strong>AzureADPreview<\/strong>. Click on the entry shown, which will bring up the Module\u2019s details. There, click <strong>Import<\/strong>, then <strong>OK<\/strong>. Note that this process can take several minutes.<\/p>\n<p>The reason we are using the AzureADPreview rather than AzureAD module is based on the need for certain cmdlets (specifically Get-AzureADAuditDirectoryLogs and Get-AzureADAuditSignInLogs) which are only available in the Preview module when this post was originally written. Once these cmdlets are available in the non-preview module, please use the regular AzureAD module.<\/p>\n<p>For more<\/p>\n<p><img decoding=\"async\" width=\"736\" height=\"40\" class=\"wp-image-38995\" src=\"https:\/\/devblogs.microsoft.com\/premier-developer\/wp-content\/uploads\/sites\/31\/2020\/04\/word-image-9.png\" srcset=\"https:\/\/devblogs.microsoft.com\/premier-developer\/wp-content\/uploads\/sites\/31\/2020\/04\/word-image-9.png 736w, https:\/\/devblogs.microsoft.com\/premier-developer\/wp-content\/uploads\/sites\/31\/2020\/04\/word-image-9-300x16.png 300w\" sizes=\"(max-width: 736px) 100vw, 736px\" \/><\/p>\n<p><img decoding=\"async\" width=\"818\" height=\"172\" class=\"wp-image-38996\" src=\"https:\/\/devblogs.microsoft.com\/premier-developer\/wp-content\/uploads\/sites\/31\/2020\/04\/word-image-10.png\" srcset=\"https:\/\/devblogs.microsoft.com\/premier-developer\/wp-content\/uploads\/sites\/31\/2020\/04\/word-image-10.png 818w, https:\/\/devblogs.microsoft.com\/premier-developer\/wp-content\/uploads\/sites\/31\/2020\/04\/word-image-10-300x63.png 300w, https:\/\/devblogs.microsoft.com\/premier-developer\/wp-content\/uploads\/sites\/31\/2020\/04\/word-image-10-768x161.png 768w\" sizes=\"(max-width: 818px) 100vw, 818px\" \/><\/p>\n<p><img decoding=\"async\" width=\"789\" height=\"167\" class=\"wp-image-38997\" src=\"https:\/\/devblogs.microsoft.com\/premier-developer\/wp-content\/uploads\/sites\/31\/2020\/04\/word-image-11.png\" srcset=\"https:\/\/devblogs.microsoft.com\/premier-developer\/wp-content\/uploads\/sites\/31\/2020\/04\/word-image-11.png 789w, https:\/\/devblogs.microsoft.com\/premier-developer\/wp-content\/uploads\/sites\/31\/2020\/04\/word-image-11-300x63.png 300w, https:\/\/devblogs.microsoft.com\/premier-developer\/wp-content\/uploads\/sites\/31\/2020\/04\/word-image-11-768x163.png 768w\" sizes=\"(max-width: 789px) 100vw, 789px\" \/><\/p>\n<h2>Configuring the Automation Account \u2013 Connections &amp; Certificates<\/h2>\n<p>The following section describes how to configure Connections and Certificates. If you chose <strong>Yes<\/strong> on the <strong>Create Azure Run As Account<\/strong> option when creating the Automation Account, then these will already be populated with entries called AzureRunAsConnection and AzureRunAsCertificate and this section can be skipped.<\/p>\n<h2>Connections<\/h2>\n<p>As a next step, we will configure the Connection used to connect to AzureAD. In the Automation Account, navigate to the <strong>Connections<\/strong> page in the <strong>Shared Resources<\/strong> section.<\/p>\n<p><img decoding=\"async\" width=\"196\" height=\"252\" class=\"wp-image-38998\" src=\"https:\/\/devblogs.microsoft.com\/premier-developer\/wp-content\/uploads\/sites\/31\/2020\/04\/word-image-12.png\" \/><\/p>\n<p>If you chose <strong>Yes<\/strong> on the <strong>Create Azure Run As Account<\/strong> option when creating the Automation Account, then this will already be populated with an entry call AzureRunAsConnection. If not, then you will need to create one by clicking <strong>Add a connection<\/strong>.<\/p>\n<p><img decoding=\"async\" width=\"307\" height=\"123\" class=\"wp-image-38999\" src=\"https:\/\/devblogs.microsoft.com\/premier-developer\/wp-content\/uploads\/sites\/31\/2020\/04\/word-image-13.png\" srcset=\"https:\/\/devblogs.microsoft.com\/premier-developer\/wp-content\/uploads\/sites\/31\/2020\/04\/word-image-13.png 307w, https:\/\/devblogs.microsoft.com\/premier-developer\/wp-content\/uploads\/sites\/31\/2020\/04\/word-image-13-300x120.png 300w\" sizes=\"(max-width: 307px) 100vw, 307px\" \/><\/p>\n<p>When creating a Connection, you will need to have a pre-existing Service Principal setup with a certificate. This is outside the scope of this blog post, but instructions can be found here: <a href=\"https:\/\/docs.microsoft.com\/azure\/active-directory\/develop\/howto-create-service-principal-portal\">https:\/\/docs.microsoft.com\/azure\/active-directory\/develop\/howto-create-service-principal-portal<\/a>. The Service Principal may need to be provided to you by someone else with permissions to create them.<\/p>\n<p>To create the Connection, you will need to provide the following information:<\/p>\n<ul>\n<li><strong>Name<\/strong>: This name will be used to identify the connection in the Runbook script.<\/li>\n<li><strong>Type<\/strong>: Chose AzureServicePrincipal.<\/li>\n<li><strong>ApplicationId<\/strong>: ID (GUID) of the Service Principal in Azure AD.<\/li>\n<li><strong>TenantId<\/strong>: ID (GUID) of the Azure AD instance the Service Principal resides in.<\/li>\n<li><strong>CertificateThumbprint<\/strong>: The thumbprint of the certificate used to create the certificate credentials for the Service Principal.<\/li>\n<li><strong>SubscriptionId<\/strong>: ID (GUID) of the subscription this automation account resides in.<\/li>\n<\/ul>\n<p>For more details, please see: <a href=\"https:\/\/docs.microsoft.com\/azure\/automation\/automation-connections\">https:\/\/docs.microsoft.com\/azure\/automation\/automation-connections<\/a>.<\/p>\n<h2>Certificates<\/h2>\n<p>In order for the Runbook to properly authenticate to Azure AD, it will also need access to the certificate identified by the thumbprint. In the Automation Account, navigate to the <strong>Certificates<\/strong> page in the <strong>Shared Resources<\/strong> section.<\/p>\n<p><img decoding=\"async\" width=\"194\" height=\"260\" class=\"wp-image-39000\" src=\"https:\/\/devblogs.microsoft.com\/premier-developer\/wp-content\/uploads\/sites\/31\/2020\/04\/word-image-14.png\" \/><\/p>\n<p>Again, if you chose <strong>Yes<\/strong> on the <strong>Create Azure Run As Account<\/strong> option when creating the Automation Account, then this will already be populated with an entry call AzureRunAsCertificate. If not, then you will need to create one by clicking <strong>Add a certificate<\/strong>.<\/p>\n<p><img decoding=\"async\" width=\"307\" height=\"127\" class=\"wp-image-39001\" src=\"https:\/\/devblogs.microsoft.com\/premier-developer\/wp-content\/uploads\/sites\/31\/2020\/04\/word-image-15.png\" srcset=\"https:\/\/devblogs.microsoft.com\/premier-developer\/wp-content\/uploads\/sites\/31\/2020\/04\/word-image-15.png 307w, https:\/\/devblogs.microsoft.com\/premier-developer\/wp-content\/uploads\/sites\/31\/2020\/04\/word-image-15-300x124.png 300w\" sizes=\"(max-width: 307px) 100vw, 307px\" \/><\/p>\n<p>You will need access to the certificate (self-signed or otherwise) used to create the Service Principal and whose thumbprint was provided in the Connection. When adding it to the automation account, provide the password used to protect the private keys and select if you want to make it exportable.<\/p>\n<p>For more details, please see: <a href=\"https:\/\/docs.microsoft.com\/azure\/automation\/shared-resources\/certificates\">https:\/\/docs.microsoft.com\/azure\/automation\/shared-resources\/certificates<\/a>.<\/p>\n<h2>Configuring the Automation Account \u2013 Credentials<\/h2>\n<p>As part of the guest login auditing runbook, we will be sending an email to the guest inviter. Therefore, we will need to store credentials to an SMTP server. If you\u2019re not interested in this feature, then feel free to skip this section.<\/p>\n<p>In the Automation Account, navigate to the <strong>Credentials<\/strong> page in the <strong>Shared Resources<\/strong> section. Select <strong>Add a credential<\/strong>.<\/p>\n<p><img decoding=\"async\" width=\"198\" height=\"221\" class=\"wp-image-39002\" src=\"https:\/\/devblogs.microsoft.com\/premier-developer\/wp-content\/uploads\/sites\/31\/2020\/04\/word-image-16.png\" \/><\/p>\n<p>To create the Credential, you will need to provide the following information:<\/p>\n<ul>\n<li><strong>Name<\/strong>: This name will be used to identify the credentials in the Runbook script. We chose \u201cEmailCredentials\u201d for this blog post.<\/li>\n<li><strong>User name<\/strong>: The user name used to connect to the SMTP server.<\/li>\n<li><strong>Password<\/strong>: The password used to connect to the SMTP server.<\/li>\n<\/ul>\n<p>Keep in mind that all credentials (as well as Connections, Certificates and protected Variables) are encrypted when stored, using a system managed key stored in a system managed Key Vault.<\/p>\n<p>For more details on Credentials, please see: <a href=\"https:\/\/docs.microsoft.com\/azure\/automation\/shared-resources\/credentials\">https:\/\/docs.microsoft.com\/azure\/automation\/shared-resources\/credentials<\/a>.<\/p>\n<h2>Configuring the Automation Account \u2013 Variables<\/h2>\n<p>We will be creating three variables. Two will hold information about the SMTP server used to send email to guest user inviter, and one will be used to hold information about the last successful run of the scripts.<\/p>\n<p>In the Automation Account, navigate to the <strong>Variables<\/strong> page in the Shared <strong>Resources sections<\/strong>. Select <strong>Add a variable<\/strong>.<\/p>\n<p><img decoding=\"async\" width=\"189\" height=\"295\" class=\"wp-image-39003\" src=\"https:\/\/devblogs.microsoft.com\/premier-developer\/wp-content\/uploads\/sites\/31\/2020\/04\/word-image-17.png\" \/><\/p>\n<p>To create the variables, you will need to provide the following information:<\/p>\n<ul>\n<li><strong>Name<\/strong>: This name will be used to identify the variables in the Runbook script. We created three variables, with the names \u201cSMTPServerAddress\u201d, \u201cFromAddress\u201d and \u201cInviterAsManagerLastRun\u201d.<\/li>\n<li><strong>Type<\/strong>: The data type of the variables. We selected \u201cString\u201d for both variables, but this is case specific.<\/li>\n<li><strong>Value<\/strong>: The value of the variables.<\/li>\n<li><strong>Encrypted<\/strong>: Whether the variables are to be encrypted. Encrypted variable values cannot be read after they are created. They can only be updated.<\/li>\n<\/ul>\n<p><img decoding=\"async\" width=\"510\" height=\"208\" class=\"wp-image-39004\" src=\"https:\/\/devblogs.microsoft.com\/premier-developer\/wp-content\/uploads\/sites\/31\/2020\/04\/word-image-18.png\" srcset=\"https:\/\/devblogs.microsoft.com\/premier-developer\/wp-content\/uploads\/sites\/31\/2020\/04\/word-image-18.png 510w, https:\/\/devblogs.microsoft.com\/premier-developer\/wp-content\/uploads\/sites\/31\/2020\/04\/word-image-18-300x122.png 300w\" sizes=\"(max-width: 510px) 100vw, 510px\" \/><\/p>\n<p>For more details on Variables, please see: <a href=\"https:\/\/docs.microsoft.com\/azure\/automation\/shared-resources\/variables\">https:\/\/docs.microsoft.com\/azure\/automation\/shared-resources\/variables<\/a>.<\/p>\n<h2>Configuring the Service Principal Permissions<\/h2>\n<p>Before we can author the Runbooks, or more specifically, before we can run them, we will need to provide the Service Principal which is used for connecting to Azure AD with the proper permissions. This will be done within Azure AD; depending on your permissions, you may need someone else to perform this task.<\/p>\n<p>In <strong>Azure Active Directory<\/strong>, navigate to the <strong>App Registrations<\/strong> section. In App Registration, find the Service Principal specified in the above connection. If you chose to have the Azure Run As Account created with the Automation Account, the App Registration will start with the name of the Account and have a random string appended. In our example, the Service Principal is \u201cGuestUserManagement_&lt;RandomString&gt;\u201d.<\/p>\n<p>Once you have your App Registration selected, navigate to <strong>API Permissions<\/strong>, click \u201cAdd Permissions\u201d, and add the permissions listed below.<\/p>\n<p><img decoding=\"async\" width=\"1284\" height=\"430\" class=\"wp-image-39005\" src=\"https:\/\/devblogs.microsoft.com\/premier-developer\/wp-content\/uploads\/sites\/31\/2020\/04\/word-image-19.png\" srcset=\"https:\/\/devblogs.microsoft.com\/premier-developer\/wp-content\/uploads\/sites\/31\/2020\/04\/word-image-19.png 1284w, https:\/\/devblogs.microsoft.com\/premier-developer\/wp-content\/uploads\/sites\/31\/2020\/04\/word-image-19-300x100.png 300w, https:\/\/devblogs.microsoft.com\/premier-developer\/wp-content\/uploads\/sites\/31\/2020\/04\/word-image-19-1024x343.png 1024w, https:\/\/devblogs.microsoft.com\/premier-developer\/wp-content\/uploads\/sites\/31\/2020\/04\/word-image-19-768x257.png 768w\" sizes=\"(max-width: 1284px) 100vw, 1284px\" \/><\/p>\n<h2>Creating the Runbooks \u2013 Set Inviter as Manager<\/h2>\n<p>Now that we\u2019ve configured the Automation Account, we need to create the Runbooks, which will contain and execute the scripts we will be building. Navigate to <strong>Runbooks<\/strong> under<strong> Process Automation<\/strong> in the Automation Account. Note that there are three sample runbooks present in new Automation Accounts. We will ignore these for the purpose of this blog post.<\/p>\n<p><img decoding=\"async\" width=\"208\" height=\"193\" class=\"wp-image-39006\" src=\"https:\/\/devblogs.microsoft.com\/premier-developer\/wp-content\/uploads\/sites\/31\/2020\/04\/word-image-20.png\" \/><\/p>\n<p>Click <strong>Create a Runbook<\/strong>, and provide the following information:<\/p>\n<ul>\n<li><strong>Name<\/strong>: Chose a clear name for this Runbook. We chose \u201cSetInviterAsManager\u201d.<\/li>\n<li><strong>Runbook type<\/strong>: Choose PowerShell.<\/li>\n<li>You will now be presented with the script authoring window. The full script can be found here: <a href=\"https:\/\/dev.azure.com\/francislacroix\/_git\/CodeShare?path=%2FGuestUserAutomationBlog%2FSetInviterAsManager.ps1\">SetInviterAsManager.ps1<\/a>, but I will walk through its parts.<\/li>\n<\/ul>\n<h2>Runbook Variables<\/h2>\n<p>The first part of the script sets the variables used in the rest of the script, providing easy access for later editing if needed. These could also be moved to Runbook Variables if desired.<\/p>\n<pre class=\"lang:ps decode:true\"># Set the variables for this Runbook\r\n$connectionName = \"AzureRunAsConnection\"\r\n$lastRunVariableName = \"InviterAsManagerLastRun\"<\/pre>\n<p>The <strong>connectionName<\/strong> is simply the name of the Connection created in the Automation Account in the previous steps. Similarly, <strong>lastRunVariableName<\/strong> is the variable name used to hold the last successful run of the script.<\/p>\n<h2>Connect to Azure AD<\/h2>\n<p>This next part of the script connects to Azure AD using the Service Principal setup in the Connection specified in the variable above. This connection will be used to perform all the operation against Azure AD.<\/p>\n<pre class=\"lang:ps decode:true\"># Connect to Azure AD\r\ntry {\r\n  Get the connection for this runbook\r\n  $servicePrincipalConnection = Get-AutomationConnection -Name $connectionName\r\n\r\n  # Connect to Azure AD using the Service Principal\r\n  Write-Output \"Logging in to Azure AD\"\r\n\r\n  Connect-AzureAD -TenantId $servicePrincipalConnection.TenantId `\r\n    -ApplicationId $servicePrincipalConnection.ApplicationId `\r\n    -CertificateThumbprint $servicePrincipalConnection.CertificateThumbprint\r\n}\r\ncatch {\r\n  if (!$servicePrincipalConnection) {\r\n    $ErrorMessage = \"Connection $connectionName not found.\"\r\n    throw $ErrorMessage\r\n  } else {\r\n    Write-Error -Message $_.Exception\r\n    throw $_.Exception\r\n  }\r\n}<\/pre>\n<p>&nbsp;<\/p>\n<h2>Get the Last Successful Run Date and Time<\/h2>\n<p>Once we\u2019ve connected to Azure AD, we\u2019ll query the variable holding the date and time of the last successful run, and build a filter based on this value.<\/p>\n<pre class=\"lang:ps decode:true\"># Get the last successful run date and time\r\ntry\r\n{\r\n  # Get the variable holding the last successful run\r\n  $lastRunDateTime = Get-AutomationVariable -Name $lastRunVariableName\r\n\r\n  # Create the required filter.\r\n  $queryStartDateTimeFilter = '{0:yyyy-MM-dd}T{0:HH:mm:sszzz}' -f $lastRunDateTime\r\n}\r\ncatch {\r\n  if (!$lastRunDateTime) {\r\n    $ErrorMessage = \"Variable $ lastRunVariableName not found.\"\r\n    throw $ErrorMessage\r\n  } else {\r\n    Write-Error -Message $_.Exception\r\n    throw $_.Exception\r\n  }\r\n}<\/pre>\n<p><span style=\"font-size: 1rem;\">The <\/span><strong style=\"font-size: 1rem;\">queryStartDateTimeFilter <\/strong><span style=\"font-size: 1rem;\"> will be used when calling the Get-AzureADAuditDirectoryLogs cmdlet. The cmdlet uses oData v3.0 filter statements, therefore we\u2019re formatting the date\/time filter to that format.<\/span><\/p>\n<h2>Get the Newly Added Users<\/h2>\n<p>Once we have a connection to Azure AD and our query filter, we need to query Azure AD for all Guest Users added since the last run of the script.<\/p>\n<pre class=\"lang:ps decode:true\"># Get the logs for all newly added users\r\nWrite-Output \"Getting Audit Logs\"\r\n$queryStartDateTime = Get-Date()\r\n$addedUserEvents = Get-AzureADAuditDirectoryLogs `\r\n  -Filter \"ActivityDisplayName eq 'Add user' and ActivityDateTime ge $queryStartDateTimeFilter\"<\/pre>\n<p><span style=\"font-size: 1rem;\">We first get the current date and time since we want to avoid missing guest users added after we run the query but before we finish processing the results of the query. This is unlikely since this is a short script, but it is a good precaution. Note that we use the \u201cAdd User\u201d event, rather than one of the Guest User specific ones. The reason is simple: the guest user events don\u2019t have the inviter information in the event data. But the Add User does. We\u2019ll just have to separate the guests from members.<\/span><\/p>\n<h2>Process the Results of the Query<\/h2>\n<p>Once we have a collection of users added to Azure AD since the last run of the script:<\/p>\n<ul>\n<li>Iterate over the collection<\/li>\n<li>Extract the ID of the initiator (inviter)<\/li>\n<li>Get the added user\u2019s object out of Azure AD<\/li>\n<li>Check to see if it\u2019s a Guest based on its UserType\n<ul>\n<li>If so, set the Manager in Azure AD to be the Inviter<\/li>\n<\/ul>\n<\/li>\n<\/ul>\n<pre class=\"lang:ps decode:true\"># Processing added users\r\nforeach ($addedUserEvent in $addedUserEvents) {\r\n  Write-Output \"Processing added user event\"\r\n\r\n  # Get the inviter reference from the InitiatedBy field\r\n  $inviterId = $addedUserEvent.InitiatedBy.User.Id\r\n\r\n  # For each TargetResources, check to see if it's a guest user, and if so, add its Manager\r\n  foreach ($targetResource in $addedUserEvent.TargetResources) {\r\n    Write-Output \"Processing target resource\"\r\n    $addedUser = Get-AzureADUser -ObjectID $targetResource.Id\r\n\r\n    if ($addedUser.UserType -eq \"Guest\") {\r\n      Write-Output \"Setting manager on guest user\"\r\n      Set-AzureADUserManager -ObjectID $addedUser.ObjectId -RefObjectID $inviterId\r\n    }\r\n  }\r\n}<\/pre>\n<p><span style=\"color: inherit; font-family: inherit; font-size: 3rem;\">Update the Last Run Variable<\/span><\/p>\n<p>Finally, we update the InviterAsManagerLastRun variable to the date and time saved before we queried Azure AD Logs<\/p>\n<pre class=\"lang:ps decode:true\">try {\r\n  # Get the variable holding the last successful run\r\n  Set-AutomationVariable -Name $lastRunVariableName -Value $queryStartDateTime\r\n}\r\ncatch {\r\n  Write-Error -Message $_.Exception\r\n  throw $_.Exception\r\n}<\/pre>\n<p><span style=\"color: inherit; font-family: inherit; font-size: 4rem;\">Creating the Runbooks \u2013 Audit Guest User Logins<\/span><\/p>\n<p>Now that we have a script to set the Manager on Guest Users, we will create another Runbook to audit inactive users, disable them and email inviters. Navigate back to <strong>Runbooks<\/strong> and create another Runbook. We named this one \u201cMonitorInactiveGuestAccounts\u201d. The full script can be found here: <a href=\"https:\/\/dev.azure.com\/francislacroix\/_git\/CodeShare?path=%2FGuestUserAutomationBlog%2FMonitorInactiveGuestAccounts.ps1\">MonitorInactiveGuestAccounts.ps1<\/a>.<\/p>\n<h2>Runbook Variables<\/h2>\n<p>Like the previous script, the first part of this script sets the variables used in the rest of the script, providing easy access for later editing if needed. Some of these could also be moved to Runbook Variables if desired.<\/p>\n<pre class=\"lang:ps decode:true\"># Set the variables for this Runbook\r\n$connectionName = \"AzureRunAsConnection\"\r\n$emailCredentialsName = \"EmailCredentials\"\r\n$smtpServerVariableName =\"SMTPServerAddress\"\r\n$fromAddressVariableName = \"FromAddress\"\r\n$queryStartDateTime = (Get-Date).AddDays(-7)\r\n$queryStartDateTimeFilter = '{0:yyyy-MM-dd}T{0:HH:mm:sszzz}' -f $queryStartDateTime\r\n$subject = \"Inactive guest user - {0}\"\r\n$body = \"{0} has been inactive for over 7 days. Please validate that {0} requires guest access and if not, remove them.\"<\/pre>\n<p><span style=\"font-size: 1rem;\">The <\/span><strong style=\"font-size: 1rem;\">queryStartDateTimeFilter <\/strong><span style=\"font-size: 1rem;\"> will be used to query for the last 7 days of login information (Azure keeps 30 days). The <\/span><strong style=\"font-size: 1rem;\">subject<\/strong><span style=\"font-size: 1rem;\"> and <\/span><strong style=\"font-size: 1rem;\">body<\/strong><span style=\"font-size: 1rem;\"> variables will have their placeholder replaced later in the script.<\/span><\/p>\n<h2>Connect to Azure AD and get the credentials and variables<\/h2>\n<p>This next part of the script connects to Azure AD using the Service Principal setup in the Connection specified in the variable above. We also get the mail credentials and the mail variables.<\/p>\n<pre class=\"lang:ps decode:true\"># Get the Connection\r\ntry {\r\n  # Get the connection for this runbook\r\n  $servicePrincipalConnection = Get-AutomationConnection -Name $connectionName\r\n\r\n  # Connect to AzureAD using the Service Principal\r\n  Write-Output \"Logging in to AzureAD\"\r\n  Connect-AzureAD -TenantId $servicePrincipalConnection.TenantId `\r\n    -ApplicationId\u00a0\u00a0$servicePrincipalConnection.ApplicationId `\r\n    -CertificateThumbprint $servicePrincipalConnection.CertificateThumbprint\r\n}\r\ncatch {\r\n  if (!$servicePrincipalConnection) {\r\n    $ErrorMessage = \"Connection $connectionName not found.\"\r\n    throw $ErrorMessage\r\n  } else {\r\n    Write-Error -Message $_.Exception\r\n    throw $_.Exception\r\n  }\r\n}\r\n\r\n# Get the email Credentials\r\ntry {\r\n  # Get the credentials for email\r\n  $emailCredentials = Get-AutomationPSCredential -Name $emailCredentialsName\r\n}\r\ncatch {\r\n  if (!$emailCredentials) {\r\n    $ErrorMessage = \"Credentials $emailCredentials not found.\"\r\n    throw $ErrorMessage\r\n  } else {\r\n    Write-Error -Message $_.Exception\r\n    throw $_.Exception\r\n  }\r\n}\r\n\r\n# Get the email variables\r\ntry {\r\n  # Get the variable holding the last successful run\r\n  $smtpServer = Get-AutomationVariable -Name $smtpServerVariableName\r\n  $fromAddress = Get-AutomationVariable -Name $fromAddressVariableName\r\n}\r\ncatch {\r\n  if (!$smtpServer) {\r\n    $ErrorMessage = \"Variable $smtpServerVariableName not found.\"\r\n    throw $ErrorMessage\r\n  } elseif (!$fromAddress) {\r\n    $ErrorMessage = \"Variable $fromAddressVariableName not found.\"\r\n    throw $ErrorMessage\r\n  } else {\r\n    Write-Error -Message $_.Exception\r\n    throw $_.Exception\r\n  }\r\n}<\/pre>\n<p><span style=\"color: inherit; font-family: inherit; font-size: 3rem;\">Get the Newly Added Users<\/span><\/p>\n<p>Once we have a connection to Azure AD, we need to get a list of all the active guest users in Azure AD.<\/p>\n<pre class=\"lang:ps decode:true\"># Get all the Guest Users\r\n$guestUsers = Get-AzureADUser -Filter \"UserType eq 'Guest' and AccountEnabled eq true\"<\/pre>\n<p><span style=\"font-size: 1rem;\">The filter used enables us to query for Guest Users (since this limitation only applies to them), and those that are enabled (or those in the Azure Portal where the sign in isn\u2019t blocked). We wan\u2019t to avoid re-sending an email notification every script run, therefore once the Guest User has been disabled, we will no longer process them.<\/span><\/p>\n<h2>Process the Results of the Query<\/h2>\n<p>Once we have a collection of active guest users:<\/p>\n<ul>\n<li>Iterate over the collection<\/li>\n<li>Check to see if there\u2019s an entry in the Sign In logs for that user in the last 7 days. If not:\n<ul>\n<li>Disable the account (set AccountEnabled to false)<\/li>\n<li>Get the guest user\u2019s manager<\/li>\n<li>Send an email to the manager<\/li>\n<\/ul>\n<\/li>\n<\/ul>\n<pre class=\"lang:ps decode:true\"># For each Guest user, validate there is a login in the last week\r\nforeach ($guestUser in $guestUsers) {\r\n  Write-Output \"Getting User's logins for the last week\"\r\n  $guestUserSignIns = Get-AzureADAuditSignInLogs -Filter \"UserID eq '$($guestUser.ObjectID)' and createdDateTime ge $queryStartDateTimeFilter\"\r\n\r\n  if ($guestUserSignIns -eq $null) {\r\n    Write-Output \"No logins, blocking sign-in and sending email to manager\"\r\n\r\n    # Block Sign-In\r\n    Set-AzureADUser -ObjectID $guestUser.ObjectID -AccountEnabled $false\r\n\r\n    # Get the manager\r\n    $manager = Get-AzureADUserManager -ObjectID $guestUser.ObjectID\r\n\r\n    # Format the subject and body\r\n    $targettedSubject = $subject -f $guestUser.DisplayName\r\n    $targettedBody = $body -f $guestUser.DisplayName\r\n    Send-MailMessage -To $manager.OtherMails[0] -Subject $targettedSubject -Body $targettedBody `\r\n      -From $fromAddress -SmtpServer $smtpServer -Credential $emailCredentials `\r\n      -Port 587 -UseSSL\r\n  }\r\n}<\/pre>\n<p><span style=\"color: inherit; font-family: inherit; font-size: 4rem;\">Scheduling The Runbooks<\/span><\/p>\n<p>Now that we have the Runbooks defined, the final step consists of scheduling them to run automatically. Each runbook will run on a separate schedule:<\/p>\n<ul>\n<li>SetInviterAsManager will run hourly<\/li>\n<li>MonitorInactiveGuestAccounts will run daily<\/li>\n<\/ul>\n<p>For each Runbook, go to its <strong>Schedules<\/strong> page under the <strong>Resources<\/strong> section. Click <strong>Add a schedule<\/strong>. This will bring up the schedule selection box.<\/p>\n<p><img decoding=\"async\" width=\"746\" height=\"333\" class=\"wp-image-39007\" src=\"https:\/\/devblogs.microsoft.com\/premier-developer\/wp-content\/uploads\/sites\/31\/2020\/04\/word-image-21.png\" srcset=\"https:\/\/devblogs.microsoft.com\/premier-developer\/wp-content\/uploads\/sites\/31\/2020\/04\/word-image-21.png 746w, https:\/\/devblogs.microsoft.com\/premier-developer\/wp-content\/uploads\/sites\/31\/2020\/04\/word-image-21-300x134.png 300w\" sizes=\"(max-width: 746px) 100vw, 746px\" \/><\/p>\n<p>Click on the <strong>Schedule<\/strong> box. If this is an existing Automation Account, there may be schedules you can use. Otherwise, we\u2019ll need to create the schedule. If you can use an existing schedule, simply click it. Otherwise, click <strong>Create a new schedule<\/strong>.<\/p>\n<p><img decoding=\"async\" width=\"627\" height=\"199\" class=\"wp-image-39009\" src=\"https:\/\/devblogs.microsoft.com\/premier-developer\/wp-content\/uploads\/sites\/31\/2020\/04\/word-image-22.png\" srcset=\"https:\/\/devblogs.microsoft.com\/premier-developer\/wp-content\/uploads\/sites\/31\/2020\/04\/word-image-22.png 627w, https:\/\/devblogs.microsoft.com\/premier-developer\/wp-content\/uploads\/sites\/31\/2020\/04\/word-image-22-300x95.png 300w\" sizes=\"(max-width: 627px) 100vw, 627px\" \/><\/p>\n<p>To create a new schedule, provide the following information:<\/p>\n<ul>\n<li><strong>Name<\/strong>: A descriptive name for the schedule<\/li>\n<li><strong>Starts<\/strong>: The start date and time of the schedule. Needs to be in the future.<\/li>\n<li><strong>Recurrence<\/strong>: If you want this schedule to reoccur on a specific frequency. For this use case, we will select <strong>Recurring<\/strong> for both schedule, with one set to <strong>Recur every<\/strong> 1 Hour and the other set to <strong>Recur every<\/strong> 1 Day.<\/li>\n<\/ul>\n<p><img decoding=\"async\" width=\"300\" height=\"602\" class=\"wp-image-39010\" src=\"https:\/\/devblogs.microsoft.com\/premier-developer\/wp-content\/uploads\/sites\/31\/2020\/04\/word-image-23.png\" srcset=\"https:\/\/devblogs.microsoft.com\/premier-developer\/wp-content\/uploads\/sites\/31\/2020\/04\/word-image-23.png 300w, https:\/\/devblogs.microsoft.com\/premier-developer\/wp-content\/uploads\/sites\/31\/2020\/04\/word-image-23-150x300.png 150w\" sizes=\"(max-width: 300px) 100vw, 300px\" \/><\/p>\n<p>Select the appropriate Schedule for each<\/p>\n<h2>Closing Comments<\/h2>\n<p>Using the various tools at our disposal, it is possible to extend the features around Guest Users and automate part of the management. Hopefully these two scripts can serve as a foundation for further development on your part.<\/p>\n<p>&nbsp;<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Augment built-in Azure AD features using Azure Automation and Microsoft Graph to manage and audit guest users.<\/p>\n","protected":false},"author":582,"featured_media":37933,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[25,1],"tags":[24,69,102,3],"class_list":["post-38992","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-azure","category-permierdev","tag-azure","tag-azure-ad","tag-powershell","tag-team"],"acf":[],"blog_post_summary":"<p>Augment built-in Azure AD features using Azure Automation and Microsoft Graph to manage and audit guest users.<\/p>\n","_links":{"self":[{"href":"https:\/\/devblogs.microsoft.com\/premier-developer\/wp-json\/wp\/v2\/posts\/38992","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/devblogs.microsoft.com\/premier-developer\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/devblogs.microsoft.com\/premier-developer\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/premier-developer\/wp-json\/wp\/v2\/users\/582"}],"replies":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/premier-developer\/wp-json\/wp\/v2\/comments?post=38992"}],"version-history":[{"count":0,"href":"https:\/\/devblogs.microsoft.com\/premier-developer\/wp-json\/wp\/v2\/posts\/38992\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/premier-developer\/wp-json\/wp\/v2\/media\/37933"}],"wp:attachment":[{"href":"https:\/\/devblogs.microsoft.com\/premier-developer\/wp-json\/wp\/v2\/media?parent=38992"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/premier-developer\/wp-json\/wp\/v2\/categories?post=38992"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/premier-developer\/wp-json\/wp\/v2\/tags?post=38992"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}