When using the trigger action When a HTTP request is received in Power Automate there are three possible options for who can trigger the flow:
- Any user in my tenant.
- Specific users in my tenant.
- Anyone.
When using the anyone option, the flow can be executed by anyone that happens to know the flow URL. It would be nice if there was an option to restrict the anyone option to trusted IP addresses, this functionality is not included by default, but it is easy to add it with a trigger condition.
The expression:
triggerOutputs().headers['CLIENT-IP']
Provides access to the IP address of the calling client and the port it used to make the request, for example:
82.68.224.86:58749
So it is almost exactly what we need to add a trigger action to restrict the HTTP trigger to a single IP, or a group of trusted IP addresses. Using this expression, we can capture only the IP address:
first(split(triggerOutputs()['headers']?['CLIENT-IP'], ':'))
So a trigger condition to restrict the flow to a single IP address would look like this:
@equals(first(split(triggerOutputs()['headers']?['CLIENT-IP'], ':')), '82.68.224.86')
To filter it to a list of trusted IP addresses you can use this expression:
@contains(
json('[ "82.68.224.86", "82.68.224.87" ]'),
first(split(triggerOutputs()['headers']?['CLIENT-IP'], ':'))
)
In the above example, both 82.68.224.86 and 82.68.224.87 would be able to execute the HTTP flow.
Other interesting outputs:
- User-Agent (e.g. Mozilla/5.0,(Windows NT 10.0; Win64; x64)
- sec-ch-ua-platform (e.g. Windows)
To create a trigger condition, click on the settings of the HTTP trigger:
and then add the trigger condition at the bottom of the settings:
Don’t forget you that trigger conditions require an @ symbol at the start of the expression.
By using this trigger condition, the flow will not execute at all when it is requested from an IP address that is not in the list of trusted IP’s.
Eliot says
I know this is probably redundant, but for some odd reason I always make a habit with most string fields that I’m going to validate on to wrap a trim() around them.
So here, that’d be:
trim(triggerOutputs()[‘headers’]?[‘CLIENT-IP’])
I also almost *always* do the if() dance for anything that I care about, too. For a string it’s simpler than most others:
if(
equals(
STRING_VALUE,
null
),
false,
if(
empty(
trim(STRING_VALUE)
),
false,
EXPRESSIONS_TO_RUN_IF_TRUE
)
)
So, here, the ‘contains()’ trigger condition would go in the EXPRESSIONS_TO_RUN_IF_TRUE. Or, if we just wanted the STRING_VALUE to exist, then replace the second if() with the empty(). 🙂
I know that this seems like a lot of over-engineering, but it’s habit, now, and therefore only takes me a few seconds to do. Plus it easily handles all(?) the simple failures/false positives that might otherwise.
Equally … there’s (perhaps) a simpler solution, here. (more in a second!)
Eliot says
I lied … I don’t have a simpler solution, lol!!
But, you could secrete the IPs in an array Parameter, which might save a couple of bits of text.
Not so simple for most to achieve, mind.
Additionally (in complicated land), if you run it through an Azure APIM you can effectively restrict access to the flow/LA to just the Managed Identity of the APIM itself.