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 2016/05/19 22:34:13 UTC

[1/2] jclouds git commit: JCLOUDS-1114: Adding support for "Placement.Tenancy" and "Placement.HostId" AWS EC2 parameters

Repository: jclouds
Updated Branches:
  refs/heads/master c96cfb617 -> f46b38dd8


JCLOUDS-1114: Adding support for "Placement.Tenancy" and "Placement.HostId" AWS EC2 parameters


Project: http://git-wip-us.apache.org/repos/asf/jclouds/repo
Commit: http://git-wip-us.apache.org/repos/asf/jclouds/commit/ccd1ef2b
Tree: http://git-wip-us.apache.org/repos/asf/jclouds/tree/ccd1ef2b
Diff: http://git-wip-us.apache.org/repos/asf/jclouds/diff/ccd1ef2b

Branch: refs/heads/master
Commit: ccd1ef2b4da33e6d4daa7fd38779388cabf61009
Parents: c96cfb6
Author: Sergey Torgashov <st...@qubell.com>
Authored: Tue May 17 17:51:28 2016 +0300
Committer: Ignasi Barrera <na...@apache.org>
Committed: Fri May 20 00:07:47 2016 +0200

----------------------------------------------------------------------
 .../aws/ec2/compute/AWSEC2TemplateOptions.java  | 55 +++++++++++++++++++-
 ...curityGroupsAsNeededAndReturnRunOptions.java |  4 ++
 .../aws/ec2/options/AWSRunInstancesOptions.java | 32 ++++++++++++
 .../org/jclouds/aws/ec2/options/Tenancy.java    | 37 +++++++++++++
 .../AWSEC2ComputeServiceApiMockTest.java        | 35 +++++++++++++
 .../options/AWSEC2TemplateOptionsTest.java      | 39 ++++++++++++++
 .../aws/ec2/features/AWSInstanceApiTest.java    | 19 ++++---
 .../ec2/options/AWSRunInstancesOptionsTest.java | 49 +++++++++++++++++
 8 files changed, 260 insertions(+), 10 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/jclouds/blob/ccd1ef2b/providers/aws-ec2/src/main/java/org/jclouds/aws/ec2/compute/AWSEC2TemplateOptions.java
----------------------------------------------------------------------
diff --git a/providers/aws-ec2/src/main/java/org/jclouds/aws/ec2/compute/AWSEC2TemplateOptions.java b/providers/aws-ec2/src/main/java/org/jclouds/aws/ec2/compute/AWSEC2TemplateOptions.java
index 760a0aa..d711ad7 100644
--- a/providers/aws-ec2/src/main/java/org/jclouds/aws/ec2/compute/AWSEC2TemplateOptions.java
+++ b/providers/aws-ec2/src/main/java/org/jclouds/aws/ec2/compute/AWSEC2TemplateOptions.java
@@ -28,6 +28,7 @@ import java.util.Map;
 import java.util.Set;
 
 import org.jclouds.aws.ec2.options.RequestSpotInstancesOptions;
+import org.jclouds.aws.ec2.options.Tenancy;
 import org.jclouds.compute.options.TemplateOptions;
 import org.jclouds.domain.LoginCredentials;
 import org.jclouds.ec2.compute.options.EC2TemplateOptions;
@@ -89,6 +90,10 @@ public class AWSEC2TemplateOptions extends EC2TemplateOptions implements Cloneab
             eTo.spotOptions(getSpotOptions());
          if (getPrivateIpAddress() != null)
             eTo.privateIpAddress(getPrivateIpAddress());
+         if (getTenancy() != null)
+            eTo.tenancy(getTenancy());
+         if (getDedicatedHostId() != null)
+            eTo.dedicatedHostId(getDedicatedHostId());
       }
    }
 
@@ -102,6 +107,8 @@ public class AWSEC2TemplateOptions extends EC2TemplateOptions implements Cloneab
    private String iamInstanceProfileArn;
    private String iamInstanceProfileName;
    private String privateIpAddress;
+   private Tenancy tenancy;
+   private String dedicatedHostId;
 
    @Override
    public boolean equals(Object o) {
@@ -116,13 +123,15 @@ public class AWSEC2TemplateOptions extends EC2TemplateOptions implements Cloneab
                && equal(this.spotPrice, that.spotPrice) && equal(this.spotOptions, that.spotOptions)
                && equal(this.groupIds, that.groupIds) && equal(this.iamInstanceProfileArn, that.iamInstanceProfileArn)
                && equal(this.iamInstanceProfileName, that.iamInstanceProfileName)
-               && equal(this.privateIpAddress, that.privateIpAddress);
+               && equal(this.privateIpAddress, that.privateIpAddress)
+               && equal(this.tenancy, that.tenancy) && equal(this.dedicatedHostId, that.dedicatedHostId);
    }
 
    @Override
    public int hashCode() {
       return Objects.hashCode(super.hashCode(), monitoringEnabled, placementGroup, noPlacementGroup, subnetId,
-               spotPrice, spotOptions, groupIds, iamInstanceProfileArn, iamInstanceProfileName, privateIpAddress);
+               spotPrice, spotOptions, groupIds, iamInstanceProfileArn, iamInstanceProfileName, privateIpAddress,
+               tenancy, dedicatedHostId);
    }
 
    @Override
@@ -142,6 +151,8 @@ public class AWSEC2TemplateOptions extends EC2TemplateOptions implements Cloneab
       toString.add("iamInstanceProfileArn", iamInstanceProfileArn);
       toString.add("iamInstanceProfileName", iamInstanceProfileName);
       toString.add("privateIpAddress", privateIpAddress);
+      toString.add("tenancy", tenancy);
+      toString.add("dedicatedHostId", dedicatedHostId);
       return toString;
    }
 
@@ -208,6 +219,22 @@ public class AWSEC2TemplateOptions extends EC2TemplateOptions implements Cloneab
    }
 
    /**
+    * Specifies the tenancy used to run instances with
+    */
+   public AWSEC2TemplateOptions tenancy(Tenancy tenancy) {
+      this.tenancy = checkNotNull(tenancy, "tenancy must be defined");
+      return this;
+   }
+
+   /**
+    * Specifies the ID of the dedicated host on which the instance should resist.
+    */
+   public AWSEC2TemplateOptions dedicatedHostId(String hostId) {
+      this.dedicatedHostId = checkNotNull(emptyToNull(hostId), "hostId must be defined");
+      return this;
+   }
+
+   /**
     * Specifies the maximum spot price to use
     */
    public AWSEC2TemplateOptions spotPrice(Float spotPrice) {
@@ -463,6 +490,22 @@ public class AWSEC2TemplateOptions extends EC2TemplateOptions implements Cloneab
       }
 
       /**
+       * @see AWSEC2TemplateOptions#tenancy
+       */
+      public static AWSEC2TemplateOptions tenancy(Tenancy tenancy) {
+         AWSEC2TemplateOptions options = new AWSEC2TemplateOptions();
+         return options.tenancy(tenancy);
+      }
+
+      /**
+       * @see AWSEC2TemplateOptions#dedicatedHostId
+       */
+      public static AWSEC2TemplateOptions dedicatedHostId(String hostId) {
+         AWSEC2TemplateOptions options = new AWSEC2TemplateOptions();
+         return options.dedicatedHostId(hostId);
+      }
+
+      /**
        * @see AWSEC2TemplateOptions#spotPrice
        */
       public static AWSEC2TemplateOptions spotPrice(Float spotPrice) {
@@ -812,4 +855,12 @@ public class AWSEC2TemplateOptions extends EC2TemplateOptions implements Cloneab
    public String getPrivateIpAddress() {
       return privateIpAddress;
    }
+
+   public Tenancy getTenancy() {
+      return tenancy;
+   }
+
+   public String getDedicatedHostId() {
+      return dedicatedHostId;
+   }
 }

http://git-wip-us.apache.org/repos/asf/jclouds/blob/ccd1ef2b/providers/aws-ec2/src/main/java/org/jclouds/aws/ec2/compute/strategy/CreateKeyPairPlacementAndSecurityGroupsAsNeededAndReturnRunOptions.java
----------------------------------------------------------------------
diff --git a/providers/aws-ec2/src/main/java/org/jclouds/aws/ec2/compute/strategy/CreateKeyPairPlacementAndSecurityGroupsAsNeededAndReturnRunOptions.java b/providers/aws-ec2/src/main/java/org/jclouds/aws/ec2/compute/strategy/CreateKeyPairPlacementAndSecurityGroupsAsNeededAndReturnRunOptions.java
index 151e00e..371271c 100644
--- a/providers/aws-ec2/src/main/java/org/jclouds/aws/ec2/compute/strategy/CreateKeyPairPlacementAndSecurityGroupsAsNeededAndReturnRunOptions.java
+++ b/providers/aws-ec2/src/main/java/org/jclouds/aws/ec2/compute/strategy/CreateKeyPairPlacementAndSecurityGroupsAsNeededAndReturnRunOptions.java
@@ -102,6 +102,10 @@ public class CreateKeyPairPlacementAndSecurityGroupsAsNeededAndReturnRunOptions
          instanceOptions.withIAMInstanceProfileName(awsTemplateOptions.getIAMInstanceProfileName());
       if (awsTemplateOptions.getPrivateIpAddress() != null)
          instanceOptions.withPrivateIpAddress(awsTemplateOptions.getPrivateIpAddress());
+      if (awsTemplateOptions.getTenancy() != null)
+         instanceOptions.withTenancy(awsTemplateOptions.getTenancy());
+      if (awsTemplateOptions.getDedicatedHostId() != null)
+         instanceOptions.withDedicatedHostId(awsTemplateOptions.getDedicatedHostId());
 
       return instanceOptions;
    }

http://git-wip-us.apache.org/repos/asf/jclouds/blob/ccd1ef2b/providers/aws-ec2/src/main/java/org/jclouds/aws/ec2/options/AWSRunInstancesOptions.java
----------------------------------------------------------------------
diff --git a/providers/aws-ec2/src/main/java/org/jclouds/aws/ec2/options/AWSRunInstancesOptions.java b/providers/aws-ec2/src/main/java/org/jclouds/aws/ec2/options/AWSRunInstancesOptions.java
index e8d5bb7..f9734ab 100644
--- a/providers/aws-ec2/src/main/java/org/jclouds/aws/ec2/options/AWSRunInstancesOptions.java
+++ b/providers/aws-ec2/src/main/java/org/jclouds/aws/ec2/options/AWSRunInstancesOptions.java
@@ -61,6 +61,22 @@ public class AWSRunInstancesOptions extends RunInstancesOptions {
    }
 
    /**
+    * Specifies the tenancy of the instance within which to launch the instance(s).
+    */
+   public AWSRunInstancesOptions withTenancy(Tenancy tenancy) {
+      formParameters.put("Placement.Tenancy", checkNotNull(tenancy, "tenancy").toString());
+      return this;
+   }
+
+   /**
+    * Specifies the ID of the dedicated host on which the instance should resist.
+    */
+   public AWSRunInstancesOptions withDedicatedHostId(String hostId) {
+      formParameters.put("Placement.HostId", checkNotNull(hostId, "hostId"));
+      return this;
+   }
+
+   /**
     * Enables monitoring for the instance.
     */
    public AWSRunInstancesOptions enableMonitoring() {
@@ -143,6 +159,22 @@ public class AWSRunInstancesOptions extends RunInstancesOptions {
       }
 
       /**
+       * @see AWSRunInstancesOptions#withTenancy(Tenancy)
+       */
+      public static AWSRunInstancesOptions withTenancy(Tenancy tenancy) {
+         AWSRunInstancesOptions options = new AWSRunInstancesOptions();
+         return options.withTenancy(tenancy);
+      }
+
+      /**
+       * @see AWSRunInstancesOptions#withDedicatedHostId(String)
+       */
+      public static AWSRunInstancesOptions withDedicatedHostId(String hostId) {
+         AWSRunInstancesOptions options = new AWSRunInstancesOptions();
+         return options.withDedicatedHostId(hostId);
+      }
+
+      /**
        * @see AWSRunInstancesOptions#enableMonitoring()
        */
       public static AWSRunInstancesOptions enableMonitoring() {

http://git-wip-us.apache.org/repos/asf/jclouds/blob/ccd1ef2b/providers/aws-ec2/src/main/java/org/jclouds/aws/ec2/options/Tenancy.java
----------------------------------------------------------------------
diff --git a/providers/aws-ec2/src/main/java/org/jclouds/aws/ec2/options/Tenancy.java b/providers/aws-ec2/src/main/java/org/jclouds/aws/ec2/options/Tenancy.java
new file mode 100644
index 0000000..ec5468a
--- /dev/null
+++ b/providers/aws-ec2/src/main/java/org/jclouds/aws/ec2/options/Tenancy.java
@@ -0,0 +1,37 @@
+/*
+ * 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.aws.ec2.options;
+
+/**
+ * Contains valid values for the 'Placement.Tenancy' parameter in the Form API for the RunInstances operation.
+ */
+public enum Tenancy {
+   DEFAULT("default"),
+   DEDICATED("dedicated"),
+   HOST("host");
+
+   private final String value;
+
+   Tenancy(String value) {
+      this.value = value;
+   }
+
+   @Override
+   public String toString() {
+      return value;
+   }
+}

http://git-wip-us.apache.org/repos/asf/jclouds/blob/ccd1ef2b/providers/aws-ec2/src/test/java/org/jclouds/aws/ec2/compute/AWSEC2ComputeServiceApiMockTest.java
----------------------------------------------------------------------
diff --git a/providers/aws-ec2/src/test/java/org/jclouds/aws/ec2/compute/AWSEC2ComputeServiceApiMockTest.java b/providers/aws-ec2/src/test/java/org/jclouds/aws/ec2/compute/AWSEC2ComputeServiceApiMockTest.java
index f647578..09f1d10 100644
--- a/providers/aws-ec2/src/test/java/org/jclouds/aws/ec2/compute/AWSEC2ComputeServiceApiMockTest.java
+++ b/providers/aws-ec2/src/test/java/org/jclouds/aws/ec2/compute/AWSEC2ComputeServiceApiMockTest.java
@@ -20,6 +20,7 @@ import static org.jclouds.aws.ec2.compute.AWSEC2TemplateOptions.Builder.blockUnt
 import static org.testng.Assert.assertEquals;
 
 import org.jclouds.aws.ec2.internal.BaseAWSEC2ApiMockTest;
+import org.jclouds.aws.ec2.options.Tenancy;
 import org.jclouds.compute.ComputeService;
 import org.jclouds.compute.domain.NodeMetadata;
 import org.jclouds.compute.domain.Template;
@@ -232,6 +233,40 @@ public class AWSEC2ComputeServiceApiMockTest extends BaseAWSEC2ApiMockTest {
       assertPosted(DEFAULT_REGION, "Action=CreateTags&Tag.1.Key=Name&Tag.1.Value=test-2baa5550&ResourceId.1=i-2baa5550");
    }
 
+   public void createNodeWithDedicatedTenancyAndHostId() throws Exception {
+      enqueueRegions(DEFAULT_REGION);
+      enqueueXml(DEFAULT_REGION, "/amzn_images.xml");
+      enqueueXml(DEFAULT_REGION, "/describe_images_cc.xml");
+      enqueueXml(DEFAULT_REGION, "/availabilityZones.xml");
+      enqueueXml(DEFAULT_REGION, "/created_securitygroup.xml");
+      enqueueXml(DEFAULT_REGION, "/new_securitygroup.xml");
+      enqueueXml(DEFAULT_REGION, "/new_securitygroup.xml");
+      enqueueXml(DEFAULT_REGION, "/authorize_securitygroup_ingress_response.xml");
+      enqueueXml(DEFAULT_REGION, "/new_instance.xml");
+      enqueueXml(DEFAULT_REGION, "/describe_instances_running-1.xml");
+      enqueueXml(DEFAULT_REGION, "/describe_images.xml");
+      enqueue(DEFAULT_REGION, new MockResponse()); // create tags
+
+      ComputeService computeService = computeService();
+
+      NodeMetadata node = Iterables.getOnlyElement(computeService.createNodesInGroup("test", 1,
+            blockUntilRunning(false).noKeyPair().tenancy(Tenancy.HOST).dedicatedHostId("TestHostId")));
+      assertEquals(node.getId(), "us-east-1/i-2baa5550");
+
+      assertPosted(DEFAULT_REGION, "Action=DescribeRegions");
+      assertPosted(DEFAULT_REGION, "Action=DescribeImages&Filter.1.Name=owner-id&Filter.1.Value.1=137112412989&Filter.1.Value.2=801119661308&Filter.1.Value.3=063491364108&Filter.1.Value.4=099720109477&Filter.1.Value.5=411009282317&Filter.2.Name=state&Filter.2.Value.1=available&Filter.3.Name=image-type&Filter.3.Value.1=machine");
+      assertPosted(DEFAULT_REGION, "Action=DescribeImages&Filter.1.Name=virtualization-type&Filter.1.Value.1=hvm&Filter.2.Name=architecture&Filter.2.Value.1=x86_64&Filter.3.Name=owner-id&Filter.3.Value.1=137112412989&Filter.3.Value.2=099720109477&Filter.4.Name=hypervisor&Filter.4.Value.1=xen&Filter.5.Name=state&Filter.5.Value.1=available&Filter.6.Name=image-type&Filter.6.Value.1=machine&Filter.7.Name=root-device-type&Filter.7.Value.1=ebs");
+      assertPosted(DEFAULT_REGION, "Action=DescribeAvailabilityZones");
+      assertPosted(DEFAULT_REGION, "Action=CreateSecurityGroup&GroupName=jclouds%23test&GroupDescription=jclouds%23test");
+      assertPosted(DEFAULT_REGION, "Action=DescribeSecurityGroups&GroupName.1=jclouds%23test");
+      assertPosted(DEFAULT_REGION, "Action=DescribeSecurityGroups&GroupName.1=jclouds%23test");
+      assertPosted(DEFAULT_REGION, "Action=AuthorizeSecurityGroupIngress&GroupId=sg-3c6ef654&IpPermissions.0.IpProtocol=tcp&IpPermissions.0.FromPort=22&IpPermissions.0.ToPort=22&IpPermissions.0.IpRanges.0.CidrIp=0.0.0.0/0&IpPermissions.1.IpProtocol=tcp&IpPermissions.1.FromPort=0&IpPermissions.1.ToPort=65535&IpPermissions.1.Groups.0.UserId=993194456877&IpPermissions.1.Groups.0.GroupId=sg-3c6ef654&IpPermissions.2.IpProtocol=udp&IpPermissions.2.FromPort=0&IpPermissions.2.ToPort=65535&IpPermissions.2.Groups.0.UserId=993194456877&IpPermissions.2.Groups.0.GroupId=sg-3c6ef654");
+      assertPosted(DEFAULT_REGION, "Action=RunInstances&ImageId=ami-8ce4b5c9&MinCount=1&MaxCount=1&InstanceType=" + getDefaultParavirtualInstanceType() + "&SecurityGroupId.1=sg-3c6ef654&UserData=I2Nsb3VkLWNvbmZpZwpyZXBvX3VwZ3JhZGU6IG5vbmUK&Placement.Tenancy=host&Placement.HostId=TestHostId");
+      assertPosted(DEFAULT_REGION, "Action=DescribeInstances&InstanceId.1=i-2baa5550");
+      assertPosted(DEFAULT_REGION, "Action=DescribeImages&ImageId.1=ami-aecd60c7");
+      assertPosted(DEFAULT_REGION, "Action=CreateTags&Tag.1.Key=Name&Tag.1.Value=test-2baa5550&ResourceId.1=i-2baa5550");
+   }
+
    public void listNodesWhereImageDoesntExist() throws Exception {
       enqueueRegions(DEFAULT_REGION);
       enqueueXml(DEFAULT_REGION, "/describe_instances_running-1.xml");

http://git-wip-us.apache.org/repos/asf/jclouds/blob/ccd1ef2b/providers/aws-ec2/src/test/java/org/jclouds/aws/ec2/compute/options/AWSEC2TemplateOptionsTest.java
----------------------------------------------------------------------
diff --git a/providers/aws-ec2/src/test/java/org/jclouds/aws/ec2/compute/options/AWSEC2TemplateOptionsTest.java b/providers/aws-ec2/src/test/java/org/jclouds/aws/ec2/compute/options/AWSEC2TemplateOptionsTest.java
index 93f0184..3828948 100644
--- a/providers/aws-ec2/src/test/java/org/jclouds/aws/ec2/compute/options/AWSEC2TemplateOptionsTest.java
+++ b/providers/aws-ec2/src/test/java/org/jclouds/aws/ec2/compute/options/AWSEC2TemplateOptionsTest.java
@@ -18,6 +18,7 @@ package org.jclouds.aws.ec2.compute.options;
 
 import static org.jclouds.aws.ec2.compute.AWSEC2TemplateOptions.Builder.authorizePublicKey;
 import static org.jclouds.aws.ec2.compute.AWSEC2TemplateOptions.Builder.blockOnPort;
+import static org.jclouds.aws.ec2.compute.AWSEC2TemplateOptions.Builder.dedicatedHostId;
 import static org.jclouds.aws.ec2.compute.AWSEC2TemplateOptions.Builder.enableMonitoring;
 import static org.jclouds.aws.ec2.compute.AWSEC2TemplateOptions.Builder.iamInstanceProfileArn;
 import static org.jclouds.aws.ec2.compute.AWSEC2TemplateOptions.Builder.iamInstanceProfileName;
@@ -28,11 +29,13 @@ import static org.jclouds.aws.ec2.compute.AWSEC2TemplateOptions.Builder.noKeyPai
 import static org.jclouds.aws.ec2.compute.AWSEC2TemplateOptions.Builder.privateIpAddress;
 import static org.jclouds.aws.ec2.compute.AWSEC2TemplateOptions.Builder.securityGroupIds;
 import static org.jclouds.aws.ec2.compute.AWSEC2TemplateOptions.Builder.securityGroups;
+import static org.jclouds.aws.ec2.compute.AWSEC2TemplateOptions.Builder.tenancy;
 import static org.testng.Assert.assertEquals;
 
 import java.io.IOException;
 
 import org.jclouds.aws.ec2.compute.AWSEC2TemplateOptions;
+import org.jclouds.aws.ec2.options.Tenancy;
 import org.jclouds.compute.options.TemplateOptions;
 import org.testng.annotations.Test;
 
@@ -425,4 +428,40 @@ public class AWSEC2TemplateOptionsTest {
    public void testPrivateIpAddressNPE() {
       privateIpAddress(null);
    }
+
+   @Test
+   public void testTenancy() {
+      AWSEC2TemplateOptions options = new AWSEC2TemplateOptions();
+      options.tenancy(Tenancy.DEDICATED);
+      assertEquals(options.getTenancy(), Tenancy.DEDICATED);
+   }
+
+   @Test
+   public void testTenancyStatic() {
+      AWSEC2TemplateOptions options = tenancy(Tenancy.HOST);
+      assertEquals(options.getTenancy(), Tenancy.HOST);
+   }
+
+   @Test(expectedExceptions = NullPointerException.class)
+   public void testTenancyNPE() {
+      tenancy(null);
+   }
+
+   @Test
+   public void testDedicatedHostId() {
+      AWSEC2TemplateOptions options = new AWSEC2TemplateOptions();
+      options.dedicatedHostId("hostId-1234");
+      assertEquals(options.getDedicatedHostId(), "hostId-1234");
+   }
+
+   @Test
+   public void testDedicatedHostIdStatic() {
+      AWSEC2TemplateOptions options = dedicatedHostId("hostId-5678");
+      assertEquals(options.getDedicatedHostId(), "hostId-5678");
+   }
+
+   @Test(expectedExceptions = NullPointerException.class)
+   public void testDedicatedHostIdStaticNPE() {
+      dedicatedHostId(null);
+   }
 }

http://git-wip-us.apache.org/repos/asf/jclouds/blob/ccd1ef2b/providers/aws-ec2/src/test/java/org/jclouds/aws/ec2/features/AWSInstanceApiTest.java
----------------------------------------------------------------------
diff --git a/providers/aws-ec2/src/test/java/org/jclouds/aws/ec2/features/AWSInstanceApiTest.java b/providers/aws-ec2/src/test/java/org/jclouds/aws/ec2/features/AWSInstanceApiTest.java
index 6630aac..b62dab6 100644
--- a/providers/aws-ec2/src/test/java/org/jclouds/aws/ec2/features/AWSInstanceApiTest.java
+++ b/providers/aws-ec2/src/test/java/org/jclouds/aws/ec2/features/AWSInstanceApiTest.java
@@ -23,6 +23,7 @@ import java.util.Map;
 
 import org.jclouds.Fallbacks.EmptySetOnNotFoundOr404;
 import org.jclouds.aws.ec2.options.AWSRunInstancesOptions;
+import org.jclouds.aws.ec2.options.Tenancy;
 import org.jclouds.aws.ec2.xml.AWSDescribeInstancesResponseHandler;
 import org.jclouds.aws.ec2.xml.AWSRunInstancesResponseHandler;
 import org.jclouds.ec2.domain.BlockDevice;
@@ -121,19 +122,21 @@ public class AWSInstanceApiTest extends BaseAWSEC2ApiTest<AWSInstanceApi> {
             String.class, int.class, int.class, RunInstancesOptions[].class);
       GeneratedHttpRequest request = processor.createRequest(
             method,
-            Lists.<Object> newArrayList("us-east-1",
-            "us-east-1a",
-            "ami-voo",
-            1,
-            5,
-            new AWSRunInstancesOptions().withKernelId("kernelId").enableMonitoring()
-                  .withSecurityGroups("group1", "group2")));
+            Lists.<Object> newArrayList(
+                  "us-east-1",
+                  "us-east-1a",
+                  "ami-voo",
+                  1,
+                  5,
+                  new AWSRunInstancesOptions().withKernelId("kernelId").enableMonitoring()
+                        .withSecurityGroups("group1", "group2")
+                        .withTenancy(Tenancy.HOST).withDedicatedHostId("hostId")));
 
       assertRequestLineEquals(request, "POST https://ec2.us-east-1.amazonaws.com/ HTTP/1.1");
       assertNonPayloadHeadersEqual(request, "Host: ec2.us-east-1.amazonaws.com\n");
       assertPayloadEquals(
             request,
-            "Action=RunInstances&ImageId=ami-voo&MinCount=1&MaxCount=5&KernelId=kernelId&Monitoring.Enabled=true&SecurityGroup.1=group1&SecurityGroup.2=group2&Placement.AvailabilityZone=us-east-1a",
+            "Action=RunInstances&ImageId=ami-voo&MinCount=1&MaxCount=5&KernelId=kernelId&Monitoring.Enabled=true&SecurityGroup.1=group1&SecurityGroup.2=group2&Placement.Tenancy=host&Placement.HostId=hostId&Placement.AvailabilityZone=us-east-1a",
             "application/x-www-form-urlencoded", false);
       assertResponseParserClassEquals(method, request, ParseSax.class);
       assertSaxResponseParserClassEquals(method, AWSRunInstancesResponseHandler.class);

http://git-wip-us.apache.org/repos/asf/jclouds/blob/ccd1ef2b/providers/aws-ec2/src/test/java/org/jclouds/aws/ec2/options/AWSRunInstancesOptionsTest.java
----------------------------------------------------------------------
diff --git a/providers/aws-ec2/src/test/java/org/jclouds/aws/ec2/options/AWSRunInstancesOptionsTest.java b/providers/aws-ec2/src/test/java/org/jclouds/aws/ec2/options/AWSRunInstancesOptionsTest.java
index 7a84452..dc60f99 100644
--- a/providers/aws-ec2/src/test/java/org/jclouds/aws/ec2/options/AWSRunInstancesOptionsTest.java
+++ b/providers/aws-ec2/src/test/java/org/jclouds/aws/ec2/options/AWSRunInstancesOptionsTest.java
@@ -19,6 +19,7 @@ package org.jclouds.aws.ec2.options;
 import static org.jclouds.aws.ec2.options.AWSRunInstancesOptions.Builder.asType;
 import static org.jclouds.aws.ec2.options.AWSRunInstancesOptions.Builder.enableMonitoring;
 import static org.jclouds.aws.ec2.options.AWSRunInstancesOptions.Builder.withBlockDeviceMappings;
+import static org.jclouds.aws.ec2.options.AWSRunInstancesOptions.Builder.withDedicatedHostId;
 import static org.jclouds.aws.ec2.options.AWSRunInstancesOptions.Builder.withIAMInstanceProfileArn;
 import static org.jclouds.aws.ec2.options.AWSRunInstancesOptions.Builder.withIAMInstanceProfileName;
 import static org.jclouds.aws.ec2.options.AWSRunInstancesOptions.Builder.withKernelId;
@@ -28,6 +29,7 @@ import static org.jclouds.aws.ec2.options.AWSRunInstancesOptions.Builder.withRam
 import static org.jclouds.aws.ec2.options.AWSRunInstancesOptions.Builder.withSecurityGroup;
 import static org.jclouds.aws.ec2.options.AWSRunInstancesOptions.Builder.withSecurityGroupId;
 import static org.jclouds.aws.ec2.options.AWSRunInstancesOptions.Builder.withSubnetId;
+import static org.jclouds.aws.ec2.options.AWSRunInstancesOptions.Builder.withTenancy;
 import static org.jclouds.aws.ec2.options.AWSRunInstancesOptions.Builder.withUserData;
 import static org.testng.Assert.assertEquals;
 
@@ -383,4 +385,51 @@ public class AWSRunInstancesOptionsTest {
       withPrivateIpAdress(null);
    }
 
+   @Test
+   public void testNullWithTenancy() {
+      AWSRunInstancesOptions options = new AWSRunInstancesOptions();
+      assertEquals(options.buildFormParameters().get("Placement.Tenancy"), ImmutableList.of());
+   }
+
+   @Test
+   public void testWithTenancy() {
+      AWSRunInstancesOptions options = new AWSRunInstancesOptions();
+      options.withTenancy(Tenancy.DEDICATED);
+      assertEquals(options.buildFormParameters().get("Placement.Tenancy"), ImmutableList.of("dedicated"));
+   }
+
+   @Test
+   public void testWithTenancyStatic() {
+      AWSRunInstancesOptions options = withTenancy(Tenancy.HOST);
+      assertEquals(options.buildFormParameters().get("Placement.Tenancy"), ImmutableList.of("host"));
+   }
+
+   @Test(expectedExceptions = NullPointerException.class)
+   public void testWithTenancyStaticNPE() {
+      withTenancy(null);
+   }
+
+   @Test
+   public void testNullWithDedicatedHostId() {
+      AWSRunInstancesOptions options = new AWSRunInstancesOptions();
+      assertEquals(options.buildFormParameters().get("Placement.HostId"), ImmutableList.of());
+   }
+
+   @Test
+   public void testWithDedicatedHostId() {
+      AWSRunInstancesOptions options = new AWSRunInstancesOptions();
+      options.withDedicatedHostId("hostId-1234");
+      assertEquals(options.buildFormParameters().get("Placement.HostId"), ImmutableList.of("hostId-1234"));
+   }
+
+   @Test
+   public void testWithDedicatedHostIdStatic() {
+      AWSRunInstancesOptions options = withDedicatedHostId("hostId-5678");
+      assertEquals(options.buildFormParameters().get("Placement.HostId"), ImmutableList.of("hostId-5678"));
+   }
+
+   @Test(expectedExceptions = NullPointerException.class)
+   public void testWithDedicatedHostIdStaticNPE() {
+      withDedicatedHostId(null);
+   }
 }


[2/2] jclouds git commit: client credentials JWT support

Posted by na...@apache.org.
client credentials JWT support


Project: http://git-wip-us.apache.org/repos/asf/jclouds/repo
Commit: http://git-wip-us.apache.org/repos/asf/jclouds/commit/f46b38dd
Tree: http://git-wip-us.apache.org/repos/asf/jclouds/tree/f46b38dd
Diff: http://git-wip-us.apache.org/repos/asf/jclouds/diff/f46b38dd

Branch: refs/heads/master
Commit: f46b38dd89626bc3d32d8260dd816d44255a8420
Parents: ccd1ef2
Author: Jim Spring <jm...@gmail.com>
Authored: Sat May 14 19:35:39 2016 -0700
Committer: Ignasi Barrera <na...@apache.org>
Committed: Fri May 20 00:10:37 2016 +0200

----------------------------------------------------------------------
 apis/oauth/README                               |  13 +-
 .../org/jclouds/oauth/v2/AuthorizationApi.java  |  14 +++
 .../config/CertificateFingerprintSupplier.java  | 113 +++++++++++++++++
 .../jclouds/oauth/v2/config/CredentialType.java |   5 +-
 .../jclouds/oauth/v2/config/OAuthModule.java    |  13 +-
 .../oauth/v2/config/OAuthProperties.java        |   9 +-
 .../oauth/v2/domain/CertificateFingerprint.java |  43 +++++++
 .../v2/domain/ClientCredentialsAuthArgs.java    |  48 ++++++++
 .../v2/domain/ClientCredentialsClaims.java      |  61 ++++++++++
 .../ClientCredentialsJWTBearerTokenFlow.java    | 120 +++++++++++++++++++
 .../ClientCredentialsClaimsToAssertion.java     |  93 ++++++++++++++
 .../oauth/v2/AuthorizationApiLiveTest.java      |  34 +++++-
 .../oauth/v2/AuthorizationApiMockTest.java      |  85 +++++++++++--
 apis/oauth/src/test/resources/testcert.pem      |  22 ++++
 14 files changed, 652 insertions(+), 21 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/jclouds/blob/f46b38dd/apis/oauth/README
----------------------------------------------------------------------
diff --git a/apis/oauth/README b/apis/oauth/README
index 7d039b1..73e6e5b 100644
--- a/apis/oauth/README
+++ b/apis/oauth/README
@@ -23,7 +23,7 @@ mvn clean install -Plive \
 -Dtest.jclouds.oauth.scope=https://www.googleapis.com/auth/prediction \
 
 
-To Run the live test against Azure Active Directory which uses the client_credentials grant type:
+To Run the live test against Azure Active Directory which uses the client_credentials grant type when using a password:
 
 mvn clean install -Plive \
 -Dtest.oauth.identity=<azure app id> \
@@ -32,3 +32,14 @@ mvn clean install -Plive \
 -Dtest.jclouds.oauth.resource=https://management.azure.com/ \
 -Dtest.jclouds.oauth.credential-type=clientCredentialsSecret
 
+To run the live test against Azure Active directory using the client_credentials grant type with a certificate and private key:
+
+mvn clean install -Plive \
+-Dtest.jclouds.oauth.credential-type=clientCredentialsP12 \
+-Dtest.jclouds.oauth.resource=https://management.azure.com/ \
+-Dtest.oauth.endpoint=https://login.microsoftonline.com/<tenant id>/oauth2/token \
+-Dtest.jclouds.oauth.audience=https://login.microsoftonline.com/<tenant id>/oauth2/token 
+-Dtest.oauth.identity=<azure app id> \
+-Dtest.oauth.credential=<path to unencrypted private key PEM file> \
+-Dtest.jclouds.oauth.certificate=<path to certificate PEM file>
+

http://git-wip-us.apache.org/repos/asf/jclouds/blob/f46b38dd/apis/oauth/src/main/java/org/jclouds/oauth/v2/AuthorizationApi.java
----------------------------------------------------------------------
diff --git a/apis/oauth/src/main/java/org/jclouds/oauth/v2/AuthorizationApi.java b/apis/oauth/src/main/java/org/jclouds/oauth/v2/AuthorizationApi.java
index b77a8de..50c6668 100644
--- a/apis/oauth/src/main/java/org/jclouds/oauth/v2/AuthorizationApi.java
+++ b/apis/oauth/src/main/java/org/jclouds/oauth/v2/AuthorizationApi.java
@@ -29,8 +29,10 @@ import org.jclouds.javax.annotation.Nullable;
 import org.jclouds.oauth.v2.OAuthFallbacks.AuthorizationExceptionOn4xx;
 import org.jclouds.oauth.v2.config.Authorization;
 import org.jclouds.oauth.v2.domain.Claims;
+import org.jclouds.oauth.v2.domain.ClientCredentialsClaims;
 import org.jclouds.oauth.v2.domain.Token;
 import org.jclouds.oauth.v2.functions.ClaimsToAssertion;
+import org.jclouds.oauth.v2.functions.ClientCredentialsClaimsToAssertion;
 import org.jclouds.rest.annotations.Endpoint;
 import org.jclouds.rest.annotations.Fallback;
 import org.jclouds.rest.annotations.FormParams;
@@ -59,4 +61,16 @@ public interface AuthorizationApi extends Closeable {
            @FormParam("resource") String resource,
            @FormParam("scope") @Nullable String scope
    );
+
+   @Named("oauth2:authorize_client_p12")
+   @POST
+   @FormParams(keys = {"grant_type", "client_assertion_type"}, values = {"client_credentials", "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"})
+   @Consumes(APPLICATION_JSON)
+   @Fallback(AuthorizationExceptionOn4xx.class)
+   Token authorize(
+            @FormParam("client_id") String client_id,
+            @FormParam("client_assertion") @ParamParser(ClientCredentialsClaimsToAssertion.class) ClientCredentialsClaims claim,
+            @FormParam("resource") String resource,
+            @FormParam("scope") @Nullable String scope
+   );
 }

http://git-wip-us.apache.org/repos/asf/jclouds/blob/f46b38dd/apis/oauth/src/main/java/org/jclouds/oauth/v2/config/CertificateFingerprintSupplier.java
----------------------------------------------------------------------
diff --git a/apis/oauth/src/main/java/org/jclouds/oauth/v2/config/CertificateFingerprintSupplier.java b/apis/oauth/src/main/java/org/jclouds/oauth/v2/config/CertificateFingerprintSupplier.java
new file mode 100644
index 0000000..b59064c
--- /dev/null
+++ b/apis/oauth/src/main/java/org/jclouds/oauth/v2/config/CertificateFingerprintSupplier.java
@@ -0,0 +1,113 @@
+/*
+ * 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.oauth.v2.config;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.base.Throwables.propagate;
+import static org.jclouds.crypto.Pems.x509Certificate;
+import static org.jclouds.oauth.v2.config.OAuthProperties.CERTIFICATE;
+import static org.jclouds.util.Throwables2.getFirstThrowableOfType;
+
+import java.io.IOException;
+import java.security.PrivateKey;
+import java.security.cert.CertificateException;
+import java.security.cert.X509Certificate;
+
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import javax.inject.Named;
+
+import org.jclouds.domain.Credentials;
+import org.jclouds.location.Provider;
+import org.jclouds.oauth.v2.domain.CertificateFingerprint;
+import org.jclouds.rest.AuthorizationException;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Supplier;
+import com.google.common.cache.CacheBuilder;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+import com.google.common.util.concurrent.UncheckedExecutionException;
+import com.google.common.hash.Hashing;
+import static com.google.common.io.BaseEncoding.base64;
+import com.google.common.hash.HashCode;
+
+/**
+ * Loads the fingerprint of a certificate associated with the {@link PrivateKey} from a pem X509Certificate.
+ */
+@Singleton // due to cache
+final class CertificateFingerprintSupplier implements Supplier<CertificateFingerprint> {
+
+    private final Supplier<Credentials> creds;
+    private final LoadingCache<Credentials, CertificateFingerprint> certCache;
+
+    @Inject CertificateFingerprintSupplier(@Provider Supplier<Credentials> creds, CertificateFingerprintForCredentials loader) {
+        this.creds = creds;
+        // throw out the certificate fingerprint related to old credentials
+        this.certCache = CacheBuilder.newBuilder().maximumSize(2).build(checkNotNull(loader, "loader"));
+    }
+
+    /**
+     * it is relatively expensive to extract a certificate from a PEM and calculate it's fingerprint.
+     * cache the relationship between current credentials so that the fingerprint is only recalculated once.
+     */
+    @VisibleForTesting
+    static final class CertificateFingerprintForCredentials extends CacheLoader<Credentials, CertificateFingerprint> {
+        @Inject(optional = true) @Named(CERTIFICATE) String certInPemFormat;
+
+        @Override public CertificateFingerprint load(Credentials in) {
+            try {
+                /**
+                 * CERTIFICATE made optional on injection so that it's not required when other OAuth methods
+                 * are used.
+                 */
+                if (certInPemFormat == null) {
+                    throw new IllegalArgumentException("certificate not specified.");
+                }
+                X509Certificate cert = null;
+                cert = x509Certificate(certInPemFormat);
+
+                /** Get the fingerprint in Base64 format */
+                byte[] encodedCert = cert.getEncoded();
+                HashCode hash = Hashing.sha1().hashBytes(encodedCert);
+                String fingerprint = base64().encode(hash.asBytes());
+
+                return CertificateFingerprint.create(fingerprint, cert);
+            } catch (CertificateException e) {
+                throw new AssertionError(e);
+            } catch (IOException e) {
+                throw propagate(e);
+            } catch (IllegalArgumentException e) {
+                throw new AuthorizationException("cannot parse cert. " + e.getMessage(), e);
+            }
+        }
+    }
+
+    @Override public CertificateFingerprint get() {
+        try {
+            // loader always throws UncheckedExecutionException so no point in using get()
+            return certCache.getUnchecked(checkNotNull(creds.get(), "credential supplier returned null"));
+        } catch (UncheckedExecutionException e) {
+            AuthorizationException authorizationException = getFirstThrowableOfType(e, AuthorizationException.class);
+            if (authorizationException != null) {
+                throw authorizationException;
+            }
+            throw e;
+        }
+    }
+}
+

http://git-wip-us.apache.org/repos/asf/jclouds/blob/f46b38dd/apis/oauth/src/main/java/org/jclouds/oauth/v2/config/CredentialType.java
----------------------------------------------------------------------
diff --git a/apis/oauth/src/main/java/org/jclouds/oauth/v2/config/CredentialType.java b/apis/oauth/src/main/java/org/jclouds/oauth/v2/config/CredentialType.java
index b80f4c4..1719cb4 100644
--- a/apis/oauth/src/main/java/org/jclouds/oauth/v2/config/CredentialType.java
+++ b/apis/oauth/src/main/java/org/jclouds/oauth/v2/config/CredentialType.java
@@ -29,7 +29,10 @@ public enum CredentialType {
    P12_PRIVATE_KEY_CREDENTIALS,
 
    /** Contents are an ID and Secret */
-   CLIENT_CREDENTIALS_SECRET;
+   CLIENT_CREDENTIALS_SECRET,
+
+   /** Contents are an ID and PEM-encoded Private Key.  The certificate is specified as it's own property. */
+   CLIENT_CREDENTIALS_P12_AND_CERTIFICATE;
 
    @Override public String toString() {
       return UPPER_UNDERSCORE.to(LOWER_CAMEL, name());

http://git-wip-us.apache.org/repos/asf/jclouds/blob/f46b38dd/apis/oauth/src/main/java/org/jclouds/oauth/v2/config/OAuthModule.java
----------------------------------------------------------------------
diff --git a/apis/oauth/src/main/java/org/jclouds/oauth/v2/config/OAuthModule.java b/apis/oauth/src/main/java/org/jclouds/oauth/v2/config/OAuthModule.java
index 97d58b7..ffc03cd 100644
--- a/apis/oauth/src/main/java/org/jclouds/oauth/v2/config/OAuthModule.java
+++ b/apis/oauth/src/main/java/org/jclouds/oauth/v2/config/OAuthModule.java
@@ -16,10 +16,11 @@
  */
 package org.jclouds.oauth.v2.config;
 
+import static org.jclouds.oauth.v2.config.OAuthProperties.CREDENTIAL_TYPE;
 import static org.jclouds.oauth.v2.config.CredentialType.BEARER_TOKEN_CREDENTIALS;
-import static org.jclouds.oauth.v2.config.CredentialType.CLIENT_CREDENTIALS_SECRET;
 import static org.jclouds.oauth.v2.config.CredentialType.P12_PRIVATE_KEY_CREDENTIALS;
-import static org.jclouds.oauth.v2.config.OAuthProperties.CREDENTIAL_TYPE;
+import static org.jclouds.oauth.v2.config.CredentialType.CLIENT_CREDENTIALS_SECRET;
+import static org.jclouds.oauth.v2.config.CredentialType.CLIENT_CREDENTIALS_P12_AND_CERTIFICATE;
 import static org.jclouds.rest.config.BinderUtils.bindHttpApi;
 
 import java.net.URI;
@@ -30,9 +31,11 @@ import javax.inject.Named;
 import javax.inject.Singleton;
 
 import org.jclouds.oauth.v2.AuthorizationApi;
+import org.jclouds.oauth.v2.domain.CertificateFingerprint;
+import org.jclouds.oauth.v2.filters.JWTBearerTokenFlow;
 import org.jclouds.oauth.v2.filters.BearerTokenFromCredentials;
+import org.jclouds.oauth.v2.filters.ClientCredentialsJWTBearerTokenFlow;
 import org.jclouds.oauth.v2.filters.ClientCredentialsSecretFlow;
-import org.jclouds.oauth.v2.filters.JWTBearerTokenFlow;
 import org.jclouds.oauth.v2.filters.OAuthFilter;
 
 import com.google.common.base.Supplier;
@@ -51,6 +54,7 @@ public final class OAuthModule extends AbstractModule {
       bindHttpApi(binder(), AuthorizationApi.class);
       bind(CredentialType.class).toProvider(CredentialTypeFromPropertyOrDefault.class);
       bind(new TypeLiteral<Supplier<PrivateKey>>() {}).annotatedWith(Authorization.class).to(PrivateKeySupplier.class);
+      bind(new TypeLiteral<Supplier<CertificateFingerprint>>() {}).annotatedWith(Authorization.class).to(CertificateFingerprintSupplier.class);
    }
 
    @Provides
@@ -76,7 +80,8 @@ public final class OAuthModule extends AbstractModule {
    protected Map<CredentialType, Class<? extends OAuthFilter>> authenticationFlowMap() {
       return ImmutableMap.of(P12_PRIVATE_KEY_CREDENTIALS, JWTBearerTokenFlow.class,
                              BEARER_TOKEN_CREDENTIALS, BearerTokenFromCredentials.class,
-                             CLIENT_CREDENTIALS_SECRET, ClientCredentialsSecretFlow.class);
+                             CLIENT_CREDENTIALS_SECRET, ClientCredentialsSecretFlow.class,
+                             CLIENT_CREDENTIALS_P12_AND_CERTIFICATE, ClientCredentialsJWTBearerTokenFlow.class);
    }
 
    @Provides

http://git-wip-us.apache.org/repos/asf/jclouds/blob/f46b38dd/apis/oauth/src/main/java/org/jclouds/oauth/v2/config/OAuthProperties.java
----------------------------------------------------------------------
diff --git a/apis/oauth/src/main/java/org/jclouds/oauth/v2/config/OAuthProperties.java b/apis/oauth/src/main/java/org/jclouds/oauth/v2/config/OAuthProperties.java
index 87b5ca9..29498f1 100644
--- a/apis/oauth/src/main/java/org/jclouds/oauth/v2/config/OAuthProperties.java
+++ b/apis/oauth/src/main/java/org/jclouds/oauth/v2/config/OAuthProperties.java
@@ -37,13 +37,20 @@ public final class OAuthProperties {
    public static final String CREDENTIAL_TYPE = "jclouds.oauth.credential-type";
 
    /**
-    * When using oauth with Azure Active Direction and Client Credentials, a "resource" must
+    * When using oauth with Azure Active Directory and Client Credentials, a "resource" must
     * be specified as part of the request.
     *
     * @see <a href="https://msdn.microsoft.com/en-us/library/azure/dn645543.aspx">doc</a>
     */
    public static final String RESOURCE = "jclouds.oauth.resource";
 
+   /**
+    * When using oauth with Azure Active Directory, Client Credentials, and using JWT
+    * authentication, the certificate associated with the Private Key must be provided.
+    * The fingerprint of the certificate is included in the JWT headers.
+    */
+   public static final String CERTIFICATE = "jclouds.oauth.certificate";
+
    private OAuthProperties() {
    }
 }

http://git-wip-us.apache.org/repos/asf/jclouds/blob/f46b38dd/apis/oauth/src/main/java/org/jclouds/oauth/v2/domain/CertificateFingerprint.java
----------------------------------------------------------------------
diff --git a/apis/oauth/src/main/java/org/jclouds/oauth/v2/domain/CertificateFingerprint.java b/apis/oauth/src/main/java/org/jclouds/oauth/v2/domain/CertificateFingerprint.java
new file mode 100644
index 0000000..a2f797b
--- /dev/null
+++ b/apis/oauth/src/main/java/org/jclouds/oauth/v2/domain/CertificateFingerprint.java
@@ -0,0 +1,43 @@
+/*
+ * 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.oauth.v2.domain;
+
+import org.jclouds.json.SerializedNames;
+
+import com.google.auto.value.AutoValue;
+
+import java.security.cert.X509Certificate;
+
+/**
+ * Details corresponding the a client_credential Azure AD Oauth request
+ */
+@AutoValue
+public abstract class CertificateFingerprint {
+    /** The fingerprint of the certificate **/
+    public abstract String fingerprint();
+
+    /** The certificate */
+    public abstract X509Certificate certificate();
+
+    @SerializedNames({ "fingerprint", "certificate" })
+    public static CertificateFingerprint create(String fingerprint, X509Certificate certificate) {
+        return new AutoValue_CertificateFingerprint(fingerprint, certificate);
+    }
+
+    CertificateFingerprint() {
+    }
+}

http://git-wip-us.apache.org/repos/asf/jclouds/blob/f46b38dd/apis/oauth/src/main/java/org/jclouds/oauth/v2/domain/ClientCredentialsAuthArgs.java
----------------------------------------------------------------------
diff --git a/apis/oauth/src/main/java/org/jclouds/oauth/v2/domain/ClientCredentialsAuthArgs.java b/apis/oauth/src/main/java/org/jclouds/oauth/v2/domain/ClientCredentialsAuthArgs.java
new file mode 100644
index 0000000..1dcad42
--- /dev/null
+++ b/apis/oauth/src/main/java/org/jclouds/oauth/v2/domain/ClientCredentialsAuthArgs.java
@@ -0,0 +1,48 @@
+/*
+ * 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.oauth.v2.domain;
+
+import org.jclouds.javax.annotation.Nullable;
+import org.jclouds.json.SerializedNames;
+
+import com.google.auto.value.AutoValue;
+
+/**
+ * Details corresponding the a client_credential Azure AD Oauth request
+ */
+@AutoValue
+public abstract class ClientCredentialsAuthArgs {
+    /** The ID of the client. **/
+    public abstract String clientId();
+
+    /** The claims for the JWT. */
+    public abstract ClientCredentialsClaims claims();
+
+    /** The resource to authorize against. **/
+    public abstract String resource();
+
+    /** The scope(s) to authorize against. **/
+    @Nullable public abstract String scope();
+
+    @SerializedNames({ "client_id", "claims", "resource", "scope" })
+    public static ClientCredentialsAuthArgs create(String clientId, ClientCredentialsClaims claims, String resource, String scope) {
+        return new AutoValue_ClientCredentialsAuthArgs(clientId, claims, resource, scope);
+    }
+
+    ClientCredentialsAuthArgs() {
+    }
+}

http://git-wip-us.apache.org/repos/asf/jclouds/blob/f46b38dd/apis/oauth/src/main/java/org/jclouds/oauth/v2/domain/ClientCredentialsClaims.java
----------------------------------------------------------------------
diff --git a/apis/oauth/src/main/java/org/jclouds/oauth/v2/domain/ClientCredentialsClaims.java b/apis/oauth/src/main/java/org/jclouds/oauth/v2/domain/ClientCredentialsClaims.java
new file mode 100644
index 0000000..3c37bef
--- /dev/null
+++ b/apis/oauth/src/main/java/org/jclouds/oauth/v2/domain/ClientCredentialsClaims.java
@@ -0,0 +1,61 @@
+/*
+ * 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.oauth.v2.domain;
+
+import org.jclouds.json.SerializedNames;
+
+import com.google.auto.value.AutoValue;
+
+/**
+ * Claims corresponding to a {@linkplain Token JWT Token} for use when making a client_credentials grant request.
+ *
+ * @see <a href="https://tools.ietf.org/html/draft-ietf-oauth-json-web-token-30#section-4">registered list</a>
+ */
+@AutoValue
+public abstract class ClientCredentialsClaims {
+    /**
+     * The issuer of this token. In Azure, it is either the email address for the Active Directory account
+     * or the ID of the application set up as a Service Principal.
+     */
+    public abstract String iss();
+
+    /** The subject of the JWT.  For Azure, "sub" is typically equal to "iss". */
+    public abstract String sub();
+
+    /**
+     * The oauth audience, who this token is intended for. For instance in JWT and for Azure
+     * Resource Manager APIs, this maps to https://login.microsoftonline.com/TENANT_ID/oauth2/token.
+     */
+    public abstract String aud();
+
+    /** The expiration time, in seconds since the epoch after which the JWT must not be accepted for processing. */
+    public abstract long exp();
+
+    /** The time before which the JWT must not be accepted for processing, in seconds since the epoch. */
+    public abstract long nbf();
+
+    /** "JWT ID", a unique identifier for the JWT. */
+    public abstract String jti();
+
+    @SerializedNames({ "iss", "sub", "aud", "exp", "nbf", "jti" })
+    public static ClientCredentialsClaims create(String iss, String sub, String aud, long exp, long nbf, String jti) {
+        return new AutoValue_ClientCredentialsClaims(iss, sub, aud, exp, nbf, jti);
+    }
+
+    ClientCredentialsClaims() {
+    }
+}

http://git-wip-us.apache.org/repos/asf/jclouds/blob/f46b38dd/apis/oauth/src/main/java/org/jclouds/oauth/v2/filters/ClientCredentialsJWTBearerTokenFlow.java
----------------------------------------------------------------------
diff --git a/apis/oauth/src/main/java/org/jclouds/oauth/v2/filters/ClientCredentialsJWTBearerTokenFlow.java b/apis/oauth/src/main/java/org/jclouds/oauth/v2/filters/ClientCredentialsJWTBearerTokenFlow.java
new file mode 100644
index 0000000..dd11940
--- /dev/null
+++ b/apis/oauth/src/main/java/org/jclouds/oauth/v2/filters/ClientCredentialsJWTBearerTokenFlow.java
@@ -0,0 +1,120 @@
+/*
+ * 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.oauth.v2.filters;
+
+import static java.util.concurrent.TimeUnit.SECONDS;
+import static org.jclouds.Constants.PROPERTY_SESSION_INTERVAL;
+import static org.jclouds.oauth.v2.config.OAuthProperties.AUDIENCE;
+import static org.jclouds.oauth.v2.config.OAuthProperties.RESOURCE;
+
+import javax.inject.Inject;
+import javax.inject.Named;
+import java.util.List;
+import java.util.UUID;
+
+import org.jclouds.domain.Credentials;
+import org.jclouds.http.HttpException;
+import org.jclouds.http.HttpRequest;
+import org.jclouds.location.Provider;
+import org.jclouds.oauth.v2.AuthorizationApi;
+import org.jclouds.oauth.v2.config.OAuthScopes;
+import org.jclouds.oauth.v2.domain.ClientCredentialsAuthArgs;
+import org.jclouds.oauth.v2.domain.ClientCredentialsClaims;
+import org.jclouds.oauth.v2.domain.Token;
+
+import com.google.common.base.Joiner;
+import com.google.common.base.Supplier;
+import com.google.common.cache.CacheBuilder;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+
+/**
+ * Authorizes new Bearer Tokens at runtime by authorizing claims needed for the http request.
+ *
+ * <h3>Cache</h3>
+ * This maintains a time-based Bearer Token cache. By default expires after 59 minutes
+ * (the maximum time a token is valid is 60 minutes).
+ * This cache and expiry period is system-wide and does not attend to per-instance expiry time
+ * (e.g. "expires_in" from Google Compute -- which is set to the standard 3600 seconds).
+ */
+public class ClientCredentialsJWTBearerTokenFlow implements OAuthFilter {
+    private static final Joiner ON_SPACE = Joiner.on(" ");
+
+    private final String resource;
+    private final String audience;
+    private final Supplier<Credentials> credentialsSupplier;
+    private final OAuthScopes scopes;
+    private final long tokenDuration;
+    private final LoadingCache<ClientCredentialsAuthArgs, Token> tokenCache;
+
+    @Inject
+    ClientCredentialsJWTBearerTokenFlow(AuthorizeToken loader, @Named(PROPERTY_SESSION_INTERVAL) long tokenDuration,
+                                        @Provider Supplier<Credentials> credentialsSupplier,
+                                        OAuthScopes scopes,
+                                        @Named(AUDIENCE) String audience,
+                                        @Named(RESOURCE) String resource) {
+        this.credentialsSupplier = credentialsSupplier;
+        this.scopes = scopes;
+        this.audience = audience;
+        this.resource = resource;
+        this.tokenDuration = tokenDuration;
+        // since the session interval is also the token expiration time requested to the server make the token expire a
+        // bit before the deadline to make sure there aren't session expiration exceptions
+        long cacheExpirationSeconds = tokenDuration > 30 ? tokenDuration - 30 : tokenDuration;
+        this.tokenCache = CacheBuilder.newBuilder().expireAfterWrite(cacheExpirationSeconds, SECONDS).build(loader);
+    }
+
+    static final class AuthorizeToken extends CacheLoader<ClientCredentialsAuthArgs, Token> {
+        private final AuthorizationApi api;
+
+        @Inject AuthorizeToken(AuthorizationApi api) {
+            this.api = api;
+        }
+
+        @Override public Token load(ClientCredentialsAuthArgs key) throws Exception {
+            return api.authorize(key.clientId(), key.claims(), key.resource(), key.scope());
+        }
+    }
+
+    @Override public HttpRequest filter(HttpRequest request) throws HttpException {
+        long now = currentTimeSeconds();
+        List<String> configuredScopes = scopes.forRequest(request);
+        ClientCredentialsClaims claims = ClientCredentialsClaims.create( //
+                credentialsSupplier.get().identity, // iss
+                credentialsSupplier.get().identity, // sub
+                audience, // aud
+                now + tokenDuration, // exp
+                now, // nbf
+                UUID.randomUUID().toString() // jti
+        );
+        ClientCredentialsAuthArgs authArgs = ClientCredentialsAuthArgs.create(
+                credentialsSupplier.get().identity,
+                claims,
+                resource == null ? "" : resource,
+                configuredScopes.isEmpty() ? null : ON_SPACE.join(configuredScopes)
+         );
+
+        Token token = tokenCache.getUnchecked(authArgs);
+        String authorization = String.format("%s %s", token.tokenType(), token.accessToken());
+        return request.toBuilder().addHeader("Authorization", authorization).build();
+    }
+
+    long currentTimeSeconds() {
+        return System.currentTimeMillis() / 1000;
+    }
+}
+

http://git-wip-us.apache.org/repos/asf/jclouds/blob/f46b38dd/apis/oauth/src/main/java/org/jclouds/oauth/v2/functions/ClientCredentialsClaimsToAssertion.java
----------------------------------------------------------------------
diff --git a/apis/oauth/src/main/java/org/jclouds/oauth/v2/functions/ClientCredentialsClaimsToAssertion.java b/apis/oauth/src/main/java/org/jclouds/oauth/v2/functions/ClientCredentialsClaimsToAssertion.java
new file mode 100644
index 0000000..b43f579
--- /dev/null
+++ b/apis/oauth/src/main/java/org/jclouds/oauth/v2/functions/ClientCredentialsClaimsToAssertion.java
@@ -0,0 +1,93 @@
+/*
+ * 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.oauth.v2.functions;
+
+import static com.google.common.base.Charsets.UTF_8;
+import static com.google.common.base.Joiner.on;
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.io.BaseEncoding.base64Url;
+import static org.jclouds.oauth.v2.config.OAuthProperties.JWS_ALG;
+
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
+import java.security.Signature;
+import java.security.SignatureException;
+import java.util.List;
+
+import javax.inject.Inject;
+import javax.inject.Named;
+
+import org.jclouds.json.Json;
+import org.jclouds.oauth.v2.config.Authorization;
+import org.jclouds.oauth.v2.domain.CertificateFingerprint;
+import org.jclouds.rest.AuthorizationException;
+
+import com.google.common.base.Function;
+import com.google.common.base.Supplier;
+import com.google.common.collect.ImmutableList;
+
+public final class ClientCredentialsClaimsToAssertion implements Function<Object, String> {
+    private static final List<String> SUPPORTED_ALGS = ImmutableList.of("RS256", "none");
+
+    private final Supplier<PrivateKey> privateKey;
+    private final Supplier<CertificateFingerprint> certFingerprint;
+    private final Json json;
+    private final String alg;
+
+    @Inject ClientCredentialsClaimsToAssertion(@Named(JWS_ALG) String alg,
+                                               @Authorization Supplier<PrivateKey> privateKey,
+                                               @Authorization Supplier<CertificateFingerprint> certFingerprint,
+                                               Json json) {
+        this.alg = alg;
+        checkArgument(SUPPORTED_ALGS.contains(alg), "%s %s not in supported list", JWS_ALG, alg, SUPPORTED_ALGS);
+        this.privateKey = privateKey;
+        this.certFingerprint = certFingerprint;
+        this.json = json;
+    }
+
+    @Override public String apply(Object input) {
+        String encodedHeader = String.format("{\"alg\":\"%s\",\"typ\":\"JWT\",\"x5t\":\"%s\"}", alg, certFingerprint.get().fingerprint());
+        String encodedClaimSet = json.toJson(input);
+
+        encodedHeader = base64Url().omitPadding().encode(encodedHeader.getBytes(UTF_8));
+        encodedClaimSet = base64Url().omitPadding().encode(encodedClaimSet.getBytes(UTF_8));
+
+        byte[] signature = alg.equals("none")
+                ? null
+                : sha256(privateKey.get(), on(".").join(encodedHeader, encodedClaimSet).getBytes(UTF_8));
+        String encodedSignature = signature != null ?  base64Url().omitPadding().encode(signature) : "";
+
+        // the final assertion in base 64 encoded {header}.{claimSet}.{signature} format
+        return on(".").join(encodedHeader, encodedClaimSet, encodedSignature);
+    }
+
+    static byte[] sha256(PrivateKey privateKey, byte[] input) {
+        try {
+            Signature signature = Signature.getInstance("SHA256withRSA");
+            signature.initSign(privateKey);
+            signature.update(input);
+            return signature.sign();
+        } catch (NoSuchAlgorithmException e) {
+            throw new AssertionError(e);
+        } catch (SignatureException e) {
+            throw new AuthorizationException(e);
+        } catch (InvalidKeyException e) {
+            throw new AuthorizationException(e);
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/jclouds/blob/f46b38dd/apis/oauth/src/test/java/org/jclouds/oauth/v2/AuthorizationApiLiveTest.java
----------------------------------------------------------------------
diff --git a/apis/oauth/src/test/java/org/jclouds/oauth/v2/AuthorizationApiLiveTest.java b/apis/oauth/src/test/java/org/jclouds/oauth/v2/AuthorizationApiLiveTest.java
index 1ded5dc..5d0d7cf 100644
--- a/apis/oauth/src/test/java/org/jclouds/oauth/v2/AuthorizationApiLiveTest.java
+++ b/apis/oauth/src/test/java/org/jclouds/oauth/v2/AuthorizationApiLiveTest.java
@@ -21,11 +21,13 @@ import static org.jclouds.oauth.v2.OAuthTestUtils.setCredential;
 import static org.jclouds.oauth.v2.config.OAuthProperties.JWS_ALG;
 import static org.jclouds.oauth.v2.config.OAuthProperties.RESOURCE;
 import static org.jclouds.oauth.v2.config.OAuthProperties.AUDIENCE;
+import static org.jclouds.oauth.v2.config.OAuthProperties.CERTIFICATE;
 import static org.jclouds.oauth.v2.config.OAuthProperties.CREDENTIAL_TYPE;
 import static org.jclouds.providers.AnonymousProviderMetadata.forApiOnEndpoint;
 import static org.testng.Assert.assertNotNull;
 
 import java.util.Properties;
+import java.util.UUID;
 
 import org.jclouds.apis.BaseApiLiveTest;
 import org.jclouds.oauth.v2.config.CredentialType;
@@ -33,6 +35,7 @@ import org.jclouds.oauth.v2.config.OAuthModule;
 import org.jclouds.oauth.v2.config.OAuthScopes;
 import org.jclouds.oauth.v2.config.OAuthScopes.SingleScope;
 import org.jclouds.oauth.v2.domain.Claims;
+import org.jclouds.oauth.v2.domain.ClientCredentialsClaims;
 import org.jclouds.oauth.v2.domain.Token;
 import org.jclouds.providers.ProviderMetadata;
 import org.testng.annotations.DataProvider;
@@ -51,6 +54,7 @@ public class AuthorizationApiLiveTest extends BaseApiLiveTest<AuthorizationApi>
    private String audience;
    private String credentialType;
    private String resource;
+   private String certificate;
 
    public AuthorizationApiLiveTest() {
       provider = "oauth";
@@ -65,7 +69,13 @@ public class AuthorizationApiLiveTest extends BaseApiLiveTest<AuthorizationApi>
    @DataProvider
    public Object[][] onlyRunForClientCredentialsSecret() {
       return (CredentialType.fromValue(credentialType) == CredentialType.CLIENT_CREDENTIALS_SECRET) ?
-            OAuthTestUtils.SINGLE_NO_ARG_INVOCATION : OAuthTestUtils.NO_INVOCATIONS;
+              OAuthTestUtils.SINGLE_NO_ARG_INVOCATION : OAuthTestUtils.NO_INVOCATIONS;
+   }
+
+   @DataProvider
+   public Object[][] onlyRunForClientCredentialsP12() {
+      return (CredentialType.fromValue(credentialType) == CredentialType.CLIENT_CREDENTIALS_P12_AND_CERTIFICATE) ?
+              OAuthTestUtils.SINGLE_NO_ARG_INVOCATION : OAuthTestUtils.NO_INVOCATIONS;
    }
 
    @Test(dataProvider = "onlyRunForP12PrivateKeyCredentials")
@@ -98,6 +108,23 @@ public class AuthorizationApiLiveTest extends BaseApiLiveTest<AuthorizationApi>
       assertNotNull(token, "no token when authorizing " + identity);
    }
 
+   @Test(dataProvider = "onlyRunForClientCredentialsP12")
+   public void authenticateClientCredentialsP12Test() throws Exception {
+      long now = System.currentTimeMillis() / 1000;
+      ClientCredentialsClaims claims = ClientCredentialsClaims.create(
+              identity, // iss
+              identity, // sub
+              audience, // aud
+              now + 3600, // exp
+              now, // iat
+              UUID.randomUUID().toString()
+      );
+
+      Token token = api.authorize(identity, claims, resource, null);
+
+      assertNotNull(token, "no token when authorizing " + claims);
+   }
+
    /** OAuth isn't registered as a provider intentionally, so we fake one. */
    @Override protected ProviderMetadata createProviderMetadata() {
       return forApiOnEndpoint(AuthorizationApi.class, endpoint).toBuilder().id("oauth").build();
@@ -121,6 +148,11 @@ public class AuthorizationApiLiveTest extends BaseApiLiveTest<AuthorizationApi>
       // Set the credential specific properties.
       if (CredentialType.fromValue(credentialType) == CredentialType.CLIENT_CREDENTIALS_SECRET) {
          resource = checkNotNull(setIfTestSystemPropertyPresent(props, RESOURCE), "test." + RESOURCE);
+      } else if (CredentialType.fromValue(credentialType) == CredentialType.CLIENT_CREDENTIALS_P12_AND_CERTIFICATE) {
+         audience = checkNotNull(setIfTestSystemPropertyPresent(props, AUDIENCE), "test.jclouds.oauth.audience");
+         resource = checkNotNull(setIfTestSystemPropertyPresent(props, RESOURCE), "test." + RESOURCE);
+         certificate = setCredential(props, CERTIFICATE);
+         credential = setCredential(props, "oauth.credential");
       } else if (CredentialType.fromValue(credentialType) == CredentialType.P12_PRIVATE_KEY_CREDENTIALS) {
          audience = checkNotNull(setIfTestSystemPropertyPresent(props, AUDIENCE), "test.jclouds.oauth.audience");
          credential = setCredential(props, "oauth.credential");

http://git-wip-us.apache.org/repos/asf/jclouds/blob/f46b38dd/apis/oauth/src/test/java/org/jclouds/oauth/v2/AuthorizationApiMockTest.java
----------------------------------------------------------------------
diff --git a/apis/oauth/src/test/java/org/jclouds/oauth/v2/AuthorizationApiMockTest.java b/apis/oauth/src/test/java/org/jclouds/oauth/v2/AuthorizationApiMockTest.java
index adc7585..89fe953 100644
--- a/apis/oauth/src/test/java/org/jclouds/oauth/v2/AuthorizationApiMockTest.java
+++ b/apis/oauth/src/test/java/org/jclouds/oauth/v2/AuthorizationApiMockTest.java
@@ -23,10 +23,12 @@ import static javax.ws.rs.core.MediaType.APPLICATION_JSON;
 import static org.jclouds.Constants.PROPERTY_MAX_RETRIES;
 import static org.jclouds.oauth.v2.config.CredentialType.P12_PRIVATE_KEY_CREDENTIALS;
 import static org.jclouds.oauth.v2.config.CredentialType.CLIENT_CREDENTIALS_SECRET;
+import static org.jclouds.oauth.v2.config.CredentialType.CLIENT_CREDENTIALS_P12_AND_CERTIFICATE;
+import static org.jclouds.oauth.v2.config.OAuthProperties.CERTIFICATE;
 import static org.jclouds.oauth.v2.config.OAuthProperties.AUDIENCE;
-import static org.jclouds.oauth.v2.config.OAuthProperties.RESOURCE;
 import static org.jclouds.oauth.v2.config.OAuthProperties.CREDENTIAL_TYPE;
 import static org.jclouds.oauth.v2.config.OAuthProperties.JWS_ALG;
+import static org.jclouds.oauth.v2.config.OAuthProperties.RESOURCE;
 import static org.jclouds.util.Strings2.toStringAndClose;
 import static org.testng.Assert.assertEquals;
 import static org.testng.Assert.fail;
@@ -42,6 +44,7 @@ import org.jclouds.oauth.v2.config.OAuthModule;
 import org.jclouds.oauth.v2.config.OAuthScopes;
 import org.jclouds.oauth.v2.config.OAuthScopes.SingleScope;
 import org.jclouds.oauth.v2.domain.Claims;
+import org.jclouds.oauth.v2.domain.ClientCredentialsClaims;
 import org.jclouds.oauth.v2.domain.Token;
 import org.jclouds.rest.AnonymousHttpApiMetadata;
 import org.jclouds.rest.AuthorizationException;
@@ -76,13 +79,28 @@ public class AuthorizationApiMockTest {
          1328569781 // iat
          );
 
+   private static final String clientCredentialsHeader = "{\"alg\":\"RS256\",\"typ\":\"JWT\",\"x5t\":\"RZk6mx4gGECvF6XWZWkK9qaGdHk=\"}";
+   private static final String clientCredentialsClaims = "{\"iss\":\"a242b44e-2c2a-3bdd-b094-6152da263c54\"," +
+         "\"sub\":\"a242b44e-2c2a-3bdd-b094-6152da263c54\",\"aud\":" +
+         "\"https://login.microsoftonline.com/a242ccee-1a1a-3bdd-b094-6152da263c54/oauth2/token\"" +
+         ",\"exp\":1328573381,\"nbf\":1328569781,\"jti\":\"abcdefgh\"}";
+
+   private static final ClientCredentialsClaims CLIENT_CREDENTIALS_CLAIMS = ClientCredentialsClaims.create(
+           "a242b44e-2c2a-3bdd-b094-6152da263c54", // iss
+           "a242b44e-2c2a-3bdd-b094-6152da263c54", // sub
+           "https://login.microsoftonline.com/a242ccee-1a1a-3bdd-b094-6152da263c54/oauth2/token", //aud
+           1328573381, // exp
+           1328569781, // nbf
+           "abcdefgh" // jti
+   );
+
    public void testGenerateJWTRequest() throws Exception {
       MockWebServer server = new MockWebServer();
 
       try {
          server.enqueue(new MockResponse().setBody("{\n"
-               + "  \"access_token\" : \"1/8xbJqaOZXSUZbHLl5EOtu1pxz3fmmetKx9W8CV4t79M\",\n"
-               + "  \"token_type\" : \"Bearer\",\n" + "  \"expires_in\" : 3600\n" + "}"));
+                 + "  \"access_token\" : \"1/8xbJqaOZXSUZbHLl5EOtu1pxz3fmmetKx9W8CV4t79M\",\n"
+                 + "  \"token_type\" : \"Bearer\",\n" + "  \"expires_in\" : 3600\n" + "}"));
          server.play();
 
          AuthorizationApi api = api(server.getUrl("/"), P12_PRIVATE_KEY_CREDENTIALS);
@@ -95,16 +113,16 @@ public class AuthorizationApiMockTest {
          assertEquals(request.getHeader("Content-Type"), "application/x-www-form-urlencoded");
 
          assertEquals(
-               new String(request.getBody(), UTF_8), //
-               "grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Ajwt-bearer&"
-                     +
-                     // Base64 Encoded Header
-                     "assertion="
-                     + Joiner.on('.').join(encoding.encode(header.getBytes(UTF_8)),
-                           encoding.encode(claims.getBytes(UTF_8)),
-                           // Base64 encoded {header}.{claims} signature (using
-                           // SHA256)
-                           "W2Lesr_98AzVYiMbzxFqmwcOjpIWlwqkC6pNn1fXND9oSDNNnFhy-AAR6DKH-x9ZmxbY80"
+                 new String(request.getBody(), UTF_8), //
+                 "grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Ajwt-bearer&"
+                         +
+                         // Base64 Encoded Header
+                         "assertion="
+                         + Joiner.on('.').join(encoding.encode(header.getBytes(UTF_8)),
+                         encoding.encode(claims.getBytes(UTF_8)),
+                         // Base64 encoded {header}.{claims} signature (using
+                         // SHA256)
+                         "W2Lesr_98AzVYiMbzxFqmwcOjpIWlwqkC6pNn1fXND9oSDNNnFhy-AAR6DKH-x9ZmxbY80"
                                  + "R5fH-OCeWumXlVgceKN8Z2SmgQsu8ElTpypQA54j_5j8vUImJ5hsOUYPeyF1U2BUzZ3L5g"
                                  + "03PXBA0YWwRU9E1ChH28dQBYuGiUmYw"));
       } finally {
@@ -161,6 +179,46 @@ public class AuthorizationApiMockTest {
       }
    }
 
+   public void testGenerateClientCredentialsJWTRequest() throws Exception {
+      MockWebServer server = new MockWebServer();
+
+      String identity = "user";
+      String resource = "http://management.azure.com/";
+      String encoded_resource = "http%3A//management.azure.com/";
+
+      try {
+         server.enqueue(new MockResponse().setBody("{\n"
+                 + "  \"access_token\" : \"1/8xbJqaOZXSUZbHLl5EOtu1pxz3fmmetKx9W8CV4t79M\",\n"
+                 + "  \"token_type\" : \"Bearer\",\n" + "  \"expires_in\" : 3600\n" + "}"));
+         server.play();
+
+         AuthorizationApi api = api(server.getUrl("/"), CLIENT_CREDENTIALS_P12_AND_CERTIFICATE);
+         assertEquals(api.authorize(identity, CLIENT_CREDENTIALS_CLAIMS, resource, null), TOKEN);
+
+         RecordedRequest request = server.takeRequest();
+         assertEquals(request.getMethod(), "POST");
+         assertEquals(request.getHeader("Accept"), APPLICATION_JSON);
+         assertEquals(request.getHeader("Content-Type"), "application/x-www-form-urlencoded");
+
+         assertEquals(
+                 new String(request.getBody(), UTF_8),
+                 "grant_type=client_credentials&" +
+                         "client_assertion_type=urn%3Aietf%3Aparams%3Aoauth%3Aclient-assertion-type%3Ajwt-bearer&" +
+                         "client_id=" + identity + "&" +
+                         "client_assertion=" +
+                         Joiner.on(".").join(encoding.encode(clientCredentialsHeader.getBytes(UTF_8)),
+                                 encoding.encode(clientCredentialsClaims.getBytes(UTF_8)),
+                                 "ip3i0YLlunb4iq8sUMlpYDKnEuzmvlLpF4NQvn_aiysO5cuT5QHuGREq" +
+                                         "gyEa-UMhfZoW49ggUWjS7YBT00r64cFE3dovaNMiZYZuVWu_" +
+                                         "FpqO2QlwV7uXqhaRIE0cyabbKG44YJwA-NE4rtFZedFMo94F" +
+                                         "6aOz2FN3en8zS9UVqmM"
+                                 ) + "&" +
+                         "resource=" + encoded_resource);
+      } finally {
+         server.shutdown();
+      }
+   }
+
    private final BaseEncoding encoding = base64Url().omitPadding();
 
    private AuthorizationApi api(URL url, CredentialType credentialType) throws IOException {
@@ -171,6 +229,7 @@ public class AuthorizationApiMockTest {
       overrides.setProperty(AUDIENCE, "https://accounts.google.com/o/oauth2/token");
       overrides.setProperty(RESOURCE, "https://management.azure.com/");
       overrides.setProperty(PROPERTY_MAX_RETRIES, "1");
+      overrides.setProperty(CERTIFICATE, toStringAndClose(OAuthTestUtils.class.getResourceAsStream("/testcert.pem")));
 
       return ContextBuilder.newBuilder(AnonymousHttpApiMetadata.forApi(AuthorizationApi.class))
             .credentials("foo", toStringAndClose(OAuthTestUtils.class.getResourceAsStream("/testpk.pem")))

http://git-wip-us.apache.org/repos/asf/jclouds/blob/f46b38dd/apis/oauth/src/test/resources/testcert.pem
----------------------------------------------------------------------
diff --git a/apis/oauth/src/test/resources/testcert.pem b/apis/oauth/src/test/resources/testcert.pem
new file mode 100644
index 0000000..19e744d
--- /dev/null
+++ b/apis/oauth/src/test/resources/testcert.pem
@@ -0,0 +1,22 @@
+-----BEGIN CERTIFICATE-----
+MIIDkTCCAnmgAwIBAgIJAISFMZeicwQVMA0GCSqGSIb3DQEBCwUAMF8xCzAJBgNV
+BAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRMwEQYDVQQHDApTYW50YSBDcnV6
+MQ8wDQYDVQQKDAZKQ1Rlc3QxFTATBgNVBAMMDGpjdGVzdHNwY2VydDAeFw0xNjA1
+MTQxNzUzMzFaFw0yNjA1MTIxNzUzMzFaMF8xCzAJBgNVBAYTAlVTMRMwEQYDVQQI
+DApDYWxpZm9ybmlhMRMwEQYDVQQHDApTYW50YSBDcnV6MQ8wDQYDVQQKDAZKQ1Rl
+c3QxFTATBgNVBAMMDGpjdGVzdHNwY2VydDCCASIwDQYJKoZIhvcNAQEBBQADggEP
+ADCCAQoCggEBALKkugQxFZq224O2Hb1Q+J8VyXs+fjbwGSErhyY0ynENvCaq2trG
+Mia0NAqtRZmG5Fn3KEYo9yCjjm0N34mer5u8X8aErvBa1LTkwrK2dQQ1oXGtbFn1
+dTqho00YwxzMxT3raDw9xXhexloDQbr/EAm4f1zu+05BSC3xSDVvvzARBPSseVfw
+gxWpS4M4aQp9M6Tv2ENYnecfl6StkaPdxaguJeVdpSoBe7piEEz1f2LEoC3Fw6De
+JIUgzQyVoffCWCA+RCf3o8GOqce0+INW50rcEv1JrGrDSUeEUYDCg+FniT9vKBsm
+sV8u3o1YxvtWmAY3KtXC7akwqHSdifecLtECAwEAAaNQME4wHQYDVR0OBBYEFL2W
+1+sMin4FbE9RFQr6FqEh9NFUMB8GA1UdIwQYMBaAFL2W1+sMin4FbE9RFQr6FqEh
+9NFUMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBADaHB0PbSooF1PkD
+2KwwYck7cm9C0jmnUVdcmJ6GrG8OdXEP14E4rUzrstFK0XSPHYoBH0p0xMISutHD
+RVLgsxXbnPhXVKaTuDspgedSAaHPFQJNEtMHHCNaSS2Vyh+2Ha9HLD7dsy+9QBKf
+Et4MMLXT/n4WVzKJcweWI/T8oIIbfHzPTo+jWYfTxxKJcL+C/GY2QREkKs+5mR2t
+N4q69ydcjeajB9F8XPbJlTDen+6ofwtQ45tS5w5EjV3SvOh4QOEVz1su9F5Ojw7Q
+oPuUKMynxeo2E6FsiNj5m+8NPLKiX2phnKMogbJyOhligj1QRR+zBc/62aPCP6Z4
+2pWp7Lc=
+-----END CERTIFICATE-----