# Automated secret expiration alerts with Azure Logic Apps

In this article, I will walk you through on how to create an automated solution using Azure Logic Apps to monitor App Registration secrets in Entra ID and send email notifications before they expire.

---

# Prerequisites

* Azure subscription with a Logic App already created
    
* A managed identity with appropriate permissions to read App Registrations
    

---

# Create a Managed Identity for the Logic App

First, you'll need to grant your Logic App's managed identity the necessary permissions. Assign the Microsoft Graph API permission [`Application.Read`](http://Application.Read)`.All` to the managed identity.

To be easier to follow, I split the design of the workflow in three parts. Let’s continue with the first.

---

# **Logic App Workflow -** Foundations

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1766244411644/e56335f2-64ae-481b-b98f-9672ba7fcf2f.png align="center")

Let’s look at the entry point of our Logic App. For the trigger, I use **Recurrence** that I set to run daily at 9 AM.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1766246260649/1f416165-96db-4659-b7ec-7454fc29d849.png align="center")

The next step is to initialize a few variables. I set a variable named `DaysThreshold` of type `Integer` with an initial value of 30. This will allow me to alert for secrets expiring within 30 days. The second variable is `ExpiringSecrets` which I initialize as an empty array (`[]`).

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1766246274446/617c7b6d-0fec-4638-92a8-ab1b8e313b8c.png align="center")

Add an HTTP action to fetch all app registrations. I do that by calling the HTTP GET `https://graph.microsoft.com/v1.0/applications` endpoint. For the authentication type use Managed Identity and the audience `https://graph.microsoft.com`.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1766246288164/7995abc5-b85b-4ebe-be57-057e54265f76.png align="center")

Finally, use a parse JSON action to parse the response from the API. You can use the following sample schema for it:

```json
{
  "type": "object",
  "properties": {
    "value": {
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "appId": {
            "type": "string"
          },
          "displayName": {
            "type": "string"
          }
        },
        "required": [
          "id",
          "appId",
          "displayName"
        ]
      }
    }
  }
}
```

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1766246301874/8bba3ea6-7659-4a50-8c8d-239b6476aea1.png align="center")

That’s all the steps we need to get started with the workflow. In the second part, I will show you how to get the secrets for each app registration and check their expiry.

---

# **Logic App Workflow -** Core Logic

Congratulations for making it this far. Let’s now complicate the workflow a little.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1766244806630/997e7eb2-e036-4bbf-ad99-b80ec66a135b.png align="center")

As we can see, there is a lot to unpack here.

Start off by adding a for each loop. This allows us to loop through each app registration. For the configuration, use the output from the previous step. From dynamic content, select **value** from the Parse JSON step.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1766246316754/ff2a021e-c118-49bf-84eb-326d8d1b7733.png align="center")

Inside the for each loop, add another HTTP action. Inside, call the following HTTP GET endpoint [`https://graph.microsoft.com/v1.0/applications/@{items('For_each')?['id']}`](https://graph.microsoft.com/v1.0/applications/items\('For_each'\)?%5B'id'%5D?$select=id,appId,displayName,passwordCredentials). Use the same authentication type as before, which is Managed Identity, and the same audience.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1766246335646/d38e0c92-d751-4e30-8b13-d053daeb22f6.png align="center")

Again, use another Parse JSON action to parse the body of the response. You can use this sample JSON schema:

```json
{
  "type": "object",
  "properties": {
    "id": {
      "type": "string"
    },
    "appId": {
      "type": "string"
    },
    "displayName": {
      "type": "string"
    },
    "passwordCredentials": {
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "customKeyIdentifier": {
            "type": [
              "string",
              "null"
            ]
          },
          "displayName": {
            "type": [
              "string",
              "null"
            ]
          },
          "endDateTime": {
            "type": "string"
          },
          "hint": {
            "type": [
              "string",
              "null"
            ]
          },
          "keyId": {
            "type": "string"
          },
          "startDateTime": {
            "type": "string"
          },
          "secretText": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      }
    },
    "keyCredentials": {
      "type": "array"
    }
  }
}
```

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1766246346347/cab10fd1-6cc5-4911-ac7b-a25a664fd7ec.png align="center")

With a condition, check if the length of the `passwordCredentials` array is greater than zero. If it is, then we do another for each loop to go through each credential separately.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1766246148385/1b473deb-606c-41ac-bec1-9701091bac7c.png align="center")

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1766246457208/eade8319-a026-4324-b1ac-fd634d9c9695.png align="center")

In the compose action, switch to expression tab and paste:

```typescript
div(sub(ticks(items('Apply_to_each')?['endDateTime']), ticks(utcNow())), 864000000000)
```

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1766246170066/4c5a1f86-a5c6-49f9-9831-c29e9820a9ec.png align="center")

Add another condition and check if the `Outputs` from the compose action is less than or equal to `DaysThreshold` we defined at the start.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1766246212112/b8dba967-0ac5-4323-83b1-be7be3c52829.png align="center")

If it’s true, add the expiring secret to the array we defined.

```typescript

json(concat('{\"appName\":\"', body('Parse_JSON_1')?['displayName'], '\",\"appId\":\"', body('Parse_JSON_1')?['appId'], '\",\"secretName\":\"', coalesce(items('For_each_1')?['displayName'], 'No Name'), '\",\"secretHint\":\"', items('For_each_1')?['hint'], '\",\"expirationDate\":\"', items('For_each_1')?['endDateTime'], '\",\"daysRemaining\":\"', string(outputs('Compose')), '\"}'))
```

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1766246228825/bd814b11-ebf7-48e8-9f15-dad9655fe0db.png align="center")

The most complicated part of the workflow is now complete. In the last part, we will take a look at how to send an email notification with a list of expiring secrets.

---

# **Logic App Workflow -** Completion

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1766245477212/1d3086ab-cc36-46c8-8b10-be7b1976e5cc.png align="center")

The condition here just checks if the length of `ExpiringSecrets` we defined at the start is greater than 0. If it is, that means we have secrets that will expire soon!

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1766246362396/6eabf91a-ddb5-49d2-8f4c-e34cd1b09dfa.png align="center")

We use the create HTML table with `ExpiringSecrets` to construct a simple table we will use in the email.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1766246373756/91f9003d-6858-425e-b8d3-537fea46a7e1.png align="center")

Final step is to add the `Send an email (V2)` action and send the email notification. I made the email body to be easy to read and understand:

```typescript
The following App Registration secrets will expire within the next 30 days:

body('Create_HTML_table')

Please renew these secrets as soon as possible to avoid service disruption.
```

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1766246406588/7b3aeb3c-f7bc-4100-97bd-692762ca1779.png align="center")

And that’s it. Lastly, save and test the workflow. Following is a sample email I received from my workflow (I edited out the appName, appId, secretName and secretHint).

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1766245801390/6cd4113e-7d19-4ff8-a904-97bf0e88df9e.png align="center")

---
