diff --git a/api/src/main/java/io/minio/Digest.java b/api/src/main/java/io/minio/Digest.java
index 7108bf826..baa101015 100644
--- a/api/src/main/java/io/minio/Digest.java
+++ b/api/src/main/java/io/minio/Digest.java
@@ -28,7 +28,7 @@
import java.util.Locale;
/** Various global static functions used. */
-class Digest {
+public class Digest {
/** Private constructor. */
private Digest() {}
diff --git a/api/src/main/java/io/minio/MinioClient.java b/api/src/main/java/io/minio/MinioClient.java
index 838b4e8de..0e05e644e 100644
--- a/api/src/main/java/io/minio/MinioClient.java
+++ b/api/src/main/java/io/minio/MinioClient.java
@@ -1034,7 +1034,13 @@ protected Response execute(
Credentials creds = (provider == null) ? null : provider.fetch();
Request request = createRequest(url, method, headerMap, body, length, creds);
if (creds != null) {
- request = Signer.signV4(request, region, creds.accessKey(), creds.secretKey());
+ request =
+ Signer.signV4S3(
+ request,
+ region,
+ creds.accessKey(),
+ creds.secretKey(),
+ request.header("x-amz-content-sha256"));
}
if (this.traceStream != null) {
diff --git a/api/src/main/java/io/minio/Signer.java b/api/src/main/java/io/minio/Signer.java
index e7154cd2c..9c02c262e 100644
--- a/api/src/main/java/io/minio/Signer.java
+++ b/api/src/main/java/io/minio/Signer.java
@@ -37,7 +37,7 @@
import okhttp3.Request;
/** Amazon AWS S3 signature V4 signer. */
-class Signer {
+public class Signer {
//
// Excerpts from @lsegal - https://github.com/aws/aws-sdk-js/issues/659#issuecomment-120477258
//
@@ -115,7 +115,7 @@ class Signer {
* @param secretKey Secret Key string.
* @param prevSignature Previous signature of chunk upload.
*/
- public Signer(
+ private Signer(
Request request,
String contentSha256,
ZonedDateTime date,
@@ -132,8 +132,14 @@ public Signer(
this.prevSignature = prevSignature;
}
- private void setScope() {
- this.scope = this.date.format(Time.SIGNER_DATE_FORMAT) + "/" + this.region + "/s3/aws4_request";
+ private void setScope(String serviceName) {
+ this.scope =
+ this.date.format(Time.SIGNER_DATE_FORMAT)
+ + "/"
+ + this.region
+ + "/"
+ + serviceName
+ + "/aws4_request";
}
private void setCanonicalHeaders() {
@@ -240,7 +246,8 @@ private void setChunkStringToSign() throws NoSuchAlgorithmException {
+ this.contentSha256;
}
- private void setSigningKey() throws NoSuchAlgorithmException, InvalidKeyException {
+ private void setSigningKey(String serviceName)
+ throws NoSuchAlgorithmException, InvalidKeyException {
String aws4SecretKey = "AWS4" + this.secretKey;
byte[] dateKey =
@@ -250,7 +257,8 @@ private void setSigningKey() throws NoSuchAlgorithmException, InvalidKeyExceptio
byte[] dateRegionKey = sumHmac(dateKey, this.region.getBytes(StandardCharsets.UTF_8));
- byte[] dateRegionServiceKey = sumHmac(dateRegionKey, "s3".getBytes(StandardCharsets.UTF_8));
+ byte[] dateRegionServiceKey =
+ sumHmac(dateRegionKey, serviceName.getBytes(StandardCharsets.UTF_8));
this.signingKey =
sumHmac(dateRegionServiceKey, "aws4_request".getBytes(StandardCharsets.UTF_8));
@@ -278,9 +286,9 @@ public static String getChunkSignature(
String chunkSha256, ZonedDateTime date, String region, String secretKey, String prevSignature)
throws NoSuchAlgorithmException, InvalidKeyException {
Signer signer = new Signer(null, chunkSha256, date, region, null, secretKey, prevSignature);
- signer.setScope();
+ signer.setScope("s3");
signer.setChunkStringToSign();
- signer.setSigningKey();
+ signer.setSigningKey("s3");
signer.setSignature();
return signer.signature;
@@ -293,32 +301,51 @@ public static String getChunkSeedSignature(Request request, String region, Strin
ZonedDateTime date = ZonedDateTime.parse(request.header("x-amz-date"), Time.AMZ_DATE_FORMAT);
Signer signer = new Signer(request, contentSha256, date, region, null, secretKey, null);
- signer.setScope();
+ signer.setScope("s3");
signer.setCanonicalRequest();
signer.setStringToSign();
- signer.setSigningKey();
+ signer.setSigningKey("s3");
signer.setSignature();
return signer.signature;
}
/** Returns signed request object for given request, region, access key and secret key. */
- public static Request signV4(Request request, String region, String accessKey, String secretKey)
+ private static Request signV4(
+ String serviceName,
+ Request request,
+ String region,
+ String accessKey,
+ String secretKey,
+ String contentSha256)
throws NoSuchAlgorithmException, InvalidKeyException {
- String contentSha256 = request.header("x-amz-content-sha256");
ZonedDateTime date = ZonedDateTime.parse(request.header("x-amz-date"), Time.AMZ_DATE_FORMAT);
Signer signer = new Signer(request, contentSha256, date, region, accessKey, secretKey, null);
- signer.setScope();
+ signer.setScope(serviceName);
signer.setCanonicalRequest();
signer.setStringToSign();
- signer.setSigningKey();
+ signer.setSigningKey(serviceName);
signer.setSignature();
signer.setAuthorization();
return request.newBuilder().header("Authorization", signer.authorization).build();
}
+ /** Returns signed request of given request for S3 service. */
+ public static Request signV4S3(
+ Request request, String region, String accessKey, String secretKey, String contentSha256)
+ throws NoSuchAlgorithmException, InvalidKeyException {
+ return signV4("s3", request, region, accessKey, secretKey, contentSha256);
+ }
+
+ /** Returns signed request of given request for STS service. */
+ public static Request signV4Sts(
+ Request request, String region, String accessKey, String secretKey, String contentSha256)
+ throws NoSuchAlgorithmException, InvalidKeyException {
+ return signV4("sts", request, region, accessKey, secretKey, contentSha256);
+ }
+
private void setPresignCanonicalRequest(int expires) throws NoSuchAlgorithmException {
this.canonicalHeaders = new TreeMap<>();
this.canonicalHeaders.put("host", this.request.headers().get("Host"));
@@ -367,10 +394,10 @@ public static HttpUrl presignV4(
ZonedDateTime date = ZonedDateTime.parse(request.header("x-amz-date"), Time.AMZ_DATE_FORMAT);
Signer signer = new Signer(request, contentSha256, date, region, accessKey, secretKey, null);
- signer.setScope();
+ signer.setScope("s3");
signer.setPresignCanonicalRequest(expires);
signer.setStringToSign();
- signer.setSigningKey();
+ signer.setSigningKey("s3");
signer.setSignature();
return signer
@@ -397,7 +424,7 @@ public static String postPresignV4(
throws NoSuchAlgorithmException, InvalidKeyException {
Signer signer = new Signer(null, null, date, region, null, secretKey, null);
signer.stringToSign = stringToSign;
- signer.setSigningKey();
+ signer.setSigningKey("s3");
signer.setSignature();
return signer.signature;
diff --git a/api/src/main/java/io/minio/credentials/AssumeRoleBaseProvider.java b/api/src/main/java/io/minio/credentials/AssumeRoleBaseProvider.java
new file mode 100644
index 000000000..13f53a0fe
--- /dev/null
+++ b/api/src/main/java/io/minio/credentials/AssumeRoleBaseProvider.java
@@ -0,0 +1,96 @@
+/*
+ * MinIO Java SDK for Amazon S3 Compatible Cloud Storage, (C) 2020 MinIO, Inc.
+ *
+ * Licensed 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
+ *
+ * https://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 io.minio.credentials;
+
+import io.minio.errors.XmlParserException;
+import java.io.IOException;
+import java.util.Arrays;
+import okhttp3.HttpUrl;
+import okhttp3.OkHttpClient;
+import okhttp3.Protocol;
+import okhttp3.Request;
+import okhttp3.Response;
+
+/** Base class to AssumeRole based providers. */
+public abstract class AssumeRoleBaseProvider implements Provider {
+ private final OkHttpClient httpClient;
+ private Credentials credentials;
+
+ public AssumeRoleBaseProvider(OkHttpClient customHttpClient) {
+ // HTTP/1.1 is only supported in default client because of HTTP/2 in OkHttpClient cause 5
+ // minutes timeout on program exit.
+ this.httpClient =
+ (customHttpClient != null)
+ ? customHttpClient
+ : new OkHttpClient().newBuilder().protocols(Arrays.asList(Protocol.HTTP_1_1)).build();
+ }
+
+ @Override
+ public synchronized Credentials fetch() {
+ if (credentials != null && !credentials.isExpired()) {
+ return credentials;
+ }
+
+ try (Response response = httpClient.newCall(getRequest()).execute()) {
+ if (!response.isSuccessful()) {
+ throw new IllegalStateException(
+ "STS service failed with HTTP status code " + response.code());
+ }
+
+ credentials = parseResponse(response);
+ return credentials;
+ } catch (XmlParserException | IOException e) {
+ throw new IllegalStateException("Unable to parse STS response", e);
+ }
+ }
+
+ protected HttpUrl.Builder newUrlBuilder(
+ HttpUrl url,
+ String action,
+ int durationSeconds,
+ String policy,
+ String roleArn,
+ String roleSessionName) {
+ HttpUrl.Builder urlBuilder =
+ url.newBuilder()
+ .addQueryParameter("Action", action)
+ .addQueryParameter("Version", "2011-06-15");
+
+ if (durationSeconds > 0) {
+ urlBuilder.addQueryParameter("DurationSeconds", String.valueOf(durationSeconds));
+ }
+
+ if (policy != null) {
+ urlBuilder.addQueryParameter("Policy", policy);
+ }
+
+ if (roleArn != null) {
+ urlBuilder.addQueryParameter("RoleArn", roleArn);
+ }
+
+ if (roleSessionName != null) {
+ urlBuilder.addQueryParameter("RoleSessionName", roleSessionName);
+ }
+
+ return urlBuilder;
+ }
+
+ protected abstract Request getRequest();
+
+ protected abstract Credentials parseResponse(Response response)
+ throws XmlParserException, IOException;
+}
diff --git a/api/src/main/java/io/minio/credentials/AssumeRoleProvider.java b/api/src/main/java/io/minio/credentials/AssumeRoleProvider.java
new file mode 100644
index 000000000..314143b1a
--- /dev/null
+++ b/api/src/main/java/io/minio/credentials/AssumeRoleProvider.java
@@ -0,0 +1,149 @@
+/*
+ * MinIO Java SDK for Amazon S3 Compatible Cloud Storage, (C) 2020 MinIO, Inc.
+ *
+ * Licensed 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
+ *
+ * https://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 io.minio.credentials;
+
+import io.minio.Digest;
+import io.minio.Signer;
+import io.minio.Time;
+import io.minio.Xml;
+import io.minio.errors.XmlParserException;
+import java.io.IOException;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.time.ZonedDateTime;
+import java.util.Objects;
+import java.util.concurrent.TimeUnit;
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import okhttp3.HttpUrl;
+import okhttp3.MediaType;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.RequestBody;
+import okhttp3.Response;
+import org.simpleframework.xml.Element;
+import org.simpleframework.xml.Namespace;
+import org.simpleframework.xml.Path;
+import org.simpleframework.xml.Root;
+
+/**
+ * Credential provider using AssumeRole
+ * API.
+ */
+public class AssumeRoleProvider extends AssumeRoleBaseProvider {
+ public static final int DEFAULT_DURATION_SECONDS = (int) TimeUnit.HOURS.toSeconds(1);
+ private final String accessKey;
+ private final String secretKey;
+ private final String region;
+ private final String contentSha256;
+ private final Request request;
+
+ public AssumeRoleProvider(
+ @Nonnull String stsEndpoint,
+ @Nonnull String accessKey,
+ @Nonnull String secretKey,
+ @Nullable Integer durationSeconds,
+ @Nullable String policy,
+ @Nullable String region,
+ @Nullable String roleArn,
+ @Nullable String roleSessionName,
+ @Nullable String externalId,
+ @Nullable OkHttpClient customHttpClient)
+ throws NoSuchAlgorithmException {
+ super(customHttpClient);
+ stsEndpoint = Objects.requireNonNull(stsEndpoint, "STS endpoint cannot be empty");
+ HttpUrl url = Objects.requireNonNull(HttpUrl.parse(stsEndpoint), "Invalid STS endpoint");
+ accessKey = Objects.requireNonNull(accessKey, "Access key must not be null");
+ if (accessKey.isEmpty()) {
+ throw new IllegalArgumentException("Access key must not be empty");
+ }
+ this.accessKey = accessKey;
+ this.secretKey = Objects.requireNonNull(secretKey, "Secret key must not be null");
+ this.region = (region != null) ? region : "";
+
+ if (externalId != null && (externalId.length() < 2 || externalId.length() > 1224)) {
+ throw new IllegalArgumentException("Length of ExternalId must be in between 2 and 1224");
+ }
+
+ durationSeconds =
+ (durationSeconds != null && durationSeconds > DEFAULT_DURATION_SECONDS)
+ ? durationSeconds
+ : DEFAULT_DURATION_SECONDS;
+
+ String host = url.host() + ":" + url.port();
+ // ignore port when port and service matches i.e HTTP -> 80, HTTPS -> 443
+ if ((url.scheme().equals("http") && url.port() == 80)
+ || (url.scheme().equals("https") && url.port() == 443)) {
+ host = url.host();
+ }
+
+ HttpUrl.Builder urlBuilder =
+ newUrlBuilder(url, "AssumeRole", durationSeconds, policy, roleArn, roleSessionName);
+ if (externalId != null) {
+ urlBuilder.addQueryParameter("ExternalId", externalId);
+ }
+
+ String data = urlBuilder.build().encodedQuery();
+ this.contentSha256 = Digest.sha256Hash(data);
+ this.request =
+ new Request.Builder()
+ .url(url)
+ .header("Host", host)
+ .method(
+ "POST",
+ RequestBody.create(MediaType.parse("application/x-www-form-urlencoded"), data))
+ .build();
+ }
+
+ @Override
+ protected Request getRequest() {
+ try {
+ return Signer.signV4Sts(
+ this.request
+ .newBuilder()
+ .header("x-amz-date", ZonedDateTime.now().format(Time.AMZ_DATE_FORMAT))
+ .build(),
+ region,
+ accessKey,
+ secretKey,
+ contentSha256);
+ } catch (NoSuchAlgorithmException | InvalidKeyException e) {
+ throw new IllegalStateException("Signature calculation failed", e);
+ }
+ }
+
+ @Override
+ protected Credentials parseResponse(Response response) throws XmlParserException, IOException {
+ AssumeRoleResponse result =
+ Xml.unmarshal(AssumeRoleResponse.class, response.body().charStream());
+ return result.credentials();
+ }
+
+ /** Object representation of response XML of AssumeRole API. */
+ @Root(name = "AssumeRoleResponse", strict = false)
+ @Namespace(reference = "https://sts.amazonaws.com/doc/2011-06-15/")
+ public static class AssumeRoleResponse {
+ @Path(value = "AssumeRoleResult")
+ @Element(name = "Credentials")
+ private Credentials credentials;
+
+ public Credentials credentials() {
+ return credentials;
+ }
+ }
+}
diff --git a/api/src/main/java/io/minio/credentials/ClientGrantsProvider.java b/api/src/main/java/io/minio/credentials/ClientGrantsProvider.java
index a390d30fe..5f96f55e0 100644
--- a/api/src/main/java/io/minio/credentials/ClientGrantsProvider.java
+++ b/api/src/main/java/io/minio/credentials/ClientGrantsProvider.java
@@ -16,10 +16,19 @@
package io.minio.credentials;
+import io.minio.Xml;
+import io.minio.errors.XmlParserException;
+import java.io.IOException;
import java.util.function.Supplier;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
+import okhttp3.HttpUrl;
import okhttp3.OkHttpClient;
+import okhttp3.Response;
+import org.simpleframework.xml.Element;
+import org.simpleframework.xml.Namespace;
+import org.simpleframework.xml.Path;
+import org.simpleframework.xml.Root;
/**
* Credential provider using supplier;
- private final HttpUrl stsEndpoint;
- private final Integer durationSeconds;
- private final String policy;
- private final String roleArn;
- private final String roleSessionName;
- private final OkHttpClient httpClient;
- private Credentials credentials;
+ protected final HttpUrl stsEndpoint;
+ protected final Integer durationSeconds;
+ protected final String policy;
+ protected final String roleArn;
+ protected final String roleSessionName;
public WebIdentityClientGrantsProvider(
@Nonnull Supplier supplier,
@@ -58,6 +48,7 @@ public WebIdentityClientGrantsProvider(
@Nullable String roleArn,
@Nullable String roleSessionName,
@Nullable OkHttpClient customHttpClient) {
+ super(customHttpClient);
this.supplier = Objects.requireNonNull(supplier, "JWT token supplier must not be null");
stsEndpoint = Objects.requireNonNull(stsEndpoint, "STS endpoint cannot be empty");
this.stsEndpoint = Objects.requireNonNull(HttpUrl.parse(stsEndpoint), "Invalid STS endpoint");
@@ -65,10 +56,9 @@ public WebIdentityClientGrantsProvider(
this.policy = policy;
this.roleArn = roleArn;
this.roleSessionName = roleSessionName;
- this.httpClient = (customHttpClient != null) ? customHttpClient : new OkHttpClient();
}
- private int getDurationSeconds(int expiry) {
+ protected int getDurationSeconds(int expiry) {
if (durationSeconds != null && durationSeconds > 0) {
expiry = durationSeconds;
}
@@ -85,93 +75,11 @@ private int getDurationSeconds(int expiry) {
}
@Override
- public synchronized Credentials fetch() {
- if (credentials != null && !credentials.isExpired()) {
- return credentials;
- }
-
+ protected Request getRequest() {
Jwt jwt = supplier.get();
-
- HttpUrl.Builder urlBuilder =
- stsEndpoint.newBuilder().addQueryParameter("Version", "2011-06-15");
-
- int durationSeconds = getDurationSeconds(jwt.expiry());
- if (durationSeconds > 0) {
- urlBuilder.addQueryParameter("DurationSeconds", String.valueOf(durationSeconds));
- }
-
- if (policy != null) {
- urlBuilder.addQueryParameter("Policy", policy);
- }
-
- if (isWebIdentity()) {
- urlBuilder
- .addQueryParameter("Action", "AssumeRoleWithWebIdentity")
- .addQueryParameter("WebIdentityToken", jwt.token());
- if (roleArn != null) {
- urlBuilder
- .addQueryParameter("RoleArn", roleArn)
- .addQueryParameter(
- "RoleSessionName",
- (roleSessionName != null)
- ? roleSessionName
- : String.valueOf(System.currentTimeMillis()));
- }
- } else {
- urlBuilder
- .addQueryParameter("Action", "AssumeRoleWithClientGrants")
- .addQueryParameter("Token", jwt.token());
- }
-
- Request request =
- new Request.Builder().url(urlBuilder.build()).method("POST", EMPTY_BODY).build();
- try (Response response = httpClient.newCall(request).execute()) {
- if (!response.isSuccessful()) {
- throw new IllegalStateException(
- "STS service failed with HTTP status code " + response.code());
- }
-
- if (isWebIdentity()) {
- WebIdentityResponse result =
- Xml.unmarshal(WebIdentityResponse.class, response.body().charStream());
- credentials = result.credentials();
- } else {
- ClientGrantsResponse result =
- Xml.unmarshal(ClientGrantsResponse.class, response.body().charStream());
- credentials = result.credentials();
- }
-
- return credentials;
- } catch (XmlParserException | IOException e) {
- throw new IllegalStateException("Unable to parse STS response", e);
- }
- }
-
- protected abstract boolean isWebIdentity();
-
- /** Object representation of response XML of AssumeRoleWithWebIdentity API. */
- @Root(name = "AssumeRoleWithWebIdentityResponse", strict = false)
- @Namespace(reference = "https://sts.amazonaws.com/doc/2011-06-15/")
- public static class WebIdentityResponse {
- @Path(value = "AssumeRoleWithWebIdentityResult")
- @Element(name = "Credentials")
- private Credentials credentials;
-
- public Credentials credentials() {
- return credentials;
- }
+ HttpUrl.Builder urlBuilder = newUrlBuilder(jwt);
+ return new Request.Builder().url(urlBuilder.build()).method("POST", EMPTY_BODY).build();
}
- /** Object representation of response XML of AssumeRoleWithClientGrants API. */
- @Root(name = "AssumeRoleWithClientGrantsResponse", strict = false)
- @Namespace(reference = "https://sts.amazonaws.com/doc/2011-06-15/")
- public static class ClientGrantsResponse {
- @Path(value = "AssumeRoleWithClientGrantsResult")
- @Element(name = "Credentials")
- private Credentials credentials;
-
- public Credentials credentials() {
- return credentials;
- }
- }
+ protected abstract HttpUrl.Builder newUrlBuilder(Jwt jwt);
}
diff --git a/api/src/main/java/io/minio/credentials/WebIdentityProvider.java b/api/src/main/java/io/minio/credentials/WebIdentityProvider.java
index c6588cab7..00d45d362 100644
--- a/api/src/main/java/io/minio/credentials/WebIdentityProvider.java
+++ b/api/src/main/java/io/minio/credentials/WebIdentityProvider.java
@@ -16,10 +16,19 @@
package io.minio.credentials;
+import io.minio.Xml;
+import io.minio.errors.XmlParserException;
+import java.io.IOException;
import java.util.function.Supplier;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
+import okhttp3.HttpUrl;
import okhttp3.OkHttpClient;
+import okhttp3.Response;
+import org.simpleframework.xml.Element;
+import org.simpleframework.xml.Namespace;
+import org.simpleframework.xml.Path;
+import org.simpleframework.xml.Root;
/**
* Credential provider using