You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@jclouds.apache.org by na...@apache.org on 2019/02/27 11:35:11 UTC

[jclouds] branch 2.1.x updated: [JCLOUDS-1428] Support for SAS token based Authentication for Azure Blob Storage (#1270)

This is an automated email from the ASF dual-hosted git repository.

nacx pushed a commit to branch 2.1.x
in repository https://gitbox.apache.org/repos/asf/jclouds.git


The following commit(s) were added to refs/heads/2.1.x by this push:
     new 0ce9261  [JCLOUDS-1428] Support for SAS token based Authentication for Azure Blob Storage (#1270)
0ce9261 is described below

commit 0ce926108e5b5469ca9235778321b475d63c837a
Author: Aliaksandra Kharushka <al...@sap.com>
AuthorDate: Wed Feb 27 12:20:22 2019 +0100

    [JCLOUDS-1428] Support for SAS token based Authentication for Azure Blob Storage (#1270)
---
 .../filters/SharedKeyLiteAuthentication.java       | 78 +++++++++++++++++++--
 .../blobstore/AzureBlobRequestSigner.java          | 79 +++++++++++++++-------
 .../azureblob/config/AzureBlobHttpApiModule.java   | 26 +++++++
 .../filters/SharedKeyLiteAuthenticationTest.java   | 38 +++++++++++
 .../config/AzureBlobHttpApiModuleTest.java         | 50 ++++++++++++++
 5 files changed, 240 insertions(+), 31 deletions(-)

diff --git a/providers/azureblob/src/main/java/org/jclouds/azure/storage/filters/SharedKeyLiteAuthentication.java b/providers/azureblob/src/main/java/org/jclouds/azure/storage/filters/SharedKeyLiteAuthentication.java
index 700c610..ad9f60d 100644
--- a/providers/azureblob/src/main/java/org/jclouds/azure/storage/filters/SharedKeyLiteAuthentication.java
+++ b/providers/azureblob/src/main/java/org/jclouds/azure/storage/filters/SharedKeyLiteAuthentication.java
@@ -21,10 +21,13 @@ import static com.google.common.io.ByteStreams.readBytes;
 import static org.jclouds.crypto.Macs.asByteProcessor;
 import static org.jclouds.util.Patterns.NEWLINE_PATTERN;
 import static org.jclouds.util.Strings2.toInputStream;
+import org.jclouds.http.Uris.UriBuilder;
 
 import java.util.Collection;
 import java.util.Map;
 import java.util.Map.Entry;
+import org.jclouds.http.Uris;
+import java.net.URI;
 
 import javax.annotation.Resource;
 import javax.inject.Inject;
@@ -67,12 +70,15 @@ import com.google.common.net.HttpHeaders;
 @Singleton
 public class SharedKeyLiteAuthentication implements HttpRequestFilter {
    private static final Collection<String> FIRST_HEADERS_TO_SIGN = ImmutableList.of(HttpHeaders.DATE);
-
    private final SignatureWire signatureWire;
    private final Supplier<Credentials> creds;
    private final Provider<String> timeStampProvider;
    private final Crypto crypto;
+   private final String credential;
+   private final String identity;
    private final HttpUtils utils;
+   private final URI storageUrl;
+   private final boolean isSAS;
 
    @Resource
    @Named(Constants.LOGGER_SIGNATURE)
@@ -81,27 +87,73 @@ public class SharedKeyLiteAuthentication implements HttpRequestFilter {
    @Inject
    public SharedKeyLiteAuthentication(SignatureWire signatureWire,
          @org.jclouds.location.Provider Supplier<Credentials> creds, @TimeStamp Provider<String> timeStampProvider,
-         Crypto crypto, HttpUtils utils) {
+         Crypto crypto, HttpUtils utils, @Named("sasAuth") boolean sasAuthentication) {
       this.crypto = crypto;
       this.utils = utils;
       this.signatureWire = signatureWire;
+      this.storageUrl = URI.create("https://" + creds.get().identity + ".blob.core.windows.net/");
       this.creds = creds;
+      this.identity = creds.get().identity;
+      this.credential = creds.get().credential;
       this.timeStampProvider = timeStampProvider;
+      this.isSAS = sasAuthentication;
    }
-
+   
+   /** 
+    * this is an updated filter method, which decides whether the SAS or SharedKeyLite 
+    * is used and applies the right filtering.  
+    */
    public HttpRequest filter(HttpRequest request) throws HttpException {
-      request = replaceDateHeader(request);
-      String signature = calculateSignature(createStringToSign(request));
-      request = replaceAuthorizationHeader(request, signature);
+      request = this.isSAS ? filterSAS(request, this.credential) : filterKey(request);
       utils.logRequest(signatureLog, request, "<<");
       return request;
    }
-
+   
+   /** 
+    * this filter method is applied only for the cases with SAS Authentication. 
+    */
+   public HttpRequest filterSAS(HttpRequest request, String credential) throws HttpException, IllegalArgumentException {
+      URI requestUri = request.getEndpoint();
+      String formattedCredential = credential.startsWith("?") ? credential.substring(1) : credential;
+      String initialQuery = requestUri.getQuery();
+      String finalQuery = initialQuery == null ? formattedCredential : initialQuery + "&" + formattedCredential;
+      String[] parametersArray = cutUri(requestUri); 
+      String containerName = parametersArray[1]; 
+      UriBuilder endpoint = Uris.uriBuilder(storageUrl).appendPath(containerName);
+      if (parametersArray.length == 3) {
+         endpoint.appendPath(parametersArray[2]).query(finalQuery);
+      } else {
+         endpoint.query("restype=container&" + finalQuery);
+      }
+      return removeAuthorizationHeader(
+         replaceDateHeader(request.toBuilder()
+            .endpoint(endpoint.build())
+            .build()));
+   }
+   
+   /**
+    * this is a 'standard' filter method, applied when SharedKeyLite authentication is used. 
+    */
+   public HttpRequest filterKey(HttpRequest request) throws HttpException {
+      request = replaceDateHeader(request);
+      String signature = calculateSignature(createStringToSign(request));
+      return replaceAuthorizationHeader(request, signature);
+   }
+   
    HttpRequest replaceAuthorizationHeader(HttpRequest request, String signature) {
       return request.toBuilder()
             .replaceHeader(HttpHeaders.AUTHORIZATION, "SharedKeyLite " + creds.get().identity + ":" + signature)
             .build();
    }
+   
+   /**
+    * this method removes Authorisation header, since it is not needed for SAS Authentication 
+    */
+   HttpRequest removeAuthorizationHeader(HttpRequest request) {
+      return request.toBuilder()
+            .removeHeader(HttpHeaders.AUTHORIZATION)
+            .build();
+   }
 
    HttpRequest replaceDateHeader(HttpRequest request) {
       Builder<String, String> builder = ImmutableMap.builder();
@@ -110,6 +162,18 @@ public class SharedKeyLiteAuthentication implements HttpRequestFilter {
       request = request.toBuilder().replaceHeaders(Multimaps.forMap(builder.build())).build();
       return request;
    }
+   
+   /**
+    * this is the method to parse container name and blob name from the HttpRequest. 
+    */ 
+   public String[] cutUri(URI uri) throws IllegalArgumentException {
+      String path = uri.getPath();
+      String[] result = path.split("/");
+      if (result.length < 2) {
+         throw new IllegalArgumentException("there is neither ContainerName nor BlobName in the URI path");
+      }
+      return result;
+   } 
 
    public String createStringToSign(HttpRequest request) {
       utils.logRequest(signatureLog, request, ">>");
diff --git a/providers/azureblob/src/main/java/org/jclouds/azureblob/blobstore/AzureBlobRequestSigner.java b/providers/azureblob/src/main/java/org/jclouds/azureblob/blobstore/AzureBlobRequestSigner.java
index a31cb1c..994b609 100644
--- a/providers/azureblob/src/main/java/org/jclouds/azureblob/blobstore/AzureBlobRequestSigner.java
+++ b/providers/azureblob/src/main/java/org/jclouds/azureblob/blobstore/AzureBlobRequestSigner.java
@@ -23,6 +23,7 @@ import java.util.Date;
 import java.util.concurrent.TimeUnit;
 import javax.inject.Inject;
 import javax.inject.Singleton;
+import javax.inject.Named;
 
 import org.jclouds.azure.storage.filters.SharedKeyLiteAuthentication;
 import org.jclouds.blobstore.BlobRequestSigner;
@@ -35,11 +36,11 @@ import org.jclouds.http.HttpRequest;
 import org.jclouds.http.Uris;
 import org.jclouds.http.options.GetOptions;
 import org.jclouds.javax.annotation.Nullable;
-
 import com.google.common.base.Supplier;
 import com.google.common.net.HttpHeaders;
 import com.google.inject.Provider;
 
+
 @Singleton
 public class AzureBlobRequestSigner implements BlobRequestSigner {
    private static final int DEFAULT_EXPIRY_SECONDS = 15 * 60;
@@ -51,19 +52,23 @@ public class AzureBlobRequestSigner implements BlobRequestSigner {
    private final Provider<String> timeStampProvider;
    private final DateService dateService;
    private final SharedKeyLiteAuthentication auth;
+   private final String credential;
+   private final boolean isSAS; 
 
    @Inject
    public AzureBlobRequestSigner(
          BlobToHttpGetOptions blob2HttpGetOptions, @TimeStamp Provider<String> timeStampProvider,
          DateService dateService, SharedKeyLiteAuthentication auth,
-         @org.jclouds.location.Provider Supplier<Credentials> creds)
+         @org.jclouds.location.Provider Supplier<Credentials> creds, @Named("sasAuth") boolean sasAuthentication)
          throws SecurityException, NoSuchMethodException {
       this.identity = creds.get().identity;
+      this.credential = creds.get().credential;
       this.storageUrl = URI.create("https://" + creds.get().identity + ".blob.core.windows.net/");
       this.blob2HttpGetOptions = checkNotNull(blob2HttpGetOptions, "blob2HttpGetOptions");
       this.timeStampProvider = checkNotNull(timeStampProvider, "timeStampProvider");
       this.dateService = checkNotNull(dateService, "dateService");
       this.auth = auth;
+      this.isSAS = sasAuthentication;
    }
 
    @Override
@@ -107,12 +112,14 @@ public class AzureBlobRequestSigner implements BlobRequestSigner {
       return sign("GET", container, name, blob2HttpGetOptions.apply(checkNotNull(options, "options")),
             DEFAULT_EXPIRY_SECONDS, null, null);
    }
-
-   private HttpRequest sign(String method, String container, String name, @Nullable GetOptions options, long expires, @Nullable Long contentLength, @Nullable String contentType) {
+   
+   /**
+    * method to sign HttpRequest when SharedKey Authentication is used 
+    */
+   private HttpRequest signKey(String method, String container, String name, @Nullable GetOptions options, long expires, @Nullable Long contentLength, @Nullable String contentType) { 
       checkNotNull(method, "method");
       checkNotNull(container, "container");
       checkNotNull(name, "name");
-
       String nowString = timeStampProvider.get();
       Date now = dateService.rfc1123DateParse(nowString);
       Date expiration = new Date(now.getTime() + TimeUnit.SECONDS.toMillis(expires));
@@ -125,7 +132,6 @@ public class AzureBlobRequestSigner implements BlobRequestSigner {
       } else {
          signedPermission = "r";
       }
-
       HttpRequest.Builder request = HttpRequest.builder()
             .method(method)
             .endpoint(Uris.uriBuilder(storageUrl).appendPath(container).appendPath(name).build())
@@ -134,23 +140,7 @@ public class AzureBlobRequestSigner implements BlobRequestSigner {
             .addQueryParam("se", iso8601)
             .addQueryParam("sr", "b")  // blob resource
             .addQueryParam("sp", signedPermission);  // permission
-
-      if (contentLength != null) {
-         request.replaceHeader(HttpHeaders.CONTENT_LENGTH, contentLength.toString());
-      }
-
-      if (contentType != null) {
-         request.replaceHeader("x-ms-blob-content-type", contentType);
-      }
-
-      if (options != null) {
-         request.headers(options.buildRequestHeaders());
-      }
-
-      if (method.equals("PUT")) {
-         request.replaceHeader("x-ms-blob-type", "BlockBlob");
-      }
-
+      request = setHeaders(request, method, options, contentLength, contentType);
       String stringToSign =
             signedPermission + "\n" +  // signedpermission
             "\n" +  // signedstart
@@ -165,9 +155,50 @@ public class AzureBlobRequestSigner implements BlobRequestSigner {
             "\n" +  // rsce
             "\n" +  // rscl
             "";  // rsct
-
       String signature = auth.calculateSignature(stringToSign);
       request.addQueryParam("sig", signature);
       return request.build();
    }
+   
+   private HttpRequest.Builder setHeaders(HttpRequest.Builder request, String method, @Nullable GetOptions options, @Nullable Long contentLength, @Nullable String contentType){
+      if (contentLength != null) {
+         request.replaceHeader(HttpHeaders.CONTENT_LENGTH, contentLength.toString());
+      }
+      if (contentType != null) {
+         request.replaceHeader("x-ms-blob-content-type", contentType);
+      }
+      if (options != null) {
+         request.headers(options.buildRequestHeaders());
+      }
+      if (method.equals("PUT")) {
+         request.replaceHeader("x-ms-blob-type", "BlockBlob");
+      }
+      return request; 
+   }
+   
+   /** 
+    * method, compatible with SAS Authentication 
+    */
+   private HttpRequest signSAS(String method, String container, String name, @Nullable GetOptions options, long expires, @Nullable Long contentLength, @Nullable String contentType) {
+      checkNotNull(method, "method");
+      checkNotNull(container, "container");
+      checkNotNull(name, "name");
+      String nowString = timeStampProvider.get();
+      HttpRequest.Builder request = HttpRequest.builder()
+            .method(method)
+            .endpoint(Uris.uriBuilder(storageUrl).appendPath(container).appendPath(name).query(this.credential).build())
+            .replaceHeader(HttpHeaders.DATE, nowString);
+      request = setHeaders(request, method, options, contentLength, contentType);
+      return request.build();
+   }  
+   
+   /**
+    * modified sign() method, which acts depending on the Auth input. 
+    */
+   public HttpRequest sign(String method, String container, String name, @Nullable GetOptions options, long expires, @Nullable Long contentLength, @Nullable String contentType) {
+      if (isSAS) {
+         return signSAS(method, container, name, options, expires, contentLength, contentType);
+      }
+      return signKey(method, container, name, options, expires, contentLength, contentType);
+   }
 }
diff --git a/providers/azureblob/src/main/java/org/jclouds/azureblob/config/AzureBlobHttpApiModule.java b/providers/azureblob/src/main/java/org/jclouds/azureblob/config/AzureBlobHttpApiModule.java
index 18365e1..fb6e2c0 100644
--- a/providers/azureblob/src/main/java/org/jclouds/azureblob/config/AzureBlobHttpApiModule.java
+++ b/providers/azureblob/src/main/java/org/jclouds/azureblob/config/AzureBlobHttpApiModule.java
@@ -18,7 +18,15 @@ package org.jclouds.azureblob.config;
 
 import static org.jclouds.Constants.PROPERTY_SESSION_INTERVAL;
 
+import static com.google.common.base.Predicates.in;
+
+import static com.google.common.collect.Iterables.all;
+import com.google.common.base.Splitter;
+import com.google.common.collect.ImmutableList;
+
 import java.util.concurrent.TimeUnit;
+import java.util.Map;
+import java.util.List;
 
 import javax.inject.Named;
 
@@ -36,6 +44,7 @@ import org.jclouds.json.config.GsonModule.DateAdapter;
 import org.jclouds.json.config.GsonModule.Iso8601DateAdapter;
 import org.jclouds.rest.ConfiguresHttpApi;
 import org.jclouds.rest.config.HttpApiModule;
+import org.jclouds.domain.Credentials;
 
 import com.google.common.base.Supplier;
 import com.google.common.base.Suppliers;
@@ -63,6 +72,23 @@ public class AzureBlobHttpApiModule extends HttpApiModule<AzureBlobClient> {
    protected String provideTimeStamp(@TimeStamp Supplier<String> cache) {
       return cache.get();
    }
+   
+   /** 
+    * checks which Authentication type is used 
+    */
+   @Named("sasAuth")
+   @Provides 
+   protected boolean authSAS(@org.jclouds.location.Provider Supplier<Credentials> creds) {
+      String credential = creds.get().credential;
+      String formattedCredential = credential.startsWith("?") ? credential.substring(1) : credential;
+      List<String> required = ImmutableList.of("sv", "se", "sig", "sp"); 
+      try {
+         Map<String, String> tokens = Splitter.on('&').withKeyValueSeparator('=').split(formattedCredential);
+         return all(required, in(tokens.keySet()));
+      } catch (Exception ex) {
+         return false;
+      }
+   }
 
    /**
     * borrowing concurrency code to ensure that caching takes place properly
diff --git a/providers/azureblob/src/test/java/org/jclouds/azure/storage/filters/SharedKeyLiteAuthenticationTest.java b/providers/azureblob/src/test/java/org/jclouds/azure/storage/filters/SharedKeyLiteAuthenticationTest.java
index 68ebaeb..5e0c226 100644
--- a/providers/azureblob/src/test/java/org/jclouds/azure/storage/filters/SharedKeyLiteAuthenticationTest.java
+++ b/providers/azureblob/src/test/java/org/jclouds/azure/storage/filters/SharedKeyLiteAuthenticationTest.java
@@ -42,6 +42,8 @@ public class SharedKeyLiteAuthenticationTest {
    private static final String ACCOUNT = "foo";
    private Injector injector;
    private SharedKeyLiteAuthentication filter;
+   private SharedKeyLiteAuthentication filterSAS;
+   private SharedKeyLiteAuthentication filterSASQuestionMark;
 
    @DataProvider(parallel = true)
    public Object[][] dataProvider() {
@@ -52,6 +54,19 @@ public class SharedKeyLiteAuthenticationTest {
                   + ".blob.core.windows.net/movies/MOV1.avi?comp=blocklist&timeout=120").build() },
             { HttpRequest.builder().method(HttpMethod.GET).endpoint("http://" + ACCOUNT + ".blob.core.windows.net/movies/MOV1.avi").build() } };
    }
+   
+   @DataProvider(name = "auth-sas-data", parallel = true)
+   public Object[][] requests(){
+      return new Object[][]{
+            { HttpRequest.builder().method(HttpMethod.PUT).endpoint("https://" + ACCOUNT 
+                  + ".blob.core.windows.net/movies/MOV1.avi?comp=block&blockid=BlockId1&timeout=60").build(), filterSAS, "https://foo.blob.core.windows.net/movies/MOV1.avi?comp=block&blockid=BlockId1&timeout=60&sv=2018-03-28&ss=b&srt=sco&sp=rwdlac&se=2019-02-13T17%3A18%3A22Z&st=2019-02-13T09%3A18%3A22Z&spr=https&sig=sMnaKSD94CzEPeGnWauTT0wBNIn%2B4ySkZO5PEAW7zs%3D"},
+            { HttpRequest.builder().method(HttpMethod.PUT).endpoint("https://" + ACCOUNT
+                  + ".blob.core.windows.net/movies/MOV1.avi?comp=blocklist&timeout=120").build(), filterSAS, "https://foo.blob.core.windows.net/movies/MOV1.avi?comp=blocklist&timeout=120&sv=2018-03-28&ss=b&srt=sco&sp=rwdlac&se=2019-02-13T17%3A18%3A22Z&st=2019-02-13T09%3A18%3A22Z&spr=https&sig=sMnaKSD94CzEPeGnWauTT0wBNIn%2B4ySkZO5PEAW7zs%3D" },
+            { HttpRequest.builder().method(HttpMethod.GET).endpoint("https://" + ACCOUNT
+                  + ".blob.core.windows.net/movies/MOV1.avi").build(), filterSAS, "https://foo.blob.core.windows.net/movies/MOV1.avi?sv=2018-03-28&ss=b&srt=sco&sp=rwdlac&se=2019-02-13T17%3A18%3A22Z&st=2019-02-13T09%3A18%3A22Z&spr=https&sig=sMnaKSD94CzEPeGnWauTT0wBNIn%2B4ySkZO5PEAW7zs%3D" }, 
+            { HttpRequest.builder().method(HttpMethod.GET).endpoint("https://" + ACCOUNT
+                  + ".blob.core.windows.net/movies/MOV1.avi").build(), filterSASQuestionMark, "https://foo.blob.core.windows.net/movies/MOV1.avi?sv=2018-03-28&ss=b&srt=sco&sp=rwdlac&se=2019-02-13T17%3A18%3A22Z&st=2019-02-13T09%3A18%3A22Z&spr=https&sig=sMnaKSD94CzEPeGnWauTT0wBNIn%2B4ySkZO5PEAW7zs%3D" } };
+   }
 
    /**
     * NOTE this test is dependent on how frequently the timestamp updates. At
@@ -74,6 +89,15 @@ public class SharedKeyLiteAuthenticationTest {
       System.out.printf("%s: %d iterations before the timestamp updated %n", Thread.currentThread().getName(),
             iterations);
    }
+   
+   /**
+    * this test is similar to testIdempotent; it checks whether request is properly filtered when it comes to SAS Authentication
+    */
+   @Test(dataProvider = "auth-sas-data") 
+   void testFilter(HttpRequest request, SharedKeyLiteAuthentication filter, String expected) {
+      request = filter.filter(request);
+      assertEquals(request.getEndpoint().toString(), expected);
+   }
 
    @Test
    void testAclQueryStringRoot() {
@@ -127,5 +151,19 @@ public class SharedKeyLiteAuthenticationTest {
             .modules(ImmutableSet.<Module> of(new MockModule(), new NullLoggingModule()))
             .buildInjector();
       filter = injector.getInstance(SharedKeyLiteAuthentication.class);
+      injector = ContextBuilder
+            .newBuilder("azureblob")
+            .endpoint("https://${jclouds.identity}.blob.core.windows.net")
+            .credentials(ACCOUNT, "sv=2018-03-28&ss=b&srt=sco&sp=rwdlac&se=2019-02-13T17:18:22Z&st=2019-02-13T09:18:22Z&spr=https&sig=sMnaKSD94CzEPeGnWauTT0wBNIn%2B4ySkZO5PEAW7zs%3D")
+            .modules(ImmutableSet.<Module> of(new MockModule(), new NullLoggingModule()))
+            .buildInjector(); 
+      filterSAS = injector.getInstance(SharedKeyLiteAuthentication.class);
+      injector = ContextBuilder
+            .newBuilder("azureblob")
+            .endpoint("https://${jclouds.identity}.blob.core.windows.net")
+            .credentials(ACCOUNT, "?sv=2018-03-28&ss=b&srt=sco&sp=rwdlac&se=2019-02-13T17:18:22Z&st=2019-02-13T09:18:22Z&spr=https&sig=sMnaKSD94CzEPeGnWauTT0wBNIn%2B4ySkZO5PEAW7zs%3D")
+            .modules(ImmutableSet.<Module> of(new MockModule(), new NullLoggingModule()))
+            .buildInjector(); 
+      filterSASQuestionMark = injector.getInstance(SharedKeyLiteAuthentication.class);
    }
 }
diff --git a/providers/azureblob/src/test/java/org/jclouds/azureblob/config/AzureBlobHttpApiModuleTest.java b/providers/azureblob/src/test/java/org/jclouds/azureblob/config/AzureBlobHttpApiModuleTest.java
new file mode 100755
index 0000000..4ae8f87
--- /dev/null
+++ b/providers/azureblob/src/test/java/org/jclouds/azureblob/config/AzureBlobHttpApiModuleTest.java
@@ -0,0 +1,50 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jclouds.azureblob.config;
+
+import static org.testng.Assert.assertEquals;
+
+import org.testng.annotations.Test;
+import com.google.common.base.Suppliers;
+import org.jclouds.domain.Credentials;
+import org.testng.annotations.DataProvider;
+
+@Test(groups = "unit", testName = "AzureBlobHttpApiModuleTest") 
+public class AzureBlobHttpApiModuleTest {
+
+   @DataProvider(name = "auth-sas-tokens")
+   public static Object[][] tokens() {
+      return new Object[][]{
+         {false, "sv=2018-03-28&se=2019-02-14T11:12:13Z"}, 
+         {false, "sv=2018-03-28&se=2019-02-14T11:12:13Z&sp=abc&st=2019-01-20T11:12:13Z"}, 
+         {false, "u2iAP01ARTewyK/MhOM1d1ASPpjqclkldsdkljfas2kfjkh895ssfslkjpXKfhg=="}, 
+         {false, "sadf;gjkhflgjkhfdlkfdljghskldjghlfdghw4986754ltjkghdlfkjghst;lyho56[09y7poinh"}, 
+         {false, "a=apple&b=banana&c=cucumber&d=diet"}, 
+         {false, "sva=swajak&sta=stancyja&spa=spakoj&sea=mora&sig=podpis"}, 
+         {true, "sv=2018-03-28&ss=b&srt=sco&sp=r&se=2019-02-13T17:03:09Z&st=2019-02-13T09:03:09Z&spr=https&sig=wNkWK%2GURTjHWhtqG6Q2Gu%2Qu%3FPukW6N4%2FIH4Mr%2F%2FO42M%3D"}, 
+         {true, "sp=rl&st=2019-02-14T08:50:26Z&se=2019-02-15T08:50:26Z&sv=2018-03-28&sig=Ukow8%2GtpQpAiVZBLcWp1%2RSpFq928MAqzp%2BdrdregaB6%3D&sr=b"}, 
+         {false, ""} 
+     };
+   }
+
+   @Test(dataProvider = "auth-sas-tokens") 
+   void testAuthSasNonSufficientParametersSvSe(boolean expected, String credential){
+      AzureBlobHttpApiModule module = new AzureBlobHttpApiModule();
+      Credentials creds = new Credentials("identity", credential);
+      assertEquals(module.authSAS(Suppliers.ofInstance(creds)), expected);
+   }
+}