Repository: serverless/serverless-azure-functions
Pull request #386 (2019-10-24 17:28:02)

feat: Support for specifying premium scaling options in serverless yaml
<!--
1. Please check out and follow our Contributing Guidelines: https://github.com/serverless/serverless-azure-functions/CONTRIBUTING.md
2. Do not remove any section of the template. If something is not applicable leave it empty but leave it in the PR
3. Please follow the template, otherwise we'll have to ask you to update it and it will take longer until your PR is merged
-->

## What did you implement:

Adds support to specify your scaling parameters for the app service plan hosting a premium functions app.
```yaml
provider:
  name: azure
  appServicePlan:
    sku:
      name: EP3 # Default is EP1
    scale:
      minWorkerCount: 3 # Default is 1
      maxWorkerCount: 10 # Default is 10
```
<!--
Briefly describe the feature if no issue exists for this PR
-->

## How did you implement it:
Created new ARM template parameters that are auto configured via serverless yaml
<!--
If this is a nontrivial change please briefly describe your implementation so its easy for us to understand and review your code.
-->

## How can we verify it:
Run unit tests with `npm run test`
<!--
Add any applicable config, commands, screenshots or other resources
to make it easy for us to verify this works. The easier you make it for us
to review a PR, the faster we can review and merge it.

Examples:
* serverless.yml - Fully functioning to easily deploy changes
* Screenshots - Showing the difference between your output and the master
* Cloud Configuration - List cloud resources and show that the correct configuration is in place
* Other - Anything else that comes to mind to help us evaluate
-->

## Todos:

_**Note: Run `npm run test:ci` to run all validation checks on proposed changes**_

- [x] Ensure there are no lint errors.  
       **Validate via `npm run lint`**  
       _Note: Some reported issues can be automatically fixed by running `npm run lint:fix`_
- [x] Write tests and confirm existing functionality is not broken.  
       **Validate via `npm test`**
- [ ] Write documentation
- [ ] Provide verification config / commands / resources
- [x] Enable "Allow edits from maintainers" for this PR
- [x] Update the messages below

**_Is this ready for review?:_** YES
**_Is it a breaking change?:_** NO


pjlittle: LGTM - would be good to learn more about what `workerSizeId` is 🙂 

Patch:
diff --git a/package-lock.json b/package-lock.json
index 1e51d1e..e9e4d8e 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,6 +1,6 @@
 {
   "name": "serverless-azure-functions",
-  "version": "1.0.2-9",
+  "version": "1.0.2-10",
   "lockfileVersion": 1,
   "requires": true,
   "dependencies": {
@@ -1646,6 +1646,15 @@
         "@types/node": "*"
       }
     },
+    "@types/xml2js": {
+      "version": "0.4.5",
+      "resolved": "https://registry.npmjs.org/@types/xml2js/-/xml2js-0.4.5.tgz",
+      "integrity": "sha512-yohU3zMn0fkhlape1nxXG2bLEGZRc1FeqF80RoHaYXJN7uibaauXfhzhOJr1Xh36sn+/tx21QAOf07b/xYVk1w==",
+      "dev": true,
+      "requires": {
+        "@types/node": "*"
+      }
+    },
     "@types/yargs": {
       "version": "12.0.12",
       "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-12.0.12.tgz",
@@ -2170,6 +2179,22 @@
             "punycode": "1.3.2",
             "querystring": "0.2.0"
           }
+        },
+        "xml2js": {
+          "version": "0.4.19",
+          "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.19.tgz",
+          "integrity": "sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q==",
+          "dev": true,
+          "requires": {
+            "sax": ">=0.6.0",
+            "xmlbuilder": "~9.0.1"
+          }
+        },
+        "xmlbuilder": {
+          "version": "9.0.7",
+          "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz",
+          "integrity": "sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0=",
+          "dev": true
         }
       }
     },
@@ -4304,7 +4329,6 @@
       "version": "1.1.3",
       "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz",
       "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==",
-      "dev": true,
       "requires": {
         "object-keys": "^1.0.12"
       }
@@ -4584,7 +4608,6 @@
       "version": "1.13.0",
       "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.13.0.tgz",
       "integrity": "sha512-vDZfg/ykNxQVwup/8E1BZhVzFfBxs9NqMzGcvIJrqg5k2/5Za2bWo40dK2J1pgLngZ7c+Shh8lwYtLGyrwPutg==",
-      "dev": true,
       "requires": {
         "es-to-primitive": "^1.2.0",
         "function-bind": "^1.1.1",
@@ -4598,7 +4621,6 @@
       "version": "1.2.0",
       "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.0.tgz",
       "integrity": "sha512-qZryBOJjV//LaxLTV6UC//WewneB3LcXOL9NP++ozKVXsIIIpm/2c13UDiD9Jp2eThsecw9m3jPqDwTyobcdbg==",
-      "dev": true,
       "requires": {
         "is-callable": "^1.1.4",
         "is-date-object": "^1.0.1",
@@ -5964,8 +5986,7 @@
     "function-bind": {
       "version": "1.1.1",
       "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
-      "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
-      "dev": true
+      "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A=="
     },
     "functional-red-black-tree": {
       "version": "1.0.1",
@@ -6412,7 +6433,6 @@
       "version": "1.0.3",
       "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
       "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
-      "dev": true,
       "requires": {
         "function-bind": "^1.1.1"
       }
@@ -6440,8 +6460,7 @@
     "has-symbols": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.0.tgz",
-      "integrity": "sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q=",
-      "dev": true
+      "integrity": "sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q="
     },
     "has-to-string-tag-x": {
       "version": "1.4.1",
@@ -6773,8 +6792,7 @@
     "is-callable": {
       "version": "1.1.4",
       "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.4.tgz",
-      "integrity": "sha512-r5p9sxJjYnArLjObpjA4xu5EKI3CuKHkJXMhT7kwbpUyIFD1n5PMAsoPvWnvtZiNz7LjkYDRZhd7FlI0eMijEA==",
-      "dev": true
+      "integrity": "sha512-r5p9sxJjYnArLjObpjA4xu5EKI3CuKHkJXMhT7kwbpUyIFD1n5PMAsoPvWnvtZiNz7LjkYDRZhd7FlI0eMijEA=="
     },
     "is-ci": {
       "version": "2.0.0",
@@ -6796,8 +6814,7 @@
     "is-date-object": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.1.tgz",
-      "integrity": "sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY=",
-      "dev": true
+      "integrity": "sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY="
     },
     "is-descriptor": {
       "version": "0.1.6",
@@ -6952,7 +6969,6 @@
       "version": "1.0.4",
       "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz",
       "integrity": "sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=",
-      "dev": true,
       "requires": {
         "has": "^1.0.1"
       }
@@ -6972,7 +6988,6 @@
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.2.tgz",
       "integrity": "sha512-HS8bZ9ox60yCJLH9snBpIwv9pYUAkcuLhSA1oero1UB5y9aiQpRA8y2ex945AOtCZL1lJDeIk3G5LthswI46Lw==",
-      "dev": true,
       "requires": {
         "has-symbols": "^1.0.0"
       }
@@ -9304,8 +9319,7 @@
     "object-keys": {
       "version": "1.1.1",
       "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
-      "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
-      "dev": true
+      "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA=="
     },
     "object-visit": {
       "version": "1.0.1",
@@ -9331,7 +9345,6 @@
       "version": "2.0.3",
       "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.0.3.tgz",
       "integrity": "sha1-h1jIRvW0B62rDyNuCYbxSwUcqhY=",
-      "dev": true,
       "requires": {
         "define-properties": "^1.1.2",
         "es-abstract": "^1.5.1"
@@ -12324,7 +12337,6 @@
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/util.promisify/-/util.promisify-1.0.0.tgz",
       "integrity": "sha512-i+6qA2MPhvoKLuxnJNpXAGhg7HphQOSUq2LKMZD0m15EiskXUkMvKdF4Uui0WYeCUGea+o2cw/ZuwehtfsrNkA==",
-      "dev": true,
       "requires": {
         "define-properties": "^1.1.2",
         "object.getownpropertydescriptors": "^2.0.3"
@@ -12637,11 +12649,6 @@
       "integrity": "sha1-SWsswQnsqNus/i3HK2A8F8WHCtQ=",
       "dev": true
     },
-    "xml": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz",
-      "integrity": "sha1-eLpyAgApxbyHuKgaPPzXS0ovweU="
-    },
     "xml-name-validator": {
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz",
@@ -12649,18 +12656,19 @@
       "dev": true
     },
     "xml2js": {
-      "version": "0.4.19",
-      "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.19.tgz",
-      "integrity": "sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q==",
+      "version": "0.4.22",
+      "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.22.tgz",
+      "integrity": "sha512-MWTbxAQqclRSTnehWWe5nMKzI3VmJ8ltiJEco8akcC6j3miOhjjfzKum5sId+CWhfxdOs/1xauYr8/ZDBtQiRw==",
       "requires": {
         "sax": ">=0.6.0",
-        "xmlbuilder": "~9.0.1"
+        "util.promisify": "~1.0.0",
+        "xmlbuilder": "~11.0.0"
       }
     },
     "xmlbuilder": {
-      "version": "9.0.7",
-      "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz",
-      "integrity": "sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0="
+      "version": "11.0.1",
+      "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz",
+      "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA=="
     },
     "xmldom": {
       "version": "0.1.27",
diff --git a/package.json b/package.json
index 12afaad..0e312d8 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "serverless-azure-functions",
-  "version": "1.0.2-9",
+  "version": "1.0.2-10",
   "description": "Provider plugin for the Serverless Framework v1.x which adds support for Azure Functions.",
   "license": "MIT",
   "main": "./lib/index.js",
@@ -61,7 +61,7 @@
     "request": "^2.81.0",
     "rimraf": "^2.7.1",
     "semver": "^6.3.0",
-    "xml": "^1.0.1"
+    "xml2js": "^0.4.22"
   },
   "devDependencies": {
     "@azure/ms-rest-js": "^1.8.7",
@@ -74,6 +74,7 @@
     "@types/request": "^2.48.1",
     "@types/serverless": "^1.18.2",
     "@types/xml": "^1.0.3",
+    "@types/xml2js": "^0.4.5",
     "@typescript-eslint/eslint-plugin": "^1.9.0",
     "@typescript-eslint/parser": "^1.9.0",
     "axios-mock-adapter": "^1.16.0",
diff --git a/src/armTemplates/resources/appServicePlan.ts b/src/armTemplates/resources/appServicePlan.ts
index c8e3457..81f331a 100644
--- a/src/armTemplates/resources/appServicePlan.ts
+++ b/src/armTemplates/resources/appServicePlan.ts
@@ -6,6 +6,10 @@ interface AppServicePlanParams extends DefaultArmParams {
   appServicePlanName: ArmParameter;
   appServicePlanSkuName: ArmParameter;
   appServicePlanSkuTier: ArmParameter;
+  appServicePlanWorkerSizeId: ArmParameter;
+  appServicePlanMinWorkerCount: ArmParameter;
+  appServicePlanMaxWorkerCount: ArmParameter;
+  appServicePlanHostingEnvironment: ArmParameter;
 }
 
 export class AppServicePlanResource implements ArmResourceTemplateGenerator {
@@ -35,6 +39,22 @@ export class AppServicePlanResource implements ArmResourceTemplateGenerator {
       appServicePlanSkuTier: {
         defaultValue: "ElasticPremium",
         type: ArmParamType.String
+      },
+      appServicePlanWorkerSizeId: {
+        defaultValue: "3",
+        type: ArmParamType.String
+      },
+      appServicePlanMinWorkerCount: {
+        defaultValue: 1,
+        type: ArmParamType.Int,
+      },
+      appServicePlanMaxWorkerCount: {
+        defaultValue: 10,
+        type: ArmParamType.Int
+      },
+      appServicePlanHostingEnvironment: {
+        defaultValue: "",
+        type: ArmParamType.String
       }
     };
 
@@ -51,10 +71,10 @@ export class AppServicePlanResource implements ArmResourceTemplateGenerator {
           "location": "[parameters('location')]",
           "properties": {
             "name": "[parameters('appServicePlanName')]",
-            "workerSizeId": "3",
-            "numberOfWorkers": "1",
-            "maximumElasticWorkerCount": "10",
-            "hostingEnvironment": ""
+            "workerSizeId": "[parameters('appServicePlanWorkerSizeId')]",
+            "numberOfWorkers": "[parameters('appServicePlanMinWorkerCount')]",
+            "maximumElasticWorkerCount": "[parameters('appServicePlanMaxWorkerCount')]",
+            "hostingEnvironment": "[parameters('appServicePlanHostingEnvironment')]"
           },
           "sku": {
             "name": "[parameters('appServicePlanSkuName')]",
@@ -68,7 +88,8 @@ export class AppServicePlanResource implements ArmResourceTemplateGenerator {
   public getParameters(config: ServerlessAzureConfig): ArmParameters {
     const resourceConfig: ResourceConfig = {
       sku: {},
-      ...config.provider.storageAccount,
+      scale: {},
+      ...config.provider.appServicePlan,
     };
 
     const params: AppServicePlanParams = {
@@ -80,9 +101,21 @@ export class AppServicePlanResource implements ArmResourceTemplateGenerator {
       },
       appServicePlanSkuTier: {
         value: resourceConfig.sku.tier,
+      },
+      appServicePlanWorkerSizeId: {
+        value: resourceConfig.scale.workerSizeId
+      },
+      appServicePlanMinWorkerCount: {
+        value: resourceConfig.scale.minWorkerCount
+      },
+      appServicePlanMaxWorkerCount: {
+        value: resourceConfig.scale.maxWorkerCount
+      },
+      appServicePlanHostingEnvironment: {
+        value: resourceConfig.hostingEnvironment
       }
     }
 
-    return params as unknown as ArmParameters;
+    return params;
   }
 }
diff --git a/src/models/apiManagement.ts b/src/models/apiManagement.ts
index d11270a..d21e21d 100644
--- a/src/models/apiManagement.ts
+++ b/src/models/apiManagement.ts
@@ -12,11 +12,20 @@ export interface ApiManagementConfig {
   backends?: BackendContract[];
   /** The API's CORS policy */
   cors?: ApiCorsPolicy;
+  /** The API's JWT validation policy */
+  jwtValidate?: ApiJwtValidatePolicy;
+  /** The API's IP Filter policy */
+  ipFilter?: ApiIpFilterPolicy;
+  /** The pricing SKU for the APIM instance */
   sku?: {
+    /** The SKU name, (consumption | developer | basic | standard | premium) */
     name?: string;
+    /** The max number of reserved nodes for the specified SKU */
     capacity?: number;
   };
+  /** The publisher e-mail associated with the APIM instance */
   publisherEmail?: string;
+  /** The publisher name associated with the APIM instance */
   publisherName?: string;
 }
 
@@ -35,3 +44,73 @@ export interface ApiCorsPolicy {
   /** A list of headers exposed during OPTION preflight requests */
   exposeHeaders: string[];
 }
+
+/**
+ * Defines an APIM JWT validation policy
+ * See https://docs.microsoft.com/en-us/azure/api-management/api-management-access-restriction-policies#ValidateJWT for more information
+ */
+export interface ApiJwtValidatePolicy {
+  /** The name of the query string parameter that contains the JWT token */
+  queryParamName?: string;
+  /** The name of the HTTP header that contains the JWT token */
+  headerName?: string;
+  /** An explicit JWT token value to validate */
+  tokenValue?: string;
+  /** The authorization scheme to acceept (ex. bearer) */
+  scheme?: string;
+  /** The HTTP status code to return for a failed response */
+  failedStatusCode?: number;
+  /** The error message to return for a failed response */
+  failedErrorMessage?: string;
+  /** Whether or not an expiration claim is required in the token */
+  requireExpirationTime?: boolean;
+  /** Whether or not tokens must be signed */
+  requireSignedTokens?: boolean;
+  /** Number of seconds to skew the clock */
+  clockSkew?: number;
+  /** String. Name of context variable that will receive token value as an object of type Jwt upon successful token validation */
+  outputTokenVariableName?: string;
+  /** Specifies the OpenID configuration used to validate the JWT token */
+  openId?: {
+    /** Link to the OpenID metadata url */
+    metadataUrl: string;
+  };
+  /** List of valid Base64 encoded signing keys */
+  signingKeys?: string[];
+  /** List of valie Base64 encoded decryption keys */
+  decryptionKeys?: string[];
+  /** List of valid audiences for the token */
+  audiences?: string[];
+  /** List of valid issuers for the token */
+  issuers?: string[];
+  /** List of claims that must exist within the token */
+  requiredClaims?: ApiJwtClaim[];
+}
+
+/**
+ * A JWT validation claim
+ */
+export interface ApiJwtClaim {
+  /** The name of the claim to validate */
+  name: string;
+  /** Whether the claim value must contain all or any value */
+  match: "all" | "any";
+  /** The seperator used to parse multi-valued claims */
+  separator?: string;
+  /** The values to match against */
+  values?: string[];
+}
+
+/**
+ * A IP Filter validation policy
+ */
+export interface ApiIpFilterPolicy {
+  /** Whether the policy should allow or forbid the address specification */
+  action: "allow" | "forbid";
+  addresses?: string[];
+  /** The range of IP addresses to apply to the policy */
+  addressRange?: {
+    from: string;
+    to: string;
+  };
+}
diff --git a/src/models/armTemplates.ts b/src/models/armTemplates.ts
index 2461140..453f1b2 100644
--- a/src/models/armTemplates.ts
+++ b/src/models/armTemplates.ts
@@ -54,7 +54,7 @@ export interface ArmParameter {
   defaultValue?: string | number;
 }
 
-export interface DefaultArmParams {
+export interface DefaultArmParams extends ArmParameters {
   location?: ArmParameter;
 }
 
diff --git a/src/models/serverless.ts b/src/models/serverless.ts
index 00393f0..9d1c8c3 100644
--- a/src/models/serverless.ts
+++ b/src/models/serverless.ts
@@ -8,7 +8,7 @@ export interface ArmTemplateConfig {
 }
 
 export interface ResourceConfig {
-  name: string;
+  name?: string;
   sku?: {
     name?: string;
     tier?: string;
@@ -49,7 +49,7 @@ export interface ServerlessAzureProvider {
   apim?: ApiManagementConfig;
   functionApp?: FunctionAppConfig;
   appInsights?: ResourceConfig;
-  appServicePlan?: ResourceConfig;
+  appServicePlan?: AppServicePlanConfig;
   storageAccount?: ResourceConfig;
   hostingEnvironment?: ResourceConfig;
   virtualNetwork?: ResourceConfig;
@@ -84,6 +84,15 @@ export interface AzureKeyVaultConfig {
   resourceGroup: string;
 }
 
+export interface AppServicePlanConfig extends ResourceConfig {
+  scale?: {
+    minWorkerCount?: number;
+    maxWorkerCount?: number;
+    workerSizeId?: string;
+  };
+  hostingEnvironment?: string;
+}
+
 /**
  * Expanding type of incomplete `Serverless` type.
  */
@@ -144,7 +153,7 @@ export interface ServerlessAzureConfig {
   provider: ServerlessAzureProvider;
   plugins: string[];
   functions: any;
-  package: {
+  package?: {
     individually: boolean;
     artifactDirectoryName: string;
     artifact: string;
diff --git a/src/services/apimPolicyBuilder.ts b/src/services/apimPolicyBuilder.ts
new file mode 100644
index 0000000..c86f1c6
--- /dev/null
+++ b/src/services/apimPolicyBuilder.ts
@@ -0,0 +1,183 @@
+import { ApiJwtValidatePolicy, ApiJwtClaim, ApiCorsPolicy, ApiIpFilterPolicy } from "../models/apiManagement";
+import { Guard } from "../shared/guard";
+import { Builder, Parser } from "xml2js";
+
+/**
+ * APIM Policy build that can be used to build polices for APIs, Backends & operations
+ */
+export class ApimPolicyBuilder {
+  private policyRoot: any = {};
+
+  public constructor() {
+    this.init();
+  }
+
+  /**
+   * Parses an APIM xml policy and loads it into a Policy Bulder
+   * @param xml The xml policy
+   */
+  public static async parse(xml: string): Promise<ApimPolicyBuilder> {
+    Guard.empty(xml, "xml");
+
+    const parser = new Parser();
+    const builder = new ApimPolicyBuilder();
+    builder.policyRoot = await parser.parseStringPromise(xml);
+
+    return builder;
+  }
+
+  /**
+   * Generates the XML policy that is compatible with APIM
+   */
+  public build(): string {
+    const builder = new Builder();
+    return builder.buildObject(this.policyRoot);
+  }
+
+  /**
+   * Adds a policy to specify the APIM backend to use for an operation
+   * @param backendId The APIM backend id
+   */
+  public setBackendService(backendId: string): ApimPolicyBuilder {
+    Guard.empty(backendId, "backendId");
+
+    this.policyRoot.policies.inbound[0]["set-backend-service"] = [
+      { $: { "id": "apim-generated-policy", "backend-id": backendId } }
+    ];
+
+    return this;
+  }
+
+  /**
+   * Applies the CORS policy to an APIM API
+   * @param corsConfig The APIM CORS configuration
+   */
+  public cors(corsConfig: ApiCorsPolicy): ApimPolicyBuilder {
+    Guard.null(corsConfig, "corsConfig");
+
+    const origins = corsConfig.allowedOrigins ? [corsConfig.allowedOrigins.map((origin) => ({ origin }))] : null;
+    const methods = corsConfig.allowedMethods ? [corsConfig.allowedMethods.map((method) => ({ method }))] : null;
+    const allowedHeaders = corsConfig.allowedHeaders ? [corsConfig.allowedHeaders.map((header) => ({ header }))] : null;
+    const exposeHeaders = corsConfig.exposeHeaders ? [corsConfig.exposeHeaders.map((header) => ({ header }))] : null;
+
+    this.policyRoot.policies.inbound[0].cors = [
+      {
+        $: { "allow-credentials": !!corsConfig.allowCredentials },
+        "allowed-origins": origins,
+        "allowed-methods": methods,
+        "allowed-headers": allowedHeaders,
+        "expose-headers": exposeHeaders
+      }
+    ];
+
+    return this;
+  }
+
+  /**
+   * Applies an IP filter configuration policy to an APIM API
+   * @param ipConfig The IP configuration policy
+   */
+  public ipFilter(ipConfig: ApiIpFilterPolicy): ApimPolicyBuilder {
+    Guard.null(ipConfig, "ipConfig");
+
+    const element = {
+      $: { action: ipConfig.action },
+      addressRange: ipConfig.addressRange,
+      address: ipConfig.addresses
+    }
+
+    Object.keys(element).forEach((key) => {
+      if (!element[key]) {
+        delete element[key];
+      }
+    });
+
+    this.policyRoot.policies.inbound[0]["ip-filter"] = [element];
+
+    return this;
+  }
+
+  /**
+   * Applies the JWT validation policy to an APIM API
+   * @param jwtPolicy The JWT validation configuration
+   */
+  public jwtValidate(jwtPolicy: ApiJwtValidatePolicy): ApimPolicyBuilder {
+    Guard.null(jwtPolicy, "jwtPolicy");
+
+    const signingKeys = jwtPolicy.signingKeys ? [jwtPolicy.signingKeys.map((key) => ({ key }))] : null;
+    const decryptionKeys = jwtPolicy.decryptionKeys ? [jwtPolicy.decryptionKeys.map((key) => ({ key }))] : null;
+    const audiences = jwtPolicy.audiences ? [jwtPolicy.audiences.map((audience) => ({ audience }))] : null;
+    const issuers = jwtPolicy.issuers ? [jwtPolicy.issuers.map((issuer) => ({ issuer }))] : null;
+    const oidConfig = jwtPolicy.openId && jwtPolicy.openId.metadataUrl ? [{ $: { url: jwtPolicy.openId.metadataUrl } }] : null;
+    const claims = jwtPolicy.requiredClaims ? [jwtPolicy.requiredClaims.map(this.createClaimElement)] : null;
+
+    const attributeMap = {
+      headerName: "header-name",
+      tokenValue: "token-value",
+      queryParamName: "query-parameter-name",
+      failedStatusCode: "failed-validation-httpcode",
+      failedErrorMessage: "failed-validation-error-message",
+      requireExpirationTime: "require-expiration-time",
+      scheme: "require-scheme",
+      requireSignedTokens: "require-signed-tokens",
+      clockSkew: "clock-skew",
+      outputTokenVariableName: "output-token-variable-name"
+    };
+
+    const attributes = {};
+
+    Object.keys(attributeMap).forEach((key) => {
+      if (jwtPolicy[key]) {
+        attributes[attributeMap[key]] = jwtPolicy[key];
+      }
+    });
+
+    const elements = {
+      $: attributes,
+      "openid-config": oidConfig,
+      "issuer-signing-keys": signingKeys,
+      "decryption-keys": decryptionKeys,
+      "audiences": audiences,
+      "issuers": issuers,
+      "required-claims": claims
+    };
+
+    Object.keys(elements).forEach((key) => {
+      if (!elements[key]) {
+        delete elements[key];
+      }
+    });
+
+    this.policyRoot.policies.inbound[0]["validate-jwt"] = [elements];
+
+    return this;
+  }
+
+  /**
+   * Initializes an empty APIM policy
+   */
+  private init(): void {
+    this.policyRoot = {
+      policies:
+      {
+        "inbound": [{ base: null }],
+        "backend": [{ base: null }],
+        "outbound": [{ base: null }],
+        "on-error": [{ base: null }]
+      }
+    }
+  }
+
+  /**
+   * Create a claim element for a JWT validation policy
+   * @param claim The JWT claim
+   */
+  private createClaimElement(claim: ApiJwtClaim) {
+    return {
+      claim: {
+        $: { name: claim.name, match: claim.match, separator: claim.separator },
+        value: claim.values
+      }
+    };
+  }
+}
diff --git a/src/services/apimService.ts b/src/services/apimService.ts
index 110a489..3c243ca 100644
--- a/src/services/apimService.ts
+++ b/src/services/apimService.ts
@@ -1,12 +1,12 @@
 import Serverless from "serverless";
-import xml from "xml";
 import { ApiManagementClient } from "@azure/arm-apimanagement";
 import { FunctionAppService } from "./functionAppService";
 import { BaseService } from "./baseService";
-import { ApiManagementConfig, ApiCorsPolicy } from "../models/apiManagement";
+import { ApiManagementConfig } from "../models/apiManagement";
+import { ApimPolicyBuilder } from "../services/apimPolicyBuilder";
 import {
   ApiContract, OperationContract,
-  PropertyContract, ApiManagementServiceResource, BackendContract,
+  PropertyContract, ApiManagementServiceResource, BackendContract, PolicyContract,
 } from "@azure/arm-apimanagement/esm/models";
 import { Site } from "@azure/arm-appservice/esm/models";
 import { Guard } from "../shared/guard";
@@ -147,7 +147,7 @@ export class ApimService extends BaseService {
 
     // Default to GET method if not specified
     if (!httpConfig.methods) {
-      httpConfig.methods = [ "GET" ]
+      httpConfig.methods = ["GET"]
     }
 
     // Infer APIM operation configuration from HTTP event if not already set
@@ -219,14 +219,7 @@ export class ApimService extends BaseService {
         isCurrent: true,
       });
 
-      if (this.apimConfig.cors) {
-        this.log(`-> Deploying CORS policy: ${apiContract.name}`);
-
-        await this.apimClient.apiPolicy.createOrUpdate(this.resourceGroup, this.apimConfig.name, apiContract.name, {
-          format: "rawxml",
-          value: this.createCorsXmlPolicy(this.apimConfig.cors)
-        });
-      }
+      await this.setApiPolicies(apiContract);
 
       return api;
     } catch (e) {
@@ -288,7 +281,7 @@ export class ApimService extends BaseService {
         responses: operation.responses || [],
       };
 
-      // Ensure a single path seperator in the operation path
+      // Ensure a single path separator in the operation path
       const operationPath = `/${api.path}/${operationConfig.urlTemplate}`.replace(/\/+/g, "/");
       const operationUrl = `${resource.gatewayUrl}${operationPath}`;
       this.log(`--> ${operationConfig.name}: [${operationConfig.method.toUpperCase()}] ${operationUrl}`);
@@ -301,9 +294,19 @@ export class ApimService extends BaseService {
         operationConfig,
       );
 
+      // Get any existing operation policy merge in backend service
+      const operationPolicy = await this.getApiOperationPolicy(api, operationConfig);
+      const policyBuilder = operationPolicy
+        ? await ApimPolicyBuilder.parse(operationPolicy.value)
+        : new ApimPolicyBuilder();
+
+      const policyXml = policyBuilder
+        .setBackendService(backend.name)
+        .build();
+
       await client.apiOperationPolicy.createOrUpdate(this.resourceGroup, this.apimConfig.name, api.name, operationConfig.name, {
         format: "rawxml",
-        value: this.createApiOperationXmlPolicy(backend.name),
+        value: policyXml,
       });
 
       return result;
@@ -337,68 +340,70 @@ export class ApimService extends BaseService {
   }
 
   /**
-   * Creates the XML payload that defines the API operation policy to link to the configured backend
+   * Sets policies on the API from serverless configuration
+   * @param apiContract The API contract
    */
-  private createApiOperationXmlPolicy(backendId: string): string {
-    const operationPolicy = [{
-      policies: [
-        {
-          inbound: [
-            { base: null },
-            {
-              "set-backend-service": [
-                {
-                  "_attr": {
-                    "id": "apim-generated-policy",
-                    "backend-id": backendId,
-                  }
-                },
-              ],
-            },
-          ],
-        },
-        { backend: [{ base: null }] },
-        { outbound: [{ base: null }] },
-        { "on-error": [{ base: null }] },
-      ]
-    }];
+  private async setApiPolicies(apiContract: ApiContract) {
+    let requireUpdate = false;
+    const existingPolicy = await this.getApiPolicy(apiContract);
+
+    const builder = existingPolicy
+      ? await ApimPolicyBuilder.parse(existingPolicy.value)
+      : new ApimPolicyBuilder();
+
+    if (this.apimConfig.cors) {
+      builder.cors(this.apimConfig.cors);
+      requireUpdate = true;
+    }
+
+    if (this.apimConfig.jwtValidate) {
+      builder.jwtValidate(this.apimConfig.jwtValidate);
+      requireUpdate = true;
+    }
+
+    if (this.apimConfig.ipFilter) {
+      builder.ipFilter(this.apimConfig.ipFilter);
+      requireUpdate = true;
+    }
+
+    if (requireUpdate) {
+      const policyXml = builder.build();
+
+      await this.apimClient.apiPolicy.createOrUpdate(this.resourceGroup, this.apimConfig.name, apiContract.name, {
+        format: "rawxml",
+        value: policyXml
+      });
+    }
+  }
+
+  /**
+   * Gets the API policy associated with the specified API
+   * @param apiContract The API to query for policies
+   */
+  private async getApiPolicy(apiContract: ApiContract): Promise<PolicyContract> {
+    Guard.null(apiContract);
 
-    return xml(operationPolicy);
+    try {
+      return await this.apimClient.apiPolicy.get(this.resourceGroup, this.apimConfig.name, apiContract.name);
+    } catch (err) {
+      return null;
+    }
   }
 
   /**
-   * Creates the XML payload that defines the specified CORS policy
-   * @param corsPolicy The CORS policy
+   * Gets the API operation policy associated with the specified operation
+   * @param apiContract The API associated with the operation
+   * @param operationContract The API operation to query for policies
    */
-  private createCorsXmlPolicy(corsPolicy: ApiCorsPolicy): string {
-    const origins = corsPolicy.allowedOrigins ? corsPolicy.allowedOrigins.map((origin) => ({ origin })) : null;
-    const methods = corsPolicy.allowedMethods ? corsPolicy.allowedMethods.map((method) => ({ method })) : null;
-    const allowedHeaders = corsPolicy.allowedHeaders ? corsPolicy.allowedHeaders.map((header) => ({ header })) : null;
-    const exposeHeaders = corsPolicy.exposeHeaders ? corsPolicy.exposeHeaders.map((header) => ({ header })) : null;
-
-    const policy = [{
-      policies: [
-        {
-          inbound: [
-            { base: null },
-            {
-              cors: [
-                { "_attr": { "allow-credentials": corsPolicy.allowCredentials } },
-                { "allowed-origins": origins },
-                { "allowed-methods": methods },
-                { "allowed-headers": allowedHeaders },
-                { "expose-headers": exposeHeaders },
-              ]
-            }
-          ],
-        },
-        { backend: [{ base: null }] },
-        { outbound: [{ base: null }] },
-        { "on-error": [{ base: null }] },
-      ]
-    }];
+  private async getApiOperationPolicy(apiContract: ApiContract, operationContract: OperationContract): Promise<PolicyContract> {
+    Guard.null(apiContract);
+    Guard.null(operationContract);
 
-    return xml(policy, { indent: "\t" });
+    try {
+      return await this.apimClient.apiOperationPolicy.get(this.resourceGroup, this.apimConfig.name, apiContract.name, operationContract.name);
+    } catch (err) {
+      return null;
+    }
   }
 
   /**


Test patch:
diff --git a/src/armTemplates/resources/appServicePlan.test.ts b/src/armTemplates/resources/appServicePlan.test.ts
index d3a76ef..8276f06 100644
--- a/src/armTemplates/resources/appServicePlan.test.ts
+++ b/src/armTemplates/resources/appServicePlan.test.ts
@@ -52,4 +52,96 @@ describe("App Service Plan Resource", () => {
 
     expect(AppServicePlanResource.getResourceName(config)).toEqual(appServicePlanName);
   });
+
+  it("generates correct default parameters", () => {
+    const config: ServerlessAzureConfig = {
+      provider: {
+        name: "azure",
+        prefix,
+        region,
+        stage,
+        resourceGroup: resourceGroupName,
+        runtime: "nodejs10.x",
+      },
+      plugins: [],
+      functions: {},
+      service: "myapp",
+    };
+
+    const appServicePlanResource = new AppServicePlanResource();
+    const params = appServicePlanResource.getParameters(config);
+
+    expect(Object.keys(params)).toHaveLength(7);
+    expect(params.appServicePlanName.value).toEqual(AppServicePlanResource.getResourceName(config));
+    expect(params.appServicePlanSkuName.value).toBeUndefined();
+    expect(params.appServicePlanSkuTier.value).toBeUndefined();
+    expect(params.appServicePlanWorkerSizeId.value).toBeUndefined();
+    expect(params.appServicePlanMinWorkerCount.value).toBeUndefined();
+    expect(params.appServicePlanMaxWorkerCount.value).toBeUndefined();
+    expect(params.appServicePlanHostingEnvironment.value).toBeUndefined();
+  });
+
+  it("generates correct specified parameters", () => {
+    const config: ServerlessAzureConfig = {
+      provider: {
+        name: "azure",
+        prefix,
+        region,
+        stage,
+        resourceGroup: resourceGroupName,
+        runtime: "nodejs10.x",
+        appServicePlan: {
+          name: "customAppServicePlanName",
+          hostingEnvironment: "customHostingEnvironment",
+          sku: {
+            name: "EP2",
+            tier: "specialTier"
+          },
+          scale: {
+            minWorkerCount: 3,
+            maxWorkerCount: 20,
+            workerSizeId: "4"
+          }
+        }
+      },
+      plugins: [],
+      functions: {},
+      service: "myapp",
+    };
+
+    const appServicePlanResource = new AppServicePlanResource();
+    const params = appServicePlanResource.getParameters(config);
+
+    expect(Object.keys(params)).toHaveLength(7);
+    expect(params.appServicePlanName.value).toEqual(config.provider.appServicePlan.name);
+    expect(params.appServicePlanSkuName.value).toEqual(config.provider.appServicePlan.sku.name);
+    expect(params.appServicePlanSkuTier.value).toEqual(config.provider.appServicePlan.sku.tier);
+    expect(params.appServicePlanWorkerSizeId.value).toEqual(config.provider.appServicePlan.scale.workerSizeId);
+    expect(params.appServicePlanMinWorkerCount.value).toEqual(config.provider.appServicePlan.scale.minWorkerCount);
+    expect(params.appServicePlanMaxWorkerCount.value).toEqual(config.provider.appServicePlan.scale.maxWorkerCount);
+    expect(params.appServicePlanHostingEnvironment.value).toEqual(config.provider.appServicePlan.hostingEnvironment);
+  });
+
+  it("generates the expected template", () => {
+    const appServicePlanResource = new AppServicePlanResource();
+    const template = appServicePlanResource.getTemplate();
+    const appServicePlan = template.resources[0];
+
+    expect(appServicePlan).toMatchObject({
+      name: "[parameters('appServicePlanName')]",
+      type: "Microsoft.Web/serverfarms",
+      location: "[parameters('location')]",
+      properties: {
+        name: "[parameters('appServicePlanName')]",
+        workerSizeId: "[parameters('appServicePlanWorkerSizeId')]",
+        numberOfWorkers: "[parameters('appServicePlanMinWorkerCount')]",
+        maximumElasticWorkerCount: "[parameters('appServicePlanMaxWorkerCount')]",
+        hostingEnvironment: "[parameters('appServicePlanHostingEnvironment')]",
+      },
+      sku: {
+        name: "[parameters('appServicePlanSkuName')]",
+        tier: "[parameters('appServicePlanSkuTier')]"
+      }
+    });
+  });
 });
diff --git a/src/services/apimPolicyBuilder.test.ts b/src/services/apimPolicyBuilder.test.ts
new file mode 100644
index 0000000..c5b30e0
--- /dev/null
+++ b/src/services/apimPolicyBuilder.test.ts
@@ -0,0 +1,129 @@
+import { MockFactory } from "../test/mockFactory";
+import { ApimPolicyBuilder } from "./apimPolicyBuilder";
+import fs from "fs";
+import { ApiIpFilterPolicy } from "../models/apiManagement";
+
+declare global {
+  interface String {
+    cleanXml(): string;
+  }
+}
+
+// Only used in this spec for testing - otherwise move it out into some mixin / utility.
+String.prototype.cleanXml = function () {
+  return this
+    .replace(/\s*</g, "<") // Replace any leading tag spaces
+    .replace(/>\s*/g, ">") // Replace and trailing tag spaces
+    .replace(/(?:\r\n|\r|\n|\t)/g, ""); // Remove any line-breaks & tabs
+};
+
+describe("APIM PolicyBuilder", () => {
+  it("can create jwt validation policy", () => {
+    const expected = fs.readFileSync(`${process.cwd()}/src/test/policies/jwt-validate.xml`)
+      .toString()
+      .cleanXml();
+
+    const jwtPolicy = MockFactory.createTestMockApiJwtPolicy();
+    jwtPolicy.openId = {
+      metadataUrl: "https://someurl"
+    };
+    jwtPolicy.requiredClaims = [
+      {
+        name: "aud",
+        match: "all",
+        separator: ":",
+        values: ["value1", "value2", "value3"]
+      },
+      {
+        name: "sub",
+        match: "any",
+        separator: ":",
+        values: ["value4", "value5", "value6"]
+      }
+    ];
+
+
+    const policyBuilder = new ApimPolicyBuilder();
+    const actual = policyBuilder
+      .jwtValidate(jwtPolicy)
+      .build()
+      .cleanXml();
+
+    expect(actual).toEqual(expected);
+  });
+
+  it("can create cors policy", () => {
+    const expected = fs.readFileSync(`${process.cwd()}/src/test/policies/cors.xml`)
+      .toString()
+      .cleanXml();
+
+    const corsPolicy = MockFactory.createTestMockApiCorsPolicy();
+
+    const policyBuilder = new ApimPolicyBuilder();
+    const actual = policyBuilder
+      .cors(corsPolicy)
+      .build()
+      .cleanXml();
+
+    expect(actual).toEqual(expected);
+  });
+
+  it("can create a combined policy that includes cors and jwt validation", () => {
+    const expected = fs.readFileSync(`${process.cwd()}/src/test/policies/combined.xml`)
+      .toString()
+      .cleanXml();
+
+    const corsPolicy = MockFactory.createTestMockApiCorsPolicy();
+    const jwtPolicy = MockFactory.createTestMockApiJwtPolicy();
+
+    const policyBuilder = new ApimPolicyBuilder();
+    const actual = policyBuilder
+      .cors(corsPolicy)
+      .jwtValidate(jwtPolicy)
+      .build()
+      .cleanXml();
+
+    expect(actual).toEqual(expected);
+  });
+
+  it("can create backend operation policy", () => {
+    const expected = fs.readFileSync(`${process.cwd()}/src/test/policies/backend-service.xml`)
+      .toString()
+      .cleanXml();
+
+    const policyBuilder = new ApimPolicyBuilder();
+    const actual = policyBuilder
+      .setBackendService("my-custom-service")
+      .build()
+      .cleanXml();
+
+    expect(actual).toEqual(expected);
+  });
+
+  it("can create an IP filter API policy", () => {
+    const expected = fs.readFileSync(`${process.cwd()}/src/test/policies/ip-filter.xml`)
+      .toString()
+      .trim()
+      .cleanXml();
+
+    const ipPolicy: ApiIpFilterPolicy = {
+      action: "allow",
+      addressRange: {
+        from: "1.1.1.1",
+        to: "1.1.1.255"
+      },
+      addresses: [
+        "2.2.2.2",
+        "3.3.3.3"
+      ]
+    };
+
+    const policyBuilder = new ApimPolicyBuilder();
+    const actual = policyBuilder
+      .ipFilter(ipPolicy)
+      .build()
+      .cleanXml();
+
+    expect(actual).toEqual(expected);
+  });
+});
diff --git a/src/services/apimService.test.ts b/src/services/apimService.test.ts
index 6e62453..49f7458 100644
--- a/src/services/apimService.test.ts
+++ b/src/services/apimService.test.ts
@@ -17,9 +17,12 @@ import {
   ApiManagementServiceGetResponse,
   OperationContract,
   ApiPolicyCreateOrUpdateResponse,
+  PolicyContract,
+  ApiPolicyGetResponse
 } from "@azure/arm-apimanagement/esm/models";
 import { AzureNamingService } from "./namingService";
-import { ApiManagementConfig } from "../models/apiManagement";
+import { ApiManagementConfig, ApiIpFilterPolicy } from "../models/apiManagement";
+import { ApimPolicyBuilder } from "./apimPolicyBuilder";
 
 describe("APIM Service", () => {
   let apimConfig: ApiManagementConfig;
@@ -56,6 +59,16 @@ describe("APIM Service", () => {
       azureCredentials: MockFactory.createTestAzureCredentials(),
       subscriptionId: "ABC123",
     };
+
+    const expectedPolicy: PolicyContract = {
+      name: "policy",
+      type: "Microsoft.ApiManagement/service/apis/policies",
+      format: "rawxml",
+      value: "<policies><inbound><base /></inbound><backend><base /></backend><outbound><base /></outbound><on-error><base /></on-error></policies>"
+    };
+
+    ApiPolicy.prototype.get = jest.fn(() => MockFactory.createTestArmSdkResponse<ApiPolicyGetResponse>(expectedPolicy, 200));
+    ApiOperationPolicy.prototype.get = jest.fn(() => MockFactory.createTestArmSdkResponse<ApiPolicyGetResponse>(expectedPolicy, 200));
   });
   it("is defined", () => {
     expect(ApimService).toBeDefined();
@@ -277,32 +290,104 @@ describe("APIM Service", () => {
       );
     });
 
-    it("deploys API CORS policy when defined within configuration", async () => {
-      Api.prototype.createOrUpdate =
-        jest.fn(() => MockFactory.createTestArmSdkResponse<ApiCreateOrUpdateResponse>(expectedApiResult, 201));
-      Backend.prototype.createOrUpdate =
-        jest.fn(() => MockFactory.createTestArmSdkResponse<BackendCreateOrUpdateResponse>(expectedBackend, 201));
-      Property.prototype.createOrUpdate =
-        jest.fn(() => MockFactory.createTestArmSdkResponse<PropertyCreateOrUpdateResponse>(expectedProperty, 201));
-      ApiPolicy.prototype.createOrUpdate =
-        jest.fn(() => MockFactory.createTestArmSdkResponse<ApiPolicyCreateOrUpdateResponse>(expectedProperty, 201));
+    describe("API Policies", () => {
+      beforeEach(() => {
+        Api.prototype.createOrUpdate =
+          jest.fn(() => MockFactory.createTestArmSdkResponse<ApiCreateOrUpdateResponse>(expectedApiResult, 201));
+        Backend.prototype.createOrUpdate =
+          jest.fn(() => MockFactory.createTestArmSdkResponse<BackendCreateOrUpdateResponse>(expectedBackend, 201));
+        Property.prototype.createOrUpdate =
+          jest.fn(() => MockFactory.createTestArmSdkResponse<PropertyCreateOrUpdateResponse>(expectedProperty, 201));
+        ApiPolicy.prototype.createOrUpdate =
+          jest.fn(() => MockFactory.createTestArmSdkResponse<ApiPolicyCreateOrUpdateResponse>(expectedProperty, 201));
+      });
 
-      const corsPolicy = MockFactory.createTestMockApiCorsPolicy();
-      serverless.service.provider["apim"]["cors"] = corsPolicy;
+      it("deploys API CORS policy when defined within configuration", async () => {
+        jest.spyOn(ApimPolicyBuilder.prototype, "cors");
+
+        const corsPolicy = MockFactory.createTestMockApiCorsPolicy();
+        serverless.service.provider["apim"]["cors"] = corsPolicy;
+
+        const apimService = new ApimService(serverless);
+        await apimService.deploy();
+
+        expect(ApimPolicyBuilder.prototype.cors).toBeCalledWith(corsPolicy);
+        expect(ApiPolicy.prototype.createOrUpdate).toBeCalledWith(
+          resourceGroupName,
+          serviceName,
+          apiName,
+          {
+            format: "rawxml",
+            value: expect.stringContaining("cors"),
+          }
+        );
+      });
 
-      const apimService = new ApimService(serverless);
-      const result = await apimService.deploy();
+      it("deploys API JWT policy when defined within configuration", async () => {
+        jest.spyOn(ApimPolicyBuilder.prototype, "jwtValidate");
+
+        const jwtPolicy = MockFactory.createTestMockApiJwtPolicy();
+        jwtPolicy.openId = {
+          metadataUrl: "https://someurl"
+        };
+        jwtPolicy.requiredClaims = [
+          {
+            name: "aud",
+            match: "all",
+            separator: ":",
+            values: ["value1", "value2", "value3"]
+          },
+          {
+            name: "sub",
+            match: "any",
+            separator: ":",
+            values: ["value4", "value5", "value6"]
+          }
+        ]
+        serverless.service.provider["apim"]["jwtValidate"] = jwtPolicy;
+
+        const apimService = new ApimService(serverless);
+        await apimService.deploy();
+
+        expect(ApimPolicyBuilder.prototype.jwtValidate).toBeCalledWith(jwtPolicy);
+        expect(ApiPolicy.prototype.createOrUpdate).toBeCalledWith(
+          resourceGroupName,
+          serviceName,
+          apiName,
+          {
+            format: "rawxml",
+            value: expect.stringContaining("validate-jwt"),
+          }
+        );
+      });
 
-      expect(result).not.toBeNull();
-      expect(ApiPolicy.prototype.createOrUpdate).toBeCalledWith(
-        resourceGroupName,
-        serviceName,
-        apiName,
-        {
-          format: "rawxml",
-          value: expect.stringContaining("cors"),
+      it("deploys API IP filter policy when defined within configuration", async () => {
+        jest.spyOn(ApimPolicyBuilder.prototype, "ipFilter");
+
+        const ipFilterPolicy: ApiIpFilterPolicy = {
+          action: "allow",
+          addresses: [
+            "1.1.1.1",
+            "2.2.2.2"
+          ]
         }
-      );
+
+        serverless.service.provider["apim"]["ipFilter"] = ipFilterPolicy;
+
+        const apimService = new ApimService(serverless);
+        await apimService.deploy();
+
+        expect(ApimPolicyBuilder.prototype.ipFilter).toBeCalledWith(ipFilterPolicy);
+        expect(ApiPolicy.prototype.createOrUpdate).toBeCalledWith(
+          resourceGroupName,
+          serviceName,
+          apiName,
+          {
+            format: "rawxml",
+            value: expect.stringContaining("ip-filter"),
+          }
+        );
+      });
     });
 
     it("returns null when APIM is not configured", async () => {
diff --git a/src/test/mockFactory.ts b/src/test/mockFactory.ts
index 298f9a8..83e8467 100644
--- a/src/test/mockFactory.ts
+++ b/src/test/mockFactory.ts
@@ -11,7 +11,7 @@ import Serverless from "serverless";
 import Service from "serverless/classes/Service";
 import Utils from "serverless/classes/Utils";
 import PluginManager from "serverless/lib/classes/PluginManager";
-import { ApiCorsPolicy, ApiManagementConfig } from "../models/apiManagement";
+import { ApiCorsPolicy, ApiManagementConfig, ApiJwtValidatePolicy } from "../models/apiManagement";
 import { ArmDeployment, ArmResourceTemplate, ArmTemplateProvisioningState, ArmParameters, ArmParamType } from "../models/armTemplates";
 import { ServicePrincipalEnvVariables } from "../models/azureProvider";
 import { Logger } from "../models/generic";
@@ -616,6 +616,17 @@ export class MockFactory {
     };
   }
 
+  public static createTestMockApiJwtPolicy(): ApiJwtValidatePolicy {
+    return {
+      headerName: "authorization",
+      scheme: "bearer",
+      audiences: ["audience1"],
+      issuers: ["issuer1"],
+      failedStatusCode: 401,
+      failedErrorMessage: "Authorization required!"
+    };
+  }
+
   public static createTestArmDeployment(): ArmDeployment {
     return {
       template: MockFactory.createTestArmTemplate(),
diff --git a/src/test/policies/backend-service.xml b/src/test/policies/backend-service.xml
new file mode 100644
index 0000000..359a68a
--- /dev/null
+++ b/src/test/policies/backend-service.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+<policies>
+  <inbound>
+    <base/>
+    <set-backend-service id="apim-generated-policy" backend-id="my-custom-service"/>
+  </inbound>
+  <backend>
+    <base/>
+  </backend>
+  <outbound>
+    <base/>
+  </outbound>
+  <on-error>
+    <base/>
+  </on-error>
+</policies>
diff --git a/src/test/policies/combined.xml b/src/test/policies/combined.xml
new file mode 100644
index 0000000..87a63b7
--- /dev/null
+++ b/src/test/policies/combined.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+<policies>
+  <inbound>
+    <base/>
+    <cors allow-credentials="false">
+      <allowed-origins>
+        <origin>*</origin>
+      </allowed-origins>
+      <allowed-methods>
+        <method>GET</method>
+        <method>POST</method>
+      </allowed-methods>
+      <allowed-headers>
+        <header>*</header>
+      </allowed-headers>
+      <expose-headers>
+        <header>*</header>
+      </expose-headers>
+    </cors>
+    <validate-jwt header-name="authorization" failed-validation-httpcode="401" failed-validation-error-message="Authorization required!" require-scheme="bearer">
+      <audiences>
+        <audience>audience1</audience>
+      </audiences>
+      <issuers>
+        <issuer>issuer1</issuer>
+      </issuers>
+    </validate-jwt>
+  </inbound>
+  <backend>
+    <base/>
+  </backend>
+  <outbound>
+    <base/>
+  </outbound>
+  <on-error>
+    <base/>
+  </on-error>
+</policies>
diff --git a/src/test/policies/cors.xml b/src/test/policies/cors.xml
new file mode 100644
index 0000000..e1cb006
--- /dev/null
+++ b/src/test/policies/cors.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+<policies>
+  <inbound>
+    <base/>
+    <cors allow-credentials="false">
+      <allowed-origins>
+        <origin>*</origin>
+      </allowed-origins>
+      <allowed-methods>
+        <method>GET</method>
+        <method>POST</method>
+      </allowed-methods>
+      <allowed-headers>
+        <header>*</header>
+      </allowed-headers>
+      <expose-headers>
+        <header>*</header>
+      </expose-headers>
+    </cors>
+  </inbound>
+  <backend>
+    <base/>
+  </backend>
+  <outbound>
+    <base/>
+  </outbound>
+  <on-error>
+    <base/>
+  </on-error>
+</policies>
diff --git a/src/test/policies/ip-filter.xml b/src/test/policies/ip-filter.xml
new file mode 100644
index 0000000..d0a6ca3
--- /dev/null
+++ b/src/test/policies/ip-filter.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+<policies>
+  <inbound>
+    <base/>
+    <ip-filter action="allow">
+      <addressRange>
+        <from>1.1.1.1</from>
+        <to>1.1.1.255</to>
+      </addressRange>
+      <address>2.2.2.2</address>
+      <address>3.3.3.3</address>
+    </ip-filter>
+  </inbound>
+  <backend>
+    <base/>
+  </backend>
+  <outbound>
+    <base/>
+  </outbound>
+  <on-error>
+    <base/>
+  </on-error>
+</policies>
diff --git a/src/test/policies/jwt-validate.xml b/src/test/policies/jwt-validate.xml
new file mode 100644
index 0000000..2d3f2e1
--- /dev/null
+++ b/src/test/policies/jwt-validate.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+<policies>
+  <inbound>
+    <base/>
+    <validate-jwt header-name="authorization" failed-validation-httpcode="401" failed-validation-error-message="Authorization required!" require-scheme="bearer">
+      <openid-config url="https://someurl"/>
+      <audiences>
+        <audience>audience1</audience>
+      </audiences>
+      <issuers>
+        <issuer>issuer1</issuer>
+      </issuers>
+      <required-claims>
+        <claim name="aud" match="all" separator=":">
+          <value>value1</value>
+          <value>value2</value>
+          <value>value3</value>
+        </claim>
+        <claim name="sub" match="any" separator=":">
+          <value>value4</value>
+          <value>value5</value>
+          <value>value6</value>
+        </claim>
+      </required-claims>
+    </validate-jwt>
+  </inbound>
+  <backend>
+    <base/>
+  </backend>
+  <outbound>
+    <base/>
+  </outbound>
+  <on-error>
+    <base/>
+  </on-error>
+</policies>
diff --git a/src/test/responses/apim-get-api-policy-200.json b/src/test/responses/apim-get-api-policy-200.json
new file mode 100644
index 0000000..848e932
--- /dev/null
+++ b/src/test/responses/apim-get-api-policy-200.json
@@ -0,0 +1,14 @@
+{
+  "value": [
+    {
+      "id": "/subscriptions/d36d0808-a967-4f73-9fdc-32ea232fc81d/resourceGroups/${resourceGroup.name}/providers/Microsoft.ApiManagement/service/${resource.name}/apis/example-api/policies/policy",
+      "type": "Microsoft.ApiManagement/service/apis/policies",
+      "name": "policy",
+      "properties": {
+        "value": "<policies><inbound><base /></inbound><backend><base /></backend><outbound><base /></outbound><on-error><base /></on-error></policies>",
+        "format": "xml"
+      }
+    }
+  ],
+  "count": 1
+}