I am trying to use Azure Pipeline to build my API Management infrastructure automatically and have successfully added the API and API Operation but having trouble defining the Operation specific Policy.
I have this policy that I based on a very useful article https://www.serverlessnotes.com/docs/expose-service-bus-queue-through-api-management
<policies>
<inbound>
<base />
<set-variable name="sasToken" value="#{
return "bob";
}" />
<set-header name="Authorization" exists-action="override">
<value>#(context.Variables.GetValueOrDefault<string>("sasToken"))</value>
</set-header>
<set-header name="Content-type" exists-action="override">
<value>application/json</value>
</set-header>
<set-header name="Ocp-Apim-Subscription-Key" exists-action="delete" />
<set-header name="BrokerProperties" exists-action="override">
<value>#{
return string.Format("{{\"SessionId\":\"{0}\"}}", "bob");
}</value>
</set-header>
<set-backend-service base-url="https://i365intfnapidevtbcoresb.servicebus.windows.net/i365intfnapidevtbcoresbqueue" />
</inbound>
<backend>
<base />
</backend>
<outbound>
<base />
</outbound>
<on-error>
<base />
</on-error>
</policies>
There is actually more code in the value bits, but for illustration I've removed them.
However if I put this in a separate file (or even an inline variable) and run the Azure Powershell command
Set-AzApiManagementPolicy -Context $apim_context -ApiId $apiId -OperationId addmessage -PolicyFilePath <path to policy xml file>
Where the $ values are variables I declared previously.
I get the error
Error Details:
[Code= ValidationError, Message= 'bob' is an unexpected token. Expecting white space. Line 5, position 21., Target=
representation]
Basically, I cant work out how to format the function thats in the value attribute or value elements. The bit starting with #{}.
I can enter the policy via the Azure API Management screen no problem, but cant do it via the Set-AzApiManagementPolicy command.
Any ideas on how to format it.
Thanks.
It will escape it for you if you use the Format parameter.
Set-AzApiManagementPolicy -Context $apim_context -Format "application/vnd.ms-azure-apim.policy.raw+xml" -ApiId $apiId -OperationId addmessage -Policy $policyString
Solved it in the end, You have to encode some of the characters to make it work. For example #quot; instead of "
So I have in my Azure Powershell script:
$policyString = '
<policies>
<inbound>
<base />
<set-variable name="sasToken" value="#{
string resourceUri = "$(azure.servicebusqueueendpoint)";
string keyName = "$(azure.servicebusaccesspolicykey)";
string key = "$(azure.servicebusaccesspolicyvalue)";
TimeSpan sinceEpoch = DateTime.UtcNow - new DateTime(1970, 1, 1);
var expiry = Convert.ToString((int)sinceEpoch.TotalSeconds + 120);
string stringToSign = System.Uri.EscapeDataString(resourceUri) + "\n" + expiry;
HMACSHA256 hmac = new HMACSHA256(Encoding.UTF8.GetBytes(key));
var signature = Convert.ToBase64String(hmac.ComputeHash(Encoding.UTF8.GetBytes(stringToSign)));
var sasToken = String.Format("SharedAccessSignature sr={0}&sig={1}&se={2}&skn={3}",
System.Uri.EscapeDataString(resourceUri),
System.Uri.EscapeDataString(signature), expiry, keyName);
return sasToken;
}" />
<set-header name="Authorization" exists-action="override">
<value>#((string)context.Variables.GetValueOrDefault("sasToken"))</value>
</set-header>
<set-header name="Content-type" exists-action="override">
<value>application/json</value>
</set-header>
<set-header name="Ocp-Apim-Subscription-Key" exists-action="delete" />
<set-header name="BrokerProperties" exists-action="override">
<value>#{
return string.Format("{{\"SessionId\":\"{0}\"}}", "1");
}</value>
</set-header>
<set-backend-service base-url="$(azure.servicebusqueueendpoint)" />
</inbound>
<backend>
<base />
</backend>
<outbound>
<base />
</outbound>
<on-error>
<base />
</on-error>
</policies>
'
Set-AzApiManagementPolicy -Context $apim_context -ApiId $apiId -OperationId addmessage -Policy $policyString
and this now seems to do the trick.
You would probably also need to do the same for c# things like:
context.Variables.GetValueOrDefault<string>
would be
context.Variables.GetValueOrDefault<string>
to make it work otherwise it conflicts with the xml
Thanks mclayton for pointing me in the right direction.
Related
I have an API management inbound policy where I can grab a users id from within a JWT. For the purpose of some testing, I then want API management to check in the policy, "Is this ID within the list of tester IDs that are allowed to access here"
<policies>
<inbound>
<base />
<validate-jwt header-name="Authorization" failed-validation-httpcode="401" failed-validation-error-message="Unauthorized. Access token is missing or invalid.">
<openid-config url="myurl" />
</validate-jwt>
<set-header name="header-value-userId" exists-action="override">
<value>#(context.Request.Headers.GetValueOrDefault("Authorization").AsJwt()?.Claims.GetValueOrDefault("oid"))</value>
</set-header>
<!--
PSEUDOCDOE below to describe my intention
How can I check that the abover header-value-userId value is within a hardcoded list at this point?
#{
string[] userList = ["user1", "user2", "user3"];
var match = userList.FirstOrDefault(x => x.Contains(header-value-userId));
if(match == null)
return bad request
}
-->
</inbound>
<backend>
<base />
</backend>
<outbound>
<base />
</outbound>
<on-error>
<base />
</on-error>
</policies>
Thanks for any help.
--- UPDATE ---
Thanks to the answer from Markus Meyer I now have this working. See below my now full working example.
<policies>
<inbound>
<base />
<validate-jwt header-name="Authorization" failed-validation-httpcode="401" failed-validation-error-message="Unauthorized. Access token is missing or invalid.">
<openid-config url="myurl" />
</validate-jwt>
<set-variable name="isValidUser" value="#{
var email = context.Request.Headers.GetValueOrDefault("Authorization").AsJwt()?.Claims.GetValueOrDefault("emails");
string[] emailList = new string[] { "email1#gmail.com", "email2#gmail.com" };
var match = emailList.Any(x => x.Contains(email));
if(match == true)
{
return true;
}
return false;
}" />
<choose>
<when condition="#(context.Variables.GetValueOrDefault<bool>("isValidUser") == false)">
<return-response>
<set-status code="401" reason="Unauthorized" />
<set-header name="Content-Type" exists-action="override">
<value>application/json</value>
</set-header>
<set-body>#("{\"status\": \"" + "User not valid" + "\"}")</set-body>
</return-response>
</when>
<otherwise>
<return-response>
<set-status code="200" reason="Valid" />
<set-header name="Content-Type" exists-action="override">
<value>application/json</value>
</set-header>
<set-body>#("{\"status\": \"" + "User valid" + "\"}")</set-body>
</return-response>
</otherwise>
</choose>
</inbound>
<backend>
<base />
</backend>
<outbound>
<base />
</outbound>
<on-error>
<base />
</on-error>
</policies>
Using the choose policy allows you to check a boolean condition.
In the current example, the condition is set in the set-variable policy
return-response will return your wanted response.
Complete example:
<policies>
<inbound>
<base />
<validate-jwt header-name="Authorization" failed-validation-httpcode="401" failed-validation-error-message="Unauthorized. Access token is missing or invalid.">
<openid-config url="myurl" />
</validate-jwt>
<set-header name="header-value-userId" exists-action="override">
<value>#(context.Request.Headers.GetValueOrDefault("Authorization").AsJwt()?.Claims.GetValueOrDefault("oid"))</value>
</set-header>
<set-variable name="isValidUser" value="#{
string[] userList = new string[] { "user1", "user2", "user3" };
var match = userList.FirstOrDefault(x => x.Contains("header-value-userId"));
if(match == null)
{
return true;
}
return false;
}" />
<choose>
<when condition="#(context.Variables.GetValueOrDefault<bool>("isValidUser") == false)">
<return-response>
<set-status code="401" reason="Unauthorized" />
<set-header name="Content-Type" exists-action="override">
<value>application/json</value>
</set-header>
<set-body>#("{\"status\": \"" + "User not valid" + "\"}")</set-body>
</return-response>
</when>
</choose>
</inbound>
<backend>
<base />
</backend>
<outbound>
<base />
</outbound>
<on-error>
<base />
</on-error>
</policies>
A few things to note:
This this the correct code for the string[]:
string[] userList = new string[] { "user1", "user2", "user3" };
There's no need to store the usereId into the header. The value can also be stored in a variable.
Trying to make a policy for getting a bearer token trough a send-request sticking it in the Authorization header and then posting JSON data to the given back-end.
But when I test it within the test tab of Azure I always receive the same error:
Even when I add <forward-request timeout="60" follow-redirects="60"/> it does not work.
I also tried it without the follow-redirects which is defaulted to false but also no effect.
I am completly new to Azure so any help would be appreciated.
Here is my policy:
<policies>
<inbound>
<base />
<send-request ignore-error="true" timeout="20" response-variable-name="bearerToken" mode="new">
<set-url>{{AuthenticationServer}}</set-url>
<set-method>POST</set-method>
<set-header name="Content-Type" exists-action="override">
<value>application/x-www-form-urlencoded</value>
</set-header>
<set-header name="Authorization" exists-action="override">
<value>Basic {{Base64encodedusernamepassword}}</value>
</set-header>
<set-body>#{
return "grant_type=client_credentials";
}</set-body>
</send-request>
<set-header name="Authorization" exists-action="override">
<value>#("Bearer " + (String)((IResponse)context.Variables["bearerToken"]).Body.As<JObject>()["access_token"])</value>
</set-header>
<!-- Don't expose APIM subscription key to the backend. -->
<!--<set-header name="Ocp-Apim-Subscription-Key" exists-action="delete" /> -->
<set-backend-service base-url="{{BaseURI}}" />
</inbound>
<backend>
<forward-request timeout="60" follow-redirects="true" />
</backend>
<outbound>
<base />
</outbound>
<on-error>
<base />
</on-error>
</policies>
The problem was that the endpoint was behind a private VNet so getting VPN access fixed the issue.
With Apim i'm trying to call a backend Api that needs a OAuth2 validation. This question are more or less similair to this: Azure API Management: Oauth2 with backend API
But there are no good answer here...
I have been reading alot about policies and caching.
But can't seem to set it up correctly. I hope to be able to cal the apim, and then the apim calls the backend api to get a token and with that token call an Api to get some output data.
I also found one where i had to setup some policies in the backend-part..
Can anyone help me set up the policies ?
my policy is like:
<policies>
<inbound>
<base />
<set-variable name="originBearer" value="#(context.Request.Headers.GetValueOrDefault("Authorization", "empty_token").Split(' ')[0].ToString())" />
<send-request ignore-error="true" timeout="20" response-variable-name="bearerToken" mode="new">
<set-url>{{lookupAccessTokenUrl}}</set-url>
<set-method>GET</set-method>
<set-header name="Content-Type" exists-action="override">
<value>application/x-www-form-urlencoded</value>
</set-header>
<set-body>#{
return "client_id={{HLR-app-client-id}}&scope={{HLR-scope}}&client_secret={{HLR-secret}}&assertion="+(string)context.Variables["originBearer"]+"&grant_type=urn:ietf:params:oauth:grant-type:client_credentials&requested_token_use=on_behalf_of";
}</set-body>
</send-request>
<set-variable name="requestResponseToken" value="#((String)((IResponse)context.Variables["bearerToken"]).Body.As<JObject>()["access_token"])" />
<set-header name="Authorization" exists-action="override">
<value>#("Bearer " + (string)context.Variables["requestResponseToken"])</value>
</set-header>
</inbound>
<backend>
<base />
</backend>
<outbound>
<base />
</outbound>
<on-error>
<base />
</on-error>
</policies>
I found the answer to my own Question :-)
I try to comment on each line, but if you take alle the code and put it together you get a policy to handle Oauth2 in a backend api.
In the inbound section, the cache-lookup-value
Assigns the value in cache to the context variable called “bearerToken”.
On first entry, the cache value will be null and the variable will not be
created.
<inbound>
<cache-lookup-value key="cacheAccessToken" variable-name="bearerToken" />
Create a variable that contains clientid and secret - needed to call the api
<set-variable name="user-password" value="{{HLR-Clientid}}:{{HLR-Secret}}"
/>
<choose>
Checks if the context variable collection contains a key called
“bearerToken” and if not found executes the code between the opening and closing
“” XML elements.
<when condition="#(!context.Variables.ContainsKey("bearerToken"))">
Initiates the request to the OAuth endpoint with a response
timeout of 20 seconds. This will put the response message into the variable
called “oauthResponse”
<send-request mode="new" response-variable-name="oauthResponse" timeout="20" ignore-error="false">
<set-url>{{lookupAccessTokenUrl}}</set-url>
<set-method>POST</set-method>
<set-header name="Content-Type" exists-action="override">
<value>application/x-www-form-urlencoded</value>
</set-header>
here you define your header Authorization and use the variable that contains clientid and password
<set-header name="Authorization" exists-action="override">
<value>#("Basic " + system.Convert.ToBase64String(Encoding.UTF8.GetBytes((string)context.Variables["user-password"])))</value>
</set-header>
<set-body>#("grant_type=client_credentials&scope={{HLR-Scope}}")</set-body>
</send-request>
Casts the response as a JSON object to allow the retrieval of the “access_token” value using an indexer and assigns it to the context variable “accessToken”.
<set-variable name="AccessToken" value="#((string)((IResponse)context.Variables["oauthResponse"]).Body.As<JObject>()["access_token"])" />
Store result in cache and where we add the contents of the variable “accessToken” into cache for a period of 3600 seconds.
<cache-store-value key="cacheAccessToken" value="#((string)context.Variables["AccessToken"])" duration="3600" />
Set the variable in a context-variable, then it can be used right now
<set-variable name="bearerToken" value="#((string)context.Variables["AccessToken"])" />
</when>
</choose>
<base />
</inbound>
<backend>
<!--Creates the request to the backend web service. Here we are placing the response from the web service into the variable called “transferWSResponse”.-->
<send-request mode="copy" response-variable-name="transferWSResponse" timeout="60" ignore-error="false">
<set-method>GET</set-method>
<!--Is the creating the “Authorization” header to be sent with the request.-->
<set-header name="Authorization" exists-action="override">
<value>#("Bearer " + (string)context.Variables["bearerToken"])</value>
</set-header>
<!--Removes the APIM subscription from being forwarded to the backend web service.-->
<set-header name="Ocp-Apim-Subscription-Key" exists-action="delete" />
<set-header name="Content-Type" exists-action="override">
<value>application/json</value>
</set-header>
</send-request>
</backend>
<outbound>
<!--Now we need to return the response message from the backend web service to the caller. This is done in the “<outbound>” policy section. Here we just simply return the value of the variable “transferWSResponse” back to the caller-->
<return-response response-variable-name="transferWSResponse" />
<base />
</outbound>
<on-error>
<base />
</on-error>
</policies>
I am looking to implement an Azure API Management policy for bank account validation and as part of that API I want to call out to a token endpoint and pass that into the bank account validation. The problem I have is around setting the inbound send-request policy to accept the query parameters from NamedValues/KeyVault.
The URL for the token validation is as below:
https://apps.applyfinancial.co.uk/validate-api/rest/authenticate?username=USERNAME.com&password=PASSWORD
I tried using the set-query-parameter policy but it appears that this is not allowed within the send-request node based on the below validation error:
Error in element 'send-request' on line 16, column 10: The element
'send-request' has invalid child element 'set-query-parameter'. List
of possible elements expected: 'set-header, set-body,
authentication-certificate, authentication-token,
authentication-token-store, authentication-managed-identity, proxy'.
One or more fields contain incorrect values:;Error in element
'send-request' on line 16, column 10: The element 'send-request' has
invalid child element 'set-query-parameter'. List of possible elements
expected: 'set-header, set-body, authentication-certificate,
authentication-token, authentication-token-store,
authentication-managed-identity, proxy'.
POLICY
<policies>
<inbound>
<!-- Send request to Token Server to validate token (see RFC 7662) -->
<send-request mode="new" response-variable-name="tokenstate" timeout="20" ignore-error="true">
<set-url>https://apps.applyfinancial.co.uk/validate-api/rest/authenticate</set-url>
<set-method>POST</set-method>
<set-query-parameter name="username" exists-action="override">
<value>{{BankValidationUsername}}</value>
</set-query-parameter>
<set-query-parameter name="password" exists-action="override">
<value>{{BankValidationPassword}}</value>
</set-query-parameter>
</send-request>
<base />
</inbound>
<backend>
<base />
</backend>
<outbound>
<base />
</outbound>
<on-error>
<base />
</on-error>
</policies>
My question is how do you set query parameters in the send-request section of an API policy?
OK,
You can't set a query parameter within the scope of the send-request but you can do it within the ionbound policy. Also it seems better to pull the KeyVault hosted Named Values in to variables and use them in the request that way.
<policies>
<inbound>
<rewrite-uri template="/" />
<set-variable name="username" value="{{BankValidationUsername}}" />
<set-variable name="password" value="{{BankValidationPassword}}" />
<set-variable name="errorresponse" value="" />
<send-request mode="new" response-variable-name="tokenstate" ignore-error="false">
<set-url>#($"https://apps.applyfinancial.co.uk/validate-api/rest/authenticate?username={(string)context.Variables["username"]}&password={(string)context.Variables["password"]}")</set-url>
<set-method>POST</set-method>
</send-request>
<set-query-parameter name="token" exists-action="override">
<value>#((string)((IResponse)context.Variables["tokenstate"]).Body.As<JObject>()["token"])</value>
</set-query-parameter>
<base />
</inbound>
<backend>
<base />
</backend>
<outbound>
<base />
</outbound>
<on-error>
<set-header name="ErrorSource" exists-action="override">
<value>#(context.LastError.Source)</value>
</set-header>
<set-header name="ErrorReason" exists-action="override">
<value>#(context.LastError.Reason)</value>
</set-header>
<set-header name="ErrorMessage" exists-action="override">
<value>#(context.LastError.Message)</value>
</set-header>
<set-header name="ErrorScope" exists-action="override">
<value>#(context.LastError.Scope)</value>
</set-header>
<set-header name="ErrorSection" exists-action="override">
<value>#(context.LastError.Section)</value>
</set-header>
<set-header name="ErrorPath" exists-action="override">
<value>#(context.LastError.Path)</value>
</set-header>
<set-header name="ErrorPolicyId" exists-action="override">
<value>#(context.LastError.PolicyId)</value>
</set-header>
<set-header name="ErrorStatusCode" exists-action="override">
<value>#(context.Response.StatusCode.ToString())</value>
</set-header>
<base />
</on-error>
</policies>
Is there any way to decrypt a bearer token in an API management policy in order to create a condition it's acr_values, for example a tenant.
Looking at the MS documentation it does not seem possible, I would be looking to achieve something like:
<when condition="#(context.Request.Headers["Authorization"] --DO MAGIC HERE-- .acr_values["tenant"] == "contoso" ">
<set-backend-service base-url="http://contoso.com/api/8.2/" />
</when>
Alternatively something like the example here but for setting the backed service:
http://devjourney.com/blog/2017/03/23/extract-jwt-claims-in-azure-api-management-policy/
Documentation I've read:
https://learn.microsoft.com/en-us/azure/api-management/api-management-transformation-policies#example-4
https://learn.microsoft.com/en-us/azure/api-management/policies/authorize-request-based-on-jwt-claims?toc=api-management/toc.json#policy
Did you try .AsJwt() method (https://learn.microsoft.com/en-us/azure/api-management/api-management-policy-expressions#ContextVariables):
<policies>
<inbound>
<base />
<set-header name="tenant" exists-action="append">
<value>#{
var jwt = context.Request.Headers.GetValueOrDefault("Authorization").AsJwt();
return jwt?.Claims.GetValueOrDefault("tenant") ?? "unknown";
}</value>
</set-header>
<choose>
<when condition="#(context.Request.Headers.GetValueOrDefault("tenant", "unknown") == "some-tenant" )">
<set-backend-service base-url="http://contoso.com/api/8.2/" />
</when>
</choose>
</inbound>
<backend>
<base />
</backend>
<outbound>
<base />
</outbound>
<on-error>
<base />
</on-error>
Also I'm not sure if you need it as a header to backend request, if not consider using set-variable policy.
Ok so I got it working in a very hacky way, you can set vales of the decrypted token in the header and then set conditions on that header.
<policies>
<inbound>
<base />
<set-header name="tenant" exists-action="append">
<value>#{
string tenant = "unknown";
string authHeader = context.Request.Headers.GetValueOrDefault("Authorization", "");
if (authHeader?.Length > 0)
{
string[] authHeaderParts = authHeader.Split(' ');
if (authHeaderParts?.Length == 2 && authHeaderParts[0].Equals("Bearer", StringComparison.InvariantCultureIgnoreCase))
{
Jwt jwt;
if (authHeaderParts[1].TryParseJwt(out jwt))
{
tenant = (jwt.Claims.GetValueOrDefault("tenant", "unknown"));
}
}
}
return tenant;
}</value>
</set-header>
<choose>
<when condition="#(context.Request.Headers.GetValueOrDefault("tenant", "unknown") == "some-tenant" )">
<set-backend-service base-url="http://contoso.com/api/8.2/" />
</when>
</choose>
</inbound>
<backend>
<base />
</backend>
<outbound>
<base />
</outbound>
<on-error>
<base />
</on-error>
A few years have passed since this has been answered, but as I found a less verbose solution, without actually modifying the request headers, i thought it would be nice to share for others:
<set-variable name="tenant" value="#{
var authHeader = context.Request.Headers.GetValueOrDefault("Authorization", "");
return authHeader.AsJwt()?.Claims.GetValueOrDefault("tenant", "");
}" />
...
<choose>
<when condition="#(context.Variables.GetValueOrDefault("tenant", "") == "your-tenant-id")">