Azure API Management (APIM)
Quick walk through Azure apim
Creating the API Management
Well to create the APIM is really quick as in all cloud services, so I prefer you to continue with the official doc, for that reason we will start here:
Setting up the API Endpoint
Let's modify the Echo API
endpoints.
Policies base structure
This is the base structure of a police in where we have the Inbound
-> Backend
-> Output
and at the end Error
. Let's explain what each part is in charge of:
policies
:inbound
: This section contains policies that are applied to incoming requests before they reach the backend service.backend
: This section contains policies that are applied to requests before they are sent to the backend service.outbound
: This section contains policies that are applied to responses before they are sent back to the client.on-error
: This section contains policies that are applied to responses before they are sent back to the client.base
: This is a reference to default policies at the corresponding policy tag.
<policies>
<inbound>
<base/>
</inbound>
<backend>
<base/>
</backend>
<outbound>
<base/>
</outbound>
<on-error>
<base/>
</on-error>
</policies>
Making our Policy
- Now Knowing the XML structure of a policy now we can start doing ours own custom policy. So let's
get the fingerprint of a cert file every we have a request
V1
- Here's a simple custom policy that get fingerprint and returns it into the header in case the request is Ok, but also in Error
<policies>
<!-- As we know the first step is to intercept the request inbound for this case -->
<inbound>
<base/>
<!-- We get the fingerprint value and set to receivedFingerprint variable -->
<set-variable name="receivedFingerprint" value="@(context.Request.Certificate?.Thumbprint)"/>
</inbound>
<backend>
<!-- We don't do anything at the backend for this case. So let it work as expected -->
<base/>
</backend>
<outbound>
<base/>
<!-- Let's return the fingerprint we get into the headers as Received-Fingerprint -->
<set-header name="Received-Fingerprint" exists-action="override">
<!-- Here we're getting the value of the variable we defined in the inbound step -->
<value>@((string)context.Variables["receivedFingerprint"])</value>
</set-header>
</outbound>
<on-error>
<!-- We're going to return the fingerprint in the error header too, because it can be helpful, but you can omit it if you want-->
<base/>
<set-header name="Received-Fingerprint" exists-action="override">
<value>@((string)context.Variables["receivedFingerprint"])</value>
</set-header>
</on-error>
</policies>
V2
- Now let's modify the policy a little to give us more information at the response
<policies>
<inbound>
<base />
<set-variable name="receivedFingerprint" value="@(context.Request.Certificate?.Thumbprint)" />
</inbound>
<backend>
<base />
</backend>
<outbound>
<base />
<!-- Here starts our conditional so the idea is to return fingerprint if it's present otherwise set default value -->
<choose>
<when condition="@(context.Variables["receivedFingerprint"] != null)">
<!-- If "receivedFingerprint" variable is found and not null, set the header with its value -->
<set-header name="Received-Fingerprint" exists-action="override">
<value>@((string)context.Variables["receivedFingerprint"])</value>
</set-header>
</when>
<otherwise>
<!-- If "receivedFingerprint" variable is not found or null, set a default value -->
<set-header name="Received-Fingerprint" exists-action="override">
<value>finger-print-not-found</value>
</set-header>
</otherwise>
</choose>
</outbound>
<on-error>
<base />
<!-- Same idea as the outbound, but in error case -->
<choose>
<when condition="@(context.Variables["receivedFingerprint"] != null)">
<!-- If "receivedFingerprint" variable is found and not null, set the header with its value -->
<set-header name="Received-Fingerprint" exists-action="override">
<value>@((string)context.Variables["receivedFingerprint"])</value>
</set-header>
</when>
<otherwise>
<!-- If "receivedFingerprint" variable is not found or null, set a default value -->
<set-header name="Received-Fingerprint" exists-action="override">
<value>finger-print-not-found</value>
</set-header>
</otherwise>
</choose>
</on-error>
</policies>
V3
- For this third version let's make a little more interesting and let's add the possible to validate if the fingerprint user is sending a fingerprint that is registered at the policy and also give more context responding a JSON at the body too
<policies>
<inbound>
<base />
<!-- Fingerprint sent by user's request-->
<set-variable name="receivedFingerprint" value="@(context.Request.Certificate?.Thumbprint)" />
<!-- Declare a list of strings where each value is a fingerprint value -->
<!-- Remember that when saving the XML at APIM encode it's base to html that's why ' is valid -->
<!-- and many times this type of declaration will save our lives -->
<set-variable name="allowedFingerprints" value="['2C44ADA1234BB1EC62C8B204E95A5aAA3414674CC','2']" />
<choose>
<!-- First we validate that fingerprint is sent and if not, simple return the error. -->
<when condition="@(string.IsNullOrEmpty(context.Variables["receivedFingerprint"] as string))">
<return-response>
<!-- We return 400 http status code with the corresponding header that fingerprint is not sent -->
<!-- but also at the JSON we return a message -->
<set-status code="400" reason="Bad Request" />
<set-header name="Received-Fingerprint" exists-action="override">
<value>finger-print-not-found</value>
</set-header>
<set-body>@("{\"error\": \"Fingerprint is missing\"}")</set-body>
</return-response>
</when>
<!-- This is hard to understand, so we will see at detail then. For now just keep in mind that if this -->
<!-- condition is true it will continue to backend and then outbound or on-error and response will be -->
<!-- handle at the corresponding xml tag logic -->
<when condition="@(((List<string>)JsonConvert.DeserializeObject<List<string>>((string)context.Variables["allowedFingerprints"])).Contains(context.Variables["receivedFingerprint"]))" />
<otherwise>
<return-response>
<!-- We return 401 http status code with the corresponding header that fingerprint is sent, but -->
<!-- it is not allowed because it's not present at the allowedFingerprints list -->
<!-- and also at the JSON we return a message -->
<set-status code="401" reason="Unauthorized" />
<set-header name="Received-Fingerprint" exists-action="override">
<value>@((string)context.Variables["receivedFingerprint"])</value>
</set-header>
<set-body>@("{\"error\": \"Fingerprint not allowed\"}")</set-body>
</return-response>
</otherwise>
</choose>
</inbound>
<backend>
<base />
</backend>
<outbound>
<base />
<!-- Simple adds the header with the fingerprint value and then continues at normal flow -->
<set-header name="Received-Fingerprint" exists-action="override">
<value>@((string)context.Variables["receivedFingerprint"])</value>
</set-header>
</outbound>
<on-error>
<base />
<return-response>
<!-- In case of any error returns 500 http status code, but also returns the fingerprint sent -->
<!-- at the header and also returns a JSON at the body with the detailed error message -->
<set-status code="500" reason="Internal Server Error" />
<set-header name="Received-Fingerprint" exists-action="override">
<value>@((string)context.Variables["receivedFingerprint"])</value>
</set-header>
<set-body>@($"{{\"error\": \"An unexpected error occurred. Details: {context.LastError.Message}\"}}")</set-body>
</return-response>
</on-error>
</policies>
- Breaking down the main condition of the policy, for this the first to know it's that any operation the is part of programing language (C# for this case) has to be enclosure by
@()
so we will have something like this:
((List<string>)JsonConvert.DeserializeObject<List<string>>((string)context.Variables["allowedFingerprints"])).Contains(context.Variables["receivedFingerprint"])
- Now with this let's digest each part:
- Variable Type Casting:This part casts the result of the
(List<string>)
JsonConvert.DeserializeObject
method to aList<string>
. TheDeserializeObject
method converts a JSON string into an object of the specified type, and in this case, it's being cast to a list of strings. - Deserialization from JSON:This part deserializes a JSON string into a list of strings. The
JsonConvert.DeserializeObject<List<string>>(...)
JsonConvert.DeserializeObject
method is from theJson.NET
library and is used to convertJSON-formatted
data into a.NET object
. In this case, it's expecting a JSON string representing a list of strings. - Accessing a Value from a Dictionary:This part accesses a value from a dictionary named
(string)context.Variables["allowedFingerprints"]
Variables
stored within an object named context. It retrieves the value associated with the key"allowedFingerprints"
and casts it to a string. - Checking for String Existence in List:This part checks if the list obtained from deserialization contains a specific string. It uses the
.Contains(context.Variables["receivedFingerprint"])
Contains method
to check if the string value stored incontext.Variables["receivedFingerprint"]
is present in the list obtained earlier.
- In summary, this line of code deserializes a JSON string stored in the variable
allowedFingerprints
into a list of strings, then checks ifreceivedFingerprint
exists in that list.
Now with this simple example we can extrapolate and create a lot of different APIM customs policies, for more context and functions you can go to this doc. Thanks for reading and see you soon :3
- Setting up the API Endpoint
- Policies base structure
- Making our Policy
- V1
- V2
- V3