Get notified on expiring Azure App Registration client secrets

Today another blog post that involves Logic Apps. I use some App Registrations in Azure, for example, to use in my Logic App flows. To authenticate with these App Registrations I use client secrets. But these client secrets expire once in a while which causes my flows to stop working if I don’t replace them before the expiring date. I would like to get a notification a few weeks before one of these secrets will expire, but I don’t see an out-of-the-box solution for this. So of course I came with the idea to solve this issue with a Logic Apps flow (again) as the information about the end date of the client secret is available via Graph API.

The solution

Via Graph API we’re able to get information on the App Registrations in our Azure tenant. This information holds the end date from the underlying client secret. The client secret information is available in an array. Because of that array, we need to initialize some variables to grab the required information to further process it in our flow. By using a condition in the flow, we can filter out the expiring client secrets based on the end date. We’re able to trigger a notification that can be sent via email or can be sent to a Teams channel.

This is what the complete flow looks like.

This flow monitors all App Registration client secrets. At the end of the post I briefly describe the extra steps which need to be taken to only monitor the App Registration of your choice.

The complete flow can also be downloaded as an ARM Template from my GitHub repository.

The requirements

To get this logic Apps flow up and running we have a few requirements.

Via an HTTP action, we’re going to query Graph API for the information we need. This HTTP uses an Azure App Registration, which is our first requirement. The App Registration is used for authentication in the HTTP actions and also for granting the required permissions.
The minimum required permission for this Logic App is shown below:
Application.Read.All

To secure the HTTP action I use Azure Key Vault, which is described in this previous post. The Key Vault holds the client secret and keeps this secure in the flow. I also added the tenant id and client id to the vault, so I only have to query the Key vault for this information.
This flow only uses one HTTP action, if you have several HTTP actions in a flow, in every HTTP action the tenant and client id needs to be added. By storing the information in a variable or in a Key Vault, we don’t have to copy/ paste the ids in every HTTP action.

Depending on how you want to receive the notifications, permissions on a (shared) mailbox or a Teams webhook connector are needed.

Setup the Logic Apps flow

Let’s configure the Logic Apps flow.

Sign in to the Azure portal and open the Logic App service. I created a blank Logic App of type Consumption.

When the creation of the Logic App is finished, open the flow. A few templates are shown, choose Recurrence.

The Logic App designer is opened and the recurrence trigger is added. Choose the recurrence settings of your choice. I choose to run the flow once a week.

The first action we’re going to add is the Get secret action, which is an Azure Key Vault action. With this action, we retrieve the tenant id, which we use in the HTTP action.
Search on Azure Key and select Get secret.

Enter the name of the Vault and select your tenant. Next, click Sign in to authenticate to the Key Vault.

Choose tenant id from the drop-down list.

Repeat the Key Vault steps to also retrieve the client id and client secret.

In the next action, we are going to initialize variables for appId, displayName, and passwordCredentials.
Search for variables and select Initialize variable (three times).

Enter a Name for the variables. For appId and displayName select type String. For passwordCredential select type Array.

The next step we’re going to add is an HTTP Action, to query Graph for the App Registrations.

Choose GET as Method.
As URI enter:

https://graph.microsoft.com/v1.0/applications?$select=id,appId,displayName,passwordCredentials

With this select, we only select the items which are relevant to the information I want to use later in this flow. If you need more information, the URL needs to be expanded with that information.

Choose Add new parameter and check Authentication. Select Active Directory OAuth as authentication type.
As tenant, client id, and secret, we add the Dynamic content Value. Make sure you pick the value from the correct Key Vault action.
Enter https://graph.microsoft.com as Audience.

This is our HTTP action.

To use the information we received (by executing this HTTP action) in the upcoming actions, we need to parse the received information with a Parse JSON action.

Add a Parse JSON action to the flow.
We’re going to parse the response body of the previous HTTP action. In the Content box, we add Body, from the previous HTTP action.

To fill the schema with the correct information, we can add an example payload. The information to use as an example payload can be ‘generated’ by running the flow. Save the flow and hit Run trigger to start the flow.

Open the Runs history from the flow and open the HTTP action. Copy the body.

Click Use sample payload in the Parse JSON action, past the information which we copied from the runs history in the new pop-up, and click Done.

This is our Parse JSON action.

If you receive an error like “Invalid type. Expected string but got Null” you can simply resolve that by changing

"type": "string"

to

    "type": ["string", "null"]

Next, we’re going to use Set variable actions to set the previously initialized variables.

Add a Set variable action. Choose appId from the drop-down list. As value we add dynamic content appId from the Parse JSON action.

This will automatically add the Set variable action in a For each action.

Add two more Set variable actions and repeat the steps for displayName and passwordCredential.

Add aother For each action, which is a Control action.
As the output select passwordCredentials from the Parse JSON action.

Add a Condition action, which is also a Control action. With this action, we’re going to determine if the end date of the secret (passwordCredential) falls between today and 14 days ago. If that’s true it is expiring and a follow up action is performed.

To grab the end date from the passwordCredentials we need to use the below expression:

items('For_each_passwordCredential')?['endDateTime']

Replace For_each_passwordCredential with the name of your last added For each action and replace the spaces with an -. Add this expression in the left text box.

Select is less than from the drop-down list.

In the right text box enter the below expression to add the current date – 14 days:

addToTime(utcNow(),14,'day')

This is how our For each action looks like with the Condition action added.

Under True, we need to add the action which sends us the notification. I choose to send an email from a shared mailbox.

We can enter text, but also use data from previous actions in the body of the email.

I used displayName from the Parse JSON action to display the App Registration name.

And I used expressions to get information from the passwordCredential variable. These expressions are comparable to the expression used to get the end date of the secret. To use the key id, display name and end date of the secret (passwordCredential), use below expressions:

items('For_each_passwordCredential')?['keyId']
items('For_each_passwordCredential')?['displayName']
items('For_each_passwordCredential')?['endDateTime']

This is my email action under True, where I also added the URL to the App Registration for which I use the appId from the Parse JSON action.

Thiss is our complete flow.

Only monitor the App Registrations of your choice

If you’re not responsible for all App Registrations, you might only want to monitor the secrets of the Apps for which you’re responsible. That’s also possible if we use the Object ID of the Apps in our HTTP Get action. In this example, I used a SharePoint List which holds the object IDs if the Apps that I want to monitor.

In our flow, we need to add a SharePoint Get Items action to our flow. This action retrieves the items from the SharePoint list, which we can later use in the HTTP action as a variable.

In our HTTP action, we use the dynamic content variable from the Get items action. This automatically adds the HTTP action in a For each action.

As the HTTP action is already in a For each action, the first Set variable action doesn’t need to be added in another For each action. The first part of the flow looks like below.

The end result

The end result is pretty simple. We receive an email in our mailbox when a client secret is about to expire. And this is the email that I received from the Logic App, which contains information regarding the expiring client secret.

That’s it for this blog post. I hope you find it helpful and you don’t let your secrets expire anymore 🙂

67 Comments

  1. Hi Peter,

    Thanks for the great article. I was falling with the following error message. “test connection failed. Error ‘REST API is not yet supported for this mailbox.” any idea how to troubleshoot?

    Thank you.

    • Hi Peter,
      Thanks for sharing the article, I looked at the same article b4 but the shared mailbox that I use is on Exchange online. no On-premises integration at all.

      Tks

  2. Awesome guide – i cant seem to use the parse json variables though in the email- what could cause that? All 3 set variables done in the

    ‘foreach appid’ section dont seem to be available.

  3. Hi Peter,

    We are getting alerts for only few app registrations client certificate expiry.
    for ex: if 5 app registration client certificate is getting expired in 1 week, we are getting alert for only 2 app registration. what could be the reason for the logic app to ignore other app registrations that also has client expiry in a week.

    Thanks
    Taj

  4. Hi Peter,

    Fellow dutchman here.

    I am having some difficulties getting the solution to work fully.
    Everything seems to work except sending from the shared mailbox.

    I’ve autorised both api connections (Keyvault and (O365) but I am still getting the following error below. Could you send in the right direction?

    Thanks in advance!

    {
    “statusCode”: 404,
    “headers”: {
    “Pragma”: “no-cache”,
    “x-ms-request-id”: “62bd15e4-671f-f3ea-fe3b-840cddb5815c”,
    “Strict-Transport-Security”: “max-age=31536000; includeSubDomains”,
    “X-Content-Type-Options”: “nosniff”,
    “X-Frame-Options”: “DENY”,
    “Cache-Control”: “no-store, no-cache”,
    “Set-Cookie”: “ARRAffinity=034a92f9ba2fc4ecf4919720c4a64dda736d4b72182be442bf2b2ab6744078db;Path=/;HttpOnly;Secure;Domain=office365-we.azconn-we-01.p.azurewebsites.net,ARRAffinitySameSite=034a92f9ba2fc4ecf4919720c4a64dda736d4b72182be442bf2b2ab6744078db;Path=/;HttpOnly;SameSite=None;Secure;Domain=office365-we.azconn-we-01.p.azurewebsites.net”,
    “Timing-Allow-Origin”: “*”,
    “x-ms-apihub-cached-response”: “true”,
    “x-ms-apihub-obo”: “true”,
    “Date”: “Mon, 14 Feb 2022 13:44:26 GMT”,
    “Content-Length”: “676”,
    “Content-Type”: “application/json”,
    “Expires”: “-1”
    },
    “body”: {
    “status”: 404,
    “message”: “Specified folder not found. The error could potentially be caused by lack of access permissions. Please verify that you have full access to the mailbox.\r\nclientRequestId: *anonymised*\r\nserviceRequestId: *anonymised*”,
    “error”: {
    “message”: “Specified folder not found. The error could potentially be caused by lack of access permissions. Please verify that you have full access to the mailbox.”,
    “code”: “ErrorFolderNotFound”,
    “originalMessage”: “The specified folder could not be found in the store.”
    },
    “source”: “office365-we.azconn-we-01.p.azurewebsites.net”
    }
    }

  5. Hi Peter,
    thanks for publishing this great article. I wanted to download the ARM-template just now but in the repo for this solution is just the readme.md file and no ARM-template anymore?
    Regarding the managed-identity improvement that was made by Luise Freese, was that only done in the License-reporting logic-app that you have in github?

    I’ll try to setup the logic-app manually now. Thanks for sharing.

    • Hi Eric,

      Something did go wrong probably, but the azuredeploy.json is added back.
      The change from App registration to Managed Identity was done for the License reporting and Autopilot deployment event Logic Apps.
      If I have time, I’ll also change the others.

      Regards,

      Peter

  6. Hi Peter,

    I want to change my rest API email connection to a gmail account, please advise.

    Please check your account info and/or permissions and try again. Details: REST API is not yet supported for this mailbox. This error can occur for sandbox (test) accounts or for accounts that are on a dedicated (on-premise) mail server.

    • Hi Peter,

      I have added Sendmail V2 connection and the connection works fine now .
      The issue now is I’m getting individual email for each and every secret . Is there anyways to get all the list of expiring secrets for the current month in a single mail. So that the recipients get a view of all expired secrets of the current month or next 30 days .

      The below is the sample email I get , as well i get the secret list which expired in 2021 as well.

      Client secrets listed are about to expire.
      SPN-xxxxxx

      Details:
      Secret ID:0000000000000000
      Display Name:
      Expiration date:2021-11-16

      App registration location : 0000000-00000-00000000-000000000

      Please take necessary action.

  7. Hello, thank you so much for info, this is great.
    I’m having a bit of challange, here is my JSON schema… I’m not getting “appID”, “displayName” as part of ParseJSON value when I try to set variable. ANy clue?

    {
    “properties”: {
    “@@odata.context”: { “type”: [“string”, “null”] },
    “value”: {
    “items”: {
    “properties”: {
    “appId”: { “type”: [“string”, “null”] },
    “displayName”: { “type”: [“string”, “null”] },
    “id”: { “type”: [“string”, “null”] },
    “passwordCredentials”: {
    “items”: {
    “properties”: {
    “customKeyIdentifier”: {},
    “displayName”: {},
    “endDateTime”: {
    “type”: [
    “string”,
    “null”
    ]
    },
    “hint”: {
    “type”: [
    “string”,
    “null”
    ]
    },
    “keyId”: {
    “type”: “string”
    },
    “secretText”: {},
    “startDateTime”: {
    “type”: [
    “string”,
    “null”
    ]
    }
    },
    “required”: [
    “customKeyIdentifier”,
    “displayName”,
    “endDateTime”,
    “hint”,
    “keyId”,
    “secretText”,
    “startDateTime”
    ],
    “type”: “object”
    },
    “type”: “array”
    }
    },
    “required”: [
    “id”,
    “appId”,
    “displayName”,
    “passwordCredentials”
    ],
    “type”: “object”
    },
    “type”: “array”
    }
    },
    “type”: “object”
    }

    • Hello Chris. I was able to get the value of appid and displayname in parse json by only adding this “type”: [
      “string”,
      “null”
      ]
      on this part.
      “passwordCredentials”: {
      “items”: {
      “properties”: {
      “customKeyIdentifier”: {},
      “displayName”: {},
      “endDateTime”: {
      “type”: [
      “string”,
      “null”
      ]

  8. Hi Peter
    I am trying to implement this in our tenant but the run always fails at the For_each_appid step with the very helpful error {“code”:”ActionFailed”,”message”:”An action failed. No dependent actions succeeded.”}
    Not quite sure where to start looking. As far as I can tell all the components are setup correctly.
    Any ideas?

    • I am getting this as well. Did you ever find a fix? I can see that I have secrets expiring but they are not alerting.

      • If it helps anyone I got the {“code”:”ActionFailed”,”message”:”An action failed. No dependent actions succeeded.”} fixed. It was a permissions issue on the shared mailbox. I had to change the connection to Exchange to use my account and grant Send on Behalf

  9. Great guide so far, I am new to Logic apps so has been very useful.

    Currently stuck, I am running the logic app to generate the example payload for the Parse JSON and the HTTP is coming back with the error;
    Forbidden
    “code”: “Authorization_RequestDenied”,
    “message”: “Insufficient privileges to complete the operation.”,

    I have checked the tenant, client, and secret fields are all using the correct key vault entries and that the App registration has the permission of;

    Application.Read.All
    User.Read

    And admin consent is given.

    Are there any other permissions that need to be included that I am missing, would love to get this working for us? Appreciate if anyone or Peter could let me know. Cheers!

  10. Hi Peter,

    Can we get all in one email , rather getting emails for each and every secret.

  11. I’ve deployed the ARM template in a test environment with 116 App Registrations, with 8 expired secrets and 2 that are about to expire in 1 week. The flow runs successful, and I receive 2 emails. The layout is as follows:

    One of the client secrets is going to expire from Azure App Registration;
    APP-01
    APP-02

    Details:
    Secret ID: 1234-ahdbc-hjsdjdsj-12234
    Display Name:
    Expiration date: 2021-11-05T09:04:13.0006862Z

    App Registration location;
    https://portal.azure.com/#blade/Microsoft_AAD_RegisteredApps/ApplicationMenuBlade/Overview/appId/
    1234-ahdbc-hjsdjdsj-12236
    1234-ahdbc-hjsdjdsj-12237

    Please take action as soon as possible.

    So the email notifies me about 2 App ID’s, 1 that is in fact expired and 1 that is ok? Then the next email that also notifies me about another App ID that is expired and 1 that is ok. So 2 emails, 4 App ID’s and 2 expired. That still leaves 6 expired behind and the 2 that about to.

    As a test, I ran it a couple of times. The 2 that are expired still come back in the 2 emails, along with 2 random app ID’s that are ok. I thought maybe there is a limit to the email, so I fixed the 2 expired App ID’s. Ran the flow again, now no emails. Not sure how to troubleshoot this?

  12. Hi All,

    I user ARM template to create Flow. but I’am getting the below error message.
    “status”: 404,
    “message”: “Operation failed because the requested resource was not found in the key vault.\r\nclientRequestId: 05f68bdf-c3a3-4028-b84a-a18c3db1fe3d”,
    “error”: {
    “message”: “Operation failed because the requested resource was not found in the key vault.”
    },
    “source”: “keyvault-ae.azconn-ae-001.p.azurewebsites.net”
    }
    Can anyone please help

  13. Hi Peter,
    Thank you very much for the great article. I tried to run, but failed at step ‘HTTP Get Azure Applications’, the error message is as below.please kindly help to advise.thanks
    ===
    error”: {
    “code”: “Authorization_RequestDenied”,
    “message”: “Insufficient privileges to complete the operation.”,
    “innerError”: {
    “date”: “2022-06-21T04:09:11”,
    “request-id”: “a6735ab5-e5ca-49f7-8889-8f4a0ca15d59”,
    “client-request-id”: “a6735ab5-e5ca-49f7-8889-8f4a0ca15d59”

    ===

    • Hi Bruce,

      The message indicates the account/ identity which is used for this action doesn’t have the required permissions. So have a look at the permissions of the account. If the permissions were just recently assigned, give it another try some later, as it can take some time before newly assigned permissions are active.

      Regards,

      Peter

  14. Hi Peter,

    The script works fine , thank you so much for this great article.
    Can we just send emails only to app owners stating that the app registration secret is gonna expire

  15. Hi Peter,

    Thanks for the great article. My company hosts application servers in Azure on behalf of clients. The application servers use an app client ID and secret to look up info from the customer’s Azure AD tenant (e.g. to list users). We do not have any other access to the client’s AAD tenant.

    We only want to monitor the expiration date for the app client secret that the client has given us. Your article says that your Logic App needs to use a separate/dedicated app registration that has the “Application.Read.All” permission assigned. However, we do not want to enumrate all app registration secret expiration information, only the expiration date for the app secret that they have assigned to us. Is this possible (does an app have the permission required to read it’s own client expiration date?)

    • To answer my own question: no, the app registration that we are working with does not have permission to read its own client secret expiration date(s). It is possible to connect using the following:

      $pscredential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $AppId, $AppSecret

      Connect-AzAccount -ServicePrincipal -Credential $pscredential -Tenant $tenantId

      But any attempt to run Get-AzADAppCredential, Get-AzADServicePrincipal, Get-AzADApplication, Get-AzADAppPermission or Get-AzADSpCredential focused on the app’s own object all result in the error “Insufficient privileges to complete the operation”. Same deal using Microsoft Graph.

  16. Hi Peter,
    Great Article… unfortunately I stuck in one part that couldn’t find exactly the solution..


    ExpressionEvaluationFailed. The execution of template action ‘For_each_passwordCredential’ failed: the result of the evaluation of ‘foreach’ expression ‘ @{items(‘For_each_appId’)?[‘passwordCredentials’]}’ is of type ‘String’. The result must be a valid array.

    Any idea on what can be done to resolve it ?

    Thanks

    • I was getting this error also when I created the app manually. I had tried modifying the code as there was a definition of passwordCredential listed as “string” but that didn’t work.

      I was able to get past it by using the JSON template provided and using that to create the app.

  17. Hi Peter,
    I am getting the following error. Bit I have provided the access in api permissions. I am able to get the details directly with graph api url. But through the flow I am getting this error
    BadRequest. Http request failed as there is an error getting AD OAuth token: ‘AADSTS700016: Application with identifier ‘8213a9c7-eb18-4e31-abba-608c7fe1568f’ was not found in the directory ‘Default Directory’. This can happen if the application has not been installed by the administrator of the tenant or consented to by any user in the tenant. You may have sent your authentication request to the wrong tenant. Trace ID: a090ca62-7358-4bd9-93b9-b1a4285f1b00 Correlation ID: 7fcfaf9b-d5b2-4f6f-aaca-1eacbd69bcdb Timestamp: 2022-09-27 10:30:39Z’.

    • I was getting this error also when I created the app manually. I was able to get past it by using the JSON template provided and using that to create the app.

  18. I run in to the same error als Liju P, BadRequest. Http request failed as there is an error getting AD OAuth token: ‘AADSTS700016: Application with identifier ‘2740699a-b6c6-4932-93fc-6c53ea1d8665’ was not found in the directory ‘ORTEC Finance’. This can happen if the application has not been installed by the administrator of the tenant or consented to by any user in the tenant. You may have sent your authentication request to the wrong tenant.

  19. Hi Peter,

    Thanks for this great article! I got it up and running without any issues!

    Is there a simple way to add fetch the Owner of the App registration and use that value as the recipient of the mail?

    Thanks!

  20. Hello bit of noob question, when importing the azuredeploy.json template into the custom deployment template editor getting an error at the very first tag stating that “Expected a JSON object, array or literal.json(0)” and as result can’t save the file. I attempted to address this by modifying the formatting without any luck. Thoughts on how to address this.

  21. Hi Peter,
    Thanks for your support.

    Is there any way we can get the app registration name in output, rather than secret name?

  22. I am getting email but only with the below text message. not getting the App details.

    One of the client secret is going to expiry
    Details:
    Secret ID:
    Display Name:
    Expiration date:

  23. Hello,
    It would be better if all setup was explained step by step, else for users who are not very familiar with the things it is complex to understand what should be added where.

  24. when you mention the statement “I also added the tenant id and client id to the vault, so I only have to query the Key vault for this information” – how is this to be done?

  25. Hi Peter ,

    thanks for this article it woks fine but i have few apps that already expired since like years so i’m receiving each timne 20 emails with those Apps i only need starting from year 2023 with close to 14 days expiration , it is possible ?

  26. Hi Peter.
    The script works fine and thank you very much for your article. But I also have a few questions.
    Can we just send emails only to app owners stating that the app registration secret is gonna expire?
    And for second question.
    I’m receiving the notification from my own email. Can we just a custom sender for this?
    Lastly,
    Can we just separate the already-expired secrets and the nearly-expired secrets?

    Thank you in advance for your response.
    Have a great day.

    • Hi Lovely ,
      for youe first question in my case some APP don’t have an owner so probably it will not work
      for the second question for the already expired secrets i tried to modify the addtotime expression didn’t work so what i end up doing as workaround by deleting the expired Secret from Apps and now i’m getting only the one that expire in 14 days only
      hope we have an fix for that from peter but that’s my work arround 😉
      thanks

    • Hi Lovely ,
      finally i found the solution for the already-expired secrets to elimninite those from receiving email you need to add in your Condition section for each paswordCredential another AND like :
      the first one is with : AND : endDatetime is greater expresion: utcnow()
      and you add another one with AND : endDatetime is less than expression: addToTime(utcNow(), 14, ‘day’)

      it works for me i don’t reveive anymnore the certicate already expired

      • few correction to my previous post
        the first one is : AND endDatetime is greater than with expression: utcnow()
        the second one is :AND endDatetime is less than with expression : addToTime(utcNow(), 14, ‘day’)

  27. Peter,

    I’m having issues with getting the endDatetime step, I put in the expression you show, but in the next screenshot it appears to be a variable from somewhere. I didn’t see in any of it where the then endDatetime variable was setup.

  28. I have 425 App registration in our Azure tenant and How do I add pagination as it is returning only 100 App registrations. I am getting Odata next link for next 100 records.Is there is a way to add paging concept in power automate http call.

  29. @Fadi Matni says:

    Can you share where you made those change specifically? I’m trying to use this logic app specifically for Enterprise Applications SAML Expiration. Thanks

  30. Hi,

    In the example you shared is something for storing secret value in vault. But in our case we don’t know the value

    We are looking for something like notify via mail with client secrets expiring in 30days for all the apps in tenant in a csv file and it should run everyday and send out a mail

    Note- i have many app registrations in tenant and i don’t know secret value

Leave a Reply

Your email address will not be published.


*