Power Automate: Renew App Registration Client Secret and Update Dataverse Environment Variable Value

Last week, we learned how to create an email notification to remind us if App Registration in Azure expires. Today, we will learn how to apply automation to generate a New Secret and update the Dataverse Environment Variable. In the flowchart, here are the steps that we will do:

Today's demonstration

Before we begin, I need to tell you that the ideal scenario if you want to put it in production is to set any connection strings into Azure KeyVault. As my environment can't create Azure KeyVault, hence I'm using normal Dataverse Environment Variable to showcase possibility to do automation. Thank you!

Dataverse Custom API

First, I created the below Custom API:

using Microsoft.Xrm.Sdk;
using Microsoft.Xrm.Sdk.Extensions;
using Microsoft.Xrm.Sdk.Query;
using Newtonsoft.Json.Linq;
using System;
using System.Linq;

namespace BlogPackage
{
    public class UpdateSecretOnEnvironmentVariableApi : PluginBase
    {
        public readonly string EnvironmentVariableNameParam = "EnvironmentVariableName";
        public readonly string ClientSecretParam = "ClientSecret";

        public UpdateSecretOnEnvironmentVariableApi() : base(typeof(UpdateSecretOnEnvironmentVariableApi))
        {
        }

        protected override void ExecuteDataversePlugin(ILocalPluginContext localPluginContext)
        {
            var environmentVariableName = localPluginContext.PluginExecutionContext.InputParameterOrDefault<string>(EnvironmentVariableNameParam);
            if (string.IsNullOrEmpty(environmentVariableName))
            {
                throw new ArgumentNullException(EnvironmentVariableNameParam);
            }

            var clientSecret = localPluginContext.PluginExecutionContext.InputParameterOrDefault<string>(ClientSecretParam);
            if (string.IsNullOrEmpty(clientSecret))
            {
                throw new ArgumentNullException(ClientSecretParam);
            }

            var environmentVariable = GetData(localPluginContext.AdminService, environmentVariableName);
            if (environmentVariable == null) return;

            var schemaName = environmentVariable.GetAttributeValue<string>("schemaname");
            var currentValue = (environmentVariable.Contains("ev.value") ?
                environmentVariable.GetAttributeValue<AliasedValue>("ev.value")?.Value?.ToString() : null) ??
                environmentVariable.GetAttributeValue<string>("defaultvalue");

            var jsonObject = JObject.Parse(currentValue);
            jsonObject["ClientSecret"] = clientSecret;

            var newJson = jsonObject.ToString();

            var req = new OrganizationRequest("UpsertEnvironmentVariable")
            {
                ["DisplayName"] = environmentVariableName,
                ["SchemaName"] = schemaName,
                ["Type"] = 100000003, //JSON
                ["Value"] = newJson
            };
            localPluginContext.AdminService.Execute(req);
        }

        private Entity GetData(IOrganizationService service, string environmentVariableName)
        {
            var query = new QueryExpression("environmentvariabledefinition")
            {
                ColumnSet = new ColumnSet("defaultvalue", "schemaname"),
                TopCount = 1
            };
            query.Criteria.AddCondition("displayname", ConditionOperator.Equal, environmentVariableName);

            var childLink =
                query.AddLink("environmentvariablevalue", "environmentvariabledefinitionid", "environmentvariabledefinitionid", JoinOperator.LeftOuter);
            childLink.EntityAlias = "ev";
            childLink.Columns = new ColumnSet("value");
            childLink.Orders.Add(new OrderExpression("createdon", OrderType.Descending));

            var result = service.RetrieveMultiple(query);

            var data = result.Entities.Any()
                ? result.Entities.FirstOrDefault()
                : null;

            return data;
        }
    }
}

The logic of the above API is as follows:

{
 "ClientID": "client_id",
 "TenantID": "tenant_id",
 "ClientSecret": "client_secret"
}

So that's why as you can see in lines 41 - 45, I can replace the value using JObject (Newtonsoft).

  • We use the out-of-the-box action called UpsertEnvironmentVariable to update the EnvironmentVariable Value. We need to pass several mandatory attributes such as DisplayName (if you did not pass this, the Display Name will be replaced with null), SchemaName, Type, and Value.

For the demonstration, I created 3 Environment Variables for 3 App Registrations for which the secret expired:

Targeted Environment Variables

Targeted Environment Variables

Once the code is ready and you deployed the Package, we need to create a new Custom API in Dataverse:

Dataverse Custom API

Dataverse Custom API

Add Privilege to The Application User

As you know, we need to invoke the Flow with a specific Application (same as last week). Hence, because we want to generate the secret programmatically. We need to add more API permissions. Go to your App > API Permissions > Add Permission and add Application.ReadWrite.All:

Add new permission

Add new permission

Power Automate Flow

Next, we need to create the Custom API. For this demonstration, I'll be using a manual trigger.

First, we need to get the access token and we need TenantId, ClientId, and also Client Secret (I pass this in the manual trigger - you can store this information using key-vault which is more secure).

Get Access Token

Get Access Token

I also provide you the Code view so you can refer to this action easily:

{
  "type": "Http",
  "inputs": {
    "uri": "https://login.microsoftonline.com/@{triggerBody()?['text']}/oauth2/v2.0/token",
    "method": "POST",
    "headers": {
      "Content-Type": "application/x-www-form-urlencoded"
    },
    "body": "grant_type=client_credentials&scope=https://graph.microsoft.com/.default&client_id=@{triggerBody()?['text_1']}&client_secret=@{triggerBody()?['text_2']}"
  },
  "runAfter": {},
  "runtimeConfiguration": {
    "contentTransfer": {
      "transferMode": "Chunked"
    }
  },
  "metadata": {
    "operationMetadataId": "da49e565-4db5-498b-979b-725286a11cf8"
  }
}

Next, we need to parse the response body to get the token by using the Parse JSON action:

{
    "type": "object",
    "properties": {
        "token_type": {
            "type": "string"
        },
        "expires_in": {
            "type": "integer"
        },
        "ext_expires_in": {
            "type": "integer"
        },
        "access_token": {
            "type": "string"
        }
    }
}

We can store the token for all MS Graph Action into a single variable named Token.

Then, we need to execute MS Graph applications API which will return all the applications:

Execute MS Graph applications API

Execute MS Graph applications API

Here is the code view for the above action:

{
  "type": "Http",
  "inputs": {
    "uri": "https://graph.microsoft.com/v1.0/applications?$select=id,displayName,passwordCredentials",
    "method": "GET",
    "headers": {
      "Authorization": "Bearer @{variables('Token')}"
    }
  },
  "runAfter": {
    "Set_Token": [
      "Succeeded"
    ]
  },
  "runtimeConfiguration": {
    "contentTransfer": {
      "transferMode": "Chunked"
    }
  },
  "metadata": {
    "operationMetadataId": "3b5ead14-6941-440d-9f69-8435c6bb4188"
  }
}

For your information, the above action will return the below response (I take the sample JSON from graph-explorer):

{
    "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#applications(id,displayName,passwordCredentials)",
    "value": [
        {
            "id": "acc848e9-e8ec-4feb-a521-8d58b5482e09",
            "displayName": "apisandboxproxy",
            "passwordCredentials": [
                {
                    "customKeyIdentifier": null,
                    "displayName": "secret",
                    "endDateTime": "2299-12-31T08:00:00Z",
                    "hint": "3/x",
                    "keyId": "331279dc-2868-4757-a744-e9649280cd4b",
                    "secretText": null,
                    "startDateTime": "2020-02-13T21:14:36.14Z"
                }
            ]
        },
        {
            "id": "cfa98ac0-a32c-4b4c-a78b-94c9912ed7b2",
            "displayName": "EduPopulationHelper",
            "passwordCredentials": [
                {
                    "customKeyIdentifier": "QwBsAGkAZQBuAHQAUwBlAGMAcgBlAHQA",
                    "displayName": null,
                    "endDateTime": "2299-12-31T08:00:00Z",
                    "hint": null,
                    "keyId": "208e4476-5dbe-4df2-8e26-56c7f709c4bf",
                    "secretText": null,
                    "startDateTime": "2018-03-27T02:49:01.2317856Z"
                }
            ]
        },
        {
            "id": "d6dbf9e0-98a4-4eea-b4c1-df8695277868",
            "displayName": "permissions-scraper-app",
            "passwordCredentials": [
                {
                    "customKeyIdentifier": null,
                    "displayName": "permissions-scraper",
                    "endDateTime": "2299-12-30T21:00:00Z",
                    "hint": "yC-",
                    "keyId": "1bada3aa-61b5-44f6-ad6e-9e75425f2e70",
                    "secretText": null,
                    "startDateTime": "2020-07-24T08:04:15.778Z"
                }
            ]
        }
    ]
}

Basically, we need to loop through the value and also the passwordCredentials objects:

Loop the value and the passwordCredentials

Loop the value and the passwordCredentials

For the condition checking, I'm comparing the passwordCredential.endDateTime less equal utcNow + 7 days:

Check if the client's secret is less than utcNow + 7 days

Check if the client's secret is less than utcNow + 7 days

If the condition is true, then we need to call MS Graph *removePassword*API:

Execute deletePassword API

Execute *removePassword*API

Next, we need to trigger the addPasswordAPI:

Execute addPassword API

Execute addPasswordAPI

When executing the addPassword API, we will get this response:

{
    "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#microsoft.graph.passwordCredential",
    "customKeyIdentifier": null,
    "displayName": "testexpired 1",
    "endDateTime": "2025-02-22T13:08:59.999Z",
    "hint": "uS6",
    "keyId": "0e565910-164d-4946-820b-d4b04b528975",
    "secretText": "secret-text",
    "startDateTime": "2025-02-22T13:05:44.6Z"
}

We need to call Parse JSON and get the secretText for the next step.

Last, we only need to call the Custom API that we prepared in the above:

Execute Custom API and pass App Registration Name + secretText

Execute Custom API and pass App Registration Name + secretText

The full operation from the Apply to each applications until the end of the flow will be as follows:

{
  "type": "Foreach",
  "foreach": "@body('Parse_JSON_Applications')?['value']",
  "actions": {
    "Apply_to_each_secrets": {
      "type": "Foreach",
      "foreach": "@items('Apply_to_each_applications')?['passwordCredentials']",
      "actions": {
        "Check_if_secret_is_expired": {
          "type": "If",
          "expression": {
            "and": [
              {
                "lessOrEquals": [
                  "@items('Apply_to_each_secrets')?['endDateTime']",
                  "@addDays(utcNow(), 7)"
                ]
              }
            ]
          },
          "actions": {
            "Execute_MSGraph_Application_addPassword": {
              "type": "Http",
              "inputs": {
                "uri": "https://graph.microsoft.com/v1.0/applications/@{items('Apply_to_each_applications')?['id']}/addPassword",
                "method": "POST",
                "headers": {
                  "Authorization": "Bearer @{variables('Token')}",
                  "Content-Type": "application/json"
                },
                "body": {
                  "passwordCredential": {
                    "displayName": "@{formatDateTime(utcNow(), 'yyyyMMdd')}"
                  }
                }
              },
              "runAfter": {
                "Execute_MSGraph_Application_removePassword": [
                  "Succeeded"
                ]
              },
              "runtimeConfiguration": {
                "contentTransfer": {
                  "transferMode": "Chunked"
                }
              }
            },
            "Parse_JSON_addPassword": {
              "type": "ParseJson",
              "inputs": {
                "content": "@body('Execute_MSGraph_Application_addPassword')",
                "schema": {
                  "type": "object",
                  "properties": {
                    "@@odata.context": {
                      "type": "string"
                    },
                    "customKeyIdentifier": {},
                    "displayName": {
                      "type": "string"
                    },
                    "endDateTime": {
                      "type": "string"
                    },
                    "hint": {
                      "type": "string"
                    },
                    "keyId": {
                      "type": "string"
                    },
                    "secretText": {
                      "type": "string"
                    },
                    "startDateTime": {
                      "type": "string"
                    }
                  }
                }
              },
              "runAfter": {
                "Execute_MSGraph_Application_addPassword": [
                  "Succeeded"
                ]
              }
            },
            "Execute_MSGraph_Application_removePassword": {
              "type": "Http",
              "inputs": {
                "uri": "https://graph.microsoft.com/v1.0/applications/@{items('Apply_to_each_applications')?['id']}/removePassword",
                "method": "POST",
                "headers": {
                  "Content-Type": "application/json",
                  "Authorization": "Bearer @{variables('Token')}"
                },
                "body": {
                  "keyId": "@{items('Apply_to_each_secrets')?['keyId']}"
                }
              },
              "runtimeConfiguration": {
                "contentTransfer": {
                  "transferMode": "Chunked"
                }
              }
            },
            "UpdateEnvironmentVariable": {
              "type": "OpenApiConnection",
              "inputs": {
                "parameters": {
                  "actionName": "tmy_UpdateSecretOnEnvironmentVariableApi",
                  "item/EnvironmentVariableName": "@items('Apply_to_each_applications')?['displayName']",
                  "item/ClientSecret": "@body('Parse_JSON_addPassword')?['secretText']"
                },
                "host": {
                  "apiId": "/providers/Microsoft.PowerApps/apis/shared_commondataserviceforapps",
                  "connection": "shared_commondataserviceforapps-1",
                  "operationId": "PerformUnboundAction"
                }
              },
              "runAfter": {
                "Parse_JSON_addPassword": [
                  "Succeeded"
                ]
              }
            }
          },
          "else": {
            "actions": {}
          }
        }
      }
    }
  },
  "runAfter": {
    "Parse_JSON_Applications": [
      "Succeeded"
    ]
  }
}

Demo

Here is the result of the execution of the Flow:

Result

Result

Happy CRM-ing! 🚀😁

Leave a comment

Your comment is sent privately to the author and isn't published on the site.