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.
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
orIn
).- 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
orset-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>
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.
β 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:
β 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)