Client Certificate is not being passed on by Azure Application Gateway - azure

I've picked up something that someone else has set up.
We have an API Management instance, siting behind an Application Gateway, which has a policy on an API:
<inbound>
<choose>
<when condition="#(context.Request.Certificate == null)">
<return-response>
<set-status code="403" reason="Client certificate required..d1PD" />
</return-response>
</when>
</choose>
<choose>
<when condition="#(!context.Request.Certificate.Verify())">
<return-response>
<set-status code="403" reason="Client certificate cannot be verified..d2PD " />
</return-response>
</when>
</choose>
<choose>
<when condition="#(!context.Deployment.Certificates.Any(c => c.Value.Thumbprint == context.Request.Certificate.Thumbprint))">
<return-response>
<set-status code="403" reason="Client certificate is untrusted or invalid..d3PD" />
</return-response>
</when>
</choose>
<base />
</inbound>
In Postman I am passing a cert and key. The Postman Console shows
Client Certificate:
keyPath:"C:\selfsigned\internalscm.X.com.key"
pemPath:"C:\selfsigned\internalscm.X.com.crt"
pfxPath:""
I'm passing Ocp-Apim-Trace in the header of the request so am getting a trace back which contains:
traceEntries {2}
inbound [10]
..
6 {4}
source : authentication-certificate
timestamp : 2019-08-06T08:55:31.3435485Z
elapsed : 00:00:00.0006857
data {2}
message : Certificate was attached to request per configuration.
certificate {...}
7 {4}
source : choose
timestamp : 2019-08-06T08:55:31.3435485Z
elapsed : 00:00:00.0007011
data {3}
message : Expression was successfully evaluated.
expression : context.Request.Certificate == null
value : true
UPDATE:
The authentication-certificate assessment is of the backend's certificate and is nothing to do with the client certificate (.key and .crt) that Postman claims to be including in the request (the same result is returned if I pass a pfx and password instead of .key and .crt).
When I hit the API that the Gateway is protecting, I can see in the trace that it is processing the client certificate (and returning a 200):
{
"source": "client-certificate-handler",
"timestamp": "2019-08-09T15:47:46.3825928Z",
"elapsed": "00:00:00.0005974",
"data": "Requesting client certificate because next handler requires access to it."
},
{
"source": "client-certificate-handler",
"timestamp": "2019-08-09T15:47:46.6950495Z",
"elapsed": "00:00:00.3225172",
"data": "Client certificate thumbprint '6C03F4E7999999999999999999999999' received."
},
{
"source": "choose",
"timestamp": "2019-08-09T15:47:46.6950495Z",
"elapsed": "00:00:00.3225288",
"data": {
"message": "Expression was successfully evaluated.",
"expression": "context.Request.Certificate == null",
"value": false
}
},
{
"source": "choose",
"timestamp": "2019-08-09T15:47:46.9606395Z",
"elapsed": "00:00:00.5849700",
"data": {
"message": "Expression was successfully evaluated.",
"expression": "!context.Request.Certificate.Verify()",
"value": false
}
},
{
"source": "choose",
"timestamp": "2019-08-09T15:47:46.9606395Z",
"elapsed": "00:00:00.5850060",
"data": {
"message": "Expression was successfully evaluated.",
"expression": "!context.Deployment.Certificates.Any(c => c.Value.Thumbprint == context.Request.Certificate.Thumbprint)",
"value": false
}
}
So it looks like AppGateway is dropping the client certificate.
The trace doesn't give me enough to start deducing why the client certificate (assuming that Postman is transmitting it to the Gateway as it does the API) is being dropped. Where should I start?
For ref, when I remove the policy the request is processed as expected.

I'm not sure if you can make AppGateway pass the certificate - you need to check their docs. The reason I'm skeptical is bacuse the whole idea of AppGateway is to look into traffic and provide protection by doing that. And the only way to d othat is to terminate SSL connection at AppGateway level. See here for more information: https://learn.microsoft.com/en-us/azure/application-gateway/ssl-overview there are two modes for AppGateway: SSL temination when AppGateway makes HTTP (not HTTPS) calls to backend, and SSL end-to-end when AppGateway uses own SSL certificate to connect to backend.
Some client certificate information can be passed to backend via server variables: https://learn.microsoft.com/en-us/azure/application-gateway/rewrite-http-headers-url#mutual-authentication-server-variables

Related

manipulate response in apim outbound policies

I have SOAP service that I have converted to rest in apim and returns the response in json using the following outbound policy:
<outbound>
<base />
<xml-to-json kind="direct" apply="always" consider-accept-header="false" />
</outbound>
Response output is like below:
"response": {"currentABNRecord": {
"ABN":
{
"identifierValue": "xxxxx",
"identifierStatusCode": "ACT",
"issuingPartyCode": null,
"replacedIndicator": "N"
}
}
}
My requirement is to "manipulate" conditionally data by modifying "identifierStatusCode" and appending a new attribute "termsAnConditions" when "identifierStatusCode" is "ACT" and if it is DEL to "DELETED" to something else like below :
"response": {"currentABNRecord": {
"ABN":
{
"identifierValue": "xxxxx",
"identifierStatusCode": "ACT",
"issuingPartyCode": null,
"replacedIndicator": "N",
"termsAnConditions": "Some terms and conditions"
}
}
}
How can I call an azure function(perform all the conditional logic in it ) in the outbound policy and return the modified object. Is this the correct approach or can I do it policies.

APIM - validate-content on consumption tier

I recently changed our APIM instance from the developer tier, to the consumption tier, and am seeing some weird behavior in the validate-content policy. On the developer tier, this policy would work as expected and return a 400 error with the appropriate error message.
Below is the policy:
<validate-content unspecified-content-type-action="prevent" max-size="102400" size-exceeded-action="prevent">
<content type="application/json" validate-as="json" action="prevent" />
</validate-content>
Below is an example from the trace and the response from developer tier (expected behavior):
//Trace
validate-content (0.100 ms)
{
"name": "application/json",
"type": "RequestBody",
"validationRule": "IncorrectMessage",
"details": "Body of the request does not conform to the definition skills-POST-request, which is associated with the content type application/json. Property 'nam' has not been defined and the schema does not allow additional properties. Line: 1, Position: 7",
"action": "Prevented"
}
//Response
HTTP/1.1 400 Bad Request
vary: Origin
{
"statusCode": 400,
"message": "Body of the request does not conform to the definition skills-POST-request, which is associated with the content type application/json. Property 'nam' has not been defined and the schema does not allow additional properties. Line: 1, Position: 7"
}
However, now the same policy on the consumption tier returns the following trace and response (incorrect behavior):
//Trace
validate-content (4.736 ms)
{
"name": "application/json",
"type": "RequestBody",
"validationRule": "IncorrectMessage",
"details": "Body of the request does not conform to the definition skills-POST-request, which is associated with the content type application/json. Property 'nam' has not been defined and the schema does not allow additional properties. Line: 1, Position: 7",
"action": "Prevented"
}
validate-content (0.714 ms)
{
"name": null,
"type": "RequestBody",
"validationRule": "ValidationException",
"details": "Body of the request cannot be validated for the content type application/json. Value cannot be null.\r\nParameter name: key",
"action": "Prevented"
}
validate-content (2.679 ms)
{
"messages": [
"Value cannot be null.\r\nParameter name: key"
]
}
//response
HTTP/1.1 500 Internal Server Error
vary: Origin
{
"statusCode": 500,
"message": "Internal server error",
"activityId": "b3d76aed-fdf0-4240-a5c1-db49fed82105"
}
This looks to be some sort of bug perhaps in the content validation policy for the consumption tier?
I entered a support request with Microsoft and they determined this was a bug in API management. The workaround was to add the following to policy:
errors-variable-name="requestBodyValidation"
So the final policy now looks like:
<validate-content unspecified-content-type-action="prevent" max-size="102400" size-exceeded-action="prevent" errors-variable-name="requestBodyValidation">
<content type="application/json" validate-as="json" action="prevent" />
</validate-content>
As per the Azure documentation,Consumption tier in APIM supports TLS Settings,External Cache,Client Certificate authentication & Graph QL API's only. Therefore validate-content APIM policy doesn’t work for APIM Services running on consumption SKU.

"JWT Validation Failed: JWT not present.." in Azure API Management Service

For reference I am attempting to reproduce the solution talked about here: https://www.tech-findings.com/2020/02/securing-logic-app-with-azure-active-directory.html to use API Management to secure an Azure Logic App.
I am getting a JWT Error. When I visit the app url in the browser it gives:
{ "statusCode": 404, "message": "Resource not found" }
In the API Management Service test I get:
HTTP/1.1 401 Unauthorized
Following the trace through it shows:
validate-jwt (-0.111 ms)
{
"message": "JWT Validation Failed: JWT not present.."
}
I did some googling and tried the solutions at:
JWT validation failure error in azure apim
and
https://learn.microsoft.com/en-us/answers/questions/108008/azure-apim-jwt-token-validation-policy.html
Here is the inbound policy of from the API Management design:
<policies>
<inbound>
<base />
<set-method id="apim-generated-policy">POST</set-method>
<rewrite-uri id="apim-generated-policy" template="/request/paths/invoke//?api-version=2016-06-01&sp=/triggers/request/run&sv=1.0&sig={{[[LOGIC APP NAME]]_request-invoke_XXXXXXXXXXXXXXXXXXXXXXXX}}" />
<validate-jwt header-name="Authorization" failed-validation-httpcode="401" failed-validation-error-message="Request is not authorized or token failed" require-expiration-time="false" require-scheme="Bearer" require-signed-tokens="true">
<openid-config url="https://login.windows.net/[[TENANT NAME]].onmicrosoft.com/.well-known/openid-configuration" />
<audiences>
<audience>[[THE ID OF A REGISTERED APP]]</audience>
</audiences>
</validate-jwt>
<set-header name="Authorization" exists-action="delete" />
<set-header name="apim-generated-policy" exists-action="delete" />
</inbound>
<backend>
<base />
</backend>
<outbound>
<base />
</outbound>
<on-error>
<base />
</on-error>
</policies>
This is the manifest of the registered app:
{
"id": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
"acceptMappedClaims": null,
"accessTokenAcceptedVersion": 2,
"addIns": [],
"allowPublicClient": null,
"appId": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
"appRoles": [],
"oauth2AllowUrlPathMatching": false,
"createdDateTime": "2020-12-22T19:48:36Z",
"disabledByMicrosoftStatus": null,
"groupMembershipClaims": null,
"identifierUris": [
"api://XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
],
"informationalUrls": {
"termsOfService": null,
"support": null,
"privacy": null,
"marketing": null
},
"keyCredentials": [],
"knownClientApplications": [],
"logoUrl": null,
"logoutUrl": null,
"name": "LabsTestApp",
"oauth2AllowIdTokenImplicitFlow": false,
"oauth2AllowImplicitFlow": false,
"oauth2Permissions": [],
"oauth2RequirePostResponse": false,
"optionalClaims": null,
"orgRestrictions": [],
"parentalControlSettings": {
"countriesBlockedForMinors": [],
"legalAgeGroupRule": "Allow"
},
"passwordCredentials": [],
"preAuthorizedApplications": [],
"publisherDomain": "[[TENANT NAME]].onmicrosoft.com",
"replyUrlsWithType": [],
"requiredResourceAccess": [],
"samlMetadataUrl": null,
"signInUrl": null,
"signInAudience": "AzureADandPersonalMicrosoftAccount",
"tags": [],
"tokenEncryptionKeyId": null
}
Hoping you can help out - point me in the right direction.
For this question, there are more than one problem in your steps.
1. You mentioned the error { "statusCode": 404, "message": "Resource not found" } when you request the url in browser. The reason is when you request it in browser, it request with Get method but the url should be request with Post method. So it shows 404 not found.
2. When you test in API Management service, it shows 401 Unauthorized. The reason for this error is you did not provide the access token or the access token you provided is invalid. The steps in the document you mentioned are incomplete, please refer to the steps below:
1). First please make sure you have completed all of the steps in the document you provided.
2). Then go to the app you registered in azure ad and click "Manifest" tab, add a appRole in the json of "Manifest".
You can specify a name(anything you want) for this role, I named the role as Writer as the screenshot above shows. And you can also specify a "id"(in GUID format) as the value of the id field in appRole. For more details of add appRole, you can refer to this document.
3). You need to register another app in azure ad as the client app. Do same register operation as your document shows to register the other app, I registered the app and named huryGetToken4. Go to this app and click "API permissions" tab, click "Add a permission" and find the original app you registered, then add the permission Writer.
After add the Writer permission, you also need to click "Grant admin consent for xxx".
Then click "Certificates & secrets" tab, click "New client secret" to generate a client secret. Copy this secret because it will just show one time.
4). Then you need to get access token, please refer to the screenshot below to request for access token.
In the screenshot above, you need to replace the <tenant id> with your tenant id in the host url. And you also need to input the first three parameters. The last parameter grant_type is static.
5). Request for the access token, you will get the response like below screenshot.
Copy the value of access_token and paste it to this page to decode the token, you can see the claim roles with Writer permission in it. This claim is what you need to check in the <validate-jwt> policy in your APIM.
6). Go to your apim and click the pencil icon of validate-jwt policy.
7). Edit the "Reauired claims" like screenshot below:
8). After that, you can test the api in APIM service. Add a header with key: Authorization, value: Bearer <your access token>(note there is a blank between Bearer and access token).

Policy rewrite-uri To Append Context Variable in Azure APIM

What is the approach to a simple append to the url of a context variable such as context.Variables["accountKey"] during a policy rewrite?
The end result should be /accounts/232.
I have success earlier in setting it
set-variable (0.003 ms)
{
"message": "Context variable was successfully set.",
"name": "accountKey",
"value": "232"
}
One of the things tried:
<policies>
<inbound>
<base />
<rewrite-uri template="/accounts/{accountKey}" />
</inbound>
But I get this error
> Error Receive
> rewrite-uri (0.260 ms) {
> "messages": [
> null,
> "Variable accountKey has no value.",
> "Variable accountKey has no value."
> ] }
Configure the inbound rule in the policy as follows:
<inbound>
<base />
<set-variable name="accountKey" value="232" />
<rewrite-uri template="#{
return "/account/" + context.Variables.GetValueOrDefault<string>("accountKey");
}"/>
</inbound>
{} in rewrite-uri are for query string parameters in the original request URL.
Find more details about rewrite-uri section in the Microsoft docs at Rewrite URL - API Management transformation policies.
I ended up using this call as shown as an alternative:
<rewrite-uri template="#($"/account/{(string)context.Variables["accountKey"]}/")" />
In my case, my URL was
https://test.com/send/
I need to add query string from context variable name (name = myName)
https://test.com/send/myName
This is working for me:
<rewrite-uri template="#($"{(string)context.Variables["name"]}")" />

Api Management sets forward-request for a one way request

I want to log to log the request and the response so I place a one-way-request in the inbound and the outbound section:
<policies>
<inbound>
<send-one-way-request mode="new">
<set-url>#("example.com")</set-url>
<set-method>POST</set-method>
<set-header name="Content-Type" exists-action="override">
<value>application/json</value>
</set-header>
<set-body>#(context.Request.Body.As<string>(preserveContent: true))</set-body>
</send-one-way-request>
</inbound>
<backend>
<base />
</backend>
<outbound>
<send-one-way-request mode="new">
<set-url>#("example.com")</set-url>
<set-method>POST</set-method>
<set-header name="Content-Type" exists-action="override">
<value>application/json</value>
</set-header>
<set-body>#(context.Response.Body.As<string>(preserveContent: true))</set-body>
</send-one-way-request>
</outbound>
</policies>
identical call, except the body.
When I check the trace I see this in the inbound section:
send-one-way-request (0 ms)
"One way request was successfully send to https://...."
forward-request (9690 ms)
{
"response": {
"status": {
"code": 200,
"reason": "OK"
},
"headers": [
{
"name": "Pragma",
"value": "no-cache"
},
{
"name": "Content-Length",
"value": "2"
},
{
"name": "Cache-Control",
"value": "no-cache"
},
{
"name": "Content-Type",
"value": "application/json; charset=utf-8"
},
{
"name": "Date",
"value": "Wed, 05 Jul 2017 07:56:14 GMT"
},
{
"name": "Expires",
"value": "-1"
},
{
"name": "Server",
"value": "Microsoft-IIS/8.0"
},
{
"name": "X-AspNet-Version",
"value": "4.0.30319"
},
{
"name": "X-Powered-By",
"value": "ASP.NET"
}
]
}
}
but in the outbound I only get:
send-one-way-request (0 ms)
"One way request was successfully send to https://...."
and no forward request. Because I use a one way request I don't expect a response from the calls and I can't remember to have the forward-request in the inbound part (didn't found them in a saved traced call with a one way request in the inbound).
Is there maybe a configuration anything else that triggers a forward-request?
Edit:
I use the azure function to handle this. When I make a typo in the subdomain the forward-request disappears, but when I make a typo in the function name it is still there... Both requests are directed to the same azure function.
Edit2:
This is getting more confuse: when I send the default body from the swagger file the request-forward is not there. If I repeat the request or if i modify the default body it appears...
Since send-one-way request policy is completely asynchronous in regards to request processing itself, there is no guarantee that reply from such request will be logged, because by the time it is received the request itself may be long processed, response returned to client along with tracing information. So it works as a best effort, if by the time when response to send-one-way-request is received the main request is still being processed response will be logged, otherwise not.
In the first example reply to one-way request in inbound is logged because there is still a lot processing to be done on the main request - send request to backend, process outbound section. But when one-way request is placed as the last statement in outbound, right before response to the client is sent - response to one-way request will come too late to be placed into trace.
Typo in subdomain may trigger longer connection time, if such domain not actively refuses connections, thus will stall response, thus it will disappear from trace.
It's all a question of timing. If you want to make sure that main request processing is stalled until you get response from these side requests use send-request policy instead.

Resources