Appearance
API Management - Authentication
To simplify authentication and secure our backend services, even ones without internal authentication such as Function Apps, we use Azure API Management (APIM) to validate JWT tokens at the gateway level. This means that a form of validation on provided access token is already applied before the request even hits the backend service.
This centralized approach guarantees that every incoming request has already been verified, so application developers can trust that incoming requests already have been pre-validated. This adds a first layer of security and simplifies authentication handling in the backend, where we no longer need to use client secrets configured for every backend.
Essentially, once a request passes through APIM, downstream services can safely assume that the token is valid and meets our authentication criteria. Note that any form of authorization still needs to be done inside the backend application itself, but token claims can be used to assist in this procedure.
Authentication scenarios
We support several authentication scenarios out of the box, in order from very little verification to more strict verification. It is preferred to use the most strict verification that is available that meets your requirements.
In most circumstances the Issuer + Audience Validation is the appropriate choice where any further authorization requirements (if any) are handled in the backend application.
Issuer Validation Only
Only use this type of authentication when your app really does not need any form of authentication other than the need to present any valid token from the Haskoning tenant. It is not suitable for any other scenario.
What It Does:
Checks that the token is issued by our trusted Azure AD tenant using OpenID.Use Case:
Suitable only for internal or generic APIs where any token from our tenant is acceptable.Benefit:
Simplifies the process by simply trusting any valid token from our identity provider.Example:
The API Inventory, which should be accessible by basically anyone that has a valid Haskoning token, it does not need further restrictions given that the data is not sensitive and should be accessible by any colleague, even if these users are Haskoning guest users.
Issuer + Audience Validation
What It Does:
In addition to validating the token’s issuer, it verifies that the token’s intended audience matches the specific API.Use Case:
When a token must be explicitly intended for a particular API, ensuring tighter control over access.Benefit:
Prevents tokens targeting other services from being used to access another API, thus isolating API access.Example:
Most of our application have a frontend and backend solution, the frontend in this case should request a token for a specific API app registration (audience) and present that token to the API. The API in this scenario knows that the token presented really is ment to be used for this specific API, and that at app registration level it was permitted to do so.
Another example would be an API-to-API communication where API A requests a token to specifically talk to API B, and that at app registration level it was permitted to do so.
Issuer + Audience + Claims Validation
What It Does:
Extends the previous checks by also validating specific claims within the token (such as user roles, group membership or other specific permissions).Use Case:
Required to use for scenarios that require fine-grained access control or when additional user context is needed.Benefit:
Provides an extra layer of security by ensuring that the token meets custom criteria beyond issuer and audience and may shift authentication related implement This however requires more configuration at the Entra ID level, which with current restriction makes this a less convenient option.
Defining API policies
Using API management, it is possible to define API policies on multiple scopes. From most broad to most narrow:
Global (all APIs)
Workspace (all APIs associated with a selected workspace, not used in Equation)
Product (all APIs associated with a selected product)
API (all operations in an API)
Operation (single operation in an API, not used in Equation)
This document has a focus for policies at the API scope, as this is the most common place to introduce authentication.
The API policy concept is very broad and is well documented, this document only describes a practical use subset of authentication related concepts. Read the official docs for more information: https://learn.microsoft.com/en-us/azure/api-management/api-management-howto-policies
Example policy
Below is an example policy:
xml
<policies>
<inbound>
<validate-jwt header-name="Authorization" failed-validation-httpcode="401" failed-validation-error-message="Unauthorized. Access token is missing or invalid.">
<openid-config url="https://login.microsoftonline.com/15f996bf-aad1-451c-8d17-9b95d025eafc/v2.0/.well-known/openid-configuration" />
<audiences>
<audience>api://aa1921f3-c3e5-4384-bb6d-d31fc3afea9b</audience>
</audiences>
<issuers>
<issuer>https://login.microsoftonline.com/15f996bf-aad1-451c-8d17-9b95d025eafc/v2.0</issuer>
<issuer>https://sts.windows.net/15f996bf-aad1-451c-8d17-9b95d025eafc/</issuer>
</issuers>
<required-claims>
<claim name="appid" match="any">
<value>5a2f6e3f-513c-4a8c-a832-e26be08ce3bf</value>
<value>0a9d846f-4164-4992-85b0-d68990ce121a</value>
</claim>
</required-claims>
</validate-jwt>
<set-backend-service backend-id="waterfuser-backend" />
<base />
</inbound>
<backend>
<base />
</backend>
<outbound>
<base />
</outbound>
<on-error>
<base />
</on-error>
</policies>This will perform the following checks:
If no JWT token is valid at all, return a
401 Unauthorizedwill be returned.Otherwise, retrieve OpenID config from
https://login.microsoftonline.com/15f996bf-aad1-451c-8d17-9b95d025eafc/v2.0/.well-known/openid-configuration. This is a standard endpoint that retrieves the OpenID config for the Haskoning tenant, this should never be changed unless explicitly needed. This configuration needs to be loaded to make sure the token is not tampered with or manually crafted by an attacker. It is possible to create a “valid” token yourself, but it will not be signed by the Entra ID in that case, loading the OpenID configuration makes sure this validation is performed and that a crafted/tampered token is rejected with a401 Unauthorized.The policy requires the audience (
audin the JWT token) to be exactlyapi://aa1921f3-c3e5-4384-bb6d-d31fc3afea9b. If the audience is different, a401 Unauthorizedwill be returned.The policy requires the audience (
issin the JWT token) to be exactlyhttps://login.microsoftonline.com/15f996bf-aad1-451c-8d17-9b95d025eafc/v2.0or exactlyhttps://sts.windows.net/15f996bf-aad1-451c-8d17-9b95d025eafc/. These two issuer endpoints are both standard for the entire Haskoning tenant and should never be changed unless explicitly needed. If the issuer is different, a401 Unauthorizedwill be returned.The
sts.windows.netissuer is actually deprecated, settingaccessTokenAcceptedVersionto2in the app registration manifest will make sure it is not used anymore.The
claimssection required theappidproperty of the JWT token to be either5a2f6e3f-513c-4a8c-a832-e26be08ce3bfor0a9d846f-4164-4992-85b0-d68990ce121a, otherwise a401 Unauthorizedwill be returned.
Only if all conditions are met, the request will be forwarded to the backend with id waterfuser-backend.
Applying with Terraform
Applying an API policy through Terraform is quite easy once you have your API policy defined.
For example, store your policy as policy.xml in your project, and then in your API definition, add the following:
hcl
resource "azurerm_api_management_api_policy" "api_policy" {
resource_group_name = 'YOUR_RG'
api_management_name = 'APIM_NAME'
api_name = 'API_NAME'
xml_content = file("${path.root}/policy.xml")
}If you prefer to use a more flexible policy template and assign variables to it, this is possible as well. In that case use the Terraform templatefile function and assign variables to it:
hcl
resource "azurerm_api_management_api_policy" "api_policy" {
resource_group_name = 'YOUR_RG'
api_management_name = 'APIM_NAME'
api_name = 'API_NAME'
xml_content = templatefile("${path.root}/policy.xml.tpl", {
"backend" = "waterfuser-acceptance"
"some_other_var = "something"
})
}And use them in your policy template, for example:
<set-backend-service backend-id="${backend}" />Conclusion
This APIM-based authentication strategy not only simplifies backend development by offloading security concerns but also provides flexible control over API access. Whether using basic issuer validation for internal APIs, adding audience checks for backend services, or enforcing strict claim validations when needed, this approach meets a wide range of security needs while remaining manageable via infrastructure-as-code with Terraform.