top of page
Search

Managing inactive user accounts in Azure AD



Inactive or stale accounts in your Azure AD can pose a security risk and also incur unnecessary license costs if a user has left the organisation or the account is no longer required. Even in organisations with mature Identity Lifecycle Management capabilities there can be a proliferation of non-human accounts (service accounts), guest accounts etc. which are not managed via the organisation's HR system.


With Windows Server Active Directory it was relatively easy to find inactive or stale accounts using the lastLogonTimestamp attribute. The lack of a similar attribute in Azure AD has made it very challenging to get details of user sign-in activities. However, Microsoft has recently released a new signInActivity resource type which exposes properties for both interactive and non-interactive user sign-in time including the lastSignInDateTime property



As of now the resource type is only exposed through the Microsoft Graph API so is not able to be used directly through the Azure AD Powershell modules and Powershell Graph SDK. It is also still in beta so requires the /beta version of Microsoft Graph. Use of the Grpaph API can create some challenges for Azure admins so to make things easier here is a step-by-step guide of how to generate a report of inactive users in Azure AD within a Powershell script. The script will work for both member and guest users and can be tailored for use with other Graph resource types.


Application Registration


Before the execution of any code an application must be registered in Azure AD and because Azure is using OAuth for the Microsoft Graph REST API we need to define permissions for the application via SCOPES.


For this example we are using the OAuth Client Credentials Grant Type. This Grant Type does not require a logged in user and is best suited for application to application scenarios where activities can be automated or run as an Azure Playbook for example. The Client Credential grant type relies on a client_id and client_secret for authentication. If we were wanting to use an OAuth flow interacting with a logged in user we would use a different Grant Type such as Authorisation Code.


For more information on Microsoft support for Client Credentials Grant Flow see here..


To register an application in Azure login to the Azure portal with the required permissions and go to the App Registration blade and select New Registration.



Type in a name for the application and select Register.



On the next screen copy the Application (Client ID) and the Directory (tenant) ID as this will be required later.


Next we need to generate a Client Secret. Go into Certificates and Secrets and select New Client Secret. Give it a name and duration.


Copy the Client Secret Value as this will be required later.


For the last step we need to set permission to the API. The required permissions are detailed in the Microsoft link above:

  • AuditLogs.Read.All

  • Organization.Read.All

Go ahead and add these permissions to the Microsoft Graph


Note that there are 2 options:

  • Delegated permissions

  • Application permissions

Remember that we are using the OAuth Client Credentials type and there is no signed-in user in the flow so use Application permissions


With OAuth there is always a requirement to grant consent so after adding the requirement permissions tick Grant admin consent for your tenant.

Feel free to add any addition permissions if you wish to explore any other Graph API capabilities


Generate an Authorisation Token.


Before we can retrieve any user login details we need to obtain a token with the required permissions.


Create a new Powershell script via Visual Studio Code or Powershell ISE and paste the following code.


$tenantID="" $contentType = "application/json" $outList = @() $Body = @{ Grant_Type = "client_credentials" Scope = "https://graph.microsoft.com/.default" Client_Id = "" Client_Secret = "" } $ConnectGraph = Invoke-RestMethod -Uri "https://login.microsoftonline.com/$tenantID/oauth2/v2.0/token" -Method POST -Body $Body $token = $ConnectGraph.access_token $headers = @{ 'Authorization' = "Bearer $token" }


Replace the $tenantID, Client_ID and Client_Secret with the values copied earlier. After running the code you should see an OAuth bearer token by typing $token at the command line.


For production please do not store the Client_Secret within the code. Instead store it in Microsoft Azure KeyVault or other credentials safe


Call the Graph API


If you want to obtain the sign-in details for a single user you can use similar to this


$queryURL = "https://graph.microsoft.com/beta/users?`$filter=startswith(displayName,'paul')&`$select=displayName,userprincipalname,signInActivity"


Note that because the query contains quotation marks you will need to insert the escape characters as shown. However for the purposes of this exercise we are looking to get the sign-in details of all users so use the following


$queryURL = 'https://graph.microsoft.com/beta/users?$select=displayName,createddatetime,userprincipalname,mail,usertype,signInActivity' $SignInData = Invoke-RestMethod -Method GET -Uri $queryUrl -Headers $headers -contentType $contentType


Note that we could select any available user attributes from Azure AD


Graph API Pagination


One additional complication when working with Microsoft Graph is that it will only return the first 999 users or entries. If your Azure tenant has more that 999 users then we will need to get the information one page at a time. For this we use the Graph @odata.nextLink property


$NextLink = $SignInData.'@Odata.NextLink' While ($Null -ne $NextLink) { #Do Something $NextLink = $SignInData.'@odata.NextLink' }



Compiling a Report


There are a number of ways to generate a report using Powershell so here is a simple example of how to output to a .csv file:


$outList = @()

ForEach ($User in $SignInData.Value) { If ($Null -ne $User.SignInActivity) { $LastSignIn = Get-Date($User.SignInActivity.LastSignInDateTime) $DaysSinceSignIn = (New-TimeSpan $LastSignIn).Days } Else { #No sign in data for user $LastSignIn = "Never or > 180 days" $DaysSinceSignIn = "N/A" } $Values = [PSCustomObject] @{ UPN = $User.UserPrincipalName DisplayName = $User.DisplayName Email = $User.Mail Created = Get-Date($User.CreatedDateTime) LastSignIn = $LastSignIn DaysSinceSignIn = $DaysSinceSignIn UserType = $User.UserType } $outList += $Values }

$outList | Export-Csv -Path '.\User_Signin_Activity.csv' -NoTypeInformation


Pulling it all Together


So the entire script will look something like this. Remember to replace the 3 variables with those of your Azure Registered app


$tenantID="" $contentType = "application/json" $outList = @() $Body = @{ Grant_Type = "client_credentials" Scope = "https://graph.microsoft.com/.default" client_Id = "" Client_Secret = "" } $ConnectGraph = Invoke-RestMethod -Uri "https://login.microsoftonline.com/$tenantID/oauth2/v2.0/token" -Method POST -Body $Body $token = $ConnectGraph.access_token $headers = @{ 'Authorization' = "Bearer $token" } #$queryURL = "https://graph.microsoft.com/beta/users?`$filter=startswith(displayName,'paul')&`$select=displayName,userprincipalname,signInActivity" $queryURL = 'https://graph.microsoft.com/beta/users?$select=displayName,createddatetime,userprincipalname,mail,usertype,signInActivity' $SignInData = Invoke-RestMethod -Method GET -Uri $queryUrl -Headers $headers -contentType $contentType ForEach ($User in $SignInData.Value) { If ($Null -ne $User.SignInActivity) { $LastSignIn = Get-Date($User.SignInActivity.LastSignInDateTime) $DaysSinceSignIn = (New-TimeSpan $LastSignIn).Days } Else { #No sign in data for user $LastSignIn = "Never or > 180 days" $DaysSinceSignIn = "N/A" } $Values = [PSCustomObject] @{ UPN = $User.UserPrincipalName DisplayName = $User.DisplayName Email = $User.Mail Created = Get-Date($User.CreatedDateTime) LastSignIn = $LastSignIn DaysSinceSignIn = $DaysSinceSignIn UserType = $User.UserType } $outList += $Values } $NextLink = $SignInData.'@Odata.NextLink' While ($NextLink -ne $Null) { $SignInData = Invoke-RestMethod -Method GET -Uri $NextLink -Headers $headers -contentType $contentType ForEach ($User in $SignInData.Value) { If ($Null -ne $User.SignInActivity) { $LastSignIn = Get-Date($User.SignInActivity.LastSignInDateTime) $DaysSinceSignIn = (New-TimeSpan $LastSignIn).Days } Else { #No sign in data for user $LastSignIn = "Never or > 180 days" $DaysSinceSignIn = "N/A" } $Values = [PSCustomObject] @{ UPN = $User.UserPrincipalName DisplayName = $User.DisplayName Email = $User.Mail Created = Get-Date($User.CreatedDateTime) LastSignIn = $LastSignIn DaysSinceSignIn = $DaysSinceSignIn UserType = $User.UserType } $outList += $Values } $NextLink = $SignInDate.'@odata.NextLink' } $outList | Export-Csv -Path '.\User_Signin_Activity.csv' -NoTypeInformation


Using a Powershell Function for the Report


As we are duplicating the report generation code due to the paging requirements we may wish to take the opportunity to create a Powershell function and call this from the relevant sections


$tenantID="" $contentType = "application/json" $outList = @() #Function to format report Function Update-Report { ForEach ($User in $SignInData.Value) { If ($Null -ne $User.SignInActivity) { $LastSignIn = Get-Date($User.SignInActivity.LastSignInDateTime) $DaysSinceSignIn = (New-TimeSpan $LastSignIn).Days } Else { #No sign in data for user $LastSignIn = "Never or > 180 days" $DaysSinceSignIn = "N/A" } $Values = [PSCustomObject] @{ UPN = $User.UserPrincipalName DisplayName = $User.DisplayName Email = $User.Mail Created = Get-Date($User.CreatedDateTime) LastSignIn = $LastSignIn DaysSinceSignIn = $DaysSinceSignIn UserType = $User.UserType } $global:outList += $Values } } #End Function #Get OAuth Bearer Token $Body = @{ Grant_Type = "client_credentials" Scope = "https://graph.microsoft.com/.default" client_Id = "" Client_Secret = "" } $ConnectGraph = Invoke-RestMethod -Uri "https://login.microsoftonline.com/$tenantID/oauth2/v2.0/token" -Method POST -Body $Body $token = $ConnectGraph.access_token $headers = @{ 'Authorization' = "Bearer $token" } # Get Token # To check Token type $headers #Query to get sign-in activity for a single user #$queryURL = "https://graph.microsoft.com/beta/users?`$filter=startswith(displayName,'paul')&`$select=displayName,userprincipalname,signInActivity" #Query to fetch user sign-in details. Can test via Graph Explorer $queryURL = 'https://graph.microsoft.com/beta/users?$select=displayName,createddatetime,userprincipalname,mail,usertype,signInActivity' #Calls Graph to get user sign-in details $SignInData = Invoke-RestMethod -Method GET -Uri $queryUrl -Headers $headers -contentType $contentType #Call Function Update-Report $NextLink = $SignInData.'@Odata.NextLink' While ($Null -ne $NextLink) { $SignInData = Invoke-RestMethod -Method GET -Uri $NextLink -Headers $headers -contentType $contentType #Call Function Update-Report $NextLink = $SignInData.'@odata.NextLink' } $outList | Export-Csv -Path '.\User_Signin_Activity.csv' -NoTypeInformation








2,665 views0 comments

Recent Posts

See All

Comments


bottom of page