├── media └── vscode-snippets │ ├── apim-vscode-snippets-1.png │ ├── apim-vscode-snippets-2.png │ └── apim-vscode-snippets-3.png ├── examples ├── oauth-proxy │ ├── oauth-proxy-token-endpoint-fragment.xml │ ├── oauth-proxy-validate-token-fragment.xml │ ├── oauth-proxy-construct-authorization-redirect-fragment.xml │ ├── oauth-proxy-slide-session-fragment.xml │ ├── oauth-proxy-sign-out.xml │ ├── oauth-proxy-sign-in.xml │ ├── readme.md │ ├── oauth-proxy-session-fragment.xml │ └── oauth-proxy-callback.xml ├── List all inbound headers.policy.xml ├── Random load balancer simpler.policy.xml ├── Forward gateway hostname to backend for generating correct urls in responses.policy.xml ├── Set cache duration using response cache control header.policy.xml ├── Encrypt data using expressions.policy.xml ├── Send request context information to the backend service.policy.xml ├── Mask async calls as synchronous.policy.xml ├── Decrypt AES Data using policy expressions.xml ├── Use custom error messages for jwt-validate policy with on-error handler.policy.xml ├── Return HTTP 405 if the HTTP Method of the request is not defined.xml ├── Route requests to regional backend instances.xml ├── Filter response content based on product name.policy.xml ├── Authenticate using Managed Identity to access Service Bus.xml ├── Random load balancer.policy.xml ├── Route requests based on size.policy.xml ├── Add correlation id to inbound request.policy.xml ├── Authenticate using Managed Identity to access Storage Account.xml ├── Perform basic authentication.policy.xml ├── Parse a JWT token using expressions.xml ├── Pre-authorize requests based on HTTP method with validate-jwt.policy.xml ├── Loopback request for service at same API Management service.xml ├── Authenticate using Managed Identity to access Event Hub.xml ├── Get OAuth2 access token from AAD and forward it to the backend.policy.xml ├── Log errors to Stackify.policy.xml ├── Extract value from XML.xml ├── Call out to an HTTP endpoint and cache the response.policy.xml ├── Get X-CSRF token from SAP gateway using send request.policy.xml ├── Backend OAuth2 Authentication With Cache.policy.xml ├── DELETE a from to blobStorage account.xml ├── Authorize requests using external authorizer.policy.xml ├── Back-end API redundancy.policy.xml ├── Query CosmosDB.policy.xml ├── Look up Key Vault certificate using Managed Service Identity and call backend.policy.xml ├── Look up Key Vault secret using Managed Service Identity.policy.xml ├── Trigger Azure Data Factory Pipeline.policy.xml ├── Handle Power Query access request to custom API.policy.xml ├── Trigger Azure Data Factory Pipeline With Parameters.policy.xml ├── Create HMAC SHA256-Signed JWT.policy.xml ├── Extracting multiple values from xml documents.policy.xml ├── Generate Azure Relay Token.policy.xml ├── GET a file from blobStorage account.xml ├── Filter on IP Address when using Application Gateway.policy.xml ├── Replay request on error.policy.xml ├── PUT a file to blobStorage account.xml ├── Forward Azure Event Grid Event.xml ├── Generate Shared Access Signature and forward request to Azure storage.policy.xml ├── Get OAuth2 access token from AAD using client id and certificate using key vault manage identity.xml ├── Return a blob URL signed with a user delegation SAS token.xml └── Request OAuth2 access token from SuccessFactors using AAD JWT token.xml ├── LICENSE ├── README.md ├── SECURITY.md └── policy-expressions └── README.md /media/vscode-snippets/apim-vscode-snippets-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure/api-management-policy-snippets/HEAD/media/vscode-snippets/apim-vscode-snippets-1.png -------------------------------------------------------------------------------- /media/vscode-snippets/apim-vscode-snippets-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure/api-management-policy-snippets/HEAD/media/vscode-snippets/apim-vscode-snippets-2.png -------------------------------------------------------------------------------- /media/vscode-snippets/apim-vscode-snippets-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure/api-management-policy-snippets/HEAD/media/vscode-snippets/apim-vscode-snippets-3.png -------------------------------------------------------------------------------- /examples/oauth-proxy/oauth-proxy-token-endpoint-fragment.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /examples/oauth-proxy/oauth-proxy-validate-token-fragment.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | {{ClientId}} 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /examples/List all inbound headers.policy.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | {% for header in context.Request.Headers %} 11 | {{- header.Key }}: {{ header.Value }} 12 | {% endfor %} 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /examples/Random load balancer simpler.policy.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /examples/Forward gateway hostname to backend for generating correct urls in responses.policy.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | @("proto=" + context.Request.OriginalUrl.Scheme + ";host=" + context.Request.OriginalUrl.Host + ";") 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /examples/Set cache duration using response cache control header.policy.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | \d+)").Groups["maxAge"]?.Value; 18 | return (!string.IsNullOrEmpty(maxAge))?int.Parse(maxAge):300; 19 | }" 20 | /> 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /examples/Encrypt data using expressions.policy.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /examples/Send request context information to the backend service.policy.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | @(context.Product.Name) 11 | 12 | 13 | 14 | 15 | @(context.User.Id) 16 | @(context.Deployment.Region) 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Microsoft Corporation. All rights reserved. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE 22 | -------------------------------------------------------------------------------- /examples/oauth-proxy/oauth-proxy-construct-authorization-redirect-fragment.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /examples/Mask async calls as synchronous.policy.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | @(context.Response.Headers["location"][0]) 19 | GET 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /examples/Decrypt AES Data using policy expressions.xml: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 8 | 9 | 10 | 17 | 18 | 19 | @{  20 | string inBody = (string)context.Variables["plainText"];  21 | return inBody;  22 | } 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /examples/Use custom error messages for jwt-validate policy with on-error handler.policy.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {{base64-encoded-hashing-secret}} 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 21 | 22 | 23 | 24 | Unauthorized. Access token is missing or invalid. 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /examples/Return HTTP 405 if the HTTP Method of the request is not defined.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | @{ 22 | return new JObject( 23 | new JProperty("status", "HTTP 405"), 24 | new JProperty("message", "Method not allowed") 25 | ).ToString(); 26 | } 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /examples/Route requests to regional backend instances.xml: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /examples/Filter response content based on product name.policy.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | @{ 22 | var response = context.Response.Body.As(); 23 | foreach (var key in new [] {"current", "minutely", "hourly", "daily", "alerts"}) { 24 | response.Property (key).Remove (); 25 | } 26 | return response.ToString(); 27 | } 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /examples/oauth-proxy/oauth-proxy-slide-session-fragment.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 21 | 22 | 23 | 24 | @($"{{CookiePrefix}}={(string)context.Variables["encryptedCookie"]}; SameSite=Lax; secure; path=/; expires={DateTimeOffset.FromUnixTimeMilliseconds((long)context.Variables["cookie-expiry"]).ToString("R")}; Secure; HttpOnly" ) 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /examples/Authenticate using Managed Identity to access Service Bus.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | @((string)context.Variables["msi-access-token"]) 16 | 17 | { 18 | "Body": "APIM sending request using AAD Token", 19 | "BrokerProperties":{"Trusted Service":"APIM"}, 20 | "UserProperties":{"Priority":"Medium","Customer":"Contoso"} 21 | } 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /examples/Random load balancer.policy.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | @{return Guid.NewGuid().ToString();} 20 | 21 | A gateway-related error occurred while processing the request. 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /examples/Route requests based on size.policy.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /examples/Add correlation id to inbound request.policy.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | @{ 15 | var guidBinary = new byte[16]; 16 | Array.Copy(Guid.NewGuid().ToByteArray(), 0, guidBinary, 0, 10); 17 | long time = DateTime.Now.Ticks; 18 | byte[] bytes = new byte[6]; 19 | unchecked 20 | { 21 | bytes[5] = (byte)(time >> 40); 22 | bytes[4] = (byte)(time >> 32); 23 | bytes[3] = (byte)(time >> 24); 24 | bytes[2] = (byte)(time >> 16); 25 | bytes[1] = (byte)(time >> 8); 26 | bytes[0] = (byte)(time); 27 | } 28 | Array.Copy(bytes, 0, guidBinary, 10, 6); 29 | return new Guid(guidBinary).ToString(); 30 | } 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /examples/Authenticate using Managed Identity to access Storage Account.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | https://{{storageaccountname}}.blob.core.windows.net/container/{{blobtoread}} 14 | GET 15 | 16 | 2019-07-07 17 | 18 | 19 | 20 | 21 | 22 | @($"{((IResponse)context.Variables["blobdata"]).Body.As() }") 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /examples/Perform basic authentication.policy.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /examples/Parse a JWT token using expressions.xml: -------------------------------------------------------------------------------- 1 | 14 | 15 | 16 | 17 | 18 | 19 | 24 | 25 | 26 | {{signing-key}} 27 | 28 | 29 | 30 | 33 | 34 | element == "sendEmail"); 38 | }"> 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /examples/Pre-authorize requests based on HTTP method with validate-jwt.policy.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | {{signing-key}} 14 | 15 | 16 | 17 | true 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | {{signing-key}} 26 | 27 | 28 | 29 | true 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | {{signing-key}} 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /examples/Loopback request for service at same API Management service.xml: -------------------------------------------------------------------------------- 1 | 12 | 13 | 14 | 15 | 16 | 17 | https://localhost/{your API Management service endpoint url} 18 | GET 19 | 20 | application/json 21 | 22 | 23 | {your API Management service domain} 24 | 25 | 26 | @($"{(string)context.Variables["subscriptionKey"]}") 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /examples/Authenticate using Managed Identity to access Event Hub.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | @(String.Concat("Bearer ",(string)context.Variables["msi-access-token"])) 21 | 22 | { "Event":"apim-using -aad token", "TrustedService":"AAD" } 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /examples/Get OAuth2 access token from AAD and forward it to the backend.policy.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | {{authorizationServer}} 17 | POST 18 | 19 | application/x-www-form-urlencoded 20 | 21 | 22 | @{ 23 | return "client_id={{clientId}}&scope={{scope}}&client_secret={{clientSecret}}&grant_type=client_credentials"; 24 | 25 | // For Azure AD v1, try return statement below 26 | // return "client_id={{clientId}}&resource={{scope}}&client_secret={{clientSecret}}&grant_type=client_credentials"; 27 | } 28 | 29 | 30 | 31 | 32 | 33 | @("Bearer " + (String)((IResponse)context.Variables["bearerToken"]).Body.As()["access_token"]) 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /examples/Log errors to Stackify.policy.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | On Error 23 | 24 | 25 | https://api.stackify.com/Log/Save 26 | POST 27 | 28 | value 29 | 30 | 31 | V1 32 | 33 | 34 | {{stackify-api-key}} 35 | 36 | 37 | @{ 38 | return new JObject( 39 | new JProperty("Environment","{{environment-name}}"), 40 | new JProperty("ServerName", context.Deployment.ServiceName), 41 | new JProperty("AppName", "{{app-name}}"), 42 | new JProperty("AppLoc", "/usr/local/stackify/stackify-agent"), 43 | new JProperty("Logger", "stackify-log-log4j12-1.0.12"), 44 | new JProperty("Platform", "java"), 45 | new JProperty("Msgs", 46 | new JArray( 47 | new JObject( 48 | new JProperty("Msg", context.LastError.Message), 49 | new JProperty("Th", "main"), 50 | new JProperty("EpochMs", (new DateTimeOffset(DateTime.Now)).ToUnixTimeSeconds() * 1000 ), 51 | new JProperty("Level", "error"), 52 | new JProperty("SrcMethod", context.LastError.Source) 53 | ))) 54 | ).ToString(); 55 | } 56 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /examples/Extract value from XML.xml: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | 13 | 14 | 15 | @{ 16 | // The XML from which you want to extract information may come from various places. 17 | var xml = "ABCDEF123456789"; 18 | //var xml = context.Request.Body.As(preserveContent: true); //read from request body while also preserving the body 19 | //var xml = context.Response.Body.As(preserveContent: true); //read from response body while also preserving the body 20 | 21 | var ret = ""; 22 | 23 | using (XmlReader reader = XmlReader.Create(new StringReader(xml))) 24 | { 25 | reader.MoveToContent(); 26 | reader.Read(); 27 | 28 | while (!reader.EOF && ret == "") 29 | { 30 | // Read until we reach the node of interest, then extract the information into a return value, which will gracefully break out of the loop 31 | if (reader.Name == "SomeCustomNestedElement") 32 | { 33 | var el = XNode.ReadFrom(reader) as XElement; 34 | ret = el == null ? "" : el.Value; 35 | } else { 36 | reader.Read(); 37 | } 38 | } 39 | } 40 | 41 | return ret; 42 | } 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Azure API Management Policy Snippets 2 | 3 | # Policy Toolkit 4 | 5 | [Azure API Management policy toolkit](https://github.com/Azure/azure-api-management-policy-toolkit) is a set of libraries and tools for authoring policy documents for Azure API Management. The toolkit was designed to help create and test policy documents with complex expressions. 6 | 7 | # Examples 8 | 9 | The `examples/` folder contains policy examples contributed by the product team and the user community. The samples are meant to be re-used verbatim, provide inspiration or serve as learning aids. Some of them are parameterized using [Named Values](https://docs.microsoft.com/en-us/azure/api-management/api-management-howto-properties) (formerly known as Properties), which look like this: `{{some-value}}`. When using parametrized samples, you will have to either define relevant Named Values or replace them with values in place. 10 | 11 | # Policy expressions cheat-sheet 12 | 13 | The `policy-expressions` folder contains a [cheat-sheet](policy-expressions/README.md) with common policy expressions that are often used when authoring Azure API Management policies. 14 | 15 | # Visual Studio Code snippets 16 | 17 | The `vscode-snippets/` folder contains user snippets for Visual Studio Code. User snippets are helpful for streamlining workflow and simplifying document editing with autocomplete and easy navigation. Please, refer to the [Visual Studio Code documentation](https://code.visualstudio.com/docs/editor/userdefinedsnippets) on how to use them. 18 | 19 | ![Azure API Management VS Code User Snippet 1](media/vscode-snippets/apim-vscode-snippets-1.png) 20 | 21 | ![Azure API Management VS Code User Snippet 2](media/vscode-snippets/apim-vscode-snippets-2.png) 22 | 23 | ![Azure API Management VS Code User Snippet 3](media/vscode-snippets/apim-vscode-snippets-3.png) 24 | 25 | # Helpful Links 26 | 27 | - [Policies Reference](https://docs.microsoft.com/en-us/azure/api-management/api-management-policies) 28 | - [Policy Expressions](https://docs.microsoft.com/en-us/azure/api-management/api-management-policy-expressions) 29 | - [Handling Errors in Policies](https://docs.microsoft.com/en-us/azure/api-management/api-management-error-handling-policies) 30 | - [Policy Toolkit](https://github.com/Azure/azure-api-management-policy-toolkit) 31 | - [APIM Samples](https://aka.ms/apim/samples) 32 | 33 | To learn about Azure API Management go [here](https://azure.microsoft.com/en-us/services/api-management/). 34 | 35 | # Contributing 36 | 37 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. 38 | -------------------------------------------------------------------------------- /examples/Call out to an HTTP endpoint and cache the response.policy.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | @{ 22 | var code = context.Request.MatchedParameters.GetValueOrDefault("place"); 23 | var key = "{{google-geo-api-key}}"; 24 | return $"https://maps.googleapis.com/maps/api/geocode/json?address={code}&key={key}"; 25 | } 26 | 27 | GET 28 | 29 | 30 | 31 | 32 | ()["results"][0]["geometry"]["location"].ToString())"/> 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Security 4 | 5 | Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/). 6 | 7 | If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://aka.ms/opensource/security/definition), please report it to us as described below. 8 | 9 | ## Reporting Security Issues 10 | 11 | **Please do not report security vulnerabilities through public GitHub issues.** 12 | 13 | Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://aka.ms/opensource/security/create-report). 14 | 15 | If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://aka.ms/opensource/security/pgpkey). 16 | 17 | You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://aka.ms/opensource/security/msrc). 18 | 19 | Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: 20 | 21 | * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) 22 | * Full paths of source file(s) related to the manifestation of the issue 23 | * The location of the affected source code (tag/branch/commit or direct URL) 24 | * Any special configuration required to reproduce the issue 25 | * Step-by-step instructions to reproduce the issue 26 | * Proof-of-concept or exploit code (if possible) 27 | * Impact of the issue, including how an attacker might exploit the issue 28 | 29 | This information will help us triage your report more quickly. 30 | 31 | If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://aka.ms/opensource/security/bounty) page for more details about our active programs. 32 | 33 | ## Preferred Languages 34 | 35 | We prefer all communications to be in English. 36 | 37 | ## Policy 38 | 39 | Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://aka.ms/opensource/security/cvd). 40 | 41 | 42 | -------------------------------------------------------------------------------- /examples/Get X-CSRF token from SAP gateway using send request.policy.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | @(context.Request.Url.ToString()) 15 | HEAD 16 | 17 | Fetch 18 | 19 | 20 | 21 | @(context.Request.Headers.GetValueOrDefault("Authorization")) 22 | 23 | 24 | 25 | 26 | 27 | 28 | @(((IResponse)context.Variables["SAPCSRFToken"]).Headers.GetValueOrDefault("x-csrf-token")) 29 | 30 | 31 | @{ 32 | string rawcookie = ((IResponse)context.Variables["SAPCSRFToken"]).Headers.GetValueOrDefault("Set-Cookie"); 33 | string[] cookies = rawcookie.Split(';'); 34 | /* new session sends a XSRF cookie */ 35 | string xsrftoken = cookies.FirstOrDefault( ss => ss.Contains("sap-XSRF")); 36 | /* existing sessions sends a SessionID. No other cases anticipated at this point. Please create a GitHub Pull-Request if you encounter uncovered settings. */ 37 | if(xsrftoken == null){ 38 | xsrftoken = cookies.FirstOrDefault( ss => ss.Contains("SAP_SESSIONID")); 39 | } 40 | 41 | return xsrftoken.Split(',')[1];} 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /examples/Backend OAuth2 Authentication With Cache.policy.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | {{authorizationServer}} 21 | POST 22 | 23 | application/x-www-form-urlencoded 24 | 25 | @{ 26 | return "client_id={{clientId}}&scope={{scope}}&client_secret={{clientSecret}}&grant_type=client_credentials"; 27 | } 28 | 29 | ())" /> 30 | 31 | 32 | 33 | 34 | 35 | 36 | @("Bearer " + (string)context.Variables["bearerToken"]) 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /examples/DELETE a from to blobStorage account.xml: -------------------------------------------------------------------------------- 1 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | @{ return "https://{{StorageAccount.Name}}.blob.core.windows.net/" + (string)context.Request.MatchedParameters("blobContainer") + "/" + (string)context.Request.MatchedParameters("fileNameWithExtension",""); } 27 | DELETE 28 | 29 | {{StorageAccount.Name}}.blob.core.windows.net 30 | 31 | 32 | {{Gateway.Name}} 33 | 34 | 35 | 2019-12-12 36 | 37 | 38 | 39 | @("Bearer " + (string)context.Variables["msi-access-token"]) 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /examples/Authorize requests using external authorizer.policy.xml: -------------------------------------------------------------------------------- 1 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | @("Bearer realm="+context.Request.OriginalUrl.Host) 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 34 | 35 | 36 | 37 | {{authorizer-url}} 38 | GET 39 | 40 | @(context.Request.Headers.GetValueOrDefault("Authorization")) 41 | 42 | 43 | 44 | ()["status"].ToString())"/> 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /examples/Back-end API redundancy.policy.xml: -------------------------------------------------------------------------------- 1 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /examples/Query CosmosDB.policy.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | https://{database-account}.documents.azure.com/dbs/{db}/colls/{coll}/docs 14 | POST 15 | 16 | @{ 17 | var verb = "post"; 18 | var resourceType = "docs"; 19 | var resourceLink = ""; 20 | var key = ""; 21 | var keyType = "master"; 22 | var tokenVersion = "1.0"; 23 | var date = context.Variables.GetValueOrDefault("requestDateString"); 24 | 25 | var hmacSha256 = new System.Security.Cryptography.HMACSHA256 { Key = Convert.FromBase64String(key) }; 26 | 27 | verb = verb ?? ""; 28 | resourceType = resourceType ?? ""; 29 | resourceLink = resourceLink ?? ""; 30 | 31 | string payLoad = string.Format("{0}\n{1}\n{2}\n{3}\n{4}\n", 32 | verb.ToLowerInvariant(), 33 | resourceType.ToLowerInvariant(), 34 | resourceLink, 35 | date.ToLowerInvariant(), 36 | "" 37 | ); 38 | 39 | byte[] hashPayLoad = hmacSha256.ComputeHash(System.Text.Encoding.UTF8.GetBytes(payLoad)); 40 | string signature = Convert.ToBase64String(hashPayLoad); 41 | 42 | return System.Uri.EscapeDataString(String.Format("type={0}&ver={1}&sig={2}", 43 | keyType, 44 | tokenVersion, 45 | signature)); 46 | } 47 | 48 | 49 | application/query+json 50 | 51 | 52 | True 53 | 54 | 55 | @(context.Variables.GetValueOrDefault("requestDateString")) 56 | 57 | 58 | 2017-02-22 59 | 60 | 61 | true 62 | 63 | 64 | @("{\"query\": \"SELECT * FROM c WHERE c.id = @id\", " + 65 | "\"parameters\": [{ \"name\": \"@id\", \"value\": \"" + context.User.Id + "\"}]}") 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /examples/Look up Key Vault certificate using Managed Service Identity and call backend.policy.xml: -------------------------------------------------------------------------------- 1 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | https://msikvtest.vault.azure.net/secrets/mycert/?api-version=2016-10-01 26 | GET 27 | 28 | 29 | 30 | ()["value"].ToString())" /> 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /examples/Look up Key Vault secret using Managed Service Identity.policy.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | https://msikvtest.vault.azure.net/secrets/mysecret/?api-version=7.0 25 | GET 26 | 27 | 28 | 29 | 30 | ())" /> 31 | 32 | 33 | 34 | 35 | 36 | 37 | application/json 38 | 39 | @((string)context.Variables["keyvaultsecretResponse"]) 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /examples/Trigger Azure Data Factory Pipeline.policy.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 10 | 11 | 12 | 37 | 38 | 39 | 40 | 41 | @(String.Concat("Bearer ", ((string)context.Variables.GetValueOrDefault("managed-identity-token")))) 42 | 43 | 44 | 45 | 46 | 47 | application/json 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /examples/Handle Power Query access request to custom API.policy.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | Bearer authorization_uri=https://login.microsoftonline.com/{{AADTenantId}}/oauth2/v2.0/authorize?response_type=code%26client_id=a672d62c-fc7b-4e81-a576-e60dc46e951d 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | https://your-custom-apim-domain 34 | 35 | 36 | https://sts.windows.net/{{AADTenantId}}/ 37 | 38 | 39 | 40 | user_impersonation 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /examples/Trigger Azure Data Factory Pipeline With Parameters.policy.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 10 | 11 | 12 | 37 | 38 | 39 | 40 | 41 | @(String.Concat("Bearer ", ((string)context.Variables.GetValueOrDefault("managed-identity-token")))) 42 | 43 | 44 | 45 | 46 | 47 | application/json 48 | 49 | 50 | { 51 | "UserEmailAddress": "{{context.Request.MatchedParameters["emailAddress"]}}" 52 | } 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /examples/Create HMAC SHA256-Signed JWT.policy.xml: -------------------------------------------------------------------------------- 1 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | @{ 37 | // 1) Construct the Base64Url-encoded header 38 | var header = new { typ = "JWT", alg = "HS256" }; 39 | var jwtHeaderBase64UrlEncoded = Convert.ToBase64String(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(header))).Replace("/", "_").Replace("+", "-"). Replace("=", ""); 40 | // As the header is a constant, you may use this equivalent Base64Url-encoded string instead to save the repetitive computation above. 41 | // var jwtHeaderBase64UrlEncoded = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9"; 42 | 43 | // 2) Construct the Base64Url-encoded payload 44 | var exp = new DateTimeOffset(DateTime.Now.AddMinutes(10)).ToUnixTimeSeconds(); // sets the expiration of the token to be 10 minutes from now 45 | var username = "john_doe"; 46 | var payload = new { exp, username }; 47 | var jwtPayloadBase64UrlEncoded = Convert.ToBase64String(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(payload))).Replace("/", "_").Replace("+", "-"). Replace("=", ""); 48 | 49 | // 3) Construct the Base64Url-encoded signature 50 | var signature = new HMACSHA256(Encoding.UTF8.GetBytes("{{hashing-secret}}")).ComputeHash(Encoding.UTF8.GetBytes($"{jwtHeaderBase64UrlEncoded}.{jwtPayloadBase64UrlEncoded}")); 51 | var jwtSignatureBase64UrlEncoded = Convert.ToBase64String(signature).Replace("/", "_").Replace("+", "-"). Replace("=", ""); 52 | 53 | // 4) Return the HMAC SHA256-signed JWT as the value for the Authorization header 54 | return $"Bearer {jwtHeaderBase64UrlEncoded}.{jwtPayloadBase64UrlEncoded}.{jwtSignatureBase64UrlEncoded}"; 55 | } 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /examples/Extracting multiple values from xml documents.policy.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 20 | 21 | {{my-send-request}} 22 | GET 23 | 24 | text/xml 25 | 26 | 27 | 28 | 29 | ()); 32 | 33 | var obj = new JObject(); 34 | obj["conditionValue"] = doc.Descendants().Where(r=>r.Name.LocalName == "conditionValue").First().Value; 35 | obj["value1"] = doc.Descendants().Where(r=>r.Name.LocalName == "value1").First().Value; 36 | obj["value3"] = doc.Descendants().Where(r=>r.Name.LocalName == "value3").First().Value; 37 | 38 | return obj; 39 | 40 | }" /> 41 | 51 | 52 | 53 | 54 | 55 | 56 | Condition was met 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | text/xml 67 | 68 | 69 | {%- assign vars = context.Variables["jsonResponse"] -%} 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /examples/Generate Azure Relay Token.policy.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | @((string)context.Variables["relaytoken"]) 44 | 45 | 46 | 47 | application/xml 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | application/json 62 | 63 | 64 | 65 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /examples/GET a file from blobStorage account.xml: -------------------------------------------------------------------------------- 1 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | @{ return "https://{{StorageAccount.Name}}.blob.core.windows.net/" + (string)context.Request.MatchedParameters("blobContainer") + "/" + (string)context.Request.MatchedParameters("fileNameWithExtension",""); } 27 | GET 28 | 29 | {{StorageAccount.Name}}.blob.core.windows.net 30 | 31 | 32 | {{Gateway.Name}} 33 | 34 | 35 | BlockBlob 36 | 37 | 38 | @{ return Guid.NewGuid().ToString(); } 39 | 40 | 41 | 2019-12-12 42 | 43 | 44 | */* 45 | 46 | 47 | 48 | @("Bearer " + (string)context.Variables["msi-access-token"]) 49 | 50 | 51 | 52 | 53 | 54 | 55 | @(((IResponse)context.Variables["result"]).Headers.GetValueOrDefault("x-ms-meta-{MyMetadataName1}","")) 56 | 57 | 58 | @(((IResponse)context.Variables["result"]).Headers.GetValueOrDefault("x-ms-meta-{MyMetadataName2}","")) 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /policy-expressions/README.md: -------------------------------------------------------------------------------- 1 | # Common policy expressions 2 | This cheat-sheet contains common policy expressions that are often used when authoring Azure API Management policies. 3 | 4 | ## Interact with HTTP headers 5 | 6 | **Get HTTP header** 7 | ```c# 8 | context.Request.Headers.GetValueOrDefault("header-name","optional-default-value") 9 | ``` 10 | **Check HTTP header existence** 11 | ```c# 12 | context.Request.Headers.ContainsKey("header-name") == true 13 | ``` 14 | **Check if HTTP header has expected value** 15 | ```c# 16 | context.Request.Headers.GetValueOrDefault("header-name", "").Equals("expected-header-value", StringComparison.OrdinalIgnoreCase) 17 | ``` 18 | ## Interact with URI parameters 19 | 20 | **Get URI parameter** 21 | ```c# 22 | context.Request.MatchedParameters.GetValueOrDefault("parameter-name","optional-default-value") 23 | ``` 24 | **Check URI parameter existence** 25 | ```c# 26 | context.Request.MatchedParameters.ContainsKey("parameter-name") == true 27 | ``` 28 | **Check if URI parameter has expected value** 29 | ```c# 30 | context.Request.MatchedParameters.GetValueOrDefault("parameter-name", "").Equals("expected-value", StringComparison.OrdinalIgnoreCase) == true 31 | ``` 32 | ## Interact with query string parameters 33 | 34 | **Get query string parameter** 35 | ```c# 36 | context.Request.Url.Query.GetValueOrDefault("parameter-name", "optional-default-value") 37 | ``` 38 | **Check query string parameter existence** 39 | ```c# 40 | context.Request.Url.Query.ContainsKey("parameter-name") == true 41 | ``` 42 | **Check if query string parameter has expected value** 43 | ```c# 44 | context.Request.Url.Query.GetValueOrDefault("parameter-name", "").Equals("expected-value", StringComparison.OrdinalIgnoreCase) == true 45 | ``` 46 | ## Interact with policy variables 47 | 48 | **Get policy variable** *(assuming type string)* 49 | ```c# 50 | context.Variables.GetValueOrDefault("variable-name","optional-default-value") 51 | ``` 52 | **Check policy variable existence** 53 | ```c# 54 | context.Variables.ContainsKey("variable-name") == true 55 | ``` 56 | **Check if policy variable has expected value** *(assuming type string)* 57 | ```c# 58 | context.Variables.GetValueOrDefault("variable-name","").Equals("expected-value", StringComparison.OrdinalIgnoreCase) 59 | ``` 60 | ## Interact with JSON bodies 61 | 62 | **Get value from JSON body** 63 | ```c# 64 | (string)context.Request.Body.As(preserveContent: true).SelectToken("root.child jsonpath") 65 | ``` 66 | **Get value from JSON response variable** 67 | ```c# 68 | (string)((IResponse)context.Variables["response-variable-name"]).Body.As().SelectToken("root.child jsonpath") 69 | ``` 70 | **Add property to JSON body** 71 | ```c# 72 | JObject body = context.Request.Body.As(); 73 | body.Add(new JProperty("property-name", "property-value")); 74 | return body.ToString(); 75 | ``` 76 | ## Interact with JSON Web Tokens 77 | 78 | **Read claim from bearer token** 79 | ```c# 80 | context.Request.Headers.GetValueOrDefault("Authorization")?.Split(' ')?[1].AsJwt()?.Claims["claim-name"].FirstOrDefault() 81 | ``` 82 | 83 | ## Interact with client certificates 84 | 85 | **Check client certificate existence** 86 | ```c# 87 | context.Request.Certificate != null 88 | ``` 89 | **Check if client certificate is valid, including a certificate revocation check** 90 | ```c# 91 | context.Request.Certificate.Verify() == true 92 | ``` 93 | **Check if client certificate is valid, excluding a certificate revocation check** 94 | ```c# 95 | context.Request.Certificate.VerifyNoRevocation() == true 96 | ``` 97 | **Check if client certificate issuer has expected value** 98 | ```c# 99 | context.Request.Certificate.Issuer == "trusted-issuer" 100 | ``` 101 | **Check if client certificate subject has expected value** 102 | ```c# 103 | context.Request.Certificate.SubjectName.Name == "expected-subject-name" 104 | ``` 105 | **Check if client certificate thumbprint has expected value** 106 | ```c# 107 | context.Request.Certificate.Thumbprint == "EXPECTED-THUMBPRINT-IN-UPPER-CASE" 108 | ``` 109 | **Check if client certificate is uploaded in API Management, based on thumbprint** 110 | ```c# 111 | context.Deployment.Certificates.Any(c => c.Value.Thumbprint == context.Request.Certificate.Thumbprint) == true 112 | ``` -------------------------------------------------------------------------------- /examples/oauth-proxy/oauth-proxy-sign-out.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | cookie.Trim().Split('=')).SingleOrDefault(cookie => cookie[0] == "oidcsession")?[1] ?? string.Empty : string.Empty)" /> 5 | 6 | 7 | 8 | 9 | 10 | 11 | @($"/oauth/signin?redirect={Uri.EscapeDataString(context.Request.OriginalUrl.ToString())}") 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | @($"{{CookiePrefix}}=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT") 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | @($"{{CookiePrefix}}=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT") 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | @((string)context.Variables["redirect"]) 56 | 57 | 58 | @($"{{CookiePrefix}}=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT") 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /examples/Filter on IP Address when using Application Gateway.policy.xml: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | @{ 17 | int HostToNetworkOrder(int host) 18 | { 19 | return (((int)HostToNetworkOrderShort((short)host) & 0xFFFF) << 16) 20 | | ((int)HostToNetworkOrderShort((short)(host >> 16)) & 0xFFFF); 21 | } 22 | short HostToNetworkOrderShort(short host) 23 | { 24 | return (short)((((int)host & 0xFF) << 8) | (int)((host >> 8) & 0xFF)); 25 | } 26 | 27 | string ipAddress = context.Request.Headers.GetValueOrDefault("x-forwarded-for",""); 28 | if (!string.IsNullOrEmpty(ipAddress)) 29 | { 30 | string[] tokens = ipAddress.Split(':'); 31 | if(tokens.Length == 2) 32 | { ipAddress = tokens[0]; } 33 | //Place IP Ranges into this list in CIDR notation (e.g. "0.0.0.0/0") and separate with commas 34 | List cidrList = new List(){ 35 | "10.0.0.0/8", 36 | "172.16.0.0/12", 37 | "192.168.0.0/16" 38 | }; 39 | foreach (string cidrAddress in cidrList) 40 | { 41 | string[] cidrParts = cidrAddress.Split('/'); 42 | string[] inputIPParts = ipAddress.Split('.'); 43 | string[] cidrIPArray = cidrParts[0].Split('.'); 44 | 45 | if (inputIPParts.Length == 4 && cidrIPArray.Length == 4) 46 | { 47 | byte[] inputIPBytes = new byte[] {Convert.ToByte(int.Parse(inputIPParts[0])), 48 | Convert.ToByte(int.Parse(inputIPParts[1])), 49 | Convert.ToByte(int.Parse(inputIPParts[2])), 50 | Convert.ToByte(int.Parse(inputIPParts[3])), }; 51 | byte[] cidrIPBytes = new byte[] {Convert.ToByte(int.Parse(cidrIPArray[0])), 52 | Convert.ToByte(int.Parse(cidrIPArray[1])), 53 | Convert.ToByte(int.Parse(cidrIPArray[2])), 54 | Convert.ToByte(int.Parse(cidrIPArray[3])), }; 55 | 56 | int cidrAddr = BitConverter.ToInt32(inputIPBytes,0); 57 | int ipAddr = BitConverter.ToInt32(cidrIPBytes,0); 58 | 59 | var host = int.Parse(cidrParts[1]); 60 | host = -1 << (32-host); 61 | var mask = HostToNetworkOrder(host); 62 | 63 | if (((ipAddr & mask) == (cidrAddr & mask))) 64 | { 65 | return "{{ipValidated}}"; 66 | } 67 | } 68 | } 69 | } 70 | return ipAddress; } 71 | 72 | 73 | 10.33.215.173 74 | 10.33.215.175 75 | {{ipValidated}} 76 | 77 | 78 | 79 | @{ 80 | return context.Variables.GetValueOrDefault("originalXForwardedForValue"); 81 | } 82 | 83 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /examples/Replay request on error.policy.xml: -------------------------------------------------------------------------------- 1 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 34 | 38 | 39 | @(context.Variables.GetValueOrDefault("replay-url")) 40 | 44 | 45 | true 46 | 47 | 48 | 51 | 52 | 53 | 54 | true 55 | 56 | 57 | @(context.Variables.GetValueOrDefault("original-error-source")) 58 | 59 | 60 | @(context.Variables.GetValueOrDefault("original-error-reason")) 61 | 62 | 63 | 64 | 65 | 66 | 67 | "false" 68 | 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /examples/PUT a file to blobStorage account.xml: -------------------------------------------------------------------------------- 1 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | @{ return "https://{{StorageAccount.Name}}.blob.core.windows.net/" + (string)context.Request.MatchedParameters("blobContainer") + "/" + (string)context.Request.MatchedParameters("fileNameWithExtension",""); } 28 | PUT 29 | 30 | {{StorageAccount.Name}}.blob.core.windows.net 31 | 32 | 33 | {{Gateway.Name}} 34 | 35 | 36 | BlockBlob 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | @((string)context.Request.Headers.GetValueOrDefault("x-ms-meta-{MyMetadataName1}")) 53 | 54 | 55 | @((string)context.Request.Headers.GetValueOrDefault("x-ms-meta-{MyMetadataName2}")) 56 | 57 | 58 | @{ return Guid.NewGuid().ToString(); } 59 | 60 | 61 | 2019-12-12 62 | 63 | 64 | application/json 65 | 66 | 67 | 68 | @("Bearer " + (string)context.Variables["msi-access-token"]) 69 | 70 | 71 | @(context.Request.Body.As()) 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | -------------------------------------------------------------------------------- /examples/Forward Azure Event Grid Event.xml: -------------------------------------------------------------------------------- 1 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | application/json 21 | 22 | {"error":"We only support one event"} 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | application/json 33 | 34 | @{ 35 | var validationResponse = new JObject(new JProperty("validationResponse",context.Variables.GetValueOrDefault("Event")["data"]["validationCode"].ToString())); 36 | return validationResponse.ToString(); 37 | } 38 | 39 | 40 | 41 | 42 | application/json 43 | 44 | 45 | @(context.Variables.GetValueOrDefault("Event")["id"].ToString()) 46 | 47 | 48 | @(context.Variables.GetValueOrDefault("Event")["subject"].ToString()) 49 | 50 | 51 | @(context.Variables.GetValueOrDefault("Event")["eventType"].ToString()) 52 | 53 | 54 | @(context.Variables.GetValueOrDefault("Event")["eventTime"].ToString()) 55 | 56 | 57 | @(context.Variables.GetValueOrDefault("Event")["dataVersion"].ToString()) 58 | 59 | 60 | @(context.Variables.GetValueOrDefault("Event")["metadataVersion"].ToString()) 61 | 62 | 63 | @(context.Variables.GetValueOrDefault("Event")["topic"].ToString()) 64 | 65 | @(context.Variables.GetValueOrDefault("Event")["data"].ToString()) 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | -------------------------------------------------------------------------------- /examples/oauth-proxy/oauth-proxy-sign-in.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Missing redirect parameter 10 | 11 | 12 | 13 | 14 | 16 | 17 | 18 | 19 | Invalid redirect 20 | 21 | 22 | 24 | 25 | 26 | 27 | Invalid redirect 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | @((string)context.Variables["idpRedirect"]) 70 | 71 | 72 | @($"{{CookiePrefix}}-{(string)context.Variables["state"]}={(string)context.Variables["cookie"]}; Secure; SameSite=None; Path=/; Expires={DateTimeOffset.Now.AddSeconds(300).ToString("R")}; Secure; HttpOnly") 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | -------------------------------------------------------------------------------- /examples/Generate Shared Access Signature and forward request to Azure storage.policy.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 44 | 60 | 66 | 75 | 76 | 77 | @((string)context.Variables["contentType"]) 78 | 79 | 80 | application/json;odata=nometadata 81 | 82 | 83 | UTF-8 84 | 85 | 86 | @((string)context.Variables["x-ms-date"]) 87 | 88 | 89 | @((string)context.Variables["x-ms-version"]) 90 | 91 | 92 | 93 | 94 | 3.0 95 | 96 | 97 | 1.0;NetFx 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | -------------------------------------------------------------------------------- /examples/Get OAuth2 access token from AAD using client id and certificate using key vault manage identity.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | https://{{KeyVaultName}}.vault.azure.net/secrets/{{CertName}}/?api-version=2016-10-01 28 | GET 29 | 30 | 31 | 32 | 33 | ()["value"].ToString())" /> 34 | 35 | 36 | 37 | 38 | 39 | 40 | 52 | () 53 | { 54 | { "aud", aud }, 55 | { "exp", exp.ToString() }, 56 | { "iss", clientID }, 57 | { "jti", Guid.NewGuid().ToString() }, 58 | { "nbf", nbf.ToString() }, 59 | { "sub", clientID } 60 | }; 61 | 62 | 63 | X509Certificate2 certificate = new X509Certificate2(Convert.FromBase64String((string)context.Variables["keyVaultCertBase64"]), (string)null); 64 | string signedClientAssertion = ""; 65 | RSACng rsa = certificate.GetRSAPrivateKey() as RSACng; 66 | 67 | //Encoding variable 68 | char Base64PadCharacter = '='; 69 | char Base64Character62 = '+'; 70 | char Base64Character63 = '/'; 71 | char Base64UrlCharacter62 = '-'; 72 | char Base64UrlCharacter63 = '_'; 73 | 74 | string certEncode = Convert.ToBase64String(certificate.GetCertHash()).Split(Base64PadCharacter)[0].Replace(Base64Character62, Base64UrlCharacter62).Replace(Base64Character63, Base64UrlCharacter63); 75 | 76 | //alg represents the desired signing algorithm, which is SHA-256 in this case 77 | //kid represents the certificate thumbprint 78 | var header = new Dictionary 79 | () 80 | { 81 | { "alg", "RS256"}, 82 | { "kid", certEncode } 83 | }; 84 | 85 | string headerEncode = Convert.ToBase64String(Encoding.UTF8.GetBytes(JObject.FromObject(header).ToString())).Split(Base64PadCharacter)[0].Replace(Base64Character62, Base64UrlCharacter62).Replace(Base64Character63, Base64UrlCharacter63); 86 | string claimsEncode = Convert.ToBase64String(Encoding.UTF8.GetBytes(JObject.FromObject(claims).ToString())).Split(Base64PadCharacter)[0].Replace(Base64Character62, Base64UrlCharacter62).Replace(Base64Character63, Base64UrlCharacter63); 87 | string token = headerEncode + "." + claimsEncode; 88 | string signature = Convert.ToBase64String(rsa.SignData(Encoding.UTF8.GetBytes(token), HashAlgorithmName.SHA256, System.Security.Cryptography.RSASignaturePadding.Pkcs1)).Split(Base64PadCharacter)[0].Replace(Base64Character62, Base64UrlCharacter62).Replace(Base64Character63, Base64UrlCharacter63); 89 | signedClientAssertion = string.Concat(token, ".", signature); 90 | return signedClientAssertion; 91 | }" /> 92 | 93 | 94 | 95 | 96 | 97 | @((string)context.Variables["authorizationServer"]) 98 | POST 99 | 100 | application/x-www-form-urlencoded 101 | 102 | 103 | @{ 104 | return "client_id={{clientId}}&resource={{scope}}&client_assertion_type=urn%3Aietf%3Aparams%3Aoauth%3Aclient-assertion-type%3Ajwt-bearer&client_assertion=" + ((string)context.Variables["signedClientAssertion"]) + "&grant_type=client_credentials"; 105 | 106 | // For identity providers other than Azure AD, try return statement below 107 | // return "client_id={{clientId}}&scope={{scope}}&client_secret={{clientSecret}}&grant_type=client_credentials"; 108 | } 109 | 110 | 111 | 112 | ()["access_token"])" /> 113 | 114 | 115 | 116 | 117 | @("Bearer " + (string)context.Variables["bearerToken"]) 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | -------------------------------------------------------------------------------- /examples/Return a blob URL signed with a user delegation SAS token.xml: -------------------------------------------------------------------------------- 1 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | @((string)context.Variables["blobUri"]) 31 | GET 32 | 33 | 2020-12-06 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | @("Missing permission to read blob " + (string)context.Variables["blobUri"] + ". Make sure the 'Storage Blob Data Reader' role is granted for APIM '"+ context.Deployment.ServiceName +"' either at the Storage Account or Container level.") 42 | 43 | 44 | 45 | 46 | 47 | @("Cannot find blob " + (string)context.Variables["blobUri"]) 48 | 49 | 50 | 51 | 52 | 53 | @("https://"+((string)context.Request.MatchedParameters["storageAccountName"]) + ".blob.core.windows.net/?restype=service&comp=userdelegationkey") 54 | POST 55 | 56 | 2020-12-06 57 | 58 | @{ 59 | return "" + 60 | "" + DateTime.UtcNow.ToString("u").Replace(" ", "T") + "" + 61 | "" + DateTime.UtcNow.AddSeconds((string)context.Request.MatchedParameters["ttlInSeconds"]).ToString("u").Replace(" ", "T") + "" + 62 | ""; 63 | } 64 | 65 | 66 | 67 | 68 | 69 | 70 | @("Missing permission to generate SAS for blob " + (string)context.Variables["blobUri"] + ". Make sure the 'Storage Blob Delegator' role is granted for APIM '"+ context.Deployment.ServiceName +"' at the Storage Account level.") 71 | 72 | 73 | 74 | 75 | @{ 76 | XmlDocument xml = new XmlDocument(); 77 | string userDelegationKeyResponse = ((IResponse)context.Variables["userdelegationkey"]).Body.As(); 78 | xml.LoadXml(userDelegationKeyResponse); 79 | var signedKeyObjectId = xml.DocumentElement.SelectSingleNode("/UserDelegationKey/SignedOid").InnerText; 80 | var signedKeyTenantId = xml.DocumentElement.SelectSingleNode("/UserDelegationKey/SignedTid").InnerText; 81 | var signedKeyStart = xml.DocumentElement.SelectSingleNode("/UserDelegationKey/SignedStart").InnerText; 82 | var signedKeyExpiry = xml.DocumentElement.SelectSingleNode("/UserDelegationKey/SignedExpiry").InnerText; 83 | var signedKeyService = xml.DocumentElement.SelectSingleNode("/UserDelegationKey/SignedService").InnerText; 84 | var signedKeyVersion = xml.DocumentElement.SelectSingleNode("/UserDelegationKey/SignedVersion").InnerText; 85 | var signedKey = xml.DocumentElement.SelectSingleNode("/UserDelegationKey/Value").InnerText; 86 | 87 | var signedPermissions = "r"; 88 | var signedStart = ""; 89 | var signedExpiry = DateTime.UtcNow.AddSeconds((string)context.Request.MatchedParameters["storageAccountName"]).ToString("yyyy-MM-ddTHH:mm:ssZ"); 90 | var canonicalizedResource = $"/blob/{((string)context.Request.MatchedParameters["storageAccountName"])}/{((string)context.Request.MatchedParameters["containerName"])}/{((string)context.Request.MatchedParameters["blobName"])}"; 91 | var signedAuthorizedUserObjectId = ""; 92 | var signedUnauthorizedUserObjectId = ""; 93 | var signedCorrelationId = ""; 94 | var signedIP = ""; 95 | var signedProtocol = "https"; 96 | var signedVersion = "2020-12-06"; 97 | var signedResource = "b"; 98 | var signedSnapshotTime = ""; 99 | var signedEncryptionScope = ""; 100 | var rscc = ""; 101 | var rscd = ""; 102 | var rsce = ""; 103 | var rscl = ""; 104 | var rsct = ""; 105 | 106 | var stringToSign = signedPermissions + "\n" + 107 | signedStart + "\n" + 108 | signedExpiry + "\n" + 109 | canonicalizedResource + "\n" + 110 | signedKeyObjectId + "\n" + 111 | signedKeyTenantId + "\n" + 112 | signedKeyStart + "\n" + 113 | signedKeyExpiry + "\n" + 114 | signedKeyService + "\n" + 115 | signedKeyVersion + "\n" + 116 | signedAuthorizedUserObjectId + "\n" + 117 | signedUnauthorizedUserObjectId + "\n" + 118 | signedCorrelationId + "\n" + 119 | signedIP + "\n" + 120 | signedProtocol + "\n" + 121 | signedVersion + "\n" + 122 | signedResource + "\n" + 123 | signedSnapshotTime + "\n" + 124 | signedEncryptionScope + "\n" + 125 | rscc + "\n" + 126 | rscd + "\n" + 127 | rsce + "\n" + 128 | rscl + "\n" + 129 | rsct; 130 | System.Security.Cryptography.HMACSHA256 hasher = new System.Security.Cryptography.HMACSHA256(Convert.FromBase64String(signedKey)); 131 | 132 | var sig = System.Net.WebUtility.UrlEncode(Convert.ToBase64String(hasher.ComputeHash(System.Text.Encoding.UTF8.GetBytes(stringToSign)))); 133 | 134 | var sas = $"?sv={signedVersion}&sp={signedPermissions}&se={signedExpiry}&sr={signedResource}&skoid={signedKeyObjectId}&sktid={signedKeyTenantId}&skt={signedKeyStart}&ske={signedKeyExpiry}&sks={signedKeyService}&skv={signedKeyVersion}&spr={signedProtocol}&sig={sig}"; 135 | 136 | return (string)context.Variables["blobUri"] + sas; 137 | } 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | -------------------------------------------------------------------------------- /examples/oauth-proxy/readme.md: -------------------------------------------------------------------------------- 1 | # OAuth Proxy Azure API Management policies 2 | 3 | The policies in this folder provide support for an OAuth Proxy that works in a similar way to App Service Authentication. 4 | 5 | ## Setup 6 | 7 | ### Required Named Values 8 | 9 | | Named Value | Purpose | 10 | | -- | -- | 11 | | AdditionalScopes | Space separated string of other scopes to request delegated consent for | 12 | | ClientId | AAD ClientId Id representing the application you are signing in against | 13 | | ClientSecret | AAD Client Secret used to exchange codes for tokens | 14 | | CookiePrefix | The name we use for the cookie used to control the oauth-proxy | 15 | | CookieEncryptionKey | 1 or 2. Selects the key (CookieEncryptionKey**1** or CookieEncryptionKey**2**) used to protect newly issued cookies. This allows you to periodically rotate keys | 16 | | CookieEncryptionKey1 | A Base 64 Encoded string of a 32 random bytes array. Used by an AES 256 encryption algorithm to encrypt cookies | 17 | | CookieEncryptionKey2 | A Base 64 Encoded string of a 32 random bytes array. Used by an AES 256 encryption algorithm to encrypt cookies | 18 | | TokenEncryptionKey | 1 or 2. Selects the key (TokenEncryptionKey**1** or TokenEncryptionKey**2**) used to protect newly issued tokens. This allows you to periodically rotate keys | 19 | | TokenEncryptionKey1 | A Base 64 Encoded string of 32 random bytes. Used as the Key for an AES 256 encryption algorithm for encrypting tokens at rest | 20 | | TokenEncryptionKey2 | A Base 64 Encoded string of 32 random bytes. Used as the Key for an AES 256 encryption algorithm for encrypting tokens at rest | 21 | | SessionCookieExpirationInSeconds | How long to allow session cookies to stay active for | 22 | | RefreshTokenExpirationInSeconds | How long to cache refresh tokens for (a good guide would be how long your average user's session lasts for) | 23 | 24 | > You can generate the Base 64 random bytes in dotnet using ``` Convert.ToBase64String(RandomNumberGenerator.GetBytes()) ```, or in bash using ```openssl rand -base64 32``` 25 | 26 | ### Required Named Values for Azure Active Directory 27 | 28 | | Named Value | Purpose | 29 | | -- | -- | 30 | | TenantId | AAD Tenant Id that owns the ClientId you want users to sign-in to | 31 | 32 | 33 | ## Fragments 34 | | Fragment File Name | Fragment Name | Purpose | How to use | 35 | | -- | -- | -- | -- | 36 | | [oauth-proxy-token-endpoint-fragment.xml](oauth-proxy-token-endpoint-fragment.xml) | oauth-proxy-token-endpoint-fragment | Identifies the token endpoint to obtains tokens from | You **must** place this fragment above the ```oauth-proxy-session-fragment``` as it sets a required variable used by other policies. | 37 | | [oauth-proxy-validate-token-fragment.xml](oauth-proxy-validate-token-fragment.xml) | oauth-proxy-validate-token-fragment | Custom token validation policy | Place it after the ```oauth-proxy-session-fragment``` to provide an additional JWT validation step on the access-token returned by your IdP. The reference implementation uses the [validate-azure-ad-token](https://learn.microsoft.com/en-us/azure/api-management/validate-azure-ad-token-policy) policy. For other IdPs use the [validate-jwt](https://learn.microsoft.com/en-us/azure/api-management/validate-jwt-policy) policy. | 38 | | [oauth-proxy-session-fragment.xml](oauth-proxy-session-fragment.xml) | oauth-proxy-session-fragment | A fragment that checks for a Session cookie, and either initiate a sign-in flow, or attaches valid tokens to the ongoing request. This will refresh tokens if necessary | Place inside the `````` policy of any Web Apps you want to protect with a session cookie | 39 | | [oauth-proxy-construct-authorization-redirect-fragment.xml](oauth-proxy-construct-authorization-redirect-fragment.xml) | oauth-proxy-construct-authorization-redirect-fragment | A fragment that constructs an OIDC Authorize request to your endpoint | If using a different IdP, use ```oauth-proxy-construct-authorization-redirect.xml``` as a guide to configuring for your IdP | 40 | | [oauth-proxy-slide-session-fragment.xml](oauth-proxy-slide-session-fragment.xml) | oauth-proxy-slide-session-fragment | A fragment that slides any issued session cookie | Place inside the `````` policy of any Web Apps you want to protect with a session cookie | 41 | 42 | ## Policies for Oidc Endpoints 43 | | Policy Name | API path | Method | Purpose | How to use | 44 | | -- | -- | -- | -- | -- | 45 | | [oauth-proxy-sign-in.xml](oauth-proxy-sign-in.xml) | ```/oauth/signin``` | GET | Initiates a front-channel code / pkce flow with an IdP | Configure this as the 'signin' operation within an API called 'OAuth' | 46 | | [oauth-proxy-callback.xml](oauth-proxy-callback.xml) | ```/oauth/callback``` | POST | Handles an IdP's callback in response to a sign-in request | Configure this as the 'callback' operation within an API called 'OAuth' | 47 | | [oauth-proxy-sign-out.xml](oauth-proxy-sign-out.xml) | ```/oauth/signout``` | GET | Clears a user's session cookie, and removes all token data from the cache. | Configure this as the 'signout' operation within an API called 'OAuth' | 48 | 49 | ## Simple policy to protect Web Applications 50 | 51 | ```xml 52 | 53 | 54 | 55 | 56 | 57 | 58 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | ``` 81 | 82 | 83 | # Policy Details 84 | 85 | ## oauth-proxy-session-fragment 86 | 87 | ### Purpose 88 | This fragment is intended to sit at a high-level around any calls you need to protect. 89 | 90 | It checks for an incoming session-cookie and either 91 | - Redirects to oauth/signin if invalid / expired 92 | - Fetches tokens from Redis, and decrypts them using the Session-Id (IV), and the TokenEncryptionKey(1 or 2) key 93 | - Renews access tokens using a refresh-token if they are nearing expiration 94 | - Appends the Bearer token to the request for use by the downstream API. 95 | 96 | ## oauth-proxy-construct-authorization-redirect-fragment 97 | 98 | ### Purpose 99 | Assigns a valid URI to the ```oauth-proxy-redirect``` variable that will redirect a User to an IdP to initiate a sign-in. 100 | 101 | The following variables are set by the ```oauth-proxy-sign-in``` policy to use here: 102 | - state 103 | - nonce 104 | - codeChallengeSha256 105 | 106 | This fragment must set a variable called ```oauth-proxy-redirect``` which initiates the sign-in flow. 107 | 108 | ## oauth-proxy-slide-session-fragment 109 | 110 | ### Purpose 111 | An Outbound processing fragment that slides the current session cookie. Use it at the same API scope as the above Session check fragment. 112 | 113 | ### Steps 114 | - Issues a new session cookie on all requests, which slides forward to ```UtcNow + SessionCookieExpirationInSeconds``` 115 | 116 | ## oauth-proxy-token-endpoint-fragment 117 | 118 | ### Purpose 119 | This fragment creates a valid URI that where we can exchange a code for a token. 120 | 121 | This fragment must set a variable called ```idpTokenEndpoint``` where we can POST to. 122 | 123 | 124 | ## oauth-proxy-sign-in 125 | 126 | ### Purpose 127 | This policy initiates an OIDC 3-legged sign-in flow. 128 | 129 | ### Steps 130 | - Checks for a valid redirect on the incoming URL 131 | - Creates state, nonce, and code-challenges which are stored in Redis 132 | - Uses the ```oauth-proxy-construct-authorization-redirect-fragment``` to construct the authorisation request 133 | - Returns a cookie with a lookup the the above state, nonce and code-challenge 134 | - 302 Redirects the browser to initiate the front-channel sign-in. 135 | 136 | ### Required QueryString Values 137 | 138 | | Query String key | Purpose | 139 | | -- | -- | 140 | | redirect | Url to redirect to after a successful sign-in flow. This must be a root path beginning with '/'. | 141 | 142 | ## oauth-proxy-callback 143 | > Implemented by [oauth-proxy-callback.xml](./oauth-proxy-callback.xml) 144 | 145 | ### Purpose 146 | This policy handles a callback from an IdP to complete an OIDC flow. 147 | 148 | ### Steps 149 | - Get the ```code``` and ```state``` parameter from the incoming querystring 150 | - Check for an incoming ```oidc``` cookie suffixed with the ```state``` parameter 151 | - Lookup the state and nonce properties from cache which were previously stored in the ```signin``` policy 152 | - Return 401 if we cannot find them 153 | - If the state parameter in the querystring from the IdP matches the cookie, and was stored in our cache, then switch the code for a token using a PKCE code- 154 | - Check the nonce in the returned token matches the nonce stored in session 155 | - Return 401 if we cannot match the nonce 156 | - Creates an IV which is round-tripped in the session cookie (not stored server-side) 157 | - Encrypts the tokens using the above IV, and the TokenEncryptionKey(1 or 2) 158 | - Store the encrypted tokens in Redis 159 | - Set a session-cookie which comprises of our cache-key, the IV, the cookies expiry timestamp. Signs it using a HMAC-SHA-512 signature creating using the SessionCookieKey(1 or 2) named value. 160 | 161 | ## oauth-proxy-sign-out 162 | > Implemented by [oauth-proxy-signout.xml](./oauth-proxy-signout.xml) 163 | 164 | ### Purpose 165 | This policy performs a User 'sign-out'. It removes the session cookies, and also removes all tokens from cache. 166 | 167 | ### Steps 168 | - Clears all cached tokens 169 | - Clears the session cookie 170 | - Redirects the user to the provided redirect parameter (returns a 200 OK if no, or invalid redirect). 171 | 172 | NB: This does not invalidate the access tokens. If any API cached the token it will still be valid until its expiry date. 173 | -------------------------------------------------------------------------------- /examples/Request OAuth2 access token from SuccessFactors using AAD JWT token.xml: -------------------------------------------------------------------------------- 1 | 5 | 9 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | api://{{APIMAADRegisteredAppClientId}} 27 | 28 | 29 | https://login.microsoftonline.com/{{AADTenantId}}/v2.0 30 | 31 | 32 | 33 | user_impersonation 34 | 35 | 36 | 37 | 38 | 39 | gzip, deflate 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | https://login.microsoftonline.com/{{AADTenantId}}/oauth2/v2.0/token 56 | POST 57 | 58 | application/x-www-form-urlencoded 59 | 60 | @{ 61 | var _AADRegisteredAppClientId = context.Variables["APIMAADRegisteredAppClientId"]; 62 | /*var _AADRegisteredAppClientSecret = context.Variables["APIMAADRegisteredAppClientSecret"];*/ 63 | var _EntraIDSAPSFResource = context.Variables["EntraIDSAPSFResource"]; 64 | var user_assertion = context.Request.Headers.GetValueOrDefault("Authorization","").Replace("Bearer ",""); 65 | var apim_assertion = context.Variables["msi-access-token"]; 66 | return $"grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer&client_assertion={apim_assertion}&client_id={_AADRegisteredAppClientId}&client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer&assertion={user_assertion}&scope={_EntraIDSAPSFResource}/.default&requested_token_use=on_behalf_of&requested_token_type=urn:ietf:params:oauth:token-type:saml2"; 67 | /* Kept as fallback only: return $"grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer&assertion={user_assertion}&client_id={_AADRegisteredAppClientId}&client_secret={_AADRegisteredAppClientSecret}&scope={_EntraIDSAPSFResource}/.default&requested_token_use=on_behalf_of&requested_token_type=urn:ietf:params:oauth:token-type:saml2"; */ 68 | } 69 | 70 | ()["access_token"])" /> 71 | 72 | 73 | https://{{SAPSFOAuthServerAdressForTokenEndpoint}}/oauth/token 74 | POST 75 | 76 | application/x-www-form-urlencoded 77 | 78 | @{ 79 | var _SAPSFApiKey = context.Variables["SAPSFApiKey"]; 80 | var _SAPSFCompanyId = context.Variables["SAPSFCompanyId"]; 81 | var assertion2 = context.Variables["accessToken"]; 82 | return $"grant_type=urn:ietf:params:oauth:grant-type:saml2-bearer&assertion={assertion2}&client_id={_SAPSFApiKey}&company_id={_SAPSFCompanyId}"; 83 | } 84 | 85 | 86 | ())" /> 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | @(context.Request.Url.ToString()) 105 | HEAD 106 | 107 | Fetch 108 | 109 | 110 | @("Bearer " + (string)context.Variables["SAPBearerToken"]) 111 | 112 | 113 | 114 | 115 | 116 | 117 | @(((IResponse)context.Variables["SAPCSRFToken"]).Headers.GetValueOrDefault("x-csrf-token")) 118 | 119 | 120 | 121 | 122 | 123 | 124 | @("Bearer " + (string)context.Variables["SAPBearerToken"]) 125 | 126 | 127 | 128 | 129 | 130 | json 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 149 | 150 | 151 | 152 | 153 | 154 | @(context.LastError.Source) 155 | 156 | 157 | @(context.LastError.Reason) 158 | 159 | 160 | @(context.LastError.Message) 161 | 162 | 163 | @(context.LastError.Scope) 164 | 165 | 166 | @(context.LastError.Section) 167 | 168 | 169 | @(context.LastError.Path) 170 | 171 | 172 | @(context.LastError.PolicyId) 173 | 174 | 175 | @(context.Response.StatusCode.ToString()) 176 | 177 | 178 | -------------------------------------------------------------------------------- /examples/oauth-proxy/oauth-proxy-session-fragment.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | x.Trim()).Select(cookie => cookie.Split('=')).SingleOrDefault(cookie => cookie[0] == "{{CookiePrefix}}")?[1] ?? string.Empty : string.Empty)" /> 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | @($"/oauth/signin?redirect={(string)context.Variables["redirect"]}") 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | @($"/oauth/signin?redirect={(string)context.Variables["redirect"]}") 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | @($"/oauth/signin?redirect={(string)context.Variables["redirect"]}") 44 | 45 | 46 | 47 | 48 | 49 | 50 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | @($"/oauth/signin?redirect={(string)context.Variables["redirect"]}") 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | @($"/oauth/signin?redirect={(string)context.Variables["redirect"]}") 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | @($"/oauth/signin?redirect={(string)context.Variables["redirect"]}") 105 | 106 | 107 | 108 | 109 | 110 | 111 | 122 | 123 | 124 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | @($"/oauth/signin?redirect={(string)context.Variables["redirect"]}") 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | @((string)context.Variables["idpTokenEndpoint"]) 171 | POST 172 | 173 | application/x-www-form-urlencoded 174 | 175 | @((string)context.Variables["tokenRefreshData"]) 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | @($"{{CookiePrefix}}=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT") 184 | 185 | 186 | @($"/oauth/signin?redirect={(string)context.Variables["redirect"]}") 187 | 188 | 189 | 190 | 191 | 192 | 193 | ())" /> 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 211 | 212 | 222 | 223 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | @($"Bearer {(string)context.Variables["accessToken"]}") 249 | 250 | 251 | @((string)context.Variables["idToken"]) 252 | 253 | 254 | 255 | 256 | 257 | @(((Jwt)context.Variables["idTokenJwt"]).Claims["name"][0]) 258 | 259 | 260 | 261 | 262 | 263 | 264 | @(((Jwt)context.Variables["idTokenJwt"]).Claims["preferred_username"][0]) 265 | 266 | 267 | 268 | 269 | -------------------------------------------------------------------------------- /examples/oauth-proxy/oauth-proxy-callback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | x.Trim()).Select(cookie => cookie.Split('=')).Single(cookie => cookie[0] == $"{{CookiePrefix}}-{(string)context.Variables["state"]}")[1])" /> 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | @($"{{CookiePrefix}}-{(string)context.Variables["state"]}=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT") 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 41 | 42 | 43 | 44 | 45 | 46 | @((string)context.Variables["idpTokenEndpoint"]) 47 | POST 48 | 49 | application/x-www-form-urlencoded 50 | 51 | @((string)context.Variables["tokenData"]) 52 | 53 | 54 | 55 | 56 | 57 | 58 | @($"{{CookiePrefix}}-{(string)context.Variables["state"]}=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT") 59 | 60 | 61 | 62 | 63 | ())" /> 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 107 | 117 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 150 | 151 | 152 | 153 | @($"{{CookiePrefix}}-{(string)context.Variables["state"]}=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT") 154 | 155 | 156 | @($"{{CookiePrefix}}={(string)context.Variables["encryptedCookie"]}; SameSite=Lax; secure; path=/; expires={DateTimeOffset.FromUnixTimeMilliseconds((long)context.Variables["cookie-expiry"]).ToString("R")}; Secure; HttpOnly") 157 | 158 | 159 | @((string)context.Variables["expected-state"]) 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | @($"{{CookiePrefix}}-{(string)context.Variables["state"]}=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT") 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | @($"{{CookiePrefix}}-{(string)context.Variables["state"]}=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT") 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | @($"{{CookiePrefix}}-{(string)context.Variables["state"]}=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT") 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | --------------------------------------------------------------------------------