Repository: serverless/serverless-azure-functions
Pull request #222 (2019-08-02 16:16:11)

feat: Cache interactive login credentials
feat: Cache interactive login credentials

Currently once a user logs in using the interactive login, there is no way to cache the credentials and the user must keep logging in through that method. This adds a simple file token cache that saves the credentials and subscription information and then uses those to login, reducing number of logins required. 

AB#359


Patch:
diff --git a/README.md b/README.md
index 7bb28e5..cd669a0 100644
--- a/README.md
+++ b/README.md
@@ -200,6 +200,9 @@ The getting started walkthrough illustrates the interactive login experience, wh
     $env:azureServicePrincipalPassword='<password>'
     ```
 
+### Example Usage
+- [Configuring API Management](docs/examples/apim.md) that sits in front of function app
+
 ### Contributing
 
 Please create issues in this repo for any problems or questions you find. Before sending a PR for any major changes, please create an issue to discuss.
diff --git a/docs/examples/apim.md b/docs/examples/apim.md
new file mode 100644
index 0000000..7faf378
--- /dev/null
+++ b/docs/examples/apim.md
@@ -0,0 +1,84 @@
+# API Management
+
+[API Management](https://azure.microsoft.com/en-us/services/api-management/) is an Azure Service for publishing, managing, securing and monitoring APIs. It can be deployed along with your Serverless function app by specifying its configuration in `serverless.yml`. Here is a basic example of how to configure API Mangement:
+
+## Simple Handler
+
+```yaml
+service: greeter
+
+provider:
+  prefix: greeter
+  name: azure
+  # Default to West US, allow for command line arg --region to override
+  region: ${opt:region, 'westus'}
+  # Default to dev, allow for command line arg -- stage to override
+  stage: ${opt:stage, 'dev'}
+  # Azure subscription ID for deployment
+  subscriptionId: 00000000-0000-0000-0000-000000000000
+
+  # Start of your API Management configuration
+  apim:
+    # API specifications
+    api:
+      # Name of the API
+      name: v1
+      subscriptionRequired: false
+      # Display name
+      displayName: v1
+      # Description of API
+      description: V1 sample app APIs
+      # HTTP protocols allowed
+      protocols:
+        - https
+      # Base path of API calls
+      path: v1
+      # Tags for ARM resource
+      tags:
+        - tag1
+        - tag2
+      # No authorization
+      authorization: none
+    # CORS Settings for APIM
+    cors:
+      allowCredentials: false
+      allowedOrigins:
+        - "*"
+      allowedMethods:
+        - GET
+        - POST
+        - PUT
+        - DELETE
+        - PATCH
+      allowedHeaders:
+        - "*"
+      exposeHeaders:
+        - "*"
+
+plugins:
+  - serverless-azure-functions
+
+functions:
+  hello:
+    handler: src/handlers/hello.handler
+
+    # API Management configuration for `hello` handler
+    apim:
+      operations:
+        # GET operation for `hello` handler
+        - method: get
+          # URL path for accessing handler
+          urlTemplate: /hello
+          # Display name inside Azure Portal
+          displayName: Hello
+    events:
+      - http: true
+        x-azure-settings:
+          methods:
+            - GET
+          authLevel : function
+      - http: true
+        x-azure-settings:
+          direction: out
+          name: res
+```
diff --git a/package-lock.json b/package-lock.json
index d057c0f..3ab7da1 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,6 +1,6 @@
 {
   "name": "serverless-azure-functions",
-  "version": "1.0.0-9",
+  "version": "1.0.0-10",
   "lockfileVersion": 1,
   "requires": true,
   "dependencies": {
@@ -1794,6 +1794,7 @@
       "version": "0.1.4",
       "resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz",
       "integrity": "sha1-DNkKVhCT810KmSVsIrcGlDP60Rc=",
+      "optional": true,
       "requires": {
         "kind-of": "^3.0.2",
         "longest": "^1.0.1",
@@ -1966,6 +1967,7 @@
       "version": "4.10.1",
       "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz",
       "integrity": "sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==",
+      "optional": true,
       "requires": {
         "bn.js": "^4.0.0",
         "inherits": "^2.0.1",
@@ -2435,7 +2437,8 @@
     "bn.js": {
       "version": "4.11.8",
       "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz",
-      "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA=="
+      "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==",
+      "optional": true
     },
     "body-parser": {
       "version": "1.19.0",
@@ -2540,7 +2543,8 @@
     "brorand": {
       "version": "1.1.0",
       "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz",
-      "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8="
+      "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=",
+      "optional": true
     },
     "browser-process-hrtime": {
       "version": "0.1.3",
@@ -2569,6 +2573,7 @@
       "version": "1.2.0",
       "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz",
       "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==",
+      "optional": true,
       "requires": {
         "buffer-xor": "^1.0.3",
         "cipher-base": "^1.0.0",
@@ -2605,6 +2610,7 @@
       "version": "4.0.1",
       "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz",
       "integrity": "sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=",
+      "optional": true,
       "requires": {
         "bn.js": "^4.1.0",
         "randombytes": "^2.0.1"
@@ -2701,7 +2707,8 @@
     "buffer-xor": {
       "version": "1.0.3",
       "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz",
-      "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk="
+      "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=",
+      "optional": true
     },
     "builtin-modules": {
       "version": "1.1.1",
@@ -2845,11 +2852,6 @@
       "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==",
       "dev": true
     },
-    "charenc": {
-      "version": "0.0.2",
-      "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz",
-      "integrity": "sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc="
-    },
     "chokidar": {
       "version": "2.1.6",
       "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.6.tgz",
@@ -2888,6 +2890,7 @@
       "version": "1.0.4",
       "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz",
       "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==",
+      "optional": true,
       "requires": {
         "inherits": "^2.0.1",
         "safe-buffer": "^5.0.1"
@@ -3265,6 +3268,7 @@
       "version": "1.2.0",
       "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz",
       "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==",
+      "optional": true,
       "requires": {
         "cipher-base": "^1.0.1",
         "inherits": "^2.0.1",
@@ -3277,6 +3281,7 @@
       "version": "1.1.7",
       "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz",
       "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==",
+      "optional": true,
       "requires": {
         "cipher-base": "^1.0.3",
         "create-hash": "^1.1.0",
@@ -3296,11 +3301,6 @@
         "which": "^1.2.9"
       }
     },
-    "crypt": {
-      "version": "0.0.2",
-      "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz",
-      "integrity": "sha1-iNf/fsDfuG9xPch7u0LQRNPmxBs="
-    },
     "crypto-browserify": {
       "version": "3.12.0",
       "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz",
@@ -3740,6 +3740,7 @@
       "version": "6.4.1",
       "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.4.1.tgz",
       "integrity": "sha512-BsXLz5sqX8OHcsh7CqBMztyXARmGQ3LWPtGjJi6DiJHq5C/qvi9P3OqgswKSDftbu8+IoI/QDTAm2fFnQ9SZSQ==",
+      "optional": true,
       "requires": {
         "bn.js": "^4.4.0",
         "brorand": "^1.0.1",
@@ -3801,6 +3802,7 @@
       "version": "0.1.7",
       "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.7.tgz",
       "integrity": "sha512-MfrRBDWzIWifgq6tJj60gkAwtLNb6sQPlcFrSOflcP1aFmmruKQ2wRnze/8V6kgyz7H3FF8Npzv78mZ7XLLflg==",
+      "optional": true,
       "requires": {
         "prr": "~1.0.1"
       }
@@ -4180,6 +4182,7 @@
       "version": "0.3.5",
       "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz",
       "integrity": "sha1-34xp7vFkeSPHFXuc6DhAYQsCzDk=",
+      "optional": true,
       "requires": {
         "d": "1",
         "es5-ext": "~0.10.14"
@@ -4194,6 +4197,7 @@
       "version": "1.0.3",
       "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz",
       "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==",
+      "optional": true,
       "requires": {
         "md5.js": "^1.3.4",
         "safe-buffer": "^5.1.1"
@@ -4723,8 +4727,7 @@
         },
         "ansi-regex": {
           "version": "2.1.1",
-          "bundled": true,
-          "optional": true
+          "bundled": true
         },
         "aproba": {
           "version": "1.2.0",
@@ -4742,13 +4745,11 @@
         },
         "balanced-match": {
           "version": "1.0.0",
-          "bundled": true,
-          "optional": true
+          "bundled": true
         },
         "brace-expansion": {
           "version": "1.1.11",
           "bundled": true,
-          "optional": true,
           "requires": {
             "balanced-match": "^1.0.0",
             "concat-map": "0.0.1"
@@ -4761,18 +4762,15 @@
         },
         "code-point-at": {
           "version": "1.1.0",
-          "bundled": true,
-          "optional": true
+          "bundled": true
         },
         "concat-map": {
           "version": "0.0.1",
-          "bundled": true,
-          "optional": true
+          "bundled": true
         },
         "console-control-strings": {
           "version": "1.1.0",
-          "bundled": true,
-          "optional": true
+          "bundled": true
         },
         "core-util-is": {
           "version": "1.0.2",
@@ -4875,8 +4873,7 @@
         },
         "inherits": {
           "version": "2.0.3",
-          "bundled": true,
-          "optional": true
+          "bundled": true
         },
         "ini": {
           "version": "1.3.5",
@@ -4886,7 +4883,6 @@
         "is-fullwidth-code-point": {
           "version": "1.0.0",
           "bundled": true,
-          "optional": true,
           "requires": {
             "number-is-nan": "^1.0.0"
           }
@@ -4899,20 +4895,17 @@
         "minimatch": {
           "version": "3.0.4",
           "bundled": true,
-          "optional": true,
           "requires": {
             "brace-expansion": "^1.1.7"
           }
         },
         "minimist": {
           "version": "0.0.8",
-          "bundled": true,
-          "optional": true
+          "bundled": true
         },
         "minipass": {
           "version": "2.3.5",
           "bundled": true,
-          "optional": true,
           "requires": {
             "safe-buffer": "^5.1.2",
             "yallist": "^3.0.0"
@@ -4929,7 +4922,6 @@
         "mkdirp": {
           "version": "0.5.1",
           "bundled": true,
-          "optional": true,
           "requires": {
             "minimist": "0.0.8"
           }
@@ -5002,8 +4994,7 @@
         },
         "number-is-nan": {
           "version": "1.0.1",
-          "bundled": true,
-          "optional": true
+          "bundled": true
         },
         "object-assign": {
           "version": "4.1.1",
@@ -5013,7 +5004,6 @@
         "once": {
           "version": "1.4.0",
           "bundled": true,
-          "optional": true,
           "requires": {
             "wrappy": "1"
           }
@@ -5089,8 +5079,7 @@
         },
         "safe-buffer": {
           "version": "5.1.2",
-          "bundled": true,
-          "optional": true
+          "bundled": true
         },
         "safer-buffer": {
           "version": "2.1.2",
@@ -5120,7 +5109,6 @@
         "string-width": {
           "version": "1.0.2",
           "bundled": true,
-          "optional": true,
           "requires": {
             "code-point-at": "^1.0.0",
             "is-fullwidth-code-point": "^1.0.0",
@@ -5138,7 +5126,6 @@
         "strip-ansi": {
           "version": "3.0.1",
           "bundled": true,
-          "optional": true,
           "requires": {
             "ansi-regex": "^2.0.0"
           }
@@ -5177,13 +5164,11 @@
         },
         "wrappy": {
           "version": "1.0.2",
-          "bundled": true,
-          "optional": true
+          "bundled": true
         },
         "yallist": {
           "version": "3.0.3",
-          "bundled": true,
-          "optional": true
+          "bundled": true
         }
       }
     },
@@ -5491,6 +5476,7 @@
       "version": "3.0.4",
       "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.4.tgz",
       "integrity": "sha1-X8hoaEfs1zSZQDMZprCj8/auSRg=",
+      "optional": true,
       "requires": {
         "inherits": "^2.0.1",
         "safe-buffer": "^5.0.1"
@@ -5500,6 +5486,7 @@
       "version": "1.1.7",
       "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz",
       "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==",
+      "optional": true,
       "requires": {
         "inherits": "^2.0.3",
         "minimalistic-assert": "^1.0.1"
@@ -5509,6 +5496,7 @@
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz",
       "integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=",
+      "optional": true,
       "requires": {
         "hash.js": "^1.0.3",
         "minimalistic-assert": "^1.0.0",
@@ -5827,7 +5815,8 @@
     "is-extglob": {
       "version": "2.1.1",
       "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
-      "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI="
+      "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=",
+      "optional": true
     },
     "is-fullwidth-code-point": {
       "version": "1.0.0",
@@ -7467,7 +7456,8 @@
     "longest": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz",
-      "integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc="
+      "integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=",
+      "optional": true
     },
     "loose-envify": {
       "version": "1.4.0",
@@ -7554,20 +7544,11 @@
         "object-visit": "^1.0.0"
       }
     },
-    "md5": {
-      "version": "2.2.1",
-      "resolved": "https://registry.npmjs.org/md5/-/md5-2.2.1.tgz",
-      "integrity": "sha1-U6s41f48iJG6RlMp6iP6wFQBJvk=",
-      "requires": {
-        "charenc": "~0.0.1",
-        "crypt": "~0.0.1",
-        "is-buffer": "~1.1.1"
-      }
-    },
     "md5.js": {
       "version": "1.3.5",
       "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz",
       "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==",
+      "optional": true,
       "requires": {
         "hash-base": "^3.0.0",
         "inherits": "^2.0.1",
@@ -7593,6 +7574,7 @@
       "version": "0.4.1",
       "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz",
       "integrity": "sha1-OpoguEYlI+RHz7x+i7gO1me/xVI=",
+      "optional": true,
       "requires": {
         "errno": "^0.1.3",
         "readable-stream": "^2.0.1"
@@ -7683,12 +7665,14 @@
     "minimalistic-assert": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
-      "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A=="
+      "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==",
+      "optional": true
     },
     "minimalistic-crypto-utils": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz",
-      "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo="
+      "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=",
+      "optional": true
     },
     "minimatch": {
       "version": "3.0.4",
@@ -8295,6 +8279,7 @@
       "version": "5.1.4",
       "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.4.tgz",
       "integrity": "sha512-Qs5duJcuvNExRfFZ99HDD3z4mAi3r9Wl/FOjEOijlxwCZs7E7mW2vjTpgQ4J8LpTF8x5v+1Vn5UQFejmWT11aw==",
+      "optional": true,
       "requires": {
         "asn1.js": "^4.0.0",
         "browserify-aes": "^1.0.0",
@@ -8403,6 +8388,7 @@
       "version": "3.0.17",
       "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.0.17.tgz",
       "integrity": "sha512-U/il5MsrZp7mGg3mSQfn742na2T+1/vHDCG5/iTI3X9MKUuYUZVLQhyRsg06mCgDBTd57TxzgZt7P+fYfjRLtA==",
+      "optional": true,
       "requires": {
         "create-hash": "^1.1.2",
         "create-hmac": "^1.1.4",
@@ -8605,7 +8591,8 @@
     "prr": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz",
-      "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY="
+      "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=",
+      "optional": true
     },
     "pseudomap": {
       "version": "1.0.2",
@@ -8672,6 +8659,7 @@
       "version": "2.1.0",
       "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
       "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==",
+      "optional": true,
       "requires": {
         "safe-buffer": "^5.1.0"
       }
@@ -9084,6 +9072,7 @@
       "version": "2.0.2",
       "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz",
       "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==",
+      "optional": true,
       "requires": {
         "hash-base": "^3.0.0",
         "inherits": "^2.0.1"
@@ -9473,6 +9462,7 @@
       "version": "2.4.11",
       "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz",
       "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==",
+      "optional": true,
       "requires": {
         "inherits": "^2.0.1",
         "safe-buffer": "^5.0.1"
@@ -9668,7 +9658,8 @@
     "source-list-map": {
       "version": "2.0.1",
       "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz",
-      "integrity": "sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw=="
+      "integrity": "sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==",
+      "optional": true
     },
     "source-map": {
       "version": "0.7.3",
@@ -10225,7 +10216,8 @@
     "tapable": {
       "version": "0.2.9",
       "resolved": "https://registry.npmjs.org/tapable/-/tapable-0.2.9.tgz",
-      "integrity": "sha512-2wsvQ+4GwBvLPLWsNfLCDYGsW6xb7aeC6utq2Qh0PFwgEy7K7dsma9Jsmb2zSQj7GvYAyUGSntLtsv++GmgL1A=="
+      "integrity": "sha512-2wsvQ+4GwBvLPLWsNfLCDYGsW6xb7aeC6utq2Qh0PFwgEy7K7dsma9Jsmb2zSQj7GvYAyUGSntLtsv++GmgL1A==",
+      "optional": true
     },
     "tar-stream": {
       "version": "1.6.2",
@@ -11075,6 +11067,7 @@
       "version": "1.3.0",
       "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.3.0.tgz",
       "integrity": "sha512-OiVgSrbGu7NEnEvQJJgdSFPl2qWKkWq5lHMhgiToIiN9w34EBnjYzSYs+VbL5KoYiLNtFFa7BZIKxRED3I32pA==",
+      "optional": true,
       "requires": {
         "source-list-map": "^2.0.0",
         "source-map": "~0.6.1"
@@ -11083,7 +11076,8 @@
         "source-map": {
           "version": "0.6.1",
           "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
-          "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
+          "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+          "optional": true
         }
       }
     },
diff --git a/package.json b/package.json
index e77d7eb..8305eba 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "serverless-azure-functions",
-  "version": "1.0.0-9",
+  "version": "1.0.0-10",
   "description": "Provider plugin for the Serverless Framework v1.x which adds support for Azure Functions.",
   "license": "MIT",
   "main": "./lib/index.js",
@@ -50,7 +50,6 @@
     "js-yaml": "^3.13.1",
     "jsonpath": "^1.0.1",
     "lodash": "^4.16.6",
-    "md5": "^2.2.1",
     "open": "^6.3.0",
     "request": "^2.81.0",
     "rimraf": "^2.6.3",
diff --git a/pipelines/ci.yml b/pipelines/ci.yml
new file mode 100644
index 0000000..a185790
--- /dev/null
+++ b/pipelines/ci.yml
@@ -0,0 +1,19 @@
+# CI should only run when PRs are opened against dev/master.
+pr:
+  - master
+  - dev
+
+pool:
+  vmImage: 'ubuntu-latest'
+
+steps:
+- task: NodeTool@0
+  displayName: 'Use Node 10.x'
+  inputs:
+    versionSpec: 10.x
+
+- task: Bash@3
+  displayName: 'Build serverless-azure-functions'
+  inputs:
+    targetType: filePath
+    filePath: ./scripts/build.sh
diff --git a/scripts/build.sh b/scripts/build.sh
new file mode 100644
index 0000000..21817d8
--- /dev/null
+++ b/scripts/build.sh
@@ -0,0 +1,6 @@
+#!/bin/bash
+set -euo pipefail
+
+npm ci
+# NOTE: build will also invoke Node prebuild, pretest and test lifecycle scripts
+npm run build
diff --git a/src/armTemplates/resources/apim.ts b/src/armTemplates/resources/apim.ts
index 5003361..2bd6652 100644
--- a/src/armTemplates/resources/apim.ts
+++ b/src/armTemplates/resources/apim.ts
@@ -5,11 +5,7 @@ import { AzureNamingService } from "../../services/namingService";
 
 export class ApimResource implements ArmResourceTemplateGenerator {
   public static getResourceName(config: ServerlessAzureConfig) {
-    return AzureNamingService.getResourceName(
-      config,
-      config.provider.apim,
-      "apim"
-    );
+    return AzureNamingService.getResourceName(config, config.provider.apim, "apim");
   }
 
   public getTemplate(): ArmResourceTemplate {
diff --git a/src/armTemplates/resources/appInsights.ts b/src/armTemplates/resources/appInsights.ts
index 9426a0b..ab72cc9 100644
--- a/src/armTemplates/resources/appInsights.ts
+++ b/src/armTemplates/resources/appInsights.ts
@@ -4,11 +4,7 @@ import { AzureNamingService } from "../../services/namingService";
 
 export class AppInsightsResource implements ArmResourceTemplateGenerator {
   public static getResourceName(config: ServerlessAzureConfig) {
-    return AzureNamingService.getResourceName(
-      config,
-      config.provider.appInsights,
-      "appinsights"
-    );
+    return AzureNamingService.getResourceName(config, config.provider.appInsights, "appinsights");
   }
 
   public getTemplate(): ArmResourceTemplate {
diff --git a/src/armTemplates/resources/appServicePlan.ts b/src/armTemplates/resources/appServicePlan.ts
index 4485cb0..50d99e6 100644
--- a/src/armTemplates/resources/appServicePlan.ts
+++ b/src/armTemplates/resources/appServicePlan.ts
@@ -4,11 +4,7 @@ import { AzureNamingService } from "../../services/namingService";
 
 export class AppServicePlanResource implements ArmResourceTemplateGenerator {
   public static getResourceName(config: ServerlessAzureConfig) {
-    return AzureNamingService.getResourceName(
-      config,
-      config.provider.appInsights,
-      "asp"
-    );
+    return AzureNamingService.getResourceName(config, config.provider.appInsights, "asp");
   }
 
   public getTemplate(): ArmResourceTemplate {
diff --git a/src/armTemplates/resources/functionApp.ts b/src/armTemplates/resources/functionApp.ts
index 1af37a3..0263490 100644
--- a/src/armTemplates/resources/functionApp.ts
+++ b/src/armTemplates/resources/functionApp.ts
@@ -5,11 +5,8 @@ import { AzureNamingService } from "../../services/namingService";
 export class FunctionAppResource implements ArmResourceTemplateGenerator {
   public static getResourceName(config: ServerlessAzureConfig) {
     const safeServiceName = config.service.replace(/\s/g, "-");
-    return AzureNamingService.getResourceName(
-      config,
-      config.provider.appInsights,
-      safeServiceName
-    );
+
+    return AzureNamingService.getResourceName(config, config.provider.appInsights, safeServiceName);
   }
 
   public getTemplate(): ArmResourceTemplate {
diff --git a/src/armTemplates/resources/hostingEnvironment.ts b/src/armTemplates/resources/hostingEnvironment.ts
index 7c8332b..0cf8671 100644
--- a/src/armTemplates/resources/hostingEnvironment.ts
+++ b/src/armTemplates/resources/hostingEnvironment.ts
@@ -4,11 +4,7 @@ import { AzureNamingService } from "../../services/namingService";
 
 export class HostingEnvironmentResource implements ArmResourceTemplateGenerator {
   public static getResourceName(config: ServerlessAzureConfig) {
-    return AzureNamingService.getResourceName(
-      config,
-      config.provider.hostingEnvironment,
-      "ase"
-    );
+    return AzureNamingService.getResourceName(config, config.provider.hostingEnvironment, "ase");
   }
 
   public getTemplate(): ArmResourceTemplate {
diff --git a/src/armTemplates/resources/storageAccount.ts b/src/armTemplates/resources/storageAccount.ts
index 69dc7cd..86832ae 100644
--- a/src/armTemplates/resources/storageAccount.ts
+++ b/src/armTemplates/resources/storageAccount.ts
@@ -5,11 +5,7 @@ import configConstants from "../../config";
 
 export class StorageAccountResource implements ArmResourceTemplateGenerator {
   public static getResourceName(config: ServerlessAzureConfig) {
-    return AzureNamingService.getSafeResourceName(
-      config,
-      configConstants.naming.maxLength.storageAccount,
-      config.provider.storageAccount
-    );
+    return AzureNamingService.getSafeResourceName(config, configConstants.naming.maxLength.storageAccount, config.provider.storageAccount);
   }
 
   public getTemplate(): ArmResourceTemplate {
diff --git a/src/plugins/login/utils/simpleFileTokenCache.ts b/src/plugins/login/utils/simpleFileTokenCache.ts
new file mode 100644
index 0000000..7f734cf
--- /dev/null
+++ b/src/plugins/login/utils/simpleFileTokenCache.ts
@@ -0,0 +1,92 @@
+import fs from "fs";
+import path from "path";
+import os from "os";
+import * as adal from "adal-node";
+
+const CONFIG_DIRECTORY = path.join(os.homedir(), ".azure");
+const DEFAULT_SLS_TOKEN_FILE = path.join(CONFIG_DIRECTORY, "slsTokenCache.json");
+
+export class SimpleFileTokenCache implements adal.TokenCache {
+  private entries: any[] = [];
+  private subscriptions: any[] = [];
+
+  public constructor(private tokenPath: string = DEFAULT_SLS_TOKEN_FILE) {
+    this.load();
+  }
+
+  public add(entries: any[], callback?: (err?: Error, result?: boolean) => void) {
+    this.entries.push(...entries);
+    this.save();
+    if (callback) {
+      callback();
+    }
+  }
+
+  public remove(entries: any[], callback?: (err?: Error, result?: null) => void) {
+    this.entries = this.entries.filter(e => {
+      return !Object.keys(entries[0]).every(key => e[key] === entries[0][key]);
+    });
+    this.save();
+    if (callback) {
+      callback();
+    }
+  }
+
+  public find(query: any, callback: (err?: Error, result?: any[]) => void) {
+    let result = this.entries.filter(e => {
+      return Object.keys(query).every(key => e[key] === query[key]);
+    });
+    callback(null, result);
+    return result;
+  }
+
+  //-------- File toke cache specific methods
+
+  public addSubs(subscriptions: any) {
+    this.subscriptions.push(...subscriptions);
+    this.subscriptions = this.subscriptions.reduce((accumulator , current) => {
+      const x = accumulator .find(item => item.id === current.id);
+      if (!x) {
+        return accumulator .concat([current]);
+      } else {
+        return accumulator ;
+      }
+    }, []);
+    this.save();
+  }
+
+
+  public clear(callback: any) {
+    this.entries = [];
+    this.subscriptions = [];
+    this.save();
+    callback();
+  }
+
+  public isEmpty() {
+    return this.entries.length === 0;
+  }
+
+  public first() {
+    return this.entries[0];
+  }
+
+  public listSubscriptions() {
+    return this.subscriptions;
+  }
+
+  private load() {
+    if (fs.existsSync(this.tokenPath)) {
+      let savedCache = JSON.parse(fs.readFileSync(this.tokenPath).toString());
+      this.entries = savedCache.entries;
+      this.entries.map(t => t.expiresOn = new Date(t.expiresOn))
+      this.subscriptions = savedCache.subscriptions;
+    } else {
+      this.save();
+    }
+  }
+
+  public save() {
+    fs.writeFileSync(this.tokenPath, JSON.stringify({ entries: this.entries, subscriptions: this.subscriptions }));
+  }
+}
diff --git a/src/plugins/offline/azureOfflinePlugin.ts b/src/plugins/offline/azureOfflinePlugin.ts
index 9fd7cb0..315de16 100644
--- a/src/plugins/offline/azureOfflinePlugin.ts
+++ b/src/plugins/offline/azureOfflinePlugin.ts
@@ -30,7 +30,13 @@ export class AzureOfflinePlugin extends AzureBasePlugin {
             usage: "Start Azure Function app - assumes offline build has already occurred",
             lifecycleEvents: [
               "start"
-            ]
+            ],
+            options: {
+              nocleanup: {
+                usage: "Do not clean up offline files after finishing process",
+                shortcut: "n",
+              }
+            }
           },
           build: {
             usage: "Build necessary files for running Azure Function App offline",
@@ -44,6 +50,12 @@ export class AzureOfflinePlugin extends AzureBasePlugin {
               "cleanup"
             ]
           }
+        },
+        options: {
+          nocleanup: {
+            usage: "Do not clean up offline files after finishing process",
+            shortcut: "n",
+          }
         }
       }
     }
diff --git a/src/services/apimService.ts b/src/services/apimService.ts
index 9257b13..f66a722 100644
--- a/src/services/apimService.ts
+++ b/src/services/apimService.ts
@@ -216,7 +216,9 @@ export class ApimService extends BaseService {
         responses: options.operation.responses || [],
       };
 
-      const operationUrl = `${service.gatewayUrl}/${api.path}${operationConfig.urlTemplate}`;
+      // Ensure a single path seperator in the operation path
+      const operationPath = `/${api.path}/${operationConfig.urlTemplate}`.replace(/\/+/g, "/");
+      const operationUrl = `${service.gatewayUrl}${operationPath}`;
       this.log(`--> Deploying API operation ${options.function}: ${operationConfig.method.toUpperCase()} ${operationUrl}`);
 
       const operation = await client.apiOperation.createOrUpdate(
diff --git a/src/services/baseService.ts b/src/services/baseService.ts
index cbf5acb..d516e69 100644
--- a/src/services/baseService.ts
+++ b/src/services/baseService.ts
@@ -129,15 +129,16 @@ export abstract class BaseService {
   /**
    * Get the access token from credentials token cache
    */
-  protected getAccessToken(): string {
-    return (this.credentials.tokenCache as any)._entries[0].accessToken;
+  protected async getAccessToken(): Promise<string> {
+    const token = await this.credentials.getToken();
+    return token ? token.accessToken : null;
   }
 
   /**
    * Sends an API request using axios HTTP library
    * @param method The HTTP method
    * @param relativeUrl The relative url
-   * @param options Additional HTTP options including headers, etc
+   * @param options Additional HTTP options including headers, etc.
    */
   protected async sendApiRequest(
     method: string,
@@ -145,7 +146,7 @@ export abstract class BaseService {
     options: any = {}
   ) {
     const defaultHeaders = {
-      Authorization: `Bearer ${this.getAccessToken()}`
+      Authorization: `Bearer ${await this.getAccessToken()}`
     };
 
     const allHeaders = {
diff --git a/src/services/functionAppService.ts b/src/services/functionAppService.ts
index b0eb5cd..61357fa 100644
--- a/src/services/functionAppService.ts
+++ b/src/services/functionAppService.ts
@@ -10,8 +10,11 @@ import { Guard } from "../shared/guard";
 import { ArmService } from "./armService";
 import { AzureBlobStorageService } from "./azureBlobStorageService";
 import { BaseService } from "./baseService";
+import { Utils } from "../shared/utils";
 
 export class FunctionAppService extends BaseService {
+  private static readonly retryCount: number = 10;
+  private static readonly retryInterval: number = 5000;
   private webClient: WebSiteManagementClient;
   private blobService: AzureBlobStorageService;
 
@@ -90,27 +93,53 @@ export class FunctionAppService extends BaseService {
     Guard.null(functionApp);
 
     const getTokenUrl = `${this.baseUrl}${functionApp.id}/functions?api-version=2016-08-01`;
-    const response = await this.sendApiRequest("GET", getTokenUrl);
+    try {
+      const response = await Utils.runWithRetry(async () => {
+        const listFunctionsResponse = await this.sendApiRequest("GET", getTokenUrl);
 
-    if (response.status !== 200) {
-      return [];
-    }
+        if (listFunctionsResponse.status !== 200 || listFunctionsResponse.data.value.length === 0) {
+          this.log("-> Function App not ready. Retrying...");
+          throw new Error(listFunctionsResponse.data);
+        }
+
+        return listFunctionsResponse;
+      }, FunctionAppService.retryCount, FunctionAppService.retryInterval);
 
-    return response.data.value.map((functionConfig) => functionConfig.properties);
+      return response.data.value.map((functionConfig) => functionConfig.properties);
+    }
+    catch (e) {
+      this.log("-> Unable to retrieve function app list");
+      throw e;
+    }
   }
 
+  /**
+   * Gets the configuration of the specified function within the function app
+   * @param functionApp The parent function app
+   * @param functionName The name of hte function
+   */
   public async getFunction(functionApp: Site, functionName: string): Promise<FunctionEnvelope> {
     Guard.null(functionApp);
     Guard.empty(functionName);
 
     const getFunctionUrl = `${this.baseUrl}${functionApp.id}/functions/${functionName}?api-version=2016-08-01`;
-    const response = await this.sendApiRequest("GET", getFunctionUrl);
 
-    if (response.status !== 200) {
+    try {
+      const response = await Utils.runWithRetry(async () => {
+        const getFunctionResponse = await this.sendApiRequest("GET", getFunctionUrl);
+
+        if (getFunctionResponse.status !== 200) {
+          this.log("-> Function app not ready. Retrying...")
+          throw new Error(response.data);
+        }
+
+        return getFunctionResponse;
+      }, FunctionAppService.retryCount, FunctionAppService.retryInterval);
+
+      return response.data.properties;
+    } catch (e) {
       return null;
     }
-
-    return response.data.properties;
   }
 
   public async uploadFunctions(functionApp: Site): Promise<any> {
@@ -122,6 +151,23 @@ export class FunctionAppService extends BaseService {
     const uploadFunctionApp = this.uploadZippedArfifactToFunctionApp(functionApp, functionZipFile);
     const uploadBlobStorage = this.uploadZippedArtifactToBlobStorage(functionZipFile);
     await Promise.all([uploadFunctionApp, uploadBlobStorage]);
+
+
+    this.log("Deployed serverless functions:")
+    const serverlessFunctions = this.serverless.service.getAllFunctions();
+    const deployedFunctions = await this.listFunctions(functionApp);
+
+    // List functions that are part of the serverless yaml config
+    deployedFunctions.forEach((functionConfig) => {
+      if (serverlessFunctions.includes(functionConfig.name)) {
+        const httpConfig = this.getFunctionHttpTriggerConfig(functionApp, functionConfig);
+
+        if (httpConfig) {
+          const method = httpConfig.methods[0].toUpperCase();
+          this.log(`-> ${functionConfig.name}: ${method} ${httpConfig.url}`);
+        }
+      }
+    });
   }
 
   /**
@@ -159,30 +205,14 @@ export class FunctionAppService extends BaseService {
       uri: `https://${scmDomain}/api/zipdeploy/`,
       json: true,
       headers: {
-        Authorization: `Bearer ${this.getAccessToken()}`,
+        Authorization: `Bearer ${await this.getAccessToken()}`,
         Accept: "*/*",
         ContentType: "application/octet-stream",
       }
     };
 
     await this.sendFile(requestOptions, functionZipFile);
-
     this.log("-> Function package uploaded successfully");
-    const serverlessFunctions = this.serverless.service.getAllFunctions();
-    const deployedFunctions = await this.listFunctions(functionApp);
-
-    this.log("Deployed serverless functions:")
-    deployedFunctions.forEach((functionConfig) => {
-      // List functions that are part of the serverless yaml config
-      if (serverlessFunctions.includes(functionConfig.name)) {
-        const httpConfig = this.getFunctionHttpTriggerConfig(functionApp, functionConfig);
-
-        if (httpConfig) {
-          const method = httpConfig.methods[0].toUpperCase();
-          this.log(`-> ${functionConfig.name}: ${method} ${httpConfig.url}`);
-        }
-      }
-    });
   }
 
   /**
diff --git a/src/services/loginService.ts b/src/services/loginService.ts
index 94d00b9..6bf433f 100644
--- a/src/services/loginService.ts
+++ b/src/services/loginService.ts
@@ -5,15 +5,16 @@ import {
   loginWithServicePrincipalSecretWithAuthResponse,
   AuthResponse,
   AzureTokenCredentialsOptions,
-  InteractiveLoginOptions,  
+  InteractiveLoginOptions,
+  DeviceTokenCredentials,  
 } from "@azure/ms-rest-nodeauth";
+import { SimpleFileTokenCache } from "../plugins/login/utils/simpleFileTokenCache";
 
 export interface AzureLoginOptions extends Serverless.Options {
   subscriptionId?: string;
 }
 
 export class AzureLoginService {
-
   /**
    * Logs in via service principal login if environment variables are
    * set or via interactive login if environment variables are not set
@@ -32,9 +33,20 @@ export class AzureLoginService {
     }
   }
 
-  public static async interactiveLogin(options: InteractiveLoginOptions): Promise<AuthResponse> {
-    await open("https://microsoft.com/devicelogin");
-    return await interactiveLoginWithAuthResponse(options);
+  public static async interactiveLogin(options?: InteractiveLoginOptions): Promise<AuthResponse> {
+    let authResp: AuthResponse = {credentials: undefined, subscriptions: []};
+    const fileTokenCache = new SimpleFileTokenCache();
+    if(fileTokenCache.isEmpty()){
+      await open("https://microsoft.com/devicelogin");
+      authResp = await interactiveLoginWithAuthResponse({...options, tokenCache: fileTokenCache});
+      fileTokenCache.addSubs(authResp.subscriptions);
+    } else {
+      authResp.credentials = new DeviceTokenCredentials(undefined, undefined, fileTokenCache.first().userId, undefined, undefined, fileTokenCache);
+      authResp.subscriptions = fileTokenCache.listSubscriptions();
+    }
+
+    return authResp;
+
   }
 
   public static async servicePrincipalLogin(clientId: string, secret: string, tenantId: string, options: AzureTokenCredentialsOptions): Promise<AuthResponse> {
diff --git a/src/services/namingService.ts b/src/services/namingService.ts
index 10c3755..8d7df13 100644
--- a/src/services/namingService.ts
+++ b/src/services/namingService.ts
@@ -1,6 +1,5 @@
 import { ServerlessAzureConfig, ResourceConfig } from "../models/serverless"
 import { Guard } from "../shared/guard"
-import md5 from "md5";
 import configConstants from "../config";
 
 export class AzureNamingService {
@@ -11,23 +10,26 @@ export class AzureNamingService {
    * {prefix}-{shortRegionName}-{shortStageName}(optionally: -{suffix})
    *
    * @param config Serverless Azure Config for service (serverless.service)
-   * @param resourceConfig
-   * @param suffix
+   * @param resourceConfig The serverless resource configuration
+   * @param suffix Optional suffix to append on the end of the generated name
    */
   public static getResourceName(config: ServerlessAzureConfig, resourceConfig?: ResourceConfig, suffix?: string) {
     if (resourceConfig && resourceConfig.name) {
       return resourceConfig.name;
     }
+
     const { prefix, region, stage } = config.provider
     let name = [
       prefix,
       this.createShortAzureRegionName(region),
       this.createShortStageName(stage),
     ].join("-");
+
     if (suffix) {
       name += `-${suffix}`;
     }
-    return name.toLocaleLowerCase();
+
+    return name.toLowerCase();
   }
 
   /**
@@ -38,43 +40,53 @@ export class AzureNamingService {
    *
    * @param config Serverless Azure Config for service (serverless.service)
    * @param maxLength Maximum length of name for resource
-   * @param resourceConfig Configuration for resource from serverless configuration
+   * @param resourceConfig The serverless resource configuration
+   * @param suffix Optional suffix to append on the end of the generated name
    * @param forbidden Regex for characters to remove from name. Defaults to non-alpha-numerics
    * @param replaceWith String to replace forbidden characters. Defaults to empty string
    */
-  public static getSafeResourceName(config: ServerlessAzureConfig, maxLength: number, resourceConfig?: ResourceConfig, forbidden = /\W+/g, replaceWith = "") {
+  public static getSafeResourceName(config: ServerlessAzureConfig, maxLength: number, resourceConfig?: ResourceConfig, suffix: string = "", forbidden: RegExp = /\W+/g, replaceWith: string = "") {
     if (resourceConfig && resourceConfig.name) {
       const { name } = resourceConfig;
+
       if (name.length > maxLength) {
         throw new Error(`Name '${name}' invalid. Should be shorter than ${maxLength} characters`);
       }
+
       return name.replace(forbidden, replaceWith);
     }
 
     const { prefix, region, stage } = config.provider;
 
-    const nameHash = md5(config.service);
-
     let safePrefix = prefix.replace(forbidden, replaceWith);
     const safeRegion = this.createShortAzureRegionName(region);
     let safeStage = this.createShortStageName(stage);
-    let safeNameHash = nameHash.substr(0, 6);
+    let safeSuffix = suffix.replace(forbidden, replaceWith);
 
-    const remaining = maxLength - (safePrefix.length + safeRegion.length + safeStage.length + safeNameHash.length);
+    const remaining = maxLength - (safePrefix.length + safeRegion.length + safeStage.length + safeSuffix.length);
 
     // Dynamically adjust the substring based on space needed
     if (remaining < 0) {
-      const partLength = Math.floor(Math.abs(remaining) / 3);
+      let partLength = Math.floor(Math.abs(remaining) / 4);
+      if (partLength < 3) {
+        partLength = 3;
+      }
+
       safePrefix = safePrefix.substr(0, partLength);
       safeStage = safeStage.substr(0, partLength);
-      safeNameHash = safeNameHash.substr(0, partLength);
+      safeSuffix = safeSuffix.substr(0, partLength);
     }
 
-    return [safePrefix, safeRegion, safeStage, safeNameHash]
+    return [safePrefix, safeRegion, safeStage, safeSuffix]
       .join("")
-      .toLocaleLowerCase();
+      .toLowerCase();
   }
 
+  /**
+   * Creates a deployment name from the serverless configuration
+   * @param config The serverless azure config
+   * @param timestamp The timestamp of the deployment
+   */
   public static getDeploymentName(config: ServerlessAzureConfig, timestamp?: string) {
     let maxLength = configConstants.naming.maxLength.deploymentName;
     const suffix = configConstants.naming.suffix.deployment;
@@ -82,11 +94,13 @@ export class AzureNamingService {
     const { deploymentName } = config.provider
 
     if (timestamp) {
-      maxLength -= timestamp.length + suffix.length;;
+      maxLength -= timestamp.length + suffix.length;
+
+      const name = (deploymentName)
+        ? deploymentName.substr(0, maxLength)
+        : [AzureNamingService.getSafeResourceName(config, maxLength, null, config.service), suffix].join("-");
 
-      const name = (deploymentName) ? deploymentName.substr(0, maxLength)
-        : [ AzureNamingService.getSafeResourceName(config, maxLength), suffix ].join("-");
-      return [ name, timestamp ].join("-");
+      return [name, timestamp].join("-");
     }
 
     return deploymentName.substr(0, maxLength);
diff --git a/src/services/offlineService.ts b/src/services/offlineService.ts
index 40ac855..7288c6c 100644
--- a/src/services/offlineService.ts
+++ b/src/services/offlineService.ts
@@ -1,4 +1,4 @@
-import { spawn } from "child_process";
+import { spawn, SpawnOptions } from "child_process";
 import fs from "fs";
 import Serverless from "serverless";
 import configConstants from "../config";
@@ -29,7 +29,7 @@ export class OfflineService extends BaseService {
     await this.packageService.createBindings();
     const filenames = Object.keys(this.localFiles);
     for (const filename of filenames) {
-      if (!fs.existsSync(filename)){
+      if (!fs.existsSync(filename)) {
         fs.writeFileSync(
           filename,
           this.localFiles[filename]
@@ -44,7 +44,7 @@ export class OfflineService extends BaseService {
     await this.packageService.cleanUp();
     const filenames = Object.keys(this.localFiles);
     for (const filename of filenames) {
-      if (fs.existsSync(filename)){
+      if (fs.existsSync(filename)) {
         this.log(`Removing file '${filename}'`);
         fs.unlinkSync(filename)
       }
@@ -62,59 +62,45 @@ export class OfflineService extends BaseService {
   /**
    * Spawn a Node child process with predefined environment variables
    * @param command CLI Command - NO ARGS
-   * @param args Array of arguments for CLI command
-   * @param env Additional environment variables to be set in addition to
-   * predefined variables in `serverless.yml`
+   * @param spawnArgs Array of arguments for CLI command
    */
-  private spawn(command: string, args?: string[], env?: any): Promise<void> {
-    env = {
-      ...this.serverless.service.provider["environment"],
-      ...env
+  private spawn(command: string, spawnArgs?: string[]): Promise<void> {
+    if (process.platform === "win32") {
+      command += ".cmd";
     }
-    this.log(`Spawning process '${command} ${args.join(" ")}'`);
-    return new Promise((resolve, reject) => {
-      const childProcess = spawn(command, args, {env});
-
-      childProcess.stdout.on("data", (data) => {
-        this.log(data, {
-          color: configConstants.funcConsoleColor,
-        }, command);
-      });
 
-      childProcess.stderr.on("data", (data) => {
-        this.log(data, {
-          color: "red",
-        }, command);
-      })
-
-      childProcess.on("message", (message) => {
-        this.log(message, {
-          color: configConstants.funcConsoleColor,
-        }, command);
-      });
+    const env = {
+      // Inherit environment from current process, most importantly, the PATH
+      ...process.env,
+      // Environment variables from serverless config are king
+      ...this.serverless.service.provider["environment"],
+    }
+    this.log(`Spawning process '${command} ${spawnArgs.join(" ")}'`);
+    return new Promise(async (resolve, reject) => {
+      const spawnOptions: SpawnOptions = { env, stdio: "inherit" };
+      const childProcess = spawn(command, spawnArgs, spawnOptions);
 
-      childProcess.on("error", (err) => {
-        this.log(`${err}`, {
-          color: "red"
-        }, command);
-        reject(err);
+      process.on("SIGINT", async () => {
+        try {
+          if (this.getOption("nocleanup")) {
+            this.log("Skipping offline file cleanup...");
+          } else {
+            await this.cleanup();
+          }
+        } catch {
+          // Swallowing `scandir` error that gets thrown after
+          // trying to remove the same directory twice
+        } finally {
+          process.exit();
+        }
       });
 
       childProcess.on("exit", (code) => {
-        this.log(`Exited with code: ${code}`, {
-          color: (code === 0) ? "green" : "red",
-        }, command);
-      });
-
-      childProcess.on("close", (code) => {
-        this.log(`Closed with code: ${code}`, {
-          color: (code === 0) ? "green" : "red",
-        }, command);
-        resolve();
-      });
-
-      childProcess.on("disconnect", () => {
-        this.log("Process disconnected");
+        if (code === 0) {
+          resolve();
+        } else {
+          reject();
+        }
       });
     });
   }
diff --git a/src/shared/bindings.json b/src/shared/bindings.json
index 57b7847..b9d17ce 100644
--- a/src/shared/bindings.json
+++ b/src/shared/bindings.json
@@ -78,7 +78,7 @@
                     ]
                 },
                 {
-                    "name": "path",
+                    "name": "eventHubName",
                     "value": "string",
                     "defaultValue": "myeventhub",
                     "required": true,
@@ -900,7 +900,7 @@
                     "name": "queueName",
                     "value": "string",
                     "defaultValue": "mysbqueue",
-                    "required": true,
+                    "required": false,
                     "label": "$serviceBusTrigger_queueName_label",
                     "help": "$serviceBusTrigger_queueName_help",
                     "validators": [
@@ -914,7 +914,7 @@
                     "name": "topicName",
                     "value": "string",
                     "defaultValue": "mysbtopic",
-                    "required": true,
+                    "required": false,
                     "label": "$serviceBusTrigger_topicName_label",
                     "help": "$serviceBusTrigger_topicName_help",
                     "validators": [
@@ -928,7 +928,7 @@
                     "name": "subscriptionName",
                     "value": "string",
                     "defaultValue": "mysubscription",
-                    "required": true,
+                    "required": false,
                     "label": "$serviceBusTrigger_subscriptionName_label",
                     "help": "$serviceBusTrigger_subscriptionName_help",
                     "validators": [
diff --git a/src/shared/utils.ts b/src/shared/utils.ts
index bf3717f..3d091fc 100644
--- a/src/shared/utils.ts
+++ b/src/shared/utils.ts
@@ -160,4 +160,39 @@ export class Utils {
       return settings && settings.direction === "out";
     });
   }
+
+  /**
+   * Runs an operation with auto retry policy
+   * @param operation The operation to run
+   * @param maxRetries The max number or retreis
+   * @param retryWaitInterval The time to wait between retries
+   */
+  public static async runWithRetry<T>(operation: (retry?: number) => Promise<T>, maxRetries: number = 3, retryWaitInterval: number = 1000) {
+    let retry = 0;
+    let error = null;
+
+    while (retry < maxRetries) {
+      try {
+        retry++;
+        return await operation(retry);
+      }
+      catch (e) {
+        error = e;
+      }
+
+      await Utils.wait(retryWaitInterval);
+    }
+
+    return Promise.reject(error);
+  }
+
+  /**
+   * Waits for the specified amount of time.
+   * @param time The amount of time to wait (default = 1000ms)
+   */
+  public static wait(time: number = 1000) {
+    return new Promise((resolve) => {
+      setTimeout(resolve, time);
+    });
+  }
 }


Test patch:
diff --git a/src/armTemplates/resources/storageAccount.test.ts b/src/armTemplates/resources/storageAccount.test.ts
index b398cda..5d20643 100644
--- a/src/armTemplates/resources/storageAccount.test.ts
+++ b/src/armTemplates/resources/storageAccount.test.ts
@@ -1,4 +1,3 @@
-import md5 from "md5";
 import { ServerlessAzureConfig } from "../../models/serverless";
 import { AzureNamingService } from "../../services/namingService";
 import { StorageAccountResource } from "./storageAccount";
@@ -35,8 +34,7 @@ describe("Storage Account Resource", () => {
         prefix: "my-long-test-prefix-name",
         region: "Australia Southeast",
         stage: "development"
-      },
-      service: "my-long-test-api",
+      }
     };
 
     const result = StorageAccountResource.getResourceName(testConfig);
@@ -145,10 +143,9 @@ describe("Storage Account Resource", () => {
     expect(value).toContain(AzureNamingService.createShortAzureRegionName(config.provider.region));
     expect(value).toContain(createSafeString(config.provider.prefix));
     expect(value).toContain(createSafeString(config.provider.stage));
-    expect(value).toContain(md5(config.service).substr(0, 3));
   }
 
   function createSafeString(value: string) {
-    return value.replace(/\W+/g, "").toLocaleLowerCase().substr(0, 3);
+    return value.replace(/\W+/g, "").toLowerCase().substr(0, 3);
   };
 });
diff --git a/src/plugins/login/azureLoginPlugin.test.ts b/src/plugins/login/azureLoginPlugin.test.ts
index 9e71595..51e43b3 100644
--- a/src/plugins/login/azureLoginPlugin.test.ts
+++ b/src/plugins/login/azureLoginPlugin.test.ts
@@ -68,7 +68,7 @@ describe("Login Plugin", () => {
       undefined // would be options
     )
     expect(AzureLoginService.interactiveLogin).not.toBeCalled();
-    expect(sls.variables["azureCredentials"]).toEqual(credentials);
+    expect(JSON.stringify(sls.variables["azureCredentials"])).toEqual(JSON.stringify(credentials));
     expect(sls.variables["subscriptionId"]).toEqual("azureSubId");
   });
 
@@ -78,7 +78,7 @@ describe("Login Plugin", () => {
     await invokeLoginHook(false, sls);
     expect(AzureLoginService.servicePrincipalLogin).not.toBeCalled();
     expect(AzureLoginService.interactiveLogin).toBeCalled();
-    expect(sls.variables["azureCredentials"]).toEqual(credentials);
+    expect(JSON.stringify(sls.variables["azureCredentials"])).toEqual(JSON.stringify(credentials));
     expect(sls.variables["subscriptionId"]).toEqual("azureSubId");
   });
 
diff --git a/src/plugins/login/utils/simpleFileTokenCache.test.ts b/src/plugins/login/utils/simpleFileTokenCache.test.ts
new file mode 100644
index 0000000..09919d9
--- /dev/null
+++ b/src/plugins/login/utils/simpleFileTokenCache.test.ts
@@ -0,0 +1,84 @@
+import fs from "fs";
+import mockFs from "mock-fs";
+import { MockFactory } from "../../../test/mockFactory";
+import { SimpleFileTokenCache } from "./simpleFileTokenCache";
+
+describe("Simple File Token Cache", () => {
+  const tokenFilePath = "slsTokenCache.json";
+
+  let fileContent = {
+    entries: [],
+    subscriptions: [],
+  };
+
+  afterEach(() => {
+    mockFs.restore();
+  });
+
+  it("Creates a load file on creation if none", () => {
+    mockFs();
+
+    const writeFileSpy = jest.spyOn(fs, "writeFileSync");
+    new SimpleFileTokenCache(tokenFilePath);
+
+    const expected = {
+      entries: [],
+      subscriptions: [],
+    };
+
+    expect(writeFileSpy).toBeCalledWith(tokenFilePath, JSON.stringify(expected));
+    writeFileSpy.mockRestore();
+  });
+
+  it("Load file on creation if available", () => {
+    fileContent.entries = MockFactory.createTestTokenCacheEntries();
+
+    mockFs({
+      "slsTokenCache.json": JSON.stringify(fileContent),
+    });
+
+    const readFileSpy = jest.spyOn(fs, "readFileSync");
+    const tokenCache = new SimpleFileTokenCache(tokenFilePath);
+
+    expect(readFileSpy).toBeCalled();
+    expect(tokenCache.first()).not.toBeNull();
+    readFileSpy.mockRestore();
+  })
+
+  it("Saves to file after token is added", () => {
+    mockFs();
+
+    const writeFileSpy = jest.spyOn(fs, "writeFileSync");
+    const tokenCache = new SimpleFileTokenCache(tokenFilePath);
+    const testEntries = MockFactory.createTestTokenCacheEntries();
+
+    tokenCache.add(testEntries);
+
+    const expected = {
+      entries: testEntries,
+      subscriptions: [],
+    };
+
+    expect(tokenCache.isEmpty()).toBe(false);
+    expect(writeFileSpy).toBeCalledWith(tokenFilePath, JSON.stringify(expected));
+    writeFileSpy.mockRestore();
+  });
+
+  it("Saves to file after subscription is added", () => {
+    mockFs();
+
+    const writeFileSpy = jest.spyOn(fs, "writeFileSync");
+    const testFileCache = new SimpleFileTokenCache(tokenFilePath);
+    const testSubs = MockFactory.createTestSubscriptions();
+
+    testFileCache.addSubs(testSubs);
+
+    const expected = {
+      entries: [],
+      subscriptions: testSubs,
+    };
+
+    expect(writeFileSpy).toBeCalledWith(tokenFilePath, JSON.stringify(expected));
+    writeFileSpy.mockRestore();
+  });
+});
diff --git a/src/services/functionAppService.test.ts b/src/services/functionAppService.test.ts
index 9de1cf7..715819a 100644
--- a/src/services/functionAppService.test.ts
+++ b/src/services/functionAppService.test.ts
@@ -211,7 +211,7 @@ describe("Function App Service", () => {
       uri: expectedUploadUrl,
       json: true,
       headers: {
-        Authorization: `Bearer ${variables["azureCredentials"].tokenCache._entries[0].accessToken}`,
+        Authorization: `Bearer ${(await variables["azureCredentials"].getToken()).accessToken}`,
         Accept: "*/*",
         ContentType: "application/octet-stream",
       }
@@ -242,7 +242,7 @@ describe("Function App Service", () => {
       uri: expectedUploadUrl,
       json: true,
       headers: {
-        Authorization: `Bearer ${variables["azureCredentials"].tokenCache._entries[0].accessToken}`,
+        Authorization: `Bearer ${(await variables["azureCredentials"].getToken()).accessToken}`,
         Accept: "*/*",
         ContentType: "application/octet-stream",
       }
@@ -276,7 +276,7 @@ describe("Function App Service", () => {
       uri: expectedUploadUrl,
       json: true,
       headers: {
-        Authorization: `Bearer ${variables["azureCredentials"].tokenCache._entries[0].accessToken}`,
+        Authorization: `Bearer ${(await variables["azureCredentials"].getToken()).accessToken}`,
         Accept: "*/*",
         ContentType: "application/octet-stream",
       }
diff --git a/src/services/loginService.test.ts b/src/services/loginService.test.ts
index a43e511..941b4fa 100644
--- a/src/services/loginService.test.ts
+++ b/src/services/loginService.test.ts
@@ -2,21 +2,50 @@ import { AzureLoginService } from "./loginService";
 jest.mock("open");
 import open from "open";
 
-jest.mock("@azure/ms-rest-nodeauth")
-import { interactiveLoginWithAuthResponse, loginWithServicePrincipalSecretWithAuthResponse } from "@azure/ms-rest-nodeauth";
+jest.mock("@azure/ms-rest-nodeauth");
+import * as nodeauth from "@azure/ms-rest-nodeauth";
+
+jest.mock("../plugins/login/utils/simpleFileTokenCache");
+import { SimpleFileTokenCache } from "../plugins/login/utils/simpleFileTokenCache";
 
 describe("Login Service", () => {
-  
-  it("logs in interactively", async () => {
+
+  it("logs in interactively with no cached login", async () => {
     // Ensure env variables are not set
     delete process.env.azureSubId;
     delete process.env.azureServicePrincipalClientId;
     delete process.env.azureServicePrincipalPassword;
     delete process.env.azureServicePrincipalTenantId;
 
+    SimpleFileTokenCache.prototype.isEmpty = jest.fn(() => true);
+
+    const emptyObj = { subscriptions: [] };
+    Object.defineProperty(nodeauth,
+      "interactiveLoginWithAuthResponse",
+      { value: jest.fn(_obj => emptyObj) }
+    );
+
+    await AzureLoginService.login();
+    expect(SimpleFileTokenCache).toBeCalled();
+    expect(open).toBeCalledWith("https://microsoft.com/devicelogin");
+    expect(nodeauth.interactiveLoginWithAuthResponse).toBeCalled();
+    expect(SimpleFileTokenCache.prototype.addSubs).toBeCalledWith(emptyObj.subscriptions);
+  });
+
+  it("logs in with a cached login", async () => {
+    // Ensure env variables are not set
+    delete process.env.azureSubId;
+    delete process.env.azureServicePrincipalClientId;
+    delete process.env.azureServicePrincipalPassword;
+    delete process.env.azureServicePrincipalTenantId;
+
+    SimpleFileTokenCache.prototype.isEmpty = jest.fn(() => false);
+    SimpleFileTokenCache.prototype.first = jest.fn(() => ({ userId: "" }));
+
     await AzureLoginService.login();
-    expect(open).toBeCalledWith("https://microsoft.com/devicelogin")
-    expect(interactiveLoginWithAuthResponse).toBeCalled();
+    expect(SimpleFileTokenCache).toBeCalled();
+    expect(nodeauth.DeviceTokenCredentials).toBeCalled();
+    expect(SimpleFileTokenCache.prototype.listSubscriptions).toBeCalled();
   });
 
   it("logs in with a service principal", async () => {
@@ -27,11 +56,11 @@ describe("Login Service", () => {
     process.env.azureServicePrincipalTenantId = "azureServicePrincipalTenantId";
 
     await AzureLoginService.login();
-    expect(loginWithServicePrincipalSecretWithAuthResponse).toBeCalledWith(
+    expect(nodeauth.loginWithServicePrincipalSecretWithAuthResponse).toBeCalledWith(
       "azureServicePrincipalClientId",
       "azureServicePrincipalPassword",
       "azureServicePrincipalTenantId",
       undefined // would be options
     );
   });
-});
\ No newline at end of file
+});
diff --git a/src/services/namingService.test.ts b/src/services/namingService.test.ts
index 33749c4..d58fbc1 100644
--- a/src/services/namingService.test.ts
+++ b/src/services/namingService.test.ts
@@ -1,7 +1,7 @@
 import { AzureNamingService } from "./namingService"
+import { ServerlessAzureConfig } from "../models/serverless";
 
 describe("Naming Service", () => {
-
   it("Creates a short name for an azure region", () => {
     const expected = "ausse";
     const actual = AzureNamingService.createShortAzureRegionName("australiasoutheast");
@@ -86,4 +86,57 @@ describe("Naming Service", () => {
     const actual = AzureNamingService.getNormalizedRegionName(expected);
     expect(actual).toEqual(expected);
   });
+
+  it("deployment name is generated correctly", () => {
+    const config: ServerlessAzureConfig = {
+      functions: [],
+      plugins: [],
+      provider: {
+        prefix: "sls",
+        name: "azure",
+        region: "westus",
+        stage: "dev",
+      },
+      service: "test-api"
+    };
+
+    const timestamp = Date.now();
+    const deploymentName = AzureNamingService.getDeploymentName(config, `t${timestamp}`);
+
+    expect(deploymentName).toEqual(`slswusdevtestapi-DEPLOYMENT-t${timestamp}`);
+    assertValidDeploymentName(config, deploymentName, timestamp);
+  });
+
+  it("deployment name with long suffix or service name generated correctly", () => {
+    const config: ServerlessAzureConfig = {
+      functions: [],
+      plugins: [],
+      provider: {
+        prefix: "sls-long-prefix-name",
+        name: "azure",
+        region: "westus",
+        stage: "multicloud",
+      },
+      service: "extra-long-service-name"
+    };
+
+    const timestamp = Date.now();
+    const deploymentName = AzureNamingService.getDeploymentName(config, `t${timestamp}`);
+
+    expect(deploymentName).toEqual(`slswusmulext-DEPLOYMENT-t${timestamp}`);
+    assertValidDeploymentName(config, deploymentName, timestamp);
+  });
+
+  function assertValidDeploymentName(config: ServerlessAzureConfig, value: string, timestamp: number) {
+    expect(value.length).toBeLessThanOrEqual(64);
+    expect(value).toContain(timestamp);
+    expect(value).toContain(AzureNamingService.createShortAzureRegionName(config.provider.region));
+    expect(value).toContain(createSafeString(config.provider.prefix));
+    expect(value).toContain(createSafeString(config.provider.stage));
+    expect(value).toContain(createSafeString(config.service));
+  }
+
+  function createSafeString(value: string) {
+    return value.replace(/\W+/g, "").toLowerCase().substr(0, 3);
+  };
 });
diff --git a/src/services/offlineService.test.ts b/src/services/offlineService.test.ts
index c8fc4f4..e9b1427 100644
--- a/src/services/offlineService.test.ts
+++ b/src/services/offlineService.test.ts
@@ -7,26 +7,28 @@ import { MockFactory } from "../test/mockFactory";
 import { OfflineService } from "./offlineService";
 
 describe("Offline Service", () => {
+  let mySpawn;
 
-  const mySpawn = mockSpawn();
-  require("child_process").spawn = mySpawn;
-  mySpawn.setDefault(mySpawn.simple(0, "Exit code"));
-
-  function createService(sls?: Serverless): OfflineService {
+  function createService(sls?: Serverless, options?: any): OfflineService {
     return new OfflineService(
       sls || MockFactory.createTestServerless(),
-      MockFactory.createTestServerlessOptions(),
+      MockFactory.createTestServerlessOptions(options),
     )
   }
 
   beforeEach(() => {
     // Mocking the file system so that files are not created in project directory
-    mockFs({})
+    mockFs({});
+
+    mySpawn = mockSpawn();
+    require("child_process").spawn = mySpawn;
+    mySpawn.setDefault(mySpawn.simple(0, "Exit code"));
   });
 
   afterEach(() => {
     mockFs.restore();
-  })
+    jest.resetAllMocks();
+  });
 
   it("builds required files for offline execution", async () => {
     const sls = MockFactory.createTestServerless();
@@ -113,7 +115,12 @@ describe("Offline Service", () => {
     rmdirSpy.mockRestore();
   });
 
-  it("instructs users how to run locally", async () => {
+  it("calls func host start on Mac OS", async () => {
+    Object.defineProperty(process, "platform", {
+      value: "darwin",
+      writable: true,
+    });
+
     const sls = MockFactory.createTestServerless();
     const service = createService(sls);
     await service.start();
@@ -123,4 +130,130 @@ describe("Offline Service", () => {
     expect(call.command).toEqual("func");
     expect(call.args).toEqual(["host", "start"]);
   });
+
+  it("calls func host start on windows", async () => {
+    Object.defineProperty(process, "platform", {
+      value: "win32",
+      writable: true,
+    });
+
+    const sls = MockFactory.createTestServerless();
+    const service = createService(sls);
+    await service.start();
+    const calls = mySpawn.calls;
+    expect(calls).toHaveLength(1);
+    const call = calls[0];
+    expect(call.command).toEqual("func.cmd");
+    expect(call.args).toEqual(["host", "start"]);
+  });
+
+  it("cleans up after offline call as default behavior", async () => {
+    mockFs({
+      hello: {
+        "function.json": "contents"
+      },
+      goodbye: {
+        "function.json": "contents"
+      },
+      "local.settings.json": "contents",
+    });
+
+    Object.defineProperty(process, "platform", {
+      value: "win32",
+      writable: true,
+    });
+
+    const sls = MockFactory.createTestServerless();
+    const processOnSpy = jest.spyOn(process, "on");
+    const unlinkSpy = jest.spyOn(fs, "unlinkSync");
+    const rmdirSpy = jest.spyOn(fs, "rmdirSync")
+
+    const service = createService(sls);
+
+    await service.start();
+
+    const calls = mySpawn.calls;
+    expect(calls).toHaveLength(1);
+    const call = calls[0];
+    expect(call.command).toEqual("func.cmd");
+    expect(call.args).toEqual(["host", "start"]);
+
+    const processOnCalls = processOnSpy.mock.calls;
+    expect(processOnCalls).toHaveLength(1);
+    expect(processOnCalls[0][0]).toEqual("SIGINT");
+    expect(processOnCalls[0][1]).toBeInstanceOf(Function);
+
+    // SIGINT handler function
+    const sigintCallback = processOnCalls[0][1] as any;
+
+    process.exit = jest.fn() as any;
+    await sigintCallback();
+
+    /* Offline Cleanup assertions*/
+
+    const unlinkCalls = unlinkSpy.mock.calls;
+
+    expect(unlinkCalls).toHaveLength(3);
+    expect(unlinkCalls[0][0]).toBe(`hello${path.sep}function.json`);
+    expect(unlinkCalls[1][0]).toBe(`goodbye${path.sep}function.json`);
+    expect(unlinkCalls[2][0]).toBe("local.settings.json");
+
+    const rmdirCalls = rmdirSpy.mock.calls;
+
+    expect(rmdirCalls[0][0]).toBe("hello");
+    expect(rmdirCalls[1][0]).toBe("goodbye");
+
+    unlinkSpy.mockRestore();
+    rmdirSpy.mockRestore();
+
+    expect(process.exit).toBeCalledTimes(1);
+  });
+
+  it("does not clean up after offline call if specified in options", async () => {
+
+    Object.defineProperty(process, "platform", {
+      value: "win32",
+      writable: true,
+    });
+
+    const processOnSpy = jest.spyOn(process, "on");
+    const unlinkSpy = jest.spyOn(fs, "unlinkSync");
+    const rmdirSpy = jest.spyOn(fs, "rmdirSync");
+
+    const sls = MockFactory.createTestServerless();
+
+
+    const service = createService(sls, {
+      "nocleanup": ""
+    });
+
+    await service.start();
+
+    const calls = mySpawn.calls;
+    expect(calls).toHaveLength(1);
+    const call = calls[0];
+    expect(call.command).toEqual("func.cmd");
+    expect(call.args).toEqual(["host", "start"]);
+
+    const processOnCalls = processOnSpy.mock.calls;
+    expect(processOnCalls).toHaveLength(1);
+    expect(processOnCalls[0][0]).toEqual("SIGINT");
+    expect(processOnCalls[0][1]).toBeInstanceOf(Function);
+
+    // SIGINT handler function
+    const sigintCallback = processOnCalls[0][1] as any;
+
+    process.exit = jest.fn() as any;
+    await sigintCallback();
+
+    /* Offline Cleanup assertions*/
+
+    expect(unlinkSpy).not.toBeCalled();
+    expect(rmdirSpy).not.toBeCalled();
+
+    unlinkSpy.mockRestore();
+    rmdirSpy.mockRestore();
+
+    expect(process.exit).toBeCalledTimes(1);
+  });
 });
diff --git a/src/services/packageService.test.ts b/src/services/packageService.test.ts
index be57599..9050927 100644
--- a/src/services/packageService.test.ts
+++ b/src/services/packageService.test.ts
@@ -16,6 +16,7 @@ describe("Package Service", () => {
     sls.config.servicePath = process.cwd();
     sls.service["functions"] = {
       hello: MockFactory.createTestAzureFunctionConfig(functionRoute),
+      eventhubHandler: MockFactory.createTestEventHubFunctionConfig(),
     }
     packageService = new PackageService(sls, MockFactory.createTestServerlessOptions());
   });
@@ -119,6 +120,46 @@ describe("Package Service", () => {
     writeFileSpy.mockRestore();
   });
 
+  it("Creates Event Hub Handler bindings and function.json ", async () => {
+    const functionName = "eventhubHandler";
+    const functionMetadata: FunctionMetadata = {
+      entryPoint: "handler",
+      handlerPath: "src/handlers/eventhubHandler",
+      params: {
+        functionsJson: {
+          bindings: [
+            MockFactory.createTestEventHubBinding("in"),
+          ]
+        }
+      },
+    };
+
+    const expectedFolderPath = path.join(sls.config.servicePath, functionName);
+    const expectedFilePath = path.join(expectedFolderPath, "function.json");
+    const expectedFunctionJson = {
+      entryPoint: "handler",
+      scriptFile: "src/handlers/eventhubHandler",
+      bindings: [
+        MockFactory.createTestEventHubBinding("in"),
+      ]
+    }
+
+    mockFs({});
+
+    const mkdirSpy = jest.spyOn(fs, "mkdirSync");
+    const writeFileSpy = jest.spyOn(fs, "writeFileSync");
+
+    await packageService.createBinding(functionName, functionMetadata);
+
+    expect(mkdirSpy).toBeCalledWith(expectedFolderPath);
+    const call = writeFileSpy.mock.calls[0];
+    expect(call[0]).toEqual(expectedFilePath);
+    expect(JSON.parse(call[1])).toEqual(expectedFunctionJson);
+
+    mkdirSpy.mockRestore();
+    writeFileSpy.mockRestore();
+  });
+
   it("createBinding does not need to create directory if function folder already exists", async () => {
     const functionName = "hello";
     const functionMetadata: FunctionMetadata = {
diff --git a/src/shared/utils.test.ts b/src/shared/utils.test.ts
index 668d6e8..517c60b 100644
--- a/src/shared/utils.test.ts
+++ b/src/shared/utils.test.ts
@@ -105,4 +105,87 @@ describe("utils", () => {
       }
     );
   });
+
+  describe("runWithRetry", () => {
+    it("returns values after 1st run", async () => {
+      const expected = "success";
+      let lastRetry = 0;
+
+      const result = await Utils.runWithRetry((retry) => {
+        lastRetry = retry;
+        return Promise.resolve(expected);
+      });
+
+      expect(lastRetry).toEqual(1);
+      expect(result).toEqual(expected);
+    });
+
+    it("returns values after successfully retry (reject promise)", async () => {
+      const expected = "success";
+      let lastRetry = 0;
+
+      const result = await Utils.runWithRetry((retry) => {
+        lastRetry = retry;
+        if (retry === 1) {
+          return Promise.reject("rejected");
+        }
+
+        return Promise.resolve(expected);
+      });
+
+      expect(lastRetry).toEqual(2);
+      expect(result).toEqual(expected);
+    });
+
+    it("returns values after successfully retry (throw error)", async () => {
+      const expected = "success";
+      let lastRetry = 0;
+
+      const result = await Utils.runWithRetry((retry) => {
+        lastRetry = retry;
+        if (retry === 1) {
+          throw new Error("Ooops!")
+        }
+
+        return Promise.resolve(expected);
+      });
+
+      expect(lastRetry).toEqual(2);
+      expect(result).toEqual(expected);
+    });
+    it("throws error after reties", async () => {
+      const maxRetries = 5;
+      let lastRetry = 0;
+
+      const test = async () => {
+        await Utils.runWithRetry((retry) => {
+          lastRetry = retry;
+          return Promise.reject("rejected");
+        }, maxRetries, 100);
+      };
+
+      await expect(test()).rejects.toEqual("rejected");
+      expect(lastRetry).toEqual(maxRetries);
+    });
+  });
+
+  describe("wait", () => {
+    const setTimeoutMock = jest.fn((resolve) => resolve());
+
+    beforeEach(() => {
+      global.setTimeout = setTimeoutMock;
+    });
+
+    it("waits 1000 by default", async () => {
+      await Utils.wait();
+
+      expect(setTimeoutMock).toBeCalledWith(expect.any(Function), 1000);
+    });
+
+    it("waits specified time", async () => {
+      await Utils.wait(2000);
+
+      expect(setTimeoutMock).toBeCalledWith(expect.any(Function), 2000);
+    });
+  });
 });
diff --git a/src/test/mockFactory.ts b/src/test/mockFactory.ts
index ed5f074..b44a7e6 100644
--- a/src/test/mockFactory.ts
+++ b/src/test/mockFactory.ts
@@ -126,22 +126,39 @@ export class MockFactory {
   public static createTestAzureCredentials(): TokenClientCredentials {
     const credentials = {
       getToken: jest.fn(() => {
-        const token: TokenResponse = {
-          tokenType: "Bearer",
-          accessToken: "ABC123",
-        };
+        const token: TokenResponse = this.createTestTokenCacheEntries()[0];
 
         return Promise.resolve(token);
       }),
       signRequest: jest.fn((resource) => Promise.resolve(resource)),
     };
 
-    // TODO: Reduce usage on tokenCache._entries[0]
-    credentials["tokenCache"] = {
-      _entries: [{ accessToken: "ABC123" }]
+    return credentials;
+  }
+
+  public static createTestTokenCacheEntries(count: number = 1): TokenResponse[] {
+    const token: TokenResponse = {
+      tokenType: "Bearer",
+      accessToken: "ABC123",
+      userId: "example@user.com",
     };
+    const result = Array(count).fill(token);
 
-    return credentials;
+    return result;
+  }
+
+  public static createTestSubscriptions(count: number = 1): any[] {
+    const sub = {
+      id: "abc-1234-5678",
+      state: "Enabled",
+      authorizationSource: "RoleBased",
+      user: { name: "example@user.com", type: "user" },
+      environmentName: "AzureCloud",
+      name: "Test Sub"
+    };
+    const result = Array(count).fill(sub);
+
+    return result;
   }
 
   public static createTestTimestamp(): string {
@@ -351,15 +368,7 @@ export class MockFactory {
 
   public static createTestVariables() {
     return {
-      azureCredentials: {
-        tokenCache: {
-          _entries: [
-            {
-              accessToken: "token"
-            }
-          ]
-        }
-      },
+      azureCredentials: this.createTestAzureCredentials(),
       subscriptionId: "azureSubId",
     }
   }
@@ -410,6 +419,18 @@ export class MockFactory {
     }
   }
 
+  public static createTestEventHubFunctionConfig(): ServerlessAzureFunctionConfig {
+    return {
+      events: [
+        {
+          http: true,
+          "x-azure-settings": MockFactory.createTestEventHubBinding("in"),
+        }
+      ],
+      handler: "handler.js",
+    }
+  }
+  
   public static createTestBinding() {
     // Only supporting HTTP for now, could support others
     return MockFactory.createTestHttpBinding();
@@ -433,6 +454,16 @@ export class MockFactory {
     }
   }
 
+  public static createTestEventHubBinding(direction: string = "in") {
+    return {
+      event: "eventHubTrigger",
+      direction,
+      name: "item",
+      eventhubname: "hello",
+      consumerGroup: "$Default",
+      connection: "EventHubsConnection"        
+    }
+  }
   public static createTestBindingsObject(name: string = "index.js") {
     return {
       scriptFile: name,