DEV Community

Daniel Jonathan
Daniel Jonathan

Posted on

πŸ” Fine-Grained Role Control for Logic App Standard Workflows with APIM + Easy Auth

In my earlier post, I showed how to enable Easy Auth for a Logic App Standard workflow:

πŸ‘‰ Enabling Easy Auth for Azure Logic Apps Standard

That secures the workflow endpoint itself with Azure Entra ID (App Service Authentication).

Now let’s take it one step further β€” use Azure API Management (APIM) to enforce role-based access before the request ever reaches the Logic App.

πŸ”Ž Note: In this setup we are only Exposing Logic Apps via APIM, not exposing them as an MCP Server through APIM.


1. Register App Roles

In the Entra App Registration that represents your API:

  • Define roles such as:
    • wf_arithmetic_add
    • wf_arithmetic_sub
  • Assign these roles to the service principals / users that should be allowed.

You will see these roles reflected in the API permissions blade once consented.

Roles


2. Onboard Logic App Standard into APIM

As of today, APIM does not offer a direct import wizard for Logic App Standard (EasyAuth enabled).

So we create the API manually:

  • In APIM, create a new API (e.g., LAStandardAPI).
  • Add operations such as Add, Sub, etc. to mirror your workflow triggers/actions.
  • Configure each operation’s backend to point to the Logic App’s HTTPS endpoint (already protected by EasyAuth).

πŸ’‘ Tip:

In Logic App Standard, each workflow is deployed under a unique folder name and exposes an HTTP trigger endpoint with a SAS token (sig=...) in the URL.

Examples:

  • https://mcpblogdemo.azurewebsites.net:443/api/wf_arithmetic_add/triggers/RcvReq/invoke?api-version=2022-05-01&sp=%2Ftriggers%2FRcvReq%2Frun&sv=1.0&sig=Key
  • https://mcpblogdemo.azurewebsites.net:443/api/wf_arithmetic_sub/triggers/RcvReq/invoke?api-version=2022-05-01&sp=%2Ftriggers%2FRcvReq%2Frun&sv=1.0&sig=Key
  • https://mcpblogdemo.azurewebsites.net:443/api/wf_arithmetic_mul/triggers/RcvReq/invoke?api-version=2022-05-01&sp=%2Ftriggers%2FRcvReq%2Frun&sv=1.0&sig=Key

These work, but they bypass role-based authorization and rely only on SAS tokens.

To simplify API Management configuration and improve security:

  • Use a consistent name for the HTTP trigger in each workflow (e.g., RcvReq or In).
  • Remove the sig query parameter when fronting through APIM (Easy Auth + JWT replaces SAS).
  • Construct the backend URL dynamically and apply a rewrite policy instead of hardcoding each workflow path.

Example backend URL pattern:

https://<logicapp>.azurewebsites.net/api/wf_arithmetic_add/triggers/RcvReq/invoke?api-version=2022-05-01

Example APIM frontend mapping:

/api/wf_arithmetic_add β†’ wf_arithmetic_add/triggers/RcvReq/invoke?api-version=2022-05-01
/api/wf_arithmetic_sub β†’ wf_arithmetic_sub/triggers/RcvReq/invoke?api-version=2022-05-01
/api/wf_arithmetic_mul β†’ wf_arithmetic_mul/triggers/RcvReq/invoke?api-version=2022-05-01

Implement this with rewrite-uri or set-backend-service in APIM, so all workflow calls are role-protected and you no longer depend on SAS tokens.


3. Validate JWT in APIM

In the inbound policy (applied at all operations), use validate-jwt with your tenant’s OpenID configuration and issuer. This configuration:

  • Uses the tenant-specific OpenID configuration (v2 endpoint).
  • Accepts tokens from the tenant issuer (https://sts.windows.net/<TENANT_ID>/).
  • Returns 401 Unauthorized when validation fails (signature/issuer issues).
  • (Optional but recommended) You can add an <audiences> block later to enforce the API’s audience if needed.
<policies>
  <inbound>
    <base />

    <validate-jwt header-name="Authorization"
                  failed-validation-httpcode="401"
                  failed-validation-error-message="Unauthorized.">
      <openid-config url="https://login.microsoftonline.com/<TENANT_ID>/v2.0/.well-known/openid-configuration" />
      <issuers>
        <issuer>https://sts.windows.net/<TENANT_ID>/</issuer>
      </issuers>

    <!-- Optional coarse filter: require at least one of the workflow roles to be present.
           This narrows tokens early but still enforces the exact role below. -->
      <!--
      <required-claims>
        <claim name="roles" match="any">
          <value>wf_arithmetic_add</value>
          <value>wf_arithmetic_sub</value>
          <value>wf_arithmetic_mul</value>
        </claim>
      </required-claims>
      -->
    </validate-jwt>

    <set-variable name="token" value="@{
      var authHeader = context.Request.Headers.GetValueOrDefault("Authorization", "");
      if (!string.IsNullOrEmpty(authHeader) && authHeader.StartsWith("Bearer "))
      {
        return authHeader.Substring(7);
      }
      return null;
    }" />

    <set-variable name="roles" value="@(
      context.Variables.GetValueOrDefault("token", "").AsJwt()?.Claims["roles"]?.FirstOrDefault() ?? ""
    )" />

    <set-variable name="roles_csv" value="@{
      var tok = (string)context.Variables.GetValueOrDefault("token", "");
      var jwt = string.IsNullOrEmpty(tok) ? null : tok.AsJwt();
      var arr = (jwt != null && jwt.Claims != null && jwt.Claims.ContainsKey("roles"))
        ? jwt.Claims["roles"]
        : new string[0];
      return string.Join(",", arr);   // e.g. "wf_arithmetic_add,wf_arithmetic_sub"
    }" />

    <set-variable name="wf" value="@{
      var p = context.Request.Url.Path ?? "";
      if (p.EndsWith("/")) { p = p.Substring(0, p.Length - 1); }
      var i = p.LastIndexOf("/");
      return (i >= 0 ? p.Substring(i + 1) : p).ToLower();
    }" />

    <set-variable name="isAuthorized" value="@{
      var wf = ((string)context.Variables.GetValueOrDefault("wf","")).ToLower();
      var roles = ((string)context.Variables.GetValueOrDefault("roles_csv","")).ToLower().Replace(" ", "");
      if (string.IsNullOrEmpty(wf) || string.IsNullOrEmpty(roles)) { return false; }

      // comma-delimited contains check to avoid partial matches
      var haystack = "," + roles + ",";
      var needle = "," + wf + ",";
      return haystack.Contains(needle);
    }" />

    <choose>
      <when condition="@((bool)context.Variables["isAuthorized"])">
        <!-- Authorized -->
      </when>
      <otherwise>
        <return-response>
          <set-status code="403" reason="Forbidden" />
          <set-body>@("{\"error\":\"Role missing or invalid\"}")</set-body>
        </return-response>
      </otherwise>
    </choose>

    <!-- Rewrite frontend /api/{workflow} to Logic App trigger backend -->
    <rewrite-uri template="@{
        var wf = (string)context.Variables["wf"];
        return $"/api/{wf}/triggers/RcvReq/invoke?api-version=2022-05-01"; 
    }" />

  </inbound>
  <backend>
    <base />
  </backend>
  <outbound>
    <base />
  </outbound> 
  <on-error>
    <base />
  </on-error>
</policies>

Enter fullscreen mode Exit fullscreen mode

PolicyOverview


4. Validation

Once the policy is applied in APIM, you can validate the behavior by invoking the API with different roles in the JWT.

βœ… Case 1: Authorized Role

When the token contains the role wf_arithmetic_add, the request to /api/wf_arithmetic_add succeeds with 200 OK.

AuthorizedRequest


❌ Case 2: Unauthorized Role

When the same token is used to call /api/wf_arithmetic_mul without the role wf_arithmetic_mul, APIM rejects the request with 403 Forbidden and returns the error:

UnAuthorizedRequest


βœ… Result

This proves that:

  • APIM validates the JWT and extracts the roles.
  • Access is only granted when the role matches the workflow name.
  • Any mismatch results in 403 Forbidden before the Logic App executes.

βœ… Role-based authorization is successfully enforced at APIM, while EasyAuth continues to protect the Logic App backend.

Top comments (0)