Skip to content

[AZ] How to Scan and Disable Inactive Accounts on Azure EntraID

Published: at 12:00 PM

Introduction

As organizations grow, so does the number of user accounts within their systems. Managing these accounts efficiently is crucial for maintaining security and compliance. In particular, inactive accounts in Azure Entra ID (formerly known as Azure Active Directory) can pose significant security risks. These dormant accounts are potential entry points for malicious actors seeking unauthorized access to sensitive data.

In this comprehensive guide, we’ll walk through how to automate the management of inactive Azure Entra ID accounts using a TypeScript application. We’ll cover everything from setting up an Azure Entra ID application, implementing the TypeScript program, to scheduling the script using Azure DevOps for regular execution.


Table of Contents

Open Table of Contents

1. Understanding the Risks of Inactive Accounts

Inactive accounts are user accounts that haven’t been used for a significant period. They can accumulate due to employee turnover, role changes, or users simply forgetting about them. These accounts are risky because:

Regularly auditing and managing these accounts helps mitigate these risks and ensures compliance with security best practices.


2. Creating an Azure Entra ID Application

To interact with Azure Entra ID programmatically, we’ll create an App Registration. This application will authenticate and manage user accounts via the Microsoft Graph API.

Steps to Create an App Registration

1. Navigate to Azure Entra ID

2. Create a New App Registration

App Registration Navigation

3. Configure the App Registration

Adding API Permissions

After creating the app registration, we need to grant it permissions to access the Microsoft Graph API.

1. Navigate to API Permissions

2. Add Permissions

3. Select Required Permissions

Search for and select the following permissions:

API Permissions

Note: The User.ReadWrite.All permission is required to enable or disable user accounts.

5. Create a Client Secret

Collect Necessary Information

You’ll need the following information for your application:


3. Implementing the TypeScript Program

We’ll create a TypeScript program that:

Setting Up the Project

1. Initialize the Project

Create a new directory for your project and initialize npm:

mkdir azure-entra-id-management
cd azure-entra-id-management
npm init -y

2. Install Dependencies

Install the required packages:

npm install @azure/identity @microsoft/microsoft-graph-client dayjs
npm install --save-dev typescript ts-node

3. Configure TypeScript

Create a tsconfig.json file:

{
  "compilerOptions": {
    "module": "commonjs",
    "esModuleInterop": true,
    "target": "es6",
    "moduleResolution": "node",
    "sourceMap": true,
    "outDir": "dist"
  },
  "lib": ["es2015"],
  "files": ["index.ts"]
}

Writing Code

1. Create the Source File and Import Required Modules

At the top of index.ts, import the necessary modules:

import { DefaultAzureCredential } from "@azure/identity";
import { Client } from "@microsoft/microsoft-graph-client";
import { TokenCredentialAuthenticationProvider } from "@microsoft/microsoft-graph-client/authProviders/azureTokenCredentials";
import dayjs from "dayjs";

Note: The isomorphic-fetch import is necessary for environments where fetch is not available globally.

2. Set Configuration Variables

Define the configuration variables and excluded accounts:

// Configuration
const INACTIVITY_THRESHOLD_MONTHS = 2; // Adjust as needed
const EXCLUDED_ACCOUNTS = ["[email protected]", "[email protected]"];

// Azure AD App Credentials - Replace with your actual credentials or use environment variables
const TENANT_ID = process.env.AZURE_TENANT_ID;
const CLIENT_ID = process.env.AZURE_CLIENT_ID;
const CLIENT_SECRET = process.env.AZURE_CLIENT_SECRET;

Security Tip: Never hard-code credentials. Use environment variables or secure credential management.

3. Set Up Authentication and Client

Create a ClientSecretCredential and initialize the Microsoft Graph client:

if (!TENANT_ID || !CLIENT_ID || !CLIENT_SECRET) {
  throw new Error(
    "Please ensure AZURE_TENANT_ID, AZURE_CLIENT_ID, and AZURE_CLIENT_SECRET are set."
  );
}

/** 1. Setup Credentials and Microsoft Graph client */
const client = Client.initWithMiddleware({
  debugLogging: false,
  authProvider: new TokenCredentialAuthenticationProvider(
    // This DefaultAzureCredential will detect the credentials from environment variables above automatically.
    new DefaultAzureCredential(),
    {
      scopes: ["https://graph.microsoft.com/.default"],
    }
  ),
});

4. Define Types

Define types for the Azure AD user and result:

type AzResult<T> = { value: Array<T> };
type AdUser = {
  userPrincipalName: string;
  id: string;
  accountEnabled: boolean;
};

5. Implement Functions

a. Retrieve Inactive Accounts
async function getInactiveAccounts(cutoffDate: Dayjs): Promise<AdUser[]> {
  /** Query all the accounts that has signInActivity date before the expected parameter date */
  const accounts = (await client
    .api("/users/")
    .filter(`signInActivity/lastSignInDateTime lt ${cutoffDate.toISOString()}`)
    .select("id,userPrincipalName,accountEnabled")
    .get()) as AzResult<AdUser>;

  /** Filter the enabled account only here
   * as the API filter has limitation that not allows to query based on both signInActivity and accountEnabled */
  return accounts.value.filter(m => m.accountEnabled);
}

Note: The Microsoft Graph API may paginate results. The loop ensures all pages are retrieved.

b. Disable Accounts
async function disableAccounts(users: AdUser[]): Promise<void> {
  return await Promise.all(
    accounts.map(async u => {
      /* Check and keep the account if found in the excludedAccounts */
      if (
        EXCLUDED_ACCOUNTS.find(a =>
          u.userPrincipalName.toLowerCase().includes(a.toLowerCase())
        )
      ) {
        console.log(
          `User account ${u.userPrincipalName} has been excluded from disabling.`
        );
        return;
      }

      // Perform the account disabling
      await client.api(`/users/${u.id}`).update({
        accountEnabled: false,
      });

      console.log(
        `User account with ID ${u.userPrincipalName} has been disabled.`
      );
    })
  );
}
c. Retrieve Disabled Accounts
async function getDisabledAccounts(): Promise<AdUser[]> {
  /** Query all the accounts that has accountEnabled is false */
  const rs = (await client
    .api("/users/")
    .filter(`accountEnabled eq false`)
    .select("id,userPrincipalName,accountEnabled")
    .get()) as AzResult<AdUser>;

  return rs.value;
}
d. Print Accounts
function printAccounts(message: string, accounts: AdUser[]): void {
  console.log(message);
  accounts.forEach((user, index) => {
    console.log(
      `${index + 1}. ${user.userPrincipalName} (Enabled: ${user.accountEnabled})`
    );
  });
}

6. Main Execution Function

Finally, create the main function to orchestrate the process:

(async () => {
  try {
    //1. Scanning account that inactive for more than 2 months
    const cutoffDate = dayjs().subtract(INACTIVITY_THRESHOLD_MONTHS, "month");
    console.log(
      `Scanning for accounts inactive since before ${cutoffDate.format("YYYY-MM-DD")}...\n`
    );

    // Retrieve inactive accounts
    const inactiveAccounts = await getInactiveAccounts(cutoffDate);
    printAccounts(
      `Found ${inactiveAccounts.length} inactive account(s):`,
      inactiveAccounts
    );

    // Disable inactive accounts
    if (inactiveAccounts.length > 0) {
      await disableAccounts(inactiveAccounts);
    } else {
      console.log("No inactive accounts to disable.");
    }

    // Retrieve and print disabled accounts
    const disabledAccounts = await getDisabledAccounts();
    printAccounts(`\nCurrently disabled accounts:`, disabledAccounts);
  } catch (error) {
    console.error("An error occurred:", error);
  }
})();

7. Running the Program

Ensure the environment variables are set:

export AZURE_TENANT_ID=your_tenant_id
export AZURE_CLIENT_ID=your_client_id
export AZURE_CLIENT_SECRET=your_client_secret

Compile and run the program:

npx ts-node src/index.ts

4. Scheduling the Script with Azure DevOps

To automate the execution of the script, we’ll use Azure DevOps to schedule it as part of a pipeline.

Steps to Schedule the Script

1. Commit the Code to a Repository

2. Create a Variable Group

Navigate to Pipelines > Library in Azure DevOps.

Variable Group

3. Create the Pipeline

In Pipelines, click “Create Pipeline” and follow the prompts to set up a YAML pipeline.

a. Define the YAML Pipeline

Create a azure-pipelines.yml file in your repository with the following content:

trigger: none

schedules:
  - cron: "0 0 * * 0" # Runs at midnight every Sunday
    displayName: "Weekly Sunday Run"
    branches:
      include:
        - main
    always: true
    batch: false

pool:
  vmImage: "ubuntu-latest"

variables:
  - group: AzureEntraIDCredentials

steps:
  - task: NodeTool@0
    inputs:
      versionSpec: "14.x" # Adjust Node.js version as needed
    displayName: "Install Node.js"

  - script: |
      npm ci
      npx ts-node src/index.ts
    displayName: "Install dependencies and run script"
    env:
      AZURE_TENANT_ID: $(AZURE_TENANT_ID)
      AZURE_CLIENT_ID: $(AZURE_CLIENT_ID)
      AZURE_CLIENT_SECRET: $(AZURE_CLIENT_SECRET)
b. Pipeline Explanation

4. Run and Monitor the Pipeline

5. Secure the Pipeline


5. Conclusion

Automating the management of inactive Azure Entra ID accounts enhances your organization’s security posture by reducing potential attack surfaces. By leveraging TypeScript and Azure DevOps, you can create a scalable and maintainable solution that integrates seamlessly with your existing workflows.

You can refer the Full Working Source Code here: drunkcoding public code

Key Takeaways

Next Topic

Important: Accessing certain Microsoft Graph API endpoints requires appropriate licensing. Ensure you have the necessary Microsoft Entra ID P2 or equivalent licenses to use the AuditLog API and other premium features.


Thank You

Thank you for taking the time to read this guide! I hope it has been helpful, feel free to explore further, and happy coding! 🌟✨

Steven | GitHub